From 3cbcc7be21c41cc16a8a6f315230655d755bf920 Mon Sep 17 00:00:00 2001 From: Maurice Renck Date: Thu, 12 Nov 2020 13:42:50 +0100 Subject: [PATCH] chore: add kirby folder for autoload --- kirby/.editorconfig | 15 + kirby/SECURITY.md | 3 + kirby/assets/whoops.css | 83 + kirby/bootstrap.php | 35 + kirby/cacert.pem | 3447 +++++ kirby/composer.json | 53 + kirby/composer.lock | 629 + kirby/config/aliases.php | 62 + kirby/config/api/authentication.php | 24 + kirby/config/api/collections.php | 72 + kirby/config/api/models.php | 20 + kirby/config/api/models/File.php | 166 + kirby/config/api/models/FileBlueprint.php | 26 + kirby/config/api/models/FileVersion.php | 83 + kirby/config/api/models/Language.php | 44 + kirby/config/api/models/Page.php | 157 + kirby/config/api/models/PageBlueprint.php | 35 + kirby/config/api/models/Role.php | 31 + kirby/config/api/models/Site.php | 72 + kirby/config/api/models/SiteBlueprint.php | 26 + kirby/config/api/models/System.php | 119 + kirby/config/api/models/Translation.php | 34 + kirby/config/api/models/User.php | 108 + kirby/config/api/models/UserBlueprint.php | 26 + kirby/config/api/routes.php | 26 + kirby/config/api/routes/auth.php | 55 + kirby/config/api/routes/files.php | 123 + kirby/config/api/routes/languages.php | 46 + kirby/config/api/routes/lock.php | 99 + kirby/config/api/routes/pages.php | 124 + kirby/config/api/routes/roles.php | 28 + kirby/config/api/routes/site.php | 107 + kirby/config/api/routes/system.php | 79 + kirby/config/api/routes/translations.php | 24 + kirby/config/api/routes/users.php | 160 + kirby/config/blueprints.php | 7 + kirby/config/blueprints/file.yml | 2 + kirby/config/blueprints/page.yml | 3 + kirby/config/blueprints/site.yml | 7 + kirby/config/components.php | 376 + kirby/config/fields.php | 28 + kirby/config/fields/checkboxes.php | 61 + kirby/config/fields/date.php | 129 + kirby/config/fields/email.php | 40 + kirby/config/fields/files.php | 138 + kirby/config/fields/gap.php | 5 + kirby/config/fields/headline.php | 27 + kirby/config/fields/hidden.php | 3 + kirby/config/fields/info.php | 24 + kirby/config/fields/line.php | 5 + kirby/config/fields/mixins/filepicker.php | 14 + kirby/config/fields/mixins/min.php | 22 + kirby/config/fields/mixins/options.php | 48 + kirby/config/fields/mixins/pagepicker.php | 14 + kirby/config/fields/mixins/picker.php | 78 + kirby/config/fields/mixins/upload.php | 72 + kirby/config/fields/mixins/userpicker.php | 13 + kirby/config/fields/multiselect.php | 32 + kirby/config/fields/number.php | 48 + kirby/config/fields/pages.php | 117 + kirby/config/fields/radio.php | 29 + kirby/config/fields/range.php | 24 + kirby/config/fields/select.php | 24 + kirby/config/fields/structure.php | 193 + kirby/config/fields/tags.php | 96 + kirby/config/fields/tel.php | 27 + kirby/config/fields/text.php | 103 + kirby/config/fields/textarea.php | 122 + kirby/config/fields/time.php | 68 + kirby/config/fields/toggle.php | 68 + kirby/config/fields/url.php | 41 + kirby/config/fields/users.php | 97 + kirby/config/helpers.php | 884 ++ kirby/config/methods.php | 568 + kirby/config/presets/files.php | 24 + kirby/config/presets/page.php | 72 + kirby/config/presets/pages.php | 57 + kirby/config/roots.php | 90 + kirby/config/routes.php | 151 + kirby/config/sections/fields.php | 66 + kirby/config/sections/files.php | 241 + kirby/config/sections/info.php | 35 + kirby/config/sections/mixins/empty.php | 21 + kirby/config/sections/mixins/headline.php | 23 + kirby/config/sections/mixins/help.php | 23 + kirby/config/sections/mixins/layout.php | 12 + kirby/config/sections/mixins/max.php | 28 + kirby/config/sections/mixins/min.php | 21 + kirby/config/sections/mixins/pagination.php | 36 + kirby/config/sections/mixins/parent.php | 43 + kirby/config/sections/pages.php | 298 + kirby/config/setup.php | 41 + kirby/config/tags.php | 260 + kirby/config/urls.php | 33 + .../parsedown-extra/ParsedownExtra.php | 627 + kirby/dependencies/parsedown/Parsedown.php | 1822 +++ kirby/i18n/rules/LICENSE | 9 + kirby/i18n/rules/ar.json | 30 + kirby/i18n/rules/az.json | 16 + kirby/i18n/rules/bg.json | 65 + kirby/i18n/rules/cs.json | 20 + kirby/i18n/rules/da.json | 10 + kirby/i18n/rules/de.json | 9 + kirby/i18n/rules/el.json | 111 + kirby/i18n/rules/eo.json | 14 + kirby/i18n/rules/et.json | 14 + kirby/i18n/rules/fa.json | 36 + kirby/i18n/rules/fi.json | 6 + kirby/i18n/rules/fr.json | 34 + kirby/i18n/rules/hi.json | 66 + kirby/i18n/rules/hr.json | 12 + kirby/i18n/rules/hu.json | 20 + kirby/i18n/rules/hy.json | 79 + kirby/i18n/rules/it.json | 13 + kirby/i18n/rules/ja.json | 172 + kirby/i18n/rules/ka.json | 35 + kirby/i18n/rules/ko.json | 11174 ++++++++++++++++ kirby/i18n/rules/lt.json | 20 + kirby/i18n/rules/lv.json | 18 + kirby/i18n/rules/mk.json | 64 + kirby/i18n/rules/my.json | 121 + kirby/i18n/rules/nb.json | 8 + kirby/i18n/rules/pl.json | 20 + kirby/i18n/rules/pt_BR.json | 187 + kirby/i18n/rules/rm.json | 16 + kirby/i18n/rules/ru.json | 68 + kirby/i18n/rules/sr.json | 72 + kirby/i18n/rules/sv_SE.json | 8 + kirby/i18n/rules/tr.json | 14 + kirby/i18n/rules/uk.json | 10 + kirby/i18n/rules/vi.json | 135 + kirby/i18n/rules/zh.json | 6937 ++++++++++ kirby/i18n/translations/bg.json | 428 + kirby/i18n/translations/ca.json | 428 + kirby/i18n/translations/cs.json | 423 + kirby/i18n/translations/da.json | 428 + kirby/i18n/translations/de.json | 423 + kirby/i18n/translations/el.json | 428 + kirby/i18n/translations/en.json | 428 + kirby/i18n/translations/es_419.json | 428 + kirby/i18n/translations/es_ES.json | 428 + kirby/i18n/translations/fa.json | 428 + kirby/i18n/translations/fi.json | 428 + kirby/i18n/translations/fr.json | 428 + kirby/i18n/translations/hu.json | 428 + kirby/i18n/translations/id.json | 423 + kirby/i18n/translations/it.json | 428 + kirby/i18n/translations/ko.json | 428 + kirby/i18n/translations/lt.json | 428 + kirby/i18n/translations/nb.json | 428 + kirby/i18n/translations/nl.json | 428 + kirby/i18n/translations/pl.json | 428 + kirby/i18n/translations/pt_BR.json | 428 + kirby/i18n/translations/pt_PT.json | 428 + kirby/i18n/translations/ru.json | 428 + kirby/i18n/translations/sk.json | 428 + kirby/i18n/translations/sv_SE.json | 428 + kirby/i18n/translations/tr.json | 428 + kirby/kirby.pub | 9 + kirby/panel/dist/apple-touch-icon.png | Bin 0 -> 4115 bytes kirby/panel/dist/css/app.css | 1 + kirby/panel/dist/favicon.png | Bin 0 -> 539 bytes kirby/panel/dist/favicon.svg | 9 + kirby/panel/dist/img/icons.svg | 424 + kirby/panel/dist/js/app.js | 1 + kirby/panel/dist/js/plugins.js | 73 + kirby/panel/dist/js/vendor.js | 30 + kirby/router.php | 12 + kirby/src/Api/Api.php | 835 ++ kirby/src/Api/Collection.php | 178 + kirby/src/Api/Model.php | 248 + kirby/src/Cache/ApcuCache.php | 86 + kirby/src/Cache/Cache.php | 242 + kirby/src/Cache/FileCache.php | 166 + kirby/src/Cache/MemCached.php | 97 + kirby/src/Cache/MemoryCache.php | 82 + kirby/src/Cache/NullCache.php | 69 + kirby/src/Cache/Value.php | 144 + kirby/src/Cms/Api.php | 335 + kirby/src/Cms/App.php | 1512 +++ kirby/src/Cms/AppCaches.php | 138 + kirby/src/Cms/AppErrors.php | 188 + kirby/src/Cms/AppPlugins.php | 794 ++ kirby/src/Cms/AppTranslations.php | 186 + kirby/src/Cms/AppUsers.php | 140 + kirby/src/Cms/Asset.php | 126 + kirby/src/Cms/Auth.php | 502 + kirby/src/Cms/Blueprint.php | 797 ++ kirby/src/Cms/Collection.php | 338 + kirby/src/Cms/Collections.php | 141 + kirby/src/Cms/Content.php | 266 + kirby/src/Cms/ContentLock.php | 232 + kirby/src/Cms/ContentLocks.php | 228 + kirby/src/Cms/ContentTranslation.php | 242 + kirby/src/Cms/Dir.php | 180 + kirby/src/Cms/Email.php | 255 + kirby/src/Cms/Event.php | 289 + kirby/src/Cms/Field.php | 257 + kirby/src/Cms/File.php | 785 ++ kirby/src/Cms/FileActions.php | 320 + kirby/src/Cms/FileBlueprint.php | 79 + kirby/src/Cms/FileFoundation.php | 248 + kirby/src/Cms/FileModifications.php | 202 + kirby/src/Cms/FilePermissions.php | 17 + kirby/src/Cms/FilePicker.php | 74 + kirby/src/Cms/FileRules.php | 278 + kirby/src/Cms/FileVersion.php | 143 + kirby/src/Cms/Filename.php | 303 + kirby/src/Cms/Files.php | 137 + kirby/src/Cms/Form.php | 92 + kirby/src/Cms/HasChildren.php | 265 + kirby/src/Cms/HasFiles.php | 226 + kirby/src/Cms/HasMethods.php | 80 + kirby/src/Cms/HasSiblings.php | 182 + kirby/src/Cms/Html.php | 30 + kirby/src/Cms/Ingredients.php | 95 + kirby/src/Cms/KirbyTag.php | 60 + kirby/src/Cms/KirbyTags.php | 45 + kirby/src/Cms/Language.php | 727 + kirby/src/Cms/LanguageRouter.php | 135 + kirby/src/Cms/LanguageRoutes.php | 154 + kirby/src/Cms/LanguageRules.php | 98 + kirby/src/Cms/Languages.php | 111 + kirby/src/Cms/Media.php | 169 + kirby/src/Cms/Model.php | 109 + kirby/src/Cms/ModelPermissions.php | 116 + kirby/src/Cms/ModelWithContent.php | 806 ++ kirby/src/Cms/Nest.php | 48 + kirby/src/Cms/NestCollection.php | 33 + kirby/src/Cms/NestObject.php | 43 + kirby/src/Cms/Page.php | 1594 +++ kirby/src/Cms/PageActions.php | 838 ++ kirby/src/Cms/PageBlueprint.php | 209 + kirby/src/Cms/PagePermissions.php | 80 + kirby/src/Cms/PagePicker.php | 265 + kirby/src/Cms/PageRules.php | 428 + kirby/src/Cms/PageSiblings.php | 237 + kirby/src/Cms/Pages.php | 527 + kirby/src/Cms/Pagination.php | 179 + kirby/src/Cms/Panel.php | 139 + kirby/src/Cms/PanelPlugins.php | 108 + kirby/src/Cms/Permissions.php | 230 + kirby/src/Cms/Picker.php | 176 + kirby/src/Cms/Plugin.php | 158 + kirby/src/Cms/PluginAssets.php | 84 + kirby/src/Cms/R.php | 25 + kirby/src/Cms/Responder.php | 222 + kirby/src/Cms/Response.php | 30 + kirby/src/Cms/Role.php | 232 + kirby/src/Cms/Roles.php | 139 + kirby/src/Cms/S.php | 26 + kirby/src/Cms/Search.php | 62 + kirby/src/Cms/Section.php | 94 + kirby/src/Cms/Site.php | 673 + kirby/src/Cms/SiteActions.php | 98 + kirby/src/Cms/SiteBlueprint.php | 60 + kirby/src/Cms/SitePermissions.php | 17 + kirby/src/Cms/SiteRules.php | 58 + kirby/src/Cms/Structure.php | 64 + kirby/src/Cms/StructureObject.php | 210 + kirby/src/Cms/System.php | 495 + kirby/src/Cms/Template.php | 201 + kirby/src/Cms/Translation.php | 193 + kirby/src/Cms/Translations.php | 78 + kirby/src/Cms/Url.php | 69 + kirby/src/Cms/User.php | 926 ++ kirby/src/Cms/UserActions.php | 354 + kirby/src/Cms/UserBlueprint.php | 47 + kirby/src/Cms/UserPermissions.php | 67 + kirby/src/Cms/UserPicker.php | 69 + kirby/src/Cms/UserRules.php | 359 + kirby/src/Cms/Users.php | 140 + kirby/src/Cms/Visitor.php | 25 + kirby/src/Data/Data.php | 127 + kirby/src/Data/Handler.php | 65 + kirby/src/Data/Json.php | 57 + kirby/src/Data/PHP.php | 94 + kirby/src/Data/Txt.php | 133 + kirby/src/Data/Xml.php | 64 + kirby/src/Data/Yaml.php | 74 + kirby/src/Database/Database.php | 665 + kirby/src/Database/Db.php | 283 + kirby/src/Database/Query.php | 1070 ++ kirby/src/Database/Sql.php | 955 ++ kirby/src/Database/Sql/Mysql.php | 59 + kirby/src/Database/Sql/Sqlite.php | 143 + kirby/src/Email/Body.php | 85 + kirby/src/Email/Email.php | 452 + kirby/src/Email/PHPMailer.php | 95 + .../src/Exception/BadMethodCallException.php | 21 + kirby/src/Exception/DuplicateException.php | 21 + kirby/src/Exception/ErrorPageException.php | 21 + kirby/src/Exception/Exception.php | 221 + .../Exception/InvalidArgumentException.php | 21 + kirby/src/Exception/LogicException.php | 20 + kirby/src/Exception/NotFoundException.php | 20 + kirby/src/Exception/PermissionException.php | 21 + kirby/src/Form/Field.php | 477 + kirby/src/Form/Fields.php | 57 + kirby/src/Form/Form.php | 270 + kirby/src/Form/Options.php | 204 + kirby/src/Form/OptionsApi.php | 242 + kirby/src/Form/OptionsQuery.php | 271 + kirby/src/Form/Validations.php | 294 + kirby/src/Http/Cookie.php | 214 + .../Http/Exceptions/NextRouteException.php | 16 + kirby/src/Http/Header.php | 316 + kirby/src/Http/Idn.php | 64 + kirby/src/Http/Params.php | 149 + kirby/src/Http/Path.php | 47 + kirby/src/Http/Query.php | 58 + kirby/src/Http/Remote.php | 404 + kirby/src/Http/Request.php | 413 + kirby/src/Http/Request/Auth/BasicAuth.php | 78 + kirby/src/Http/Request/Auth/BearerAuth.php | 54 + kirby/src/Http/Request/Body.php | 129 + kirby/src/Http/Request/Data.php | 84 + kirby/src/Http/Request/Files.php | 73 + kirby/src/Http/Request/Query.php | 98 + kirby/src/Http/Response.php | 317 + kirby/src/Http/Route.php | 230 + kirby/src/Http/Router.php | 169 + kirby/src/Http/Server.php | 169 + kirby/src/Http/Uri.php | 563 + kirby/src/Http/Url.php | 287 + kirby/src/Http/Visitor.php | 252 + kirby/src/Image/Camera.php | 93 + kirby/src/Image/Darkroom.php | 145 + kirby/src/Image/Darkroom/GdLib.php | 109 + kirby/src/Image/Darkroom/ImageMagick.php | 228 + kirby/src/Image/Dimensions.php | 430 + kirby/src/Image/Exif.php | 296 + kirby/src/Image/Image.php | 310 + kirby/src/Image/Location.php | 136 + kirby/src/Session/AutoSession.php | 171 + kirby/src/Session/FileSessionStore.php | 484 + kirby/src/Session/Session.php | 767 ++ kirby/src/Session/SessionData.php | 255 + kirby/src/Session/SessionStore.php | 110 + kirby/src/Session/Sessions.php | 288 + kirby/src/Text/KirbyTag.php | 149 + kirby/src/Text/KirbyTags.php | 56 + kirby/src/Text/Markdown.php | 79 + kirby/src/Text/SmartyPants.php | 129 + kirby/src/Toolkit/A.php | 697 + kirby/src/Toolkit/Collection.php | 1423 ++ kirby/src/Toolkit/Component.php | 290 + kirby/src/Toolkit/Config.php | 21 + kirby/src/Toolkit/Controller.php | 66 + kirby/src/Toolkit/Dir.php | 439 + kirby/src/Toolkit/Escape.php | 145 + kirby/src/Toolkit/F.php | 835 ++ kirby/src/Toolkit/Facade.php | 36 + kirby/src/Toolkit/File.php | 340 + kirby/src/Toolkit/Html.php | 571 + kirby/src/Toolkit/I18n.php | 231 + kirby/src/Toolkit/Iterator.php | 181 + kirby/src/Toolkit/Mime.php | 343 + kirby/src/Toolkit/Obj.php | 106 + kirby/src/Toolkit/Pagination.php | 506 + kirby/src/Toolkit/Properties.php | 151 + kirby/src/Toolkit/Query.php | 244 + kirby/src/Toolkit/Silo.php | 73 + kirby/src/Toolkit/Str.php | 1083 ++ kirby/src/Toolkit/Tpl.php | 49 + kirby/src/Toolkit/V.php | 521 + kirby/src/Toolkit/View.php | 136 + kirby/src/Toolkit/Xml.php | 433 + kirby/views/fatal.php | 11 + kirby/views/panel.php | 47 + kirby/views/php.php | 11 + kirby/views/snippets/footer.php | 2 + kirby/views/snippets/header.php | 39 + 373 files changed, 95076 insertions(+) create mode 100644 kirby/.editorconfig create mode 100644 kirby/SECURITY.md create mode 100644 kirby/assets/whoops.css create mode 100644 kirby/bootstrap.php create mode 100644 kirby/cacert.pem create mode 100644 kirby/composer.json create mode 100644 kirby/composer.lock create mode 100644 kirby/config/aliases.php create mode 100644 kirby/config/api/authentication.php create mode 100644 kirby/config/api/collections.php create mode 100644 kirby/config/api/models.php create mode 100644 kirby/config/api/models/File.php create mode 100644 kirby/config/api/models/FileBlueprint.php create mode 100644 kirby/config/api/models/FileVersion.php create mode 100644 kirby/config/api/models/Language.php create mode 100644 kirby/config/api/models/Page.php create mode 100644 kirby/config/api/models/PageBlueprint.php create mode 100644 kirby/config/api/models/Role.php create mode 100644 kirby/config/api/models/Site.php create mode 100644 kirby/config/api/models/SiteBlueprint.php create mode 100644 kirby/config/api/models/System.php create mode 100644 kirby/config/api/models/Translation.php create mode 100644 kirby/config/api/models/User.php create mode 100644 kirby/config/api/models/UserBlueprint.php create mode 100644 kirby/config/api/routes.php create mode 100644 kirby/config/api/routes/auth.php create mode 100644 kirby/config/api/routes/files.php create mode 100644 kirby/config/api/routes/languages.php create mode 100644 kirby/config/api/routes/lock.php create mode 100644 kirby/config/api/routes/pages.php create mode 100644 kirby/config/api/routes/roles.php create mode 100644 kirby/config/api/routes/site.php create mode 100644 kirby/config/api/routes/system.php create mode 100644 kirby/config/api/routes/translations.php create mode 100644 kirby/config/api/routes/users.php create mode 100644 kirby/config/blueprints.php create mode 100644 kirby/config/blueprints/file.yml create mode 100644 kirby/config/blueprints/page.yml create mode 100644 kirby/config/blueprints/site.yml create mode 100755 kirby/config/components.php create mode 100644 kirby/config/fields.php create mode 100644 kirby/config/fields/checkboxes.php create mode 100644 kirby/config/fields/date.php create mode 100644 kirby/config/fields/email.php create mode 100644 kirby/config/fields/files.php create mode 100644 kirby/config/fields/gap.php create mode 100644 kirby/config/fields/headline.php create mode 100644 kirby/config/fields/hidden.php create mode 100644 kirby/config/fields/info.php create mode 100644 kirby/config/fields/line.php create mode 100644 kirby/config/fields/mixins/filepicker.php create mode 100644 kirby/config/fields/mixins/min.php create mode 100644 kirby/config/fields/mixins/options.php create mode 100644 kirby/config/fields/mixins/pagepicker.php create mode 100644 kirby/config/fields/mixins/picker.php create mode 100644 kirby/config/fields/mixins/upload.php create mode 100644 kirby/config/fields/mixins/userpicker.php create mode 100644 kirby/config/fields/multiselect.php create mode 100644 kirby/config/fields/number.php create mode 100644 kirby/config/fields/pages.php create mode 100644 kirby/config/fields/radio.php create mode 100644 kirby/config/fields/range.php create mode 100644 kirby/config/fields/select.php create mode 100644 kirby/config/fields/structure.php create mode 100644 kirby/config/fields/tags.php create mode 100644 kirby/config/fields/tel.php create mode 100644 kirby/config/fields/text.php create mode 100644 kirby/config/fields/textarea.php create mode 100644 kirby/config/fields/time.php create mode 100644 kirby/config/fields/toggle.php create mode 100644 kirby/config/fields/url.php create mode 100644 kirby/config/fields/users.php create mode 100755 kirby/config/helpers.php create mode 100644 kirby/config/methods.php create mode 100644 kirby/config/presets/files.php create mode 100644 kirby/config/presets/page.php create mode 100644 kirby/config/presets/pages.php create mode 100644 kirby/config/roots.php create mode 100644 kirby/config/routes.php create mode 100644 kirby/config/sections/fields.php create mode 100644 kirby/config/sections/files.php create mode 100644 kirby/config/sections/info.php create mode 100644 kirby/config/sections/mixins/empty.php create mode 100644 kirby/config/sections/mixins/headline.php create mode 100644 kirby/config/sections/mixins/help.php create mode 100644 kirby/config/sections/mixins/layout.php create mode 100644 kirby/config/sections/mixins/max.php create mode 100644 kirby/config/sections/mixins/min.php create mode 100644 kirby/config/sections/mixins/pagination.php create mode 100644 kirby/config/sections/mixins/parent.php create mode 100644 kirby/config/sections/pages.php create mode 100644 kirby/config/setup.php create mode 100644 kirby/config/tags.php create mode 100644 kirby/config/urls.php create mode 100644 kirby/dependencies/parsedown-extra/ParsedownExtra.php create mode 100755 kirby/dependencies/parsedown/Parsedown.php create mode 100644 kirby/i18n/rules/LICENSE create mode 100755 kirby/i18n/rules/ar.json create mode 100755 kirby/i18n/rules/az.json create mode 100755 kirby/i18n/rules/bg.json create mode 100755 kirby/i18n/rules/cs.json create mode 100755 kirby/i18n/rules/da.json create mode 100755 kirby/i18n/rules/de.json create mode 100755 kirby/i18n/rules/el.json create mode 100755 kirby/i18n/rules/eo.json create mode 100755 kirby/i18n/rules/et.json create mode 100755 kirby/i18n/rules/fa.json create mode 100755 kirby/i18n/rules/fi.json create mode 100755 kirby/i18n/rules/fr.json create mode 100755 kirby/i18n/rules/hi.json create mode 100755 kirby/i18n/rules/hr.json create mode 100755 kirby/i18n/rules/hu.json create mode 100755 kirby/i18n/rules/hy.json create mode 100755 kirby/i18n/rules/it.json create mode 100644 kirby/i18n/rules/ja.json create mode 100755 kirby/i18n/rules/ka.json create mode 100644 kirby/i18n/rules/ko.json create mode 100755 kirby/i18n/rules/lt.json create mode 100755 kirby/i18n/rules/lv.json create mode 100755 kirby/i18n/rules/mk.json create mode 100755 kirby/i18n/rules/my.json create mode 100755 kirby/i18n/rules/nb.json create mode 100755 kirby/i18n/rules/pl.json create mode 100755 kirby/i18n/rules/pt_BR.json create mode 100755 kirby/i18n/rules/rm.json create mode 100755 kirby/i18n/rules/ru.json create mode 100755 kirby/i18n/rules/sr.json create mode 100755 kirby/i18n/rules/sv_SE.json create mode 100755 kirby/i18n/rules/tr.json create mode 100755 kirby/i18n/rules/uk.json create mode 100755 kirby/i18n/rules/vi.json create mode 100755 kirby/i18n/rules/zh.json create mode 100644 kirby/i18n/translations/bg.json create mode 100644 kirby/i18n/translations/ca.json create mode 100644 kirby/i18n/translations/cs.json create mode 100644 kirby/i18n/translations/da.json create mode 100644 kirby/i18n/translations/de.json create mode 100644 kirby/i18n/translations/el.json create mode 100644 kirby/i18n/translations/en.json create mode 100644 kirby/i18n/translations/es_419.json create mode 100644 kirby/i18n/translations/es_ES.json create mode 100644 kirby/i18n/translations/fa.json create mode 100644 kirby/i18n/translations/fi.json create mode 100644 kirby/i18n/translations/fr.json create mode 100644 kirby/i18n/translations/hu.json create mode 100644 kirby/i18n/translations/id.json create mode 100644 kirby/i18n/translations/it.json create mode 100644 kirby/i18n/translations/ko.json create mode 100644 kirby/i18n/translations/lt.json create mode 100644 kirby/i18n/translations/nb.json create mode 100644 kirby/i18n/translations/nl.json create mode 100644 kirby/i18n/translations/pl.json create mode 100644 kirby/i18n/translations/pt_BR.json create mode 100644 kirby/i18n/translations/pt_PT.json create mode 100644 kirby/i18n/translations/ru.json create mode 100644 kirby/i18n/translations/sk.json create mode 100644 kirby/i18n/translations/sv_SE.json create mode 100644 kirby/i18n/translations/tr.json create mode 100644 kirby/kirby.pub create mode 100644 kirby/panel/dist/apple-touch-icon.png create mode 100644 kirby/panel/dist/css/app.css create mode 100644 kirby/panel/dist/favicon.png create mode 100644 kirby/panel/dist/favicon.svg create mode 100644 kirby/panel/dist/img/icons.svg create mode 100644 kirby/panel/dist/js/app.js create mode 100644 kirby/panel/dist/js/plugins.js create mode 100644 kirby/panel/dist/js/vendor.js create mode 100644 kirby/router.php create mode 100644 kirby/src/Api/Api.php create mode 100644 kirby/src/Api/Collection.php create mode 100644 kirby/src/Api/Model.php create mode 100644 kirby/src/Cache/ApcuCache.php create mode 100644 kirby/src/Cache/Cache.php create mode 100644 kirby/src/Cache/FileCache.php create mode 100644 kirby/src/Cache/MemCached.php create mode 100644 kirby/src/Cache/MemoryCache.php create mode 100644 kirby/src/Cache/NullCache.php create mode 100644 kirby/src/Cache/Value.php create mode 100644 kirby/src/Cms/Api.php create mode 100644 kirby/src/Cms/App.php create mode 100644 kirby/src/Cms/AppCaches.php create mode 100644 kirby/src/Cms/AppErrors.php create mode 100644 kirby/src/Cms/AppPlugins.php create mode 100644 kirby/src/Cms/AppTranslations.php create mode 100644 kirby/src/Cms/AppUsers.php create mode 100644 kirby/src/Cms/Asset.php create mode 100644 kirby/src/Cms/Auth.php create mode 100644 kirby/src/Cms/Blueprint.php create mode 100644 kirby/src/Cms/Collection.php create mode 100644 kirby/src/Cms/Collections.php create mode 100644 kirby/src/Cms/Content.php create mode 100644 kirby/src/Cms/ContentLock.php create mode 100644 kirby/src/Cms/ContentLocks.php create mode 100644 kirby/src/Cms/ContentTranslation.php create mode 100644 kirby/src/Cms/Dir.php create mode 100644 kirby/src/Cms/Email.php create mode 100644 kirby/src/Cms/Event.php create mode 100644 kirby/src/Cms/Field.php create mode 100644 kirby/src/Cms/File.php create mode 100644 kirby/src/Cms/FileActions.php create mode 100644 kirby/src/Cms/FileBlueprint.php create mode 100644 kirby/src/Cms/FileFoundation.php create mode 100644 kirby/src/Cms/FileModifications.php create mode 100644 kirby/src/Cms/FilePermissions.php create mode 100644 kirby/src/Cms/FilePicker.php create mode 100644 kirby/src/Cms/FileRules.php create mode 100644 kirby/src/Cms/FileVersion.php create mode 100644 kirby/src/Cms/Filename.php create mode 100644 kirby/src/Cms/Files.php create mode 100644 kirby/src/Cms/Form.php create mode 100644 kirby/src/Cms/HasChildren.php create mode 100644 kirby/src/Cms/HasFiles.php create mode 100644 kirby/src/Cms/HasMethods.php create mode 100644 kirby/src/Cms/HasSiblings.php create mode 100644 kirby/src/Cms/Html.php create mode 100644 kirby/src/Cms/Ingredients.php create mode 100644 kirby/src/Cms/KirbyTag.php create mode 100644 kirby/src/Cms/KirbyTags.php create mode 100644 kirby/src/Cms/Language.php create mode 100644 kirby/src/Cms/LanguageRouter.php create mode 100644 kirby/src/Cms/LanguageRoutes.php create mode 100644 kirby/src/Cms/LanguageRules.php create mode 100644 kirby/src/Cms/Languages.php create mode 100644 kirby/src/Cms/Media.php create mode 100644 kirby/src/Cms/Model.php create mode 100644 kirby/src/Cms/ModelPermissions.php create mode 100644 kirby/src/Cms/ModelWithContent.php create mode 100644 kirby/src/Cms/Nest.php create mode 100644 kirby/src/Cms/NestCollection.php create mode 100644 kirby/src/Cms/NestObject.php create mode 100644 kirby/src/Cms/Page.php create mode 100644 kirby/src/Cms/PageActions.php create mode 100644 kirby/src/Cms/PageBlueprint.php create mode 100644 kirby/src/Cms/PagePermissions.php create mode 100644 kirby/src/Cms/PagePicker.php create mode 100644 kirby/src/Cms/PageRules.php create mode 100644 kirby/src/Cms/PageSiblings.php create mode 100644 kirby/src/Cms/Pages.php create mode 100644 kirby/src/Cms/Pagination.php create mode 100644 kirby/src/Cms/Panel.php create mode 100644 kirby/src/Cms/PanelPlugins.php create mode 100644 kirby/src/Cms/Permissions.php create mode 100644 kirby/src/Cms/Picker.php create mode 100644 kirby/src/Cms/Plugin.php create mode 100644 kirby/src/Cms/PluginAssets.php create mode 100644 kirby/src/Cms/R.php create mode 100644 kirby/src/Cms/Responder.php create mode 100644 kirby/src/Cms/Response.php create mode 100644 kirby/src/Cms/Role.php create mode 100644 kirby/src/Cms/Roles.php create mode 100644 kirby/src/Cms/S.php create mode 100644 kirby/src/Cms/Search.php create mode 100644 kirby/src/Cms/Section.php create mode 100644 kirby/src/Cms/Site.php create mode 100644 kirby/src/Cms/SiteActions.php create mode 100644 kirby/src/Cms/SiteBlueprint.php create mode 100644 kirby/src/Cms/SitePermissions.php create mode 100644 kirby/src/Cms/SiteRules.php create mode 100644 kirby/src/Cms/Structure.php create mode 100644 kirby/src/Cms/StructureObject.php create mode 100644 kirby/src/Cms/System.php create mode 100644 kirby/src/Cms/Template.php create mode 100644 kirby/src/Cms/Translation.php create mode 100644 kirby/src/Cms/Translations.php create mode 100755 kirby/src/Cms/Url.php create mode 100644 kirby/src/Cms/User.php create mode 100644 kirby/src/Cms/UserActions.php create mode 100644 kirby/src/Cms/UserBlueprint.php create mode 100644 kirby/src/Cms/UserPermissions.php create mode 100644 kirby/src/Cms/UserPicker.php create mode 100644 kirby/src/Cms/UserRules.php create mode 100644 kirby/src/Cms/Users.php create mode 100644 kirby/src/Cms/Visitor.php create mode 100644 kirby/src/Data/Data.php create mode 100644 kirby/src/Data/Handler.php create mode 100644 kirby/src/Data/Json.php create mode 100644 kirby/src/Data/PHP.php create mode 100644 kirby/src/Data/Txt.php create mode 100644 kirby/src/Data/Xml.php create mode 100644 kirby/src/Data/Yaml.php create mode 100644 kirby/src/Database/Database.php create mode 100644 kirby/src/Database/Db.php create mode 100644 kirby/src/Database/Query.php create mode 100644 kirby/src/Database/Sql.php create mode 100644 kirby/src/Database/Sql/Mysql.php create mode 100644 kirby/src/Database/Sql/Sqlite.php create mode 100644 kirby/src/Email/Body.php create mode 100644 kirby/src/Email/Email.php create mode 100644 kirby/src/Email/PHPMailer.php create mode 100644 kirby/src/Exception/BadMethodCallException.php create mode 100644 kirby/src/Exception/DuplicateException.php create mode 100644 kirby/src/Exception/ErrorPageException.php create mode 100644 kirby/src/Exception/Exception.php create mode 100644 kirby/src/Exception/InvalidArgumentException.php create mode 100644 kirby/src/Exception/LogicException.php create mode 100644 kirby/src/Exception/NotFoundException.php create mode 100644 kirby/src/Exception/PermissionException.php create mode 100644 kirby/src/Form/Field.php create mode 100644 kirby/src/Form/Fields.php create mode 100644 kirby/src/Form/Form.php create mode 100644 kirby/src/Form/Options.php create mode 100644 kirby/src/Form/OptionsApi.php create mode 100644 kirby/src/Form/OptionsQuery.php create mode 100644 kirby/src/Form/Validations.php create mode 100644 kirby/src/Http/Cookie.php create mode 100644 kirby/src/Http/Exceptions/NextRouteException.php create mode 100644 kirby/src/Http/Header.php create mode 100644 kirby/src/Http/Idn.php create mode 100644 kirby/src/Http/Params.php create mode 100644 kirby/src/Http/Path.php create mode 100644 kirby/src/Http/Query.php create mode 100644 kirby/src/Http/Remote.php create mode 100644 kirby/src/Http/Request.php create mode 100644 kirby/src/Http/Request/Auth/BasicAuth.php create mode 100644 kirby/src/Http/Request/Auth/BearerAuth.php create mode 100644 kirby/src/Http/Request/Body.php create mode 100644 kirby/src/Http/Request/Data.php create mode 100644 kirby/src/Http/Request/Files.php create mode 100644 kirby/src/Http/Request/Query.php create mode 100644 kirby/src/Http/Response.php create mode 100644 kirby/src/Http/Route.php create mode 100644 kirby/src/Http/Router.php create mode 100644 kirby/src/Http/Server.php create mode 100644 kirby/src/Http/Uri.php create mode 100644 kirby/src/Http/Url.php create mode 100644 kirby/src/Http/Visitor.php create mode 100644 kirby/src/Image/Camera.php create mode 100644 kirby/src/Image/Darkroom.php create mode 100644 kirby/src/Image/Darkroom/GdLib.php create mode 100644 kirby/src/Image/Darkroom/ImageMagick.php create mode 100644 kirby/src/Image/Dimensions.php create mode 100644 kirby/src/Image/Exif.php create mode 100644 kirby/src/Image/Image.php create mode 100644 kirby/src/Image/Location.php create mode 100644 kirby/src/Session/AutoSession.php create mode 100644 kirby/src/Session/FileSessionStore.php create mode 100644 kirby/src/Session/Session.php create mode 100644 kirby/src/Session/SessionData.php create mode 100644 kirby/src/Session/SessionStore.php create mode 100644 kirby/src/Session/Sessions.php create mode 100644 kirby/src/Text/KirbyTag.php create mode 100644 kirby/src/Text/KirbyTags.php create mode 100644 kirby/src/Text/Markdown.php create mode 100644 kirby/src/Text/SmartyPants.php create mode 100644 kirby/src/Toolkit/A.php create mode 100644 kirby/src/Toolkit/Collection.php create mode 100644 kirby/src/Toolkit/Component.php create mode 100644 kirby/src/Toolkit/Config.php create mode 100644 kirby/src/Toolkit/Controller.php create mode 100644 kirby/src/Toolkit/Dir.php create mode 100644 kirby/src/Toolkit/Escape.php create mode 100644 kirby/src/Toolkit/F.php create mode 100644 kirby/src/Toolkit/Facade.php create mode 100644 kirby/src/Toolkit/File.php create mode 100644 kirby/src/Toolkit/Html.php create mode 100644 kirby/src/Toolkit/I18n.php create mode 100644 kirby/src/Toolkit/Iterator.php create mode 100644 kirby/src/Toolkit/Mime.php create mode 100644 kirby/src/Toolkit/Obj.php create mode 100644 kirby/src/Toolkit/Pagination.php create mode 100644 kirby/src/Toolkit/Properties.php create mode 100644 kirby/src/Toolkit/Query.php create mode 100644 kirby/src/Toolkit/Silo.php create mode 100644 kirby/src/Toolkit/Str.php create mode 100644 kirby/src/Toolkit/Tpl.php create mode 100644 kirby/src/Toolkit/V.php create mode 100644 kirby/src/Toolkit/View.php create mode 100644 kirby/src/Toolkit/Xml.php create mode 100644 kirby/views/fatal.php create mode 100644 kirby/views/panel.php create mode 100644 kirby/views/php.php create mode 100644 kirby/views/snippets/footer.php create mode 100644 kirby/views/snippets/header.php diff --git a/kirby/.editorconfig b/kirby/.editorconfig new file mode 100644 index 0000000..adbc151 --- /dev/null +++ b/kirby/.editorconfig @@ -0,0 +1,15 @@ +# This file is for unifying the coding style for different editors and IDEs +# editorconfig.org + +# PHP PSR-2 Coding Standards +# http://www.php-fig.org/psr/psr-2/ + +root = true + +[*.php] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 \ No newline at end of file diff --git a/kirby/SECURITY.md b/kirby/SECURITY.md new file mode 100644 index 0000000..ae42a38 --- /dev/null +++ b/kirby/SECURITY.md @@ -0,0 +1,3 @@ +# Security Policy + +Please see the [Security Policy on the Kirby website](https://getkirby.com/security) for a list of the currently supported Kirby versions and of past security incidents as well as for information on how to report security vulnerabilities in the Kirby core or in the Panel. diff --git a/kirby/assets/whoops.css b/kirby/assets/whoops.css new file mode 100644 index 0000000..bd35464 --- /dev/null +++ b/kirby/assets/whoops.css @@ -0,0 +1,83 @@ +body { + background: #efefef; + font: normal normal 400 12px/1.5 -apple-system, BlinkMacSystemFont, Segoe UI, + Roboto, Helvetica, Arial, sans-serif; +} + +.left-panel { + background: transparent; +} + +header { + background-color: #313740; +} + +.exc-title-primary { + color: hsl(0, 71%, 55%); +} + +.frame.active { + color: hsl(0, 71%, 55%); + box-shadow: inset -5px 0 0 0 #d16464; +} + +.frame:not(.active):hover { + background: rgba(203, 215, 229, 0.5); +} + +.rightButton { + color: #999; + box-shadow: inset 0 0 0 1px #777; + border-radius: 0; +} + +.rightButton:hover { + box-shadow: inset 0 0 0 1px #555; + color: #777; +} + +.details-heading { + color: #7e9abf; + font-weight: 500; +} + +.frame-code { + background: #000; +} + +pre.code-block, +code.code-block, +.frame-args.code-block, +.frame-args.code-block samp { + background: #16171a; +} + +.linenums li.current { + background: transparent; +} + +.linenums li.current.active { + background: rgba(209, 100, 100, 0.3); +} + +pre .atv, +code .atv, +pre .str, +code .str { + color: #a7bd68; +} + +pre .tag, +code .tag { + color: #d16464; +} + +pre .kwd, +code .kwd { + color: #8abeb7; +} + +pre .atn, +code .atn { + color: #de935f; +} diff --git a/kirby/bootstrap.php b/kirby/bootstrap.php new file mode 100644 index 0000000..83d7f42 --- /dev/null +++ b/kirby/bootstrap.php @@ -0,0 +1,35 @@ +=') === false || + version_compare(PHP_VERSION, '7.5.0', '<') === false +) { + die(include __DIR__ . '/views/php.php'); +} + +if (is_file($autoloader = dirname(__DIR__) . '/vendor/autoload.php')) { + + /** + * Always prefer a site-wide Composer autoloader + * if it exists, it means that the user has probably + * installed additional packages + */ + include $autoloader; +} elseif (is_file($autoloader = __DIR__ . '/vendor/autoload.php')) { + + /** + * Fall back to the local autoloader if that exists + */ + include $autoloader; +} else { + + /** + * If neither one exists, don't bother searching; + * it's a custom directory setup and the users need to + * load the autoloader themselves + */ +} diff --git a/kirby/cacert.pem b/kirby/cacert.pem new file mode 100644 index 0000000..f9bd706 --- /dev/null +++ b/kirby/cacert.pem @@ -0,0 +1,3447 @@ +## +## Bundle of CA Root Certificates +## +## Certificate data from Mozilla as of: Wed Jul 22 03:12:14 2020 GMT +## +## This is a bundle of X.509 certificates of public Certificate Authorities +## (CA). These were automatically extracted from Mozilla's root certificates +## file (certdata.txt). This file can be found in the mozilla source tree: +## https://hg.mozilla.org/releases/mozilla-release/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt +## +## It contains the certificates in PEM format and therefore +## can be directly used with curl / libcurl / php_curl, or with +## an Apache+mod_ssl webserver for SSL client authentication. +## Just configure this file as the SSLCACertificateFile. +## +## Conversion done with mk-ca-bundle.pl version 1.28. +## SHA256: cc6408bd4be7fbfb8699bdb40ccb7f6de5780d681d87785ea362646e4dad5e8e +## + + +GlobalSign Root CA +================== +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQkUx +GTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jvb3QgQ0ExGzAZBgNVBAMTEkds +b2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAwMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNV +BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYD +VQQDExJHbG9iYWxTaWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa +DuaZjc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavpxy0Sy6sc +THAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp1Wrjsok6Vjk4bwY8iGlb +Kk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdGsnUOhugZitVtbNV4FpWi6cgKOOvyJBNP +c1STE4U6G7weNLWLBYy5d4ux2x8gkasJU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrX +gzT/LCrBbBlDSgeF59N89iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0BAQUF +AAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOzyj1hTdNGCbM+w6Dj +Y1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE38NflNUVyRRBnMRddWQVDf9VMOyG +j/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymPAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhH +hm4qxFYxldBniYUr+WymXUadDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveC +X4XSQRjbgbMEHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- + +GlobalSign Root CA - R2 +======================= +-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4GA1UECxMXR2xv +YmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh +bFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT +aWduIFJvb3QgQ0EgLSBSMjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln +bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6 +ErPLv4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8eoLrvozp +s6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklqtTleiDTsvHgMCJiEbKjN +S7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzdC9XZzPnqJworc5HGnRusyMvo4KD0L5CL +TfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pazq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6C +ygPCm48CAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUm+IHV2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5nbG9i +YWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG3lm0mi3f3BmGLjAN +BgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4GsJ0/WwbgcQ3izDJr86iw8bmEbTUsp +9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu +01yiPqFbQfXf5WRDLenVOavSot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG7 +9G+dwfCMNYxdAfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 +TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== +-----END CERTIFICATE----- + +Entrust.net Premium 2048 Secure Server CA +========================================= +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChMLRW50cnVzdC5u +ZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBpbmNvcnAuIGJ5IHJlZi4gKGxp +bWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNV +BAMTKkVudHJ1c3QubmV0IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQx +NzUwNTFaFw0yOTA3MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3 +d3d3LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTEl +MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMqRW50cnVzdC5u +ZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEArU1LqRKGsuqjIAcVFmQqK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOL +Gp18EzoOH1u3Hs/lJBQesYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSr +hRSGlVuXMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVTXTzW +nLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/HoZdenoVve8AjhUi +VBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH4QIDAQABo0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJ +KoZIhvcNAQEFBQADggEBADubj1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPy +T/4xmf3IDExoU8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf +zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5bu/8j72gZyxKT +J1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+bYQLCIt+jerXmCHG8+c8eS9e +nNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/ErfF6adulZkMV8gzURZVE= +-----END CERTIFICATE----- + +Baltimore CyberTrust Root +========================= +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJRTESMBAGA1UE +ChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3li +ZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoXDTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMC +SUUxEjAQBgNVBAoTCUJhbHRpbW9yZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFs +dGltb3JlIEN5YmVyVHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKME +uyKrmD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjrIZ3AQSsB +UnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeKmpYcqWe4PwzV9/lSEy/C +G9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSuXmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9 +XbIGevOF6uvUA65ehD5f/xXtabz5OTZydc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjpr +l3RjM71oGDHweI12v/yejl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoI +VDaGezq1BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEB +BQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT929hkTI7gQCvlYpNRh +cL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3WgxjkzSswF07r51XgdIGn9w/xZchMB5 +hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsa +Y71k5h+3zvDyny67G7fyUIhzksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9H +RCwBXbsdtTLSR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE----- + +Entrust Root Certification Authority +==================================== +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMCVVMxFjAUBgNV +BAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0Lm5ldC9DUFMgaXMgaW5jb3Jw +b3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMWKGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsG +A1UEAxMkRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0 +MloXDTI2MTEyNzIwNTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMu +MTkwNwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSByZWZlcmVu +Y2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNVBAMTJEVudHJ1c3QgUm9v +dCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ALaVtkNC+sZtKm9I35RMOVcF7sN5EUFoNu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYsz +A9u3g3s+IIRe7bJWKKf44LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOww +Cj0Yzfv9KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGIrb68 +j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi94DkZfs0Nw4pgHBN +rziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOBsDCBrTAOBgNVHQ8BAf8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAigA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1 +MzQyWjAfBgNVHSMEGDAWgBRokORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DH +hmak8fdLQ/uEvW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9tO1KzKtvn1ISM +Y/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6ZuaAGAT/3B+XxFNSRuzFVJ7yVTa +v52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTS +W3iDVuycNsMm4hH2Z0kdkquM++v/eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0 +tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +GeoTrust Global CA +================== +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVTMRYwFAYDVQQK +Ew1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9iYWwgQ0EwHhcNMDIwNTIxMDQw +MDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5j +LjEbMBkGA1UEAxMSR2VvVHJ1c3QgR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA2swYYzD99BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjo +BbdqfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDviS2Aelet +8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU1XupGc1V3sjs0l44U+Vc +T4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+bw8HHa8sHo9gOeL6NlMTOdReJivbPagU +vTLrGAMoUgRx5aszPeE4uwc2hGKceeoWMPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBTAephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVk +DBF9qn1luMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKInZ57Q +zxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfStQWVYrmm3ok9Nns4 +d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcFPseKUgzbFbS9bZvlxrFUaKnjaZC2 +mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Unhw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6p +XE0zX5IJL4hmXXeXxx12E6nV5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvm +Mw== +-----END CERTIFICATE----- + +GeoTrust Universal CA +===================== +-----BEGIN CERTIFICATE----- +MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN +R2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVyc2FsIENBMB4XDTA0MDMwNDA1 +MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IElu +Yy4xHjAcBgNVBAMTFUdlb1RydXN0IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAKYVVaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9t +JPi8cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTTQjOgNB0e +RXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFhF7em6fgemdtzbvQKoiFs +7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2vc7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d +8Lsrlh/eezJS/R27tQahsiFepdaVaH/wmZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7V +qnJNk22CDtucvc+081xdVHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3Cga +Rr0BHdCXteGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZf9hB +Z3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfReBi9Fi1jUIxaS5BZu +KGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+nhutxx9z3SxPGWX9f5NAEC7S8O08 +ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0 +XG0D08DYj3rWMB8GA1UdIwQYMBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIB +hjANBgkqhkiG9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc +aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fXIwjhmF7DWgh2 +qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzynANXH/KttgCJwpQzgXQQpAvvL +oJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0zuzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsK +xr2EoyNB3tZ3b4XUhRxQ4K5RirqNPnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxF +KyDuSN/n3QmOGKjaQI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2 +DFKWkoRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9ER/frslK +xfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQtDF4JbAiXfKM9fJP/P6EU +p8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/SfuvmbJxPgWp6ZKy7PtXny3YuxadIwVyQD8vI +P/rmMuGNG2+k5o7Y+SlIis5z/iw= +-----END CERTIFICATE----- + +GeoTrust Universal CA 2 +======================= +-----BEGIN CERTIFICATE----- +MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN +R2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVyc2FsIENBIDIwHhcNMDQwMzA0 +MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3Qg +SW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0 +DE81WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUGFF+3Qs17 +j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdqXbboW0W63MOhBW9Wjo8Q +JqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxLse4YuU6W3Nx2/zu+z18DwPw76L5GG//a +QMJS9/7jOvdqdzXQ2o3rXhhqMcceujwbKNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2 +WP0+GfPtDCapkzj4T8FdIgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP +20gaXT73y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRthAAn +ZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgocQIgfksILAAX/8sgC +SqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4Lt1ZrtmhN79UNdxzMk+MBB4zsslG +8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2 ++/CfXGJx7Tz0RzgQKzAfBgNVHSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8E +BAMCAYYwDQYJKoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z +dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQL1EuxBRa3ugZ +4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgrFg5fNuH8KrUwJM/gYwx7WBr+ +mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSoag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpq +A1Ihn0CoZ1Dy81of398j9tx4TuaYT1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpg +Y+RdM4kX2TGq2tbzGDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiP +pm8m1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJVOCiNUW7d +FGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH6aLcr34YEoP9VhdBLtUp +gn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwXQMAJKOSLakhT2+zNVVXxxvjpoixMptEm +X36vWkzaH6byHCx+rgIW0lbQL1dTR+iS +-----END CERTIFICATE----- + +Comodo AAA Services root +======================== +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS +R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg +TGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAw +MFoXDTI4MTIzMTIzNTk1OVowezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hl +c3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV +BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQuaBtDFcCLNSS1UY8y2bmhG +C1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe3M/vg4aijJRPn2jymJBGhCfHdr/jzDUs +i14HZGWCwEiwqJH5YZ92IFCokcdmtet4YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszW +Y19zjNoFmag4qMsXeDZRrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjH +Ypy+g8cmez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQUoBEK +Iz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wewYDVR0f +BHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNl +cy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2Vz +LmNybDANBgkqhkiG9w0BAQUFAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm +7l3sAg9g1o1QGE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2G9w84FoVxp7Z +8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsil2D4kF501KKaU73yqWjgom7C +12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- + +QuoVadis Root CA +================ +-----BEGIN CERTIFICATE----- +MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJCTTEZMBcGA1UE +ChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 +eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAz +MTkxODMzMzNaFw0yMTAzMTcxODMzMzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRp +cyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQD +EyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Ypli4kVEAkOPcahdxYTMuk +J0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2DrOpm2RgbaIr1VxqYuvXtdj182d6UajtL +F8HVj71lODqV0D1VNk7feVcxKh7YWWVJWCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeL +YzcS19Dsw3sgQUSj7cugF+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWen +AScOospUxbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCCAk4w +PQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVvdmFkaXNvZmZzaG9y +ZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREwggENMIIBCQYJKwYBBAG+WAABMIH7 +MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNlIG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmlj +YXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJs +ZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh +Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYIKwYBBQUHAgEW +Fmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3TKbkGGew5Oanwl4Rqy+/fMIGu +BgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rqy+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkw +FwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MS4wLAYDVQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6 +tlCLMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSkfnIYj9lo +fFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf87C9TqnN7Az10buYWnuul +LsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1RcHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2x +gI4JVrmcGmD+XcHXetwReNDWXcG31a0ymQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi +5upZIof4l/UO/erMkqQWxFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi +5nrQNiOKSnQ2+Q== +-----END CERTIFICATE----- + +QuoVadis Root CA 2 +================== +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT +EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMjAeFw0wNjExMjQx +ODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCaGMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6 +XJxgFyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55JWpzmM+Yk +lvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bBrrcCaoF6qUWD4gXmuVbB +lDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp+ARz8un+XJiM9XOva7R+zdRcAitMOeGy +lZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt +66/3FsvbzSUr5R/7mp/iUcw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1Jdxn +wQ5hYIizPtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og/zOh +D7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UHoycR7hYQe7xFSkyy +BNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuIyV77zGHcizN300QyNQliBJIWENie +J0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1Ud +DgQWBBQahGK8SEwzJQTU7tD2A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGU +a6FJpEcwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2fBluornFdLwUv +Z+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzng/iN/Ae42l9NLmeyhP3ZRPx3 +UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2BlfF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodm +VjB3pjd4M1IQWK4/YY7yarHvGH5KWWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK ++JDSV6IZUaUtl0HaB0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrW +IozchLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPRTUIZ3Ph1 +WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWDmbA4CD/pXvk1B+TJYm5X +f6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0ZohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II +4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8 +VCLAAVBpQ570su9t+Oza8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- + +QuoVadis Root CA 3 +================== +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT +EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMzAeFw0wNjExMjQx +OTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDMV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNgg +DhoB4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUrH556VOij +KTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd8lyyBTNvijbO0BNO/79K +DDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9CabwvvWhDFlaJKjdhkf2mrk7AyxRllDdLkgbv +BNDInIjbC3uBr7E9KsRlOni27tyAsdLTmZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwp +p5ijJUMv7/FfJuGITfhebtfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8 +nT8KKdjcT5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDtWAEX +MJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZc6tsgLjoC2SToJyM +Gf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A4iLItLRkT9a6fUg+qGkM17uGcclz +uD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYDVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHT +BgkrBgEEAb5YAAMwgcUwgZMGCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmlj +YXRlIGNvbnN0aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVudC4wLQYIKwYB +BQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2NwczALBgNVHQ8EBAMCAQYwHQYD +VR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4GA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4 +ywLQoUmkRzBFMQswCQYDVQQGEwJCTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UE +AxMSUXVvVmFkaXMgUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZV +qyM07ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSemd1o417+s +hvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd+LJ2w/w4E6oM3kJpK27z +POuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2 +Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadNt54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp +8kokUvd0/bpO5qgdAm6xDYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBC +bjPsMZ57k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6szHXu +g/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0jWy10QJLZYxkNc91p +vGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeTmJlglFwjz1onl14LBQaTNx47aTbr +qZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- + +Security Communication Root CA +============================== +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP +U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw +HhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP +U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw +8yl89f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJDKaVv0uM +DPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9Ms+k2Y7CI9eNqPPYJayX +5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/NQV3Is00qVUarH9oe4kA92819uZKAnDfd +DJZkndwi92SL32HeFZRSFaB9UslLqCHJxrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2 +JChzAgMBAAGjPzA9MB0GA1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vGkl3g +0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfrUj94nK9NrvjVT8+a +mCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5Bw+SUEmK3TGXX8npN6o7WWWXlDLJ +s58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJUJRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ +6rBK+1YWc26sTfcioU+tHXotRSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAi +FL39vmwLAw== +-----END CERTIFICATE----- + +Sonera Class 2 Root CA +====================== +-----BEGIN CERTIFICATE----- +MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEPMA0GA1UEChMG +U29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAxMDQwNjA3Mjk0MFoXDTIxMDQw +NjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNVBAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJh +IENsYXNzMiBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3 +/Ei9vX+ALTU74W+oZ6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybT +dXnt5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s3TmVToMG +f+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2EjvOr7nQKV0ba5cTppCD8P +tOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu8nYybieDwnPz3BjotJPqdURrBGAgcVeH +nfO+oJAjPYok4doh28MCAwEAAaMzMDEwDwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITT +XjwwCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt +0jSv9zilzqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/3DEI +cbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvDFNr450kkkdAdavph +Oe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6Tk6ezAyNlNzZRZxe7EJQY670XcSx +EtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLH +llpwrN9M +-----END CERTIFICATE----- + +XRamp Global CA Root +==================== +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UE +BhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2Vj +dXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwHhcNMDQxMTAxMTcxNDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMx +HjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkg +U2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS638eMpSe2OAtp87ZOqCwu +IR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCPKZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMx +foArtYzAQDsRhtDLooY2YKTVMIJt2W7QDxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FE +zG+gSqmUsE3a56k0enI4qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqs +AxcZZPRaJSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNViPvry +xS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASsjVy16bYbMDYGA1UdHwQvMC0wK6Ap +oCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMC +AQEwDQYJKoZIhvcNAQEFBQADggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc +/Kh4ZzXxHfARvbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLaIR9NmXmd4c8n +nxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSyi6mx5O+aGtA9aZnuqCij4Tyz +8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQO+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- + +Go Daddy Class 2 CA +=================== +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMY +VGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkG +A1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g +RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQAD +ggENADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv +2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+qN1j3hybX2C32 +qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiOr18SPaAIBQi2XKVlOARFmR6j +YGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmY +vLEHZ6IVDd2gWMZEewo+YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0O +BBYEFNLEsNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h/t2o +atTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMu +MTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wim +PQoZ+YeAEW5p5JYXMP80kWNyOO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKt +I3lpjbi2Tc7PTMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mERdEr/VxqHD3VI +Ls9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5CufReYNnyicsbkqWletNw+vHX/b +vZ8= +-----END CERTIFICATE----- + +Starfield Class 2 CA +==================== +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzElMCMGA1UEChMc +U3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZpZWxkIENsYXNzIDIg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQwNjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBo +MQswCQYDVQQGEwJVUzElMCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAG +A1UECxMpU3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqG +SIb3DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf8MOh2tTY +bitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN+lq2cwQlZut3f+dZxkqZ +JRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVm +epsZGD3/cVE8MC5fvj13c7JdBmzDI1aaK4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSN +F4Azbl5KXZnJHoe0nRrA1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HF +MIHCMB0GA1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fRzt0f +hvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNo +bm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBDbGFzcyAyIENlcnRpZmljYXRpb24g +QXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGs +afPzWdqbAYcaT1epoXkJKtv3L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLM +PUxA2IGvd56Deruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynpVSJYACPq4xJD +KVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEYWQPJIrSPnNVeKtelttQKbfi3 +QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE----- + +Taiwan GRCA +=========== +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/MQswCQYDVQQG +EwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4X +DTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1owPzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dv +dmVybm1lbnQgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qN +w8XRIePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1qgQdW8or5 +BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKyyhwOeYHWtXBiCAEuTk8O +1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAtsF/tnyMKtsc2AtJfcdgEWFelq16TheEfO +htX7MfP6Mb40qij7cEwdScevLJ1tZqa2jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wov +J5pGfaENda1UhhXcSTvxls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7 +Q3hub/FCVGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHKYS1t +B6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoHEgKXTiCQ8P8NHuJB +O9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThNXo+EHWbNxWCWtFJaBYmOlXqYwZE8 +lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1UdDgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNV +HRMEBTADAQH/MDkGBGcqBwAEMTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg2 +09yewDL7MTqKUWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ +TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyfqzvS/3WXy6Tj +Zwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaKZEk9GhiHkASfQlK3T8v+R0F2 +Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFEJPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlU +D7gsL0u8qV1bYH+Mh6XgUmMqvtg7hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6Qz +DxARvBMB1uUO07+1EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+Hbk +Z6MmnD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WXudpVBrkk +7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44VbnzssQwmSNOXfJIoRIM3BKQ +CZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDeLMDDav7v3Aun+kbfYNucpllQdSNpc5Oy ++fwC00fmcc4QAu4njIT/rEUNE1yDMuAlpYYsfPQS +-----END CERTIFICATE----- + +DigiCert Assured ID Root CA +=========================== +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw +IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzEx +MTEwMDAwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL +ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0Ew +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7cJpSIqvTO +9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYPmDI2dsze3Tyoou9q+yHy +UmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW +/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpy +oeb6pNnVFzF1roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf +GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRF +66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkq +hkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2Bc +EkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38Fn +SbNd67IJKusm7Xi+fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i +8b5QZ7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +DigiCert Global Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw +HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAw +MDAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 +dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsBCSDMAZOn +TjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97nh6Vfe63SKMI2tavegw5 +BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt43C/dxC//AH2hdmoRBBYMql1GNXRor5H +4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7PT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y +7vrTC0LUq7dBMtoM1O/4gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQAB +o2MwYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbRTLtm +8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEF +BQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/EsrhMAtudXH/vTBH1jLuG2cenTnmCmr +EbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIt +tep3Sp+dWOIrWcBAI+0tKIJFPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886 +UAb3LujEV0lsYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +DigiCert High Assurance EV Root CA +================================== +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSsw +KQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAw +MFoXDTMxMTExMDAwMDAwMFowbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ +MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFu +Y2UgRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm+9S75S0t +Mqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTWPNt0OKRKzE0lgvdKpVMS +OO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEMxChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3 +MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFBIk5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQ +NAQTXKFx01p8VdteZOE3hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUe +h10aUAsgEsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSY +JhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3NecnzyIZgYIVyHbIUf4KmeqvxgydkAQ +V8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6zeM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFp +myPInngiK3BD41VHMWEZ71jFhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkK +mNEVX58Svnw2Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep+OkuE6N36B9K +-----END CERTIFICATE----- + +DST Root CA X3 +============== +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQK +ExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4X +DTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1 +cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmT +rE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9 +UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRy +xXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40d +utolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQ +MA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikug +dB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjE +GB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bw +RLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubS +fZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- + +SwissSign Gold CA - G2 +====================== +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkNIMRUw +EwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2lnbiBHb2xkIENBIC0gRzIwHhcN +MDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBFMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dp +c3NTaWduIEFHMR8wHQYDVQQDExZTd2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUq +t2/876LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+bbqBHH5C +jCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c6bM8K8vzARO/Ws/BtQpg +vd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqEemA8atufK+ze3gE/bk3lUIbLtK/tREDF +ylqM2tIrfKjuvqblCqoOpd8FUrdVxyJdMmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvR +AiTysybUa9oEVeXBCsdtMDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuend +jIj3o02yMszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69yFGkO +peUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPiaG59je883WX0XaxR +7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxMgI93e2CaHt+28kgeDrpOVG2Y4OGi +GqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUWyV7lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64 +OfPAeGZe6Drn8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe645R88a7A3hfm +5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczOUYrHUDFu4Up+GC9pWbY9ZIEr +44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOf +Mke6UiI0HTJ6CVanfCU2qT1L2sCCbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6m +Gu6uLftIdxf+u+yvGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxp +mo/a77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCChdiDyyJk +vC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid392qgQmwLOM7XdVAyksLf +KzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEppLd6leNcG2mqeSz53OiATIgHQv2ieY2Br +NU0LbbqhPcCT4H8js1WtciVORvnSFu+wZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6Lqj +viOvrv1vA+ACOzB2+httQc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- + +SwissSign Silver CA - G2 +======================== +-----BEGIN CERTIFICATE----- +MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ0gxFTAT +BgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMB4X +DTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0NlowRzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3 +aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644 +N0MvFz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7brYT7QbNHm ++/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieFnbAVlDLaYQ1HTWBCrpJH +6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH6ATK72oxh9TAtvmUcXtnZLi2kUpCe2Uu +MGoM9ZDulebyzYLs2aFK7PayS+VFheZteJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5h +qAaEuSh6XzjZG6k4sIN/c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5 +FZGkECwJMoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRHHTBs +ROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTfjNFusB3hB48IHpmc +celM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb65i/4z3GcRm25xBWNOHkDRUjvxF3X +CO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUF6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRB +tjpbO8tFnb0cwpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 +cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBAHPGgeAn0i0P +4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShpWJHckRE1qTodvBqlYJ7YH39F +kWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L +3XWgwF15kIwb4FDm3jH+mHtwX6WQ2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx +/uNncqCxv1yL5PqZIseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFa +DGi8aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2Xem1ZqSqP +e97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQRdAtq/gsD/KNVV4n+Ssuu +WxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJ +DIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ub +DgEj8Z+7fNzcbBGXJbLytGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u +-----END CERTIFICATE----- + +GeoTrust Primary Certification Authority +======================================== +-----BEGIN CERTIFICATE----- +MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMoR2VvVHJ1c3QgUHJpbWFyeSBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjExMjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgx +CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQ +cmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9AWbK7hWN +b6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjAZIVcFU2Ix7e64HXprQU9 +nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE07e9GceBrAqg1cmuXm2bgyxx5X9gaBGge +RwLmnWDiNpcB3841kt++Z8dtd1k7j53WkBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGt +tm/81w7a4DSwDRp35+MImO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJKoZI +hvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ16CePbJC/kRYkRj5K +Ts4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl4b7UVXGYNTq+k+qurUKykG/g/CFN +NWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6KoKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHa +Floxt/m0cYASSJlyc1pZU8FjUjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG +1riR/aYNKxoUAT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= +-----END CERTIFICATE----- + +thawte Primary Root CA +====================== +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCBqTELMAkGA1UE +BhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2 +aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhv +cml6ZWQgdXNlIG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3 +MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwg +SW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMv +KGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNVBAMT +FnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCs +oPD7gFnUnMekz52hWXMJEEUMDSxuaPFsW0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ +1CRfBsDMRJSUjQJib+ta3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGc +q/gcfomk6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6Sk/K +aAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94JNqR32HuHUETVPm4p +afs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XPr87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUF +AAOCAQEAeRHAS7ORtvzw6WfUDW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeE +uzLlQRHAd9mzYJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX +xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2/qxAeeWsEG89 +jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/LHbTY5xZ3Y+m4Q6gLkH3LpVH +z7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7jVaMaA== +-----END CERTIFICATE----- + +VeriSign Class 3 Public Primary Certification Authority - G5 +============================================================ +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCByjELMAkGA1UE +BhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBO +ZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVk +IHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCB +yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2ln +biBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2lnbiwgSW5jLiAtIEZvciBh +dXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmlt +YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCvJAgIKXo1nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKz +j/i5Vbext0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIzSdhD +Y2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQGBO+QueQA5N06tRn/ +Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+rCpSx4/VBEnkjWNHiDxpg8v+R70r +fk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/ +BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2Uv +Z2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy +aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKvMzEzMA0GCSqG +SIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzEp6B4Eq1iDkVwZMXnl2YtmAl+ +X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKE +KQsTb47bDN0lAtukixlE0kF6BWlKWE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiC +Km0oHw0LxOXnGiYZ4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vE +ZV8NhnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq +-----END CERTIFICATE----- + +SecureTrust CA +============== +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQG +EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xFzAVBgNVBAMTDlNlY3VyZVRy +dXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIzMTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAe +BgNVBAoTF1NlY3VyZVRydXN0IENvcnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQX +OZEzZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO0gMdA+9t +DWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIaowW8xQmxSPmjL8xk037uH +GFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b +01k/unK8RCSc43Oz969XL0Imnal0ugBS8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmH +ursCAwEAAaOBnTCBmjATBgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCegJYYj +aHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ +KoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt36Z3q059c4EVlew3KW+JwULKUBRSu +SceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHf +mbx8IVQr5Fiiu1cprp6poxkmD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZ +nMUFdAvnZyPSCPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- + +Secure Global CA +================ +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQG +EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBH +bG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkxMjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEg +MB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwg +Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jx +YDiJiQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa/FHtaMbQ +bqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJjnIFHovdRIWCQtBJwB1g +8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnIHmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYV +HDGA76oYa8J719rO+TMg1fW9ajMtgQT7sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi +0XPnj3pDAgMBAAGjgZ0wgZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCswKaAn +oCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsGAQQBgjcVAQQDAgEA +MA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0LURYD7xh8yOOvaliTFGCRsoTciE6+ +OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXOH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cn +CDpOGR86p1hcF895P4vkp9MmI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/5 +3CYNv6ZHdAbYiNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- + +COMODO Certification Authority +============================== +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCBgTELMAkGA1UE +BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG +A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNVBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eTAeFw0wNjEyMDEwMDAwMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEb +MBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFD +T01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH ++7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTww +xHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV +4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA +1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5OnKVI +rLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9k +b2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOC +AQEAPpiem/Yb6dc5t3iuHXIYSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CP +OGEIqB6BCsAvIC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4zJVSk/BwJVmc +IGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5ddBA6+C4OmF4O5MBKgxTMVBbkN ++8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IBZQ== +-----END CERTIFICATE----- + +Network Solutions Certificate Authority +======================================= +-----BEGIN CERTIFICATE----- +MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQG +EwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydOZXR3b3Jr +IFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMx +MjM1OTU5WjBiMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu +MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwzc7MEL7xx +jOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPPOCwGJgl6cvf6UDL4wpPT +aaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rlmGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXT +crA/vGp97Eh/jcOrqnErU2lBUzS1sLnFBgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc +/Qzpf14Dl847ABSHJ3A4qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMB +AAGjgZcwgZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwubmV0c29sc3NsLmNv +bS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3JpdHkuY3JsMA0GCSqGSIb3DQEBBQUA +A4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc86fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q +4LqILPxFzBiwmZVRDuwduIj/h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/ +GGUsyfJj4akH/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv +wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHNpGxlaKFJdlxD +ydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey +-----END CERTIFICATE----- + +COMODO ECC Certification Authority +================================== +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTELMAkGA1UEBhMC +R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE +ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwHhcNMDgwMzA2MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0Ix +GzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSRFtSrYpn1PlILBs5BAH+X +4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0JcfRK9ChQtP6IHG4/bC8vCVlbpVsLM5ni +wz2J+Wos77LTBumjQjBAMB0GA1UdDgQWBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VG +FAkK+qDmfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdvGDeA +U/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +OISTE WISeKey Global Root GA CA +=============================== +-----BEGIN CERTIFICATE----- +MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCBijELMAkGA1UE +BhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHlyaWdodCAoYykgMjAwNTEiMCAG +A1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBH +bG9iYWwgUm9vdCBHQSBDQTAeFw0wNTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYD +VQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIw +IAYDVQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5 +IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy0+zAJs9 +Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxRVVuuk+g3/ytr6dTqvirdqFEr12bDYVxg +Asj1znJ7O7jyTmUIms2kahnBAbtzptf2w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbD +d50kc3vkDIzh2TbhmYsFmQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ +/yxViJGg4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t94B3R +LoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ +KoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOxSPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vIm +MMkQyh2I+3QZH4VFvbBsUfk2ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4 ++vg1YFkCExh8vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa +hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZiFj4A4xylNoEY +okxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ/L7fCg0= +-----END CERTIFICATE----- + +Certigna +======== +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNVBAYTAkZSMRIw +EAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4XDTA3MDYyOTE1MTMwNVoXDTI3 +MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwI +Q2VydGlnbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7q +XOEm7RFHYeGifBZ4QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyH +GxnygQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbwzBfsV1/p +ogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q130yGLMLLGq/jj8UEYkg +DncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKf +Irjxwo1p3Po6WAbfAgMBAAGjgbwwgbkwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQ +tCRZvgHyUtVF9lo53BEwZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJ +BgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzjAQ/J +SP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG9w0BAQUFAAOCAQEA +hQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8hbV6lUmPOEvjvKtpv6zf+EwLHyzs+ +ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFncfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1klu +PBS1xp81HlDQwY9qcEQCYsuuHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY +1gkIl2PlwS6wt0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- + +Cybertrust Global Root +====================== +-----BEGIN CERTIFICATE----- +MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYGA1UEChMPQ3li +ZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBSb290MB4XDTA2MTIxNTA4 +MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQD +ExZDeWJlcnRydXN0IEdsb2JhbCBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA ++Mi8vRRQZhP/8NN57CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW +0ozSJ8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2yHLtgwEZL +AfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iPt3sMpTjr3kfb1V05/Iin +89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNzFtApD0mpSPCzqrdsxacwOUBdrsTiXSZT +8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAYXSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2 +MDSgMqAwhi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3JsMB8G +A1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUAA4IBAQBW7wojoFRO +lZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMjWqd8BfP9IjsO0QbE2zZMcwSO5bAi +5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUxXOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2 +hO0j9n0Hq0V+09+zv+mKts2oomcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+T +X3EJIrduPuocA06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW +WL1WMRJOEcgh4LMRkWXbtKaIOM5V +-----END CERTIFICATE----- + +ePKI Root Certification Authority +================================= +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQG +EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xKjAoBgNVBAsMIWVQS0kg +Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMx +MjdaMF4xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEq +MCgGA1UECwwhZVBLSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAHSyZbCUNs +IZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAhijHyl3SJCRImHJ7K2RKi +lTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3XDZoTM1PRYfl61dd4s5oz9wCGzh1NlDiv +qOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX +12ruOzjjK9SXDrkb5wdJfzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0O +WQqraffAsgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uUWH1+ +ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLSnT0IFaUQAS2zMnao +lQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pHdmX2Os+PYhcZewoozRrSgx4hxyy/ +vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJipNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXi +Zo1jDiVN1Rmy5nk3pyKdVDECAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/Qkqi +MAwGA1UdEwQFMAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGBuvl2ICO1J2B0 +1GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6YlPwZpVnPDimZI+ymBV3QGypzq +KOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkPJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdV +xrsStZf0X4OFunHB2WyBEXYKCrC/gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEP +NXubrjlpC2JgQCA2j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+r +GNm65ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUBo2M3IUxE +xJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS/jQ6fbjpKdx2qcgw+BRx +gMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2zGp1iro2C6pSe3VkQw63d4k3jMdXH7Ojy +sP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTEW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmOD +BCEIZ43ygknQW/2xzQ+DhNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- + +certSIGN ROOT CA +================ +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYTAlJPMREwDwYD +VQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTAeFw0wNjA3MDQxNzIwMDRa +Fw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UE +CxMQY2VydFNJR04gUk9PVCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7I +JUqOtdu0KBuqV5Do0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHH +rfAQUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5dRdY4zTW2 +ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQOA7+j0xbm0bqQfWwCHTD +0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwvJoIQ4uNllAoEwF73XVv4EOLQunpL+943 +AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B +Af8EBAMCAcYwHQYDVR0OBBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IB +AQA+0hyJLjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecYMnQ8 +SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ44gx+FkagQnIl6Z0 +x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6IJd1hJyMctTEHBDa0GpC9oHRxUIlt +vBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNwi/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7Nz +TogVZ96edhBiIL5VaZVDADlN9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- + +GeoTrust Primary Certification Authority - G3 +============================================= +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UE +BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChjKSAyMDA4IEdlb1RydXN0 +IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFy +eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIz +NTk1OVowgZgxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAo +YykgMjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNVBAMT +LUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz+uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5j +K/BGvESyiaHAKAxJcCGVn2TAppMSAmUmhsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdE +c5IiaacDiGydY8hS2pgn5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3C +IShwiP/WJmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exALDmKu +dlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZChuOl1UcCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMR5yo6hTgMdHNxr +2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IBAQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9 +cr5HqQ6XErhK8WTTOd8lNNTBzU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbE +Ap7aDHdlDkQNkv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD +AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUHSJsMC8tJP33s +t/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2Gspki4cErx5z481+oghLrGREt +-----END CERTIFICATE----- + +thawte Primary Root CA - G2 +=========================== +-----BEGIN CERTIFICATE----- +MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDELMAkGA1UEBhMC +VVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMpIDIwMDcgdGhhd3RlLCBJbmMu +IC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3Qg +Q0EgLSBHMjAeFw0wNzExMDUwMDAwMDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEV +MBMGA1UEChMMdGhhd3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBG +b3IgYXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAt +IEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/BebfowJPDQfGAFG6DAJS +LSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6papu+7qzcMBniKI11KOasf2twu8x+qi5 +8/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU +mtgAMADna3+FGO6Lts6KDPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUN +G4k8VIZ3KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41oxXZ3K +rr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg== +-----END CERTIFICATE----- + +thawte Primary Root CA - G3 +=========================== +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCBrjELMAkGA1UE +BhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2 +aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIwMDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhv +cml6ZWQgdXNlIG9ubHkxJDAiBgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0w +ODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh +d3RlLCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9uMTgwNgYD +VQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTEkMCIG +A1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAsr8nLPvb2FvdeHsbnndmgcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2At +P0LMqmsywCPLLEHd5N/8YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC ++BsUa0Lfb1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS99irY +7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2SzhkGcuYMXDhpxwTW +vGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUkOQIDAQABo0IwQDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJ +KoZIhvcNAQELBQADggEBABpA2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweK +A3rD6z8KLFIWoCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu +t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7cKUGRIjxpp7sC +8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fMm7v/OeZWYdMKp8RcTGB7BXcm +er/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZuMdRAGmI0Nj81Aa6sY6A= +-----END CERTIFICATE----- + +GeoTrust Primary Certification Authority - G2 +============================================= +-----BEGIN CERTIFICATE----- +MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChjKSAyMDA3IEdlb1RydXN0IElu +Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1 +OVowgZgxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg +MjAwNyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNVBAMTLUdl +b1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjB2MBAGByqGSM49AgEG +BSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcLSo17VDs6bl8VAsBQps8lL33KSLjHUGMc +KiEIfJo22Av+0SbFWDEwKCXzXV2juLaltJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+ +EVXVMAoGCCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGTqQ7m +ndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBuczrD6ogRLQy7rQkgu2 +npaqBA+K +-----END CERTIFICATE----- + +VeriSign Universal Root Certification Authority +=============================================== +-----BEGIN CERTIFICATE----- +MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCBvTELMAkGA1UE +BhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBO +ZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVk +IHVzZSBvbmx5MTgwNgYDVQQDEy9WZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9u +IEF1dGhvcml0eTAeFw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJV +UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv +cmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl +IG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNhbCBSb290IENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj +1mCOkdeQmIN65lgZOIzF9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGP +MiJhgsWHH26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+HLL72 +9fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN/BMReYTtXlT2NJ8I +AfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPTrJ9VAMf2CGqUuV/c4DPxhGD5WycR +tPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0G +CCsGAQUFBwEMBGEwX6FdoFswWTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2O +a8PPgGrUSBgsexkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud +DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4sAPmLGd75JR3 +Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+seQxIcaBlVZaDrHC1LGmWazx +Y8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTx +P/jgdFcrGJ2BtMQo2pSXpXDrrB2+BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+P +wGZsY6rp2aQW9IHRlRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4 +mJO37M2CYfE45k+XmCpajQ== +-----END CERTIFICATE----- + +VeriSign Class 3 Public Primary Certification Authority - G4 +============================================================ +-----BEGIN CERTIFICATE----- +MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjELMAkGA1UEBhMC +VVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3 +b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVz +ZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjEL +MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBU +cnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRo +b3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5 +IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8 +Utpkmw4tXNherJI9/gHmGUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGz +rl0Bp3vefLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEw +HzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVyaXNpZ24u +Y29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMWkf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMD +A2gAMGUCMGYhDBgmYFo4e1ZC4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIx +AJw9SDkjOVgaFRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== +-----END CERTIFICATE----- + +NetLock Arany (Class Gold) Főtanúsítvány +======================================== +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTERMA8G +A1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3MDUGA1UECwwuVGFuw7pzw610 +dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBB +cmFueSAoQ2xhc3MgR29sZCkgRsWRdGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgx +MjA2MTUwODIxWjCBpzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxO +ZXRMb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNzIEdvbGQpIEbFkXRhbsO6 +c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu +0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrTlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw +/HpYzY6b7cNGbIRwXdrzAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAk +H3B5r9s5VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRGILdw +fzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2BJtr+UBdADTHLpl1 +neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2MU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwW +qZw8UQCgwBEIBaeZ5m8BiFRhbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTta +YtOUZcTh5m2C+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2FuLjbvrW5Kfna +NwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2XjG4Kvte9nHfRCaexOYNkbQu +dZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +Hongkong Post Root CA 1 +======================= +-----BEGIN CERTIFICATE----- +MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoT +DUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMB4XDTAzMDUx +NTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25n +IFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1 +ApzQjVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEnPzlTCeqr +auh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjhZY4bXSNmO7ilMlHIhqqh +qZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9nnV0ttgCXjqQesBCNnLsak3c78QA3xMY +V18meMjWCnl3v/evt3a5pQuEF10Q6m/hq5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNV +HRMBAf8ECDAGAQH/AgEDMA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7i +h9legYsCmEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI37pio +l7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clBoiMBdDhViw+5Lmei +IAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJsEhTkYY2sEJCehFC78JZvRZ+K88ps +T/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpOfMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilT +c4afU9hDDl3WY4JxHYB0yvbiAmvZWg== +-----END CERTIFICATE----- + +SecureSign RootCA11 +=================== +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDErMCkGA1UEChMi +SmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoGA1UEAxMTU2VjdXJlU2lnbiBS +b290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSsw +KQYDVQQKEyJKYXBhbiBDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1 +cmVTaWduIFJvb3RDQTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvL +TJszi1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8h9uuywGO +wvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOVMdrAG/LuYpmGYz+/3ZMq +g6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rP +O7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitA +bpSACW22s293bzUIUPsCh8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZX +t94wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKCh +OBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xmKbabfSVSSUOrTC4r +bnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQX5Ucv+2rIrVls4W6ng+4reV6G4pQ +Oh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWrQbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01 +y8hSyn+B/tlr0/cR7SXf+Of5pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061 +lgeLKBObjBmNQSdJQO7e5iNEOdyhIta6A/I= +-----END CERTIFICATE----- + +Microsec e-Szigno Root CA 2009 +============================== +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYDVQQGEwJIVTER +MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jv +c2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTAeFw0wOTA2MTYxMTMwMThaFw0yOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UE +BwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUt +U3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvPkd6mJviZpWNwrZuuyjNA +fW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tccbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG +0IMZfcChEhyVbUr02MelTTMuhTlAdX4UfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKA +pxn1ntxVUwOXewdI/5n7N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm +1HxdrtbCxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1+rUC +AwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTLD8bf +QkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAbBgNVHREE +FDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqGSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0o +lZMEyL/azXm4Q5DwpL7v8u8hmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfX +I/OMn74dseGkddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c2Pm2G2JwCz02 +yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5tHMN1Rq41Bab2XD0h7lbwyYIi +LXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +GlobalSign Root CA - R3 +======================= +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xv +YmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh +bFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT +aWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln +bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWt +iHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ +0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3 +rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjl +OCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2 +xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7 +lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8 +EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1E +bddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18 +YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7r +kpeDMdmztcpHWD9f +-----END CERTIFICATE----- + +Autoridad de Certificacion Firmaprofesional CIF A62634068 +========================================================= +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UEBhMCRVMxQjBA +BgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2 +MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEyMzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIw +QAYDVQQDDDlBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBB +NjI2MzQwNjgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDD +Utd9thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQMcas9UX4P +B99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefGL9ItWY16Ck6WaVICqjaY +7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15iNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqH +ECNZXyAFGUftaI6SEspd/NYrspI8IM/hX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyI +plD9amML9ZMWGxmPsu2bm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctX +MbScyJCyZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirjaEbsX +LZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/TKI8xWVvTyQKmtFLK +bpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF6NkBiDkal4ZkQdU7hwxu+g/GvUgU +vzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVhOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1Ud +EwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNH +DhpkLzCBpgYDVR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp +cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBvACAAZABlACAA +bABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBlAGwAbwBuAGEAIAAwADgAMAAx +ADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx +51tkljYyGOylMnfX40S2wBEqgLk9am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qk +R71kMrv2JYSiJ0L1ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaP +T481PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS3a/DTg4f +Jl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5kSeTy36LssUzAKh3ntLFl +osS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF3dvd6qJ2gHN99ZwExEWN57kci57q13XR +crHedUTnQn3iV2t93Jm8PYMo6oCTjcVMZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoR +saS8I8nkvof/uZS2+F0gStRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTD +KCOM/iczQ0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQBjLMi +6Et8Vcad+qMUu2WFbm5PEn4KPJ2V +-----END CERTIFICATE----- + +Izenpe.com +========== +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4MQswCQYDVQQG +EwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wHhcNMDcxMjEz +MTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMu +QS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ +03rKDx6sp4boFmVqscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAK +ClaOxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6HLmYRY2xU ++zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFXuaOKmMPsOzTFlUFpfnXC +PCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQDyCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxT +OTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbK +F7jJeodWLBoBHmy+E60QrLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK +0GqfvEyNBjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8Lhij+ +0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIBQFqNeb+Lz0vPqhbB +leStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+HMh3/1uaD7euBUbl8agW7EekFwID +AQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+ +SVpFTlBFIFMuQS4gLSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBG +NjIgUzgxQzBBBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O +BBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUAA4ICAQB4pgwWSp9MiDrAyw6l +Fn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWblaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbga +kEyrkgPH7UIBzg/YsfqikuFgba56awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8q +hT/AQKM6WfxZSzwoJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Cs +g1lwLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCTVyvehQP5 +aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGkLhObNA5me0mrZJfQRsN5 +nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJbUjWumDqtujWTI6cfSN01RpiyEGjkpTHC +ClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZo +Q0iy2+tzJOeRf1SktoA+naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1Z +WrOZyGlsQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +Chambers of Commerce Root - 2008 +================================ +-----BEGIN CERTIFICATE----- +MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYDVQQGEwJFVTFD +MEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNv +bS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMu +QS4xKTAnBgNVBAMTIENoYW1iZXJzIG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEy +Mjk1MFoXDTM4MDczMTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNl +ZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIwEAYDVQQF +EwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJl +cnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW928sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKA +XuFixrYp4YFs8r/lfTJqVKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorj +h40G072QDuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR5gN/ +ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfLZEFHcpOrUMPrCXZk +NNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05aSd+pZgvMPMZ4fKecHePOjlO+Bd5g +D2vlGts/4+EhySnB8esHnFIbAURRPHsl18TlUlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331 +lubKgdaX8ZSD6e2wsWsSaR6s+12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ +0wlf2eOKNcx5Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj +ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAxhduub+84Mxh2 +EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNVHQ4EFgQU+SSsD7K1+HnA+mCI +G8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1+HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJ +BgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNh +bWVyZmlybWEuY29tL2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENh +bWVyZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDiC +CQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUH +AgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZIhvcNAQEFBQADggIBAJASryI1 +wqM58C7e6bXpeHxIvj99RZJe6dqxGfwWPJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH +3qLPaYRgM+gQDROpI9CF5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbU +RWpGqOt1glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaHFoI6 +M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2pSB7+R5KBWIBpih1 +YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MDxvbxrN8y8NmBGuScvfaAFPDRLLmF +9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QGtjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcK +zBIKinmwPQN/aUv0NCB9szTqjktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvG +nrDQWzilm1DefhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg +OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZd0jQ +-----END CERTIFICATE----- + +Global Chambersign Root - 2008 +============================== +-----BEGIN CERTIFICATE----- +MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYDVQQGEwJFVTFD +MEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNv +bS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMu +QS4xJzAlBgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMx +NDBaFw0zODA3MzExMjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUg +Y3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJ +QTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD +aGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDf +VtPkOpt2RbQT2//BthmLN0EYlVJH6xedKYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXf +XjaOcNFccUMd2drvXNL7G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0 +ZJJ0YPP2zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4ddPB +/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyGHoiMvvKRhI9lNNgA +TH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2Id3UwD2ln58fQ1DJu7xsepeY7s2M +H/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3VyJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfe +Ox2YItaswTXbo6Al/3K1dh3ebeksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSF +HTynyQbehP9r6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh +wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsogzCtLkykPAgMB +AAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQWBBS5CcqcHtvTbDprru1U8VuT +BjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDprru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UE +BhMCRVUxQzBBBgNVBAcTOk1hZHJpZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJm +aXJtYS5jb20vYWRkcmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJm +aXJtYSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiCCQDJzdPp +1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0 +dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZIhvcNAQEFBQADggIBAICIf3DekijZBZRG +/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZUohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6 +ReAJ3spED8IXDneRRXozX1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/s +dZ7LoR/xfxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVza2Mg +9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yydYhz2rXzdpjEetrHH +foUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMdSqlapskD7+3056huirRXhOukP9Du +qqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9OAP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETr +P3iZ8ntxPjzxmKfFGBI/5rsoM0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVq +c5iJWzouE4gev8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z +09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B +-----END CERTIFICATE----- + +Go Daddy Root Certificate Authority - G2 +======================================== +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMu +MTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8G +A1UEAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq +9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD ++qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutd +fMh8+7ArU6SSYmlRJQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMl +NAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9 +BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096ac +vNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r +5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYV +N8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1 +-----END CERTIFICATE----- + +Starfield Root Certificate Authority - G2 +========================================= +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s +b2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVsZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0 +eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAw +DgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQg +VGVjaG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZpY2F0ZSBB +dXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3twQP89o/8ArFv +W59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMgnLRJdzIpVv257IzdIvpy3Cdhl+72WoTs +bhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNk +N3mSwOxGXn/hbVNMYq/NHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7Nf +ZTD4p7dNdloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0HZbU +JtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0GCSqGSIb3DQEBCwUAA4IBAQARWfol +TwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjUsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx +4mcujJUDJi5DnUox9g61DLu34jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUw +F5okxBDgBPfg8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1mMpYjn0q7pBZ +c2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +Starfield Services Root Certificate Authority - G2 +================================================== +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s +b2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRl +IEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNV +BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxT +dGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2VydmljZXMg +Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20pOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2 +h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm28xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4Pa +hHQUw2eeBGg6345AWh1KTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLP +LJGmpufehRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk6mFB +rMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMA0GCSqG +SIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMIbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPP +E95Dz+I0swSdHynVv/heyNXBve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTy +xQGjhdByPq1zqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn0q23KXB56jza +YyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCNsSi6 +-----END CERTIFICATE----- + +AffirmTrust Commercial +====================== +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMB4XDTEw +MDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly +bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6Eqdb +DuKPHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yrba0F8PrV +C8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPALMeIrJmqbTFeurCA+ukV6 +BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1yHp52UKqK39c/s4mT6NmgTWvRLpUHhww +MmWd5jyTXlBOeuM61G7MGvv50jeuJCqrVwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNV +HQ4EFgQUnZPGU4teyq8/nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYGXUPG +hi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNjvbz4YYCanrHOQnDi +qX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivtZ8SOyUOyXGsViQK8YvxO8rUzqrJv +0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9gN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0kh +sUlHRUe072o0EclNmsxZt9YCnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +AffirmTrust Networking +====================== +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMB4XDTEw +MDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly +bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SE +Hi3yYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbuakCNrmreI +dIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRLQESxG9fhwoXA3hA/Pe24 +/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gb +h+0t+nvujArjqWaJGctB+d1ENmHP4ndGyH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNV +HQ4EFgQUBx/S55zawm6iQLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfOtDIu +UFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzuQY0x2+c06lkh1QF6 +12S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZLgo/bNjR9eUJtGxUAArgFU2HdW23 +WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4uolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9 +/ZFvgrG+CJPbFEfxojfHRZ48x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +AffirmTrust Premium +=================== +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UEBhMCVVMxFDAS +BgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMB4XDTEwMDEy +OTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRy +dXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxBLfqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtn +BKAQJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ+jjeRFcV +5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrSs8PhaJyJ+HoAVt70VZVs ++7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmd +GPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d770O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5R +p9EixAqnOEhss/n/fauGV+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NI +S+LI+H+SqHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S5u04 +6uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4IaC1nEWTJ3s7xgaVY5 +/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TXOwF0lkLgAOIua+rF7nKsu7/+6qqo ++Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYEFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByv +MiPIs0laUZx2KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B8OWycvpEgjNC +6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQMKSOyARiqcTtNd56l+0OOF6S +L5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK ++4w1IX2COPKpVJEZNZOUbWo6xbLQu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmV +BtWVyuEklut89pMFu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFg +IxpHYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8GKa1qF60 +g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaORtGdFNrHF+QFlozEJLUb +zxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6eKeC2uAloGRwYQw== +-----END CERTIFICATE----- + +AffirmTrust Premium ECC +======================= +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMCVVMxFDASBgNV +BAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQcmVtaXVtIEVDQzAeFw0xMDAx +MjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1U +cnVzdDEgMB4GA1UEAwwXQWZmaXJtVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAQNMF4bFZ0D0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQ +N8O9ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0GA1UdDgQW +BBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAK +BggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/VsaobgxCd05DhT1wV/GzTjxi+zygk8N53X +57hG8f2h4nECMEJZh0PUUd+60wkyWs6Iflc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKM +eQ== +-----END CERTIFICATE----- + +Certum Trusted Network CA +========================= +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK +ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIy +MTIwNzM3WhcNMjkxMjMxMTIwNzM3WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBU +ZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MSIwIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rHUV+rpDKmYYe2bg+G0jAC +l/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LMTXPb865Px1bVWqeWifrzq2jUI4ZZJ88J +J7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVUBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4 +fOQtf/WsX+sWn7Et0brMkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0 +cvW0QM8xAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNVHRMB +Af8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNVHQ8BAf8EBAMCAQYw +DQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15ysHhE49wcrwn9I0j6vSrEuVUEtRCj +jSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfLI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1 +mS1FhIrlQgnXdAIv94nYmem8J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5aj +Zt3hrvJBW8qYVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +TWCA Root Certification Authority +================================= +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJ +VEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMzWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQG +EwJUVzESMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NB +IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFEAcK0HMMx +QhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HHK3XLfJ+utdGdIzdjp9xC +oi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeXRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP +4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/zrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1r +y+UPizgN7gr8/g+YnzAx3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkqhkiG +9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeCMErJk/9q56YAf4lC +mtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdlsXebQ79NqZp4VKIV66IIArB6nCWlW +QtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62Dlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVY +T0bf+215WfKEIlKuD8z7fDvnaspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocny +Yh0igzyXxfkZYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +Security Communication RootCA2 +============================== +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc +U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMeU2VjdXJpdHkgQ29tbXVuaWNh +dGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoXDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMC +SlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3Vy +aXR5IENvbW11bmljYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ANAVOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGrzbl+dp++ ++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVMVAX3NuRFg3sUZdbcDE3R +3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQhNBqyjoGADdH5H5XTz+L62e4iKrFvlNV +spHEfbmwhRkGeC7bYRr6hfVKkaHnFtWOojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1K +EOtOghY6rCcMU/Gt1SSwawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8 +QIH4D5csOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB +CwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpFcoJxDjrSzG+ntKEj +u/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXcokgfGT+Ok+vx+hfuzU7jBBJV1uXk +3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6q +tnRGEmyR7jTV7JqR50S+kDFy1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29 +mvVXIwAHIRc/SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +EC-ACC +====== +-----BEGIN CERTIFICATE----- +MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB8zELMAkGA1UE +BhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2VydGlmaWNhY2lvIChOSUYgUS0w +ODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYD +VQQLEyxWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UE +CxMsSmVyYXJxdWlhIEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMT +BkVDLUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQGEwJFUzE7 +MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8gKE5JRiBRLTA4MDExNzYt +SSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBDZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZl +Z2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQubmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJh +cnF1aWEgRW50aXRhdHMgZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUND +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R85iK +w5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm4CgPukLjbo73FCeT +ae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaVHMf5NLWUhdWZXqBIoH7nF2W4onW4 +HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNdQlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0a +E9jD2z3Il3rucO2n5nzbcc8tlGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw +0JDnJwIDAQABo4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4opvpXY0wfwYD +VR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBodHRwczovL3d3dy5jYXRjZXJ0 +Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5l +dC92ZXJhcnJlbCAwDQYJKoZIhvcNAQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJ +lF7W2u++AVtd0x7Y/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNa +Al6kSBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhyRp/7SNVe +l+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOSAgu+TGbrIP65y7WZf+a2 +E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xlnJ2lYJU6Un/10asIbvPuW/mIPX64b24D +5EI= +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions RootCA 2011 +======================================================= +-----BEGIN CERTIFICATE----- +MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1IxRDBCBgNVBAoT +O0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9y +aXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z +IFJvb3RDQSAyMDExMB4XDTExMTIwNjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYT +AkdSMUQwQgYDVQQKEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z +IENlcnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNo +IEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPzdYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI +1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJfel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa +71HFK9+WXesyHgLacEnsbgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u +8yBRQlqD75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSPFEDH +3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNVHRMBAf8EBTADAQH/ +MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp5dgTBCPuQSUwRwYDVR0eBEAwPqA8 +MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQub3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQu +b3JnMA0GCSqGSIb3DQEBBQUAA4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVt +XdMiKahsog2p6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 +TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7dIsXRSZMFpGD +/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8AcysNnq/onN694/BtZqhFLKPM58N +7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXIl7WdmplNsDz4SgCbZN2fOUvRJ9e4 +-----END CERTIFICATE----- + +Actalis Authentication Root CA +============================== +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSVQxDjAM +BgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UE +AwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDky +MjExMjIwMlowazELMAkGA1UEBhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlz +IFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNvUTufClrJ +wkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX4ay8IMKx4INRimlNAJZa +by/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9KK3giq0itFZljoZUj5NDKd45RnijMCO6 +zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1f +YVEiVRvjRuPjPdA1YprbrxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2 +oxgkg4YQ51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2Fbe8l +EfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxeKF+w6D9Fz8+vm2/7 +hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4Fv6MGn8i1zeQf1xcGDXqVdFUNaBr8 +EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbnfpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5 +jF66CyCU3nuDuP/jVo23Eek7jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLY +iDrIn3hm7YnzezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQALe3KHwGCmSUyI +WOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70jsNjLiNmsGe+b7bAEzlgqqI0 +JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDzWochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKx +K3JCaKygvU5a2hi/a5iB0P2avl4VSM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+ +Xlff1ANATIGk0k9jpwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC +4yyXX04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+OkfcvHlXHo +2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7RK4X9p2jIugErsWx0Hbhz +lefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btUZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXem +OR/qnuOf0GZvBeyqdn6/axag67XH/JJULysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9 +vwGYT7JZVEc+NHt4bVaTLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +Trustis FPS Root CA +=================== +-----BEGIN CERTIFICATE----- +MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQG +EwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQLExNUcnVzdGlzIEZQUyBSb290 +IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTExMzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNV +BAoTD1RydXN0aXMgTGltaXRlZDEcMBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQ +RUN+AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihHiTHcDnlk +H5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjjvSkCqPoc4Vu5g6hBSLwa +cY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zt +o3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlBOrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEA +AaNTMFEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAd +BgNVHQ4EFgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01GX2c +GE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmWzaD+vkAMXBJV+JOC +yinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP41BIy+Q7DsdwyhEQsb8tGD+pmQQ9P +8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZEf1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHV +l/9D7S3B2l0pKoU/rGXuhg8FjZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYl +iB6XzCGcKQENZetX2fNXlrtIzYE= +-----END CERTIFICATE----- + +Buypass Class 2 Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU +QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMiBSb290IENBMB4X +DTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1owTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 +eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1 +g1Lr6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPVL4O2fuPn +9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC911K2GScuVr1QGbNgGE41b +/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHxMlAQTn/0hpPshNOOvEu/XAFOBz3cFIqU +CqTqc/sLUegTBxj6DvEr0VQVfTzh97QZQmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeff +awrbD02TTqigzXsu8lkBarcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgI +zRFo1clrUs3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLiFRhn +Bkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRSP/TizPJhk9H9Z2vX +Uq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN9SG9dKpN6nIDSdvHXx1iY8f93ZHs +M+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxPAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFMmAd+BikoL1RpzzuvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF +AAOCAgEAU18h9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3tOluwlN5E40EI +osHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo+fsicdl9sz1Gv7SEr5AcD48S +aq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYd +DnkM/crqJIByw5c/8nerQyIKx+u2DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWD +LfJ6v9r9jv6ly0UsH8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0 +oyLQI+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK75t98biGC +wWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h3PFaTWwyI0PurKju7koS +CTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPzY11aWOIv4x3kqdbQCtCev9eBCfHJxyYN +rJgWVqA= +-----END CERTIFICATE----- + +Buypass Class 3 Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU +QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMyBSb290IENBMB4X +DTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFowTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 +eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRH +sJ8YZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3EN3coTRiR +5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9tznDDgFHmV0ST9tD+leh +7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX0DJq1l1sDPGzbjniazEuOQAnFN44wOwZ +ZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH +2xc519woe2v1n/MuwU8XKhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV +/afmiSTYzIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvSO1UQ +RwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D34xFMFbG02SrZvPA +Xpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgPK9Dx2hzLabjKSWJtyNBjYt1gD1iq +j6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFEe4zf/lb+74suwvTg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF +AAOCAgEAACAjQTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXSIGrs/CIBKM+G +uIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2HJLw5QY33KbmkJs4j1xrG0aG +Q0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsaO5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8 +ZORK15FTAaggiG6cX0S5y2CBNOxv033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2 +KSb12tjE8nVhz36udmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz +6MkEkbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg413OEMXbug +UZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvDu79leNKGef9JOxqDDPDe +eOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq4/g7u9xN12TyUb7mqqta6THuBrxzvxNi +Cp/HuZc= +-----END CERTIFICATE----- + +T-TeleSec GlobalRoot Class 3 +============================ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM +IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU +cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgx +MDAxMTAyOTU2WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz +dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD +ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN8ELg63iIVl6bmlQdTQyK +9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/RLyTPWGrTs0NvvAgJ1gORH8EGoel15YU +NpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZF +iP0Zf3WHHx+xGwpzJFu5ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W +0eDrXltMEnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1A/d2O2GCahKqGFPr +AyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOyWL6ukK2YJ5f+AbGwUgC4TeQbIXQb +fsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzT +ucpH9sry9uetuUg/vBa3wW306gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7h +P0HHRwA11fXT91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4pTpPDpFQUWw== +-----END CERTIFICATE----- + +EE Certification Centre Root CA +=============================== +-----BEGIN CERTIFICATE----- +MIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQG +EwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEoMCYGA1UEAwwfRUUgQ2Vy +dGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIw +MTAxMDMwMTAxMDMwWhgPMjAzMDEyMTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlB +UyBTZXJ0aWZpdHNlZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRy +ZSBSb290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUyeuuOF0+W2Ap7kaJjbMeM +TC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvObntl8jixwKIy72KyaOBhU8E2lf/slLo2 +rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIwWFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw +93X2PaRka9ZP585ArQ/dMtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtN +P2MbRMNE1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/zQas8fElyalL1BSZ +MEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYBBQUHAwMGCCsGAQUFBwMEBggrBgEF +BQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEFBQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+Rj +xY6hUFaTlrg4wCQiZrxTFGGVv9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqM +lIpPnTX/dqQGE5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u +uSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIWiAYLtqZLICjU +3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/vGVCJYMzpJJUPwssd8m92kMfM +dcGWxZ0= +-----END CERTIFICATE----- + +D-TRUST Root Class 3 CA 2 2009 +============================== +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTAe +Fw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NThaME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxE +LVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOAD +ER03UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42tSHKXzlA +BF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9RySPocq60vFYJfxLLHLGv +KZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsMlFqVlNpQmvH/pStmMaTJOKDfHR+4CS7z +p+hnUquVH+BGPtikw8paxTGA6Eian5Rp/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUC +AwEAAaOCARowggEWMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ +4PGEMA4GA1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVjdG9y +eS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUyMENBJTIwMiUyMDIw +MDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRlcmV2b2NhdGlvbmxpc3QwQ6BBoD+G +PWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAw +OS5jcmwwDQYJKoZIhvcNAQELBQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm +2H6NMLVwMeniacfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4KzCUqNQT4YJEV +dT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8PIWmawomDeCTmGCufsYkl4ph +X5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3YJohw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +D-TRUST Root Class 3 CA 2 EV 2009 +================================= +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw +OTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUwNDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw +OTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfS +egpnljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM03TP1YtHh +zRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6ZqQTMFexgaDbtCHu39b+T +7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lRp75mpoo6Kr3HGrHhFPC+Oh25z1uxav60 +sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure35 +11H3a6UCAwEAAaOCASQwggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyv +cop9NteaHNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFwOi8v +ZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xhc3MlMjAzJTIwQ0El +MjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1ERT9jZXJ0aWZpY2F0ZXJldm9jYXRp +b25saXN0MEagRKBChkBodHRwOi8vd3d3LmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xh +c3NfM19jYV8yX2V2XzIwMDkuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+ +PPoeUSbrh/Yp3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNFCSuGdXzfX2lX +ANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7naxpeG0ILD5EJt/rDiZE4OJudA +NCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqXKVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVv +w9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +CA Disig Root R2 +================ +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNVBAYTAlNLMRMw +EQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMuMRkwFwYDVQQDExBDQSBEaXNp +ZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQyMDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sx +EzARBgNVBAcTCkJyYXRpc2xhdmExEzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERp +c2lnIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbC +w3OeNcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNHPWSb6Wia +xswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3Ix2ymrdMxp7zo5eFm1tL7 +A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbeQTg06ov80egEFGEtQX6sx3dOy1FU+16S +GBsEWmjGycT6txOgmLcRK7fWV8x8nhfRyyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqV +g8NTEQxzHQuyRpDRQjrOQG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa +5Beny912H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJQfYE +koopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUDi/ZnWejBBhG93c+A +Ak9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORsnLMOPReisjQS1n6yqEm70XooQL6i +Fh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5u +Qu0wDQYJKoZIhvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqfGopTpti72TVV +sRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkblvdhuDvEK7Z4bLQjb/D907Je +dR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W8 +1k/BfDxujRNt+3vrMNDcTa/F1balTFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjx +mHHEt38OFdAlab0inSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01 +utI3gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18DrG5gPcFw0 +sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3OszMOl6W8KjptlwlCFtaOg +UxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8xL4ysEr3vQCj8KWefshNPZiTEUxnpHikV +7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +ACCVRAIZ1 +========= +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UEAwwJQUNDVlJB +SVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQswCQYDVQQGEwJFUzAeFw0xMTA1 +MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQBgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwH +UEtJQUNDVjENMAsGA1UECgwEQUNDVjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCbqau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gM +jmoYHtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWoG2ioPej0 +RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpAlHPrzg5XPAOBOp0KoVdD +aaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhrIA8wKFSVf+DuzgpmndFALW4ir50awQUZ +0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDG +WuzndN9wrqODJerWx5eHk6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs7 +8yM2x/474KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMOm3WR +5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpacXpkatcnYGMN285J +9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPluUsXQA+xtrn13k/c4LOsOxFwYIRK +Q26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYIKwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRw +Oi8vd3d3LmFjY3YuZXMvZmlsZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEu +Y3J0MB8GCCsGAQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeTVfZW6oHlNsyM +Hj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIGCCsGAQUFBwICMIIBFB6CARAA +QQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUAcgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBh +AO0AegAgAGQAZQAgAGwAYQAgAEEAQwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUA +YwBuAG8AbABvAGcA7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBj +AHQAcgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAAQwBQAFMA +IABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUAczAwBggrBgEFBQcCARYk +aHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2MuaHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0 +dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRtaW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2 +MV9kZXIuY3JsMA4GA1UdDwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZI +hvcNAQEFBQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdpD70E +R9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gUJyCpZET/LtZ1qmxN +YEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+mAM/EKXMRNt6GGT6d7hmKG9Ww7Y49 +nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepDvV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJ +TS+xJlsndQAJxGJ3KQhfnlmstn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3 +sCPdK6jT2iWH7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szAh1xA2syVP1Xg +Nce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xFd3+YJ5oyXSrjhO7FmGYvliAd +3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2HpPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3p +EfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +TWCA Global Root CA +=================== +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcxEjAQBgNVBAoT +CVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMTVFdDQSBHbG9iYWwgUm9vdCBD +QTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQK +EwlUQUlXQU4tQ0ExEDAOBgNVBAsTB1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2C +nJfF10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz0ALfUPZV +r2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfChMBwqoJimFb3u/Rk28OKR +Q4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbHzIh1HrtsBv+baz4X7GGqcXzGHaL3SekV +tTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1W +KKD+u4ZqyPpcC1jcxkt2yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99 +sy2sbZCilaLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYPoA/p +yJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQABDzfuBSO6N+pjWxn +kjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcEqYSjMq+u7msXi7Kx/mzhkIyIqJdI +zshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6g +cFGn90xHNcgL1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WFH6vPNOw/KP4M +8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNoRI2T9GRwoD2dKAXDOXC4Ynsg +/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlg +lPx4mI88k1HtQJAH32RjJMtOcQWh15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryP +A9gK8kxkRr05YuWW6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3m +i4TWnsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5jwa19hAM8 +EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWzaGHQRiapIVJpLesux+t3 +zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmyKwbQBM0= +-----END CERTIFICATE----- + +TeliaSonera Root CA v1 +====================== +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAwNzEUMBIGA1UE +CgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJvb3QgQ0EgdjEwHhcNMDcxMDE4 +MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYDVQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwW +VGVsaWFTb25lcmEgUm9vdCBDQSB2MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+ +6yfwIaPzaSZVfp3FVRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA +3GV17CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+XZ75Ljo1k +B1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+/jXh7VB7qTCNGdMJjmhn +Xb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxH +oLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkmdtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3 +F0fUTPHSiXk+TT2YqGHeOh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJ +oWjiUIMusDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4pgd7 +gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fsslESl1MpWtTwEhDc +TwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQarMCpgKIv7NHfirZ1fpoeDVNAgMB +AAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qW +DNXr+nuqF+gTEjANBgkqhkiG9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNm +zqjMDfz1mgbldxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1TjTQpgcmLNkQfW +pb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBedY2gea+zDTYa4EzAvXUYNR0PV +G6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpc +c41teyWRyu5FrgZLAMzTsVlQ2jqIOylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOT +JsjrDNYmiLbAJM+7vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2 +qReWt88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcnHL/EVlP6 +Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVxSK236thZiNSQvxaz2ems +WWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- + +E-Tugra Certification Authority +=============================== +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNVBAYTAlRSMQ8w +DQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamls +ZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN +ZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMw +NTEyMDk0OFoXDTIzMDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmEx +QDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxl +cmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQD +DB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEA4vU/kwVRHoViVF56C/UYB4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vd +hQd2h8y/L5VMzH2nPbxHD5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5K +CKpbknSFQ9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEoq1+g +ElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3Dk14opz8n8Y4e0ypQ +BaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcHfC425lAcP9tDJMW/hkd5s3kc91r0 +E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsutdEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gz +rt48Ue7LE3wBf4QOXVGUnhMMti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAq +jqFGOjGY5RH8zLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn +rFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUXU8u3Zg5mTPj5 +dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6Jyr+zE7S6E5UMA8GA1UdEwEB +/wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5XPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEG +MA0GCSqGSIb3DQEBCwUAA4ICAQAFNzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAK +kEh47U6YA5n+KGCRHTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jO +XKqYGwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c77NCR807 +VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3+GbHeJAAFS6LrVE1Uweo +a2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WKvJUawSg5TB9D0pH0clmKuVb8P7Sd2nCc +dlqMQ1DujjByTd//SffGqWfZbawCEeI6FiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEV +KV0jq9BgoRJP3vQXzTLlyb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gT +Dx4JnW2PAJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpDy4Q0 +8ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8dNL/+I5c30jn6PQ0G +C7TbO6Orb1wdtn7os4I07QZcJA== +-----END CERTIFICATE----- + +T-TeleSec GlobalRoot Class 2 +============================ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM +IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU +cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgx +MDAxMTA0MDE0WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz +dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD +ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUdAqSzm1nzHoqvNK38DcLZ +SBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiCFoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/F +vudocP05l03Sx5iRUKrERLMjfTlH6VJi1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx970 +2cu+fjOlbpSD8DT6IavqjnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGV +WOHAD3bZwI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/WSA2AHmgoCJrjNXy +YdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhyNsZt+U2e+iKo4YFWz827n+qrkRk4 +r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPACuvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNf +vNoBYimipidx5joifsFvHZVwIEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR +3p1m0IvVVGb6g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlPBSeOE6Fuwg== +-----END CERTIFICATE----- + +Atos TrustedRoot 2011 +===================== +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UEAwwVQXRvcyBU +cnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0xMTA3MDcxNDU4 +MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMMFUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsG +A1UECgwEQXRvczELMAkGA1UEBhMCREUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCV +hTuXbyo7LjvPpvMpNb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr +54rMVD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+SZFhyBH+ +DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ4J7sVaE3IqKHBAUsR320 +HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0Lcp2AMBYHlT8oDv3FdU9T1nSatCQujgKR +z3bFmx5VdJx4IbHwLfELn8LVlhgf8FQieowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7R +l+lwrrw7GWzbITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZ +bNshMBgGA1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +CwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8jvZfza1zv7v1Apt+h +k6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kPDpFrdRbhIfzYJsdHt6bPWHJxfrrh +TZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pcmaHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a9 +61qn8FYiqTxlVMYVqL2Gns2Dlmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G +3mB/ufNPRJLvKrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +QuoVadis Root CA 1 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakE +PBtVwedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWerNrwU8lm +PNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF34168Xfuw6cwI2H44g4hWf6 +Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh4Pw5qlPafX7PGglTvF0FBM+hSo+LdoIN +ofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXpUhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/l +g6AnhF4EwfWQvTA9xO+oabw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV +7qJZjqlc3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/GKubX +9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSthfbZxbGL0eUQMk1f +iyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KOTk0k+17kBL5yG6YnLUlamXrXXAkg +t3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOtzCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZI +hvcNAQELBQADggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2cDMT/uFPpiN3 +GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUNqXsCHKnQO18LwIE6PWThv6ct +Tr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP ++V04ikkwj+3x6xn0dxoxGE1nVGwvb2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh +3jRJjehZrJ3ydlo28hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fa +wx/kNSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNjZgKAvQU6 +O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhpq1467HxpvMc7hU6eFbm0 +FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFtnh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOV +hMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +QuoVadis Root CA 2 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFh +ZiFfqq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMWn4rjyduY +NM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ymc5GQYaYDFCDy54ejiK2t +oIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+o +MiwMzAkd056OXbxMmO7FGmh77FOm6RQ1o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+l +V0POKa2Mq1W/xPtbAd0jIaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZo +L1NesNKqIcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz8eQQ +sSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43ehvNURG3YBZwjgQQvD +6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l7ZizlWNof/k19N+IxWA1ksB8aRxh +lRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALGcC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZI +hvcNAQELBQADggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RCroijQ1h5fq7K +pVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0GaW/ZZGYjeVYg3UQt4XAoeo0L9 +x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4nlv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgz +dWqTHBLmYF5vHX/JHyPLhGGfHoJE+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6X +U/IyAgkwo1jwDQHVcsaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+Nw +mNtddbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNgKCLjsZWD +zYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeMHVOyToV7BjjHLPj4sHKN +JeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4WSr2Rz0ZiC3oheGe7IUIarFsNMkd7Egr +O3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +QuoVadis Root CA 3 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286 +IxSR/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNuFoM7pmRL +Mon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXRU7Ox7sWTaYI+FrUoRqHe +6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+cra1AdHkrAj80//ogaX3T7mH1urPnMNA3 +I4ZyYUUpSFlob3emLoG+B01vr87ERRORFHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3U +VDmrJqMz6nWB2i3ND0/kA9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f7 +5li59wzweyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634RylsSqi +Md5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBpVzgeAVuNVejH38DM +dyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0QA4XN8f+MFrXBsj6IbGB/kE+V9/Yt +rQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZI +hvcNAQELBQADggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnIFUBhynLWcKzS +t/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5WvvoxXqA/4Ti2Tk08HS6IT7SdEQ +TXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFgu/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9Du +DcpmvJRPpq3t/O5jrFc/ZSXPsoaP0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGib +Ih6BJpsQBJFxwAYf3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmD +hPbl8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+DhcI00iX +0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HNPlopNLk9hM6xZdRZkZFW +dSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ywaZWWDYWGWVjUTR939+J399roD1B0y2 +PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +DigiCert Assured ID Root G2 +=========================== +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw +IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgw +MTE1MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL +ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSAn61UQbVH +35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4HteccbiJVMWWXvdMX0h5i89vq +bFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9HpEgjAALAcKxHad3A2m67OeYfcgnDmCXRw +VWmvo2ifv922ebPynXApVfSr/5Vh88lAbx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OP +YLfykqGxvYmJHzDNw6YuYjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+Rn +lTGNAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTO +w0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPIQW5pJ6d1Ee88hjZv +0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I0jJmwYrA8y8678Dj1JGG0VDjA9tz +d29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4GnilmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAW +hsI6yLETcDbYz+70CjTVW0z9B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0M +jomZmWzwPDCvON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +DigiCert Assured ID Root G3 +=========================== +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYD +VQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQ +BgcqhkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJfZn4f5dwb +RXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17QRSAPWXYQ1qAk8C3eNvJs +KTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgF +UaFNN6KDec6NHSrkhDAKBggqhkjOPQQDAwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5Fy +YZ5eEJJZVrmDxxDnOOlYJjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy +1vUhZscv6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +DigiCert Global Root G2 +======================= +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw +HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUx +MjAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 +dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI2/Ou8jqJ +kTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx1x7e/dfgy5SDN67sH0NO +3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQq2EGnI/yuum06ZIya7XzV+hdG82MHauV +BJVJ8zUtluNJbd134/tJS7SsVQepj5WztCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyM +UNGPHgm+F6HmIcr9g+UQvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQAB +o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV5uNu +5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY1Yl9PMWLSn/pvtsr +F9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4NeF22d+mQrvHRAiGfzZ0JFrabA0U +WTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NGFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBH +QRFXGU7Aj64GxJUTFy8bJZ918rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/ +iyK5S9kJRaTepLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +DigiCert Global Root G3 +======================= +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYD +VQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAw +MDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5k +aWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FGfp4tn+6O +YwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPOZ9wj/wMco+I+o0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNp +Yim8S8YwCgYIKoZIzj0EAwMDaAAwZQIxAK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y +3maTD/HMsQmP3Wyr+mt/oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34 +VOKa5Vt8sycX +-----END CERTIFICATE----- + +DigiCert Trusted Root G4 +======================== +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEw +HwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3yithZwuEp +pz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9o +k3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7Fsa +vOvJz82sNEBfsXpm7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGY +QJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6 +MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCiEhtm +mnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7 +f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFH +dL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8 +oR7FwI+isX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBhjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2SV1EY+CtnJYY +ZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd+SeuMIW59mdNOj6PWTkiU0Tr +yF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWcfFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy +7zBZLq7gcfJW5GqXb5JQbZaNaHqasjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iah +ixTXTBmyUEFxPT9NcCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN +5r5N0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie4u1Ki7wb +/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mIr/OSmbaz5mEP0oUA51Aa +5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tK +G48BtieVU+i2iW1bvGjUI+iLUaJW+fCmgKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP +82Z+ +-----END CERTIFICATE----- + +COMODO RSA Certification Authority +================================== +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCBhTELMAkGA1UE +BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG +A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMTE5MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMC +R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE +ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR6FSS0gpWsawNJN3Fz0Rn +dJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8Xpz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZ +FGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+ +5eNu/Nio5JIk2kNrYrhV/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pG +x8cgoLEfZd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z+pUX +2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7wqP/0uK3pN/u6uPQL +OvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZahSL0896+1DSJMwBGB7FY79tOi4lu3 +sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVICu9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+C +GCe01a60y1Dma/RMhnEw6abfFobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5 +WdYgGq/yapiqcrxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvlwFTPoCWOAvn9sKIN9SCYPBMt +rFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+ +nq6PK7o9mfjYcwlYRm6mnPTXJ9OV2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSg +tZx8jb8uk2IntznaFxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwW +sRqZCuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiKboHGhfKp +pC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmckejkk9u+UJueBPSZI9FoJA +zMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yLS0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHq +ZJx64SIDqZxubw5lT2yHh17zbqD5daWbQOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk52 +7RH89elWsn2/x20Kk4yl0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7I +LaZRfyHBNVOFBkpdn627G190 +-----END CERTIFICATE----- + +USERTrust RSA Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCBiDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK +ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK +ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCAEmUXNg7D2wiz +0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2j +Y0K2dvKpOyuR+OJv0OwWIJAJPuLodMkYtJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFn +RghRy4YUVD+8M/5+bJz/Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O ++T23LLb2VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT79uq +/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6c0Plfg6lZrEpfDKE +Y1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmTYo61Zs8liM2EuLE/pDkP2QKe6xJM +lXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97lc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8 +yexDJtC/QV9AqURE9JnnV4eeUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+ +eLf8ZxXhyVeEHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPFUp/L+M+ZBn8b2kMVn54CVVeW +FPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KOVWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ +7l8wXEskEVX/JJpuXior7gtNn3/3ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQ +Eg9zKC7F4iRO/Fjs8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM +8WcRiQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYzeSf7dNXGi +FSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZXHlKYC6SQK5MNyosycdi +yA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9c +J2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRBVXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGw +sAvgnEzDHNb842m1R0aBL6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gx +Q+6IHdfGjjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +USERTrust ECC Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqfloI+d61SRvU8Za2EurxtW2 +0eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinngo4N+LZfQYcTxmdwlkWOrfzCjtHDix6Ez +nPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNV +HQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBB +HU6+4WMBzzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbWRNZu +9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R4 +=========================== +-----BEGIN CERTIFICATE----- +MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprl +OQcJFspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAwDgYDVR0P +AQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61FuOJAf/sKbvu+M8k8o4TV +MAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGXkPoUVy0D7O48027KqGx2vKLeuwIgJ6iF +JzWbVsaj8kfSt24bAgAXqmemFZHe+pTsewv4n4Q= +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R5 +=========================== +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6 +SFkc8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8kehOvRnkmS +h5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYIKoZIzj0EAwMDaAAwZQIxAOVpEslu28Yx +uglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7 +yFz9SO8NdCKoCOJuxUnOxwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +Staat der Nederlanden Root CA - G3 +================================== +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJOTDEeMBwGA1UE +CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFhdCBkZXIgTmVkZXJsYW5kZW4g +Um9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloXDTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMC +TkwxHjAcBgNVBAoMFVN0YWF0IGRlciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5l +ZGVybGFuZGVuIFJvb3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4y +olQPcPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WWIkYFsO2t +x1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqXxz8ecAgwoNzFs21v0IJy +EavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFyKJLZWyNtZrVtB0LrpjPOktvA9mxjeM3K +Tj215VKb8b475lRgsGYeCasH/lSJEULR9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUur +mkVLoR9BvUhTFXFkC4az5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU5 +1nus6+N86U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7Ngzp +07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHPbMk7ccHViLVlvMDo +FxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXtBznaqB16nzaeErAMZRKQFWDZJkBE +41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTtXUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleu +yjWcLhL75LpdINyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD +U5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwpLiniyMMB8jPq +KqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8Ipf3YF3qKS9Ysr1YvY2WTxB1 +v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixpgZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA +8KCWAg8zxXHzniN9lLf9OtMJgwYh/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b +8KKaa8MFSu1BYBQw0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0r +mj1AfsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq4BZ+Extq +1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR1VmiiXTTn74eS9fGbbeI +JG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/QFH1T/U67cjF68IeHRaVesd+QnGTbksV +tzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM94B7IWcnMFk= +-----END CERTIFICATE----- + +Staat der Nederlanden EV Root CA +================================ +-----BEGIN CERTIFICATE----- +MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJOTDEeMBwGA1UE +CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFhdCBkZXIgTmVkZXJsYW5kZW4g +RVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0yMjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5M +MR4wHAYDVQQKDBVTdGFhdCBkZXIgTmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRl +cmxhbmRlbiBFViBSb290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkk +SzrSM4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nCUiY4iKTW +O0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3dZ//BYY1jTw+bbRcwJu+r +0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46prfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8 +Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13lpJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gV +XJrm0w912fxBmJc+qiXbj5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr +08C+eKxCKFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS/ZbV +0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0XcgOPvZuM5l5Tnrmd +74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH1vI4gnPah1vlPNOePqc7nvQDs/nx +fRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrPpx9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwa +ivsnuL8wbqg7MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI +eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u2dfOWBfoqSmu +c0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHSv4ilf0X8rLiltTMMgsT7B/Zq +5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTCwPTxGfARKbalGAKb12NMcIxHowNDXLldRqAN +b/9Zjr7dn3LDWyvfjFvO5QxGbJKyCqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tN +f1zuacpzEPuKqf2evTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi +5Dp6Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIaGl6I6lD4 +WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeLeG9QgkRQP2YGiqtDhFZK +DyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGy +eUN51q1veieQA6TqJIc/2b3Z6fJfUEkc7uzXLg== +-----END CERTIFICATE----- + +IdenTrust Commercial Root CA 1 +============================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBKMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBS +b290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQwMTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzES +MBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENB +IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ld +hNlT3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU+ehcCuz/ +mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gpS0l4PJNgiCL8mdo2yMKi +1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1bVoE/c40yiTcdCMbXTMTEl3EASX2MN0C +XZ/g1Ue9tOsbobtJSdifWwLziuQkkORiT0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl +3ZBWzvurpWCdxJ35UrCLvYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzy +NeVJSQjKVsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZKdHzV +WYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHTc+XvvqDtMwt0viAg +xGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hvl7yTmvmcEpB4eoCHFddydJxVdHix +uuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5NiGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZI +hvcNAQELBQADggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwtLRvM7Kqas6pg +ghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93nAbowacYXVKV7cndJZ5t+qnt +ozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmV +YjzlVYA211QC//G5Xc7UI2/YRYRKW2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUX +feu+h1sXIFRRk0pTAwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/ro +kTLql1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG4iZZRHUe +2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZmUlO+KWA2yUPHGNiiskz +Z2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7R +cGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +IdenTrust Public Sector Root CA 1 +================================= +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBNMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3Rv +ciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcNMzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJV +UzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBS +b290IENBIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTy +P4o7ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGyRBb06tD6 +Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlSbdsHyo+1W/CD80/HLaXI +rcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF/YTLNiCBWS2ab21ISGHKTN9T0a9SvESf +qy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoS +mJxZZoY+rfGwyj4GD3vwEUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFn +ol57plzy9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9VGxyh +LrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ2fjXctscvG29ZV/v +iDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsVWaFHVCkugyhfHMKiq3IXAAaOReyL +4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gDW/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8B +Af8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMw +DQYJKoZIhvcNAQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHVDRDtfULAj+7A +mgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9TaDKQGXSc3z1i9kKlT/YPyNt +GtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8GlwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFt +m6/n6J91eEyrRjuazr8FGF1NFTwWmhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMx +NRF4eKLg6TCMf4DfWN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4 +Mhn5+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJtshquDDI +ajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhAGaQdp/lLQzfcaFpPz+vC +ZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ +3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +Entrust Root Certification Authority - G2 +========================================= +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMCVVMxFjAUBgNV +BAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVy +bXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ug +b25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIw +HhcNMDkwNzA3MTcyNTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoT +DUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMx +OTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP +/vaCeb9zYQYKpSfYs1/TRU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXz +HHfV1IWNcCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hWwcKU +s/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1U1+cPvQXLOZprE4y +TGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0jaWvYkxN4FisZDQSA/i2jZRjJKRx +AgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ6 +0B7vfec7aVHUbI2fkBJmqzANBgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5Z +iXMRrEPR9RP/jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v1fN2D807iDgi +nWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4RnAuknZoh8/CbCzB428Hch0P+ +vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmHVHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xO +e4pIb4tF9g== +-----END CERTIFICATE----- + +Entrust Root Certification Authority - EC1 +========================================== +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkGA1UEBhMCVVMx +FjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVn +YWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXpl +ZCB1c2Ugb25seTEzMDEGA1UEAxMqRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +IC0gRUMxMB4XDTEyMTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYw +FAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2Fs +LXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQg +dXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt +IEVDMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHy +AsWfoPZb1YsGGYZPUxBtByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef +9eNi1KlHBz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVCR98crlOZF7ZvHH3h +vxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nXhTcGtXsI/esni0qU+eH6p44mCOh8 +kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- + +CFCA EV ROOT +============ +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjEwMC4GA1UE +CgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNB +IEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkxMjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEw +MC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQD +DAxDRkNBIEVWIFJPT1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnV +BU03sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpLTIpTUnrD +7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5/ZOkVIBMUtRSqy5J35DN +uF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp7hZZLDRJGqgG16iI0gNyejLi6mhNbiyW +ZXvKWfry4t3uMCz7zEasxGPrb382KzRzEpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7 +xzbh72fROdOXW3NiGUgthxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9f +py25IGvPa931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqotaK8K +gWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNgTnYGmE69g60dWIol +hdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfVPKPtl8MeNPo4+QgO48BdK4PRVmrJ +tqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hvcWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAf +BgNVHSMEGDAWgBTj/i39KNALtbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB +/wQEAwIBBjAdBgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObTej/tUxPQ4i9q +ecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdLjOztUmCypAbqTuv0axn96/Ua +4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBSESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sG +E5uPhnEFtC+NiWYzKXZUmhH4J/qyP5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfX +BDrDMlI1Dlb4pd19xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjn +aH9dCi77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN5mydLIhy +PDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe/v5WOaHIz16eGWRGENoX +kbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+ZAAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3C +ekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +OISTE WISeKey Global Root GB CA +=============================== +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBtMQswCQYDVQQG +EwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl +ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAw +MzJaFw0zOTEyMDExNTEwMzFaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYD +VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEds +b2JhbCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3HEokKtaX +scriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGxWuR51jIjK+FTzJlFXHtP +rby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk +9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNku7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4o +Qnc/nSMbsrY9gBQHTC5P99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvg +GUpuuy9rM2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZI +hvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrghcViXfa43FK8+5/ea4n32cZiZBKpD +dHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0 +VQreUGdNZtGn//3ZwLWoo4rOZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEui +HZeeevJuQHHfaPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- + +SZAFIR ROOT CA2 +=============== +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQELBQAwUTELMAkG +A1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6ZW5pb3dhIFMuQS4xGDAWBgNV +BAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkwNzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJ +BgNVBAYTAlBMMSgwJgYDVQQKDB9LcmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYD +VQQDDA9TWkFGSVIgUk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5Q +qEvNQLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT3PSQ1hNK +DJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw3gAeqDRHu5rr/gsUvTaE +2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr63fE9biCloBK0TXC5ztdyO4mTp4CEHCdJ +ckm1/zuVnsHMyAHs6A6KCpbns6aH5db5BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwi +ieDhZNRnvDF5YTy7ykHNXGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P +AQH/BAQDAgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsFAAOC +AQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw8PRBEew/R40/cof5 +O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOGnXkZ7/e7DDWQw4rtTw/1zBLZpD67 +oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCPoky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul +4+vJhaAlIDf7js4MNIThPIGyd05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6 ++/NNIxuZMzSgLvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- + +Certum Trusted Network CA 2 +=========================== +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCBgDELMAkGA1UE +BhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNVBAsTHkNlcnR1 +bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29y +ayBDQSAyMCIYDzIwMTExMDA2MDgzOTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQ +TDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENB +IDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWADGSdhhuWZGc/IjoedQF9 +7/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+o +CgCXhVqqndwpyeI1B+twTUrWwbNWuKFBOJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40b +Rr5HMNUuctHFY9rnY3lEfktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2p +uTRZCr+ESv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1mo130 +GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02isx7QBlrd9pPPV3WZ +9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOWOZV7bIBaTxNyxtd9KXpEulKkKtVB +Rgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgezTv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pye +hizKV/Ma5ciSixqClnrDvFASadgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vM +BhBgu4M1t15n3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZI +hvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQF/xlhMcQSZDe28cmk4gmb3DW +Al45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTfCVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuA +L55MYIR4PSFk1vtBHxgP58l1cb29XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMo +clm2q8KMZiYcdywmdjWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tM +pkT/WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jbAoJnwTnb +w3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksqP/ujmv5zMnHCnsZy4Ypo +J/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Kob7a6bINDd82Kkhehnlt4Fj1F4jNy3eFm +ypnTycUm/Q1oBEauttmbjL4ZvrHG8hnjXALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLX +is7VmFxWlgPF7ncGNf/P5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7 +zAYspsbiDrW5viSP +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions RootCA 2015 +======================================================= +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcT +BkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0 +aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAx +MTIxWjCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMg +QWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNV +BAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIw +MTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDC+Kk/G4n8PDwEXT2QNrCROnk8Zlrv +bTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+eh +iGsxr/CL0BgzuNtFajT0AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+ +6PAQZe104S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06CojXd +FPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV9Cz82XBST3i4vTwr +i5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrDgfgXy5I2XdGj2HUb4Ysn6npIQf1F +GQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2 +fu/Z8VFRfS0myGlZYeCsargqNhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9mu +iNX6hME6wGkoLfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVdctA4GGqd83EkVAswDQYJKoZI +hvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0IXtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+ +D1hYc2Ryx+hFjtyp8iY/xnmMsVMIM4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrM +d/K4kPFox/la/vot9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+y +d+2VZ5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/eaj8GsGsVn +82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnhX9izjFk0WaSrT2y7Hxjb +davYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQl033DlZdwJVqwjbDG2jJ9SrcR5q+ss7F +Jej6A7na+RZukYT1HCjI/CbM1xyQVqdfbzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVt +J94Cj8rDtSvK6evIIVM4pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGa +JI7ZjnHKe7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0vm9q +p/UsQu0yrbYhnr68 +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions ECC RootCA 2015 +=========================================================== +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0 +aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9u +cyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgRUNDIFJvb3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEw +MzcxMlowgaoxCzAJBgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmlj +IEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUQwQgYD +VQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIEVDQyBSb290 +Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKgQehLgoRc4vgxEZmGZE4JJS+dQS8KrjVP +dJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJajq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoK +Vlp8aQuqgAkkbH7BRqNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O +BBYEFLQiC4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaeplSTA +GiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7SofTUwJCA3sS61kFyjn +dc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- + +ISRG Root X1 +============ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UE +BhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQD +EwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQG +EwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMT +DElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54r +Vygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj1 +3Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8K +b4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCN +Aymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ +4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf +1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFu +hjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQH +usEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/r +OPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4G +A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY +9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV +0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwt +hDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJw +TdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nx +e5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZA +JzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahD +YVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9n +JEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJ +m+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- + +AC RAIZ FNMT-RCM +================ +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsxCzAJBgNVBAYT +AkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTAeFw0wODEw +MjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJD +TTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBALpxgHpMhm5/yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcf +qQgfBBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAzWHFctPVr +btQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxFtBDXaEAUwED653cXeuYL +j2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z374jNUUeAlz+taibmSXaXvMiwzn15Cou +08YfxGyqxRxqAQVKL9LFwag0Jl1mpdICIfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mw +WsXmo8RZZUc1g16p6DULmbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnT +tOmlcYF7wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peSMKGJ +47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2ZSysV4999AeU14EC +ll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMetUqIJ5G+GR4of6ygnXYMgrwTJbFaa +i0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FPd9xf3E6Jobd2Sn9R2gzL+HYJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1o +dHRwOi8vd3d3LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1RXxlDPiyN8+s +D8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYMLVN0V2Ue1bLdI4E7pWYjJ2cJ +j+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrT +Qfv6MooqtyuGC2mDOL7Nii4LcK2NJpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW ++YJF1DngoABd15jmfZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7 +Ixjp6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp1txyM/1d +8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B9kiABdcPUXmsEKvU7ANm +5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wokRqEIr9baRRmW1FMdW4R58MD3R++Lj8UG +rp1MYp3/RgT408m2ECVAdf4WqslKYIYvuu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- + +Amazon Root CA 1 +================ +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsFADA5MQswCQYD +VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAxMB4XDTE1 +MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv +bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBALJ4gHHKeNXjca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgH +FzZM9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qwIFAGbHrQ +gLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6VOujw5H5SNz/0egwLX0t +dHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L93FcXmn/6pUCyziKrlA4b9v7LWIbxcce +VOF34GfID5yHI9Y/QCB/IIDEgEw+OyQmjgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3 +DQEBCwUAA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDIU5PM +CCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUsN+gDS63pYaACbvXy +8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vvo/ufQJVtMVT8QtPHRh8jrdkPSHCa +2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2 +xJNDd2ZhwLnoQdeXeGADbkpyrqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- + +Amazon Root CA 2 +================ +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwFADA5MQswCQYD +VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAyMB4XDTE1 +MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv +bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBAK2Wny2cSkxKgXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4 +kHbZW0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg1dKmSYXp +N+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K8nu+NQWpEjTj82R0Yiw9 +AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvd +fLC6HM783k81ds8P+HgfajZRRidhW+mez/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAEx +kv8LV/SasrlX6avvDXbR8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSS +btqDT6ZjmUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz7Mt0 +Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6+XUyo05f7O0oYtlN +c/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI0u1ufm8/0i2BWSlmy5A5lREedCf+ +3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSw +DPBMMPQFWAJI/TPlUq9LhONmUjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oA +A7CXDpO8Wqj2LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kSk5Nrp+gvU5LE +YFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl7uxMMne0nxrpS10gxdr9HIcW +xkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygmbtmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQ +gj9sAq+uEjonljYE1x2igGOpm/HlurR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbW +aQbLU8uz/mtBzUF+fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoV +Yh63n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE76KlXIx3 +KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H9jVlpNMKVv/1F2Rs76gi +JUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT4PsJYGw= +-----END CERTIFICATE----- + +Amazon Root CA 3 +================ +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5MQswCQYDVQQG +EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAzMB4XDTE1MDUy +NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ +MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZB +f8ANm+gBG1bG8lKlui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjr +Zt6jQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSrttvXBp43 +rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkrBqWTrBqYaGFy+uGh0Psc +eGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteMYyRIHN8wfdVoOw== +-----END CERTIFICATE----- + +Amazon Root CA 4 +================ +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5MQswCQYDVQQG +EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSA0MB4XDTE1MDUy +NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ +MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN +/sGKe0uoe0ZLY7Bi9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri +83BkM6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WBMAoGCCqGSM49BAMDA2gA +MGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlwCkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1 +AE47xDqUEpHJWEadIRNyp4iciuRMStuW1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- + +TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 +============================================= +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIxGDAWBgNVBAcT +D0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxpbXNlbCB2ZSBUZWtub2xvamlr +IEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0wKwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24g +TWVya2V6aSAtIEthbXUgU00xNjA0BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRp +ZmlrYXNpIC0gU3VydW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYD +VQQGEwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXllIEJpbGlt +c2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklUQUsxLTArBgNVBAsTJEth +bXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBTTTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11 +IFNNIFNTTCBLb2sgU2VydGlmaWthc2kgLSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAr3UwM6q7a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y8 +6Ij5iySrLqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INrN3wc +wv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2XYacQuFWQfw4tJzh0 +3+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/iSIzL+aFCr2lqBs23tPcLG07xxO9 +WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4fAJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQU +ZT/HiobGPN08VFw1+DrtUgxHV8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJ +KoZIhvcNAQELBQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPfIPP54+M638yc +lNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4lzwDGrpDxpa5RXI4s6ehlj2R +e37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0j +q5Rm+K37DwhuJi1/FwcJsoz7UMCflo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- + +GDCA TrustAUTH R5 ROOT +====================== +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCQ04xMjAw +BgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8wHQYDVQQD +DBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVow +YjELMAkGA1UEBhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJjDp6L3TQs +AlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBjTnnEt1u9ol2x8kECK62p +OqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+uKU49tm7srsHwJ5uu4/Ts765/94Y9cnrr +pftZTqfrlYwiOXnhLQiPzLyRuEH3FMEjqcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ +9Cy5WmYqsBebnh52nUpmMUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQ +xXABZG12ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloPzgsM +R6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3GkL30SgLdTMEZeS1SZ +D2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeCjGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4 +oR24qoAATILnsn8JuLwwoC8N9VKejveSswoAHQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx +9hoh49pwBiFYFIeFd3mqgnkCAwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlR +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZmDRd9FBUb1Ov9 +H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5COmSdI31R9KrO9b7eGZONn35 +6ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ryL3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd ++PwyvzeG5LuOmCd+uh8W4XAR8gPfJWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQ +HtZa37dG/OaG+svgIHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBD +F8Io2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV09tL7ECQ +8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQXR4EzzffHqhmsYzmIGrv +/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrqT8p+ck0LcIymSLumoRT2+1hEmRSuqguT +aaApJUqlyyvdimYHFngVV3Eb7PVHhPOeMTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- + +TrustCor RootCert CA-1 +====================== +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIJANqb7HHzA7AZMA0GCSqGSIb3DQEBCwUAMIGkMQswCQYDVQQGEwJQQTEP +MA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3Ig +U3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3Jp +dHkxHzAdBgNVBAMMFlRydXN0Q29yIFJvb3RDZXJ0IENBLTEwHhcNMTYwMjA0MTIzMjE2WhcNMjkx +MjMxMTcyMzE2WjCBpDELMAkGA1UEBhMCUEExDzANBgNVBAgMBlBhbmFtYTEUMBIGA1UEBwwLUGFu +YW1hIENpdHkxJDAiBgNVBAoMG1RydXN0Q29yIFN5c3RlbXMgUy4gZGUgUi5MLjEnMCUGA1UECwwe +VHJ1c3RDb3IgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MR8wHQYDVQQDDBZUcnVzdENvciBSb290Q2Vy +dCBDQS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv463leLCJhJrMxnHQFgKq1mq +jQCj/IDHUHuO1CAmujIS2CNUSSUQIpidRtLByZ5OGy4sDjjzGiVoHKZaBeYei0i/mJZ0PmnK6bV4 +pQa81QBeCQryJ3pS/C3Vseq0iWEk8xoT26nPUu0MJLq5nux+AHT6k61sKZKuUbS701e/s/OojZz0 +JEsq1pme9J7+wH5COucLlVPat2gOkEz7cD+PSiyU8ybdY2mplNgQTsVHCJCZGxdNuWxu72CVEY4h +gLW9oHPY0LJ3xEXqWib7ZnZ2+AYfYW0PVcWDtxBWcgYHpfOxGgMFZA6dWorWhnAbJN7+KIor0Gqw +/Hqi3LJ5DotlDwIDAQABo2MwYTAdBgNVHQ4EFgQU7mtJPHo/DeOxCbeKyKsZn3MzUOcwHwYDVR0j +BBgwFoAU7mtJPHo/DeOxCbeKyKsZn3MzUOcwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwDQYJKoZIhvcNAQELBQADggEBACUY1JGPE+6PHh0RU9otRCkZoB5rMZ5NDp6tPVxBb5UrJKF5 +mDo4Nvu7Zp5I/5CQ7z3UuJu0h3U/IJvOcs+hVcFNZKIZBqEHMwwLKeXx6quj7LUKdJDHfXLy11yf +ke+Ri7fc7Waiz45mO7yfOgLgJ90WmMCV1Aqk5IGadZQ1nJBfiDcGrVmVCrDRZ9MZyonnMlo2HD6C +qFqTvsbQZJG2z9m2GM/bftJlo6bEjhcxwft+dtvTheNYsnd6djtsL1Ac59v2Z3kf9YKVmgenFK+P +3CghZwnS1k1aHBkcjndcw5QkPTJrS37UeJSDvjdNzl/HHk484IkzlQsPpTLWPFp5LBk= +-----END CERTIFICATE----- + +TrustCor RootCert CA-2 +====================== +-----BEGIN CERTIFICATE----- +MIIGLzCCBBegAwIBAgIIJaHfyjPLWQIwDQYJKoZIhvcNAQELBQAwgaQxCzAJBgNVBAYTAlBBMQ8w +DQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQwIgYDVQQKDBtUcnVzdENvciBT +eXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRydXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0 +eTEfMB0GA1UEAwwWVHJ1c3RDb3IgUm9vdENlcnQgQ0EtMjAeFw0xNjAyMDQxMjMyMjNaFw0zNDEy +MzExNzI2MzlaMIGkMQswCQYDVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5h +bWEgQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U +cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRydXN0Q29yIFJvb3RDZXJ0 +IENBLTIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnIG7CKqJiJJWQdsg4foDSq8Gb +ZQWU9MEKENUCrO2fk8eHyLAnK0IMPQo+QVqedd2NyuCb7GgypGmSaIwLgQ5WoD4a3SwlFIIvl9Nk +RvRUqdw6VC0xK5mC8tkq1+9xALgxpL56JAfDQiDyitSSBBtlVkxs1Pu2YVpHI7TYabS3OtB0PAx1 +oYxOdqHp2yqlO/rOsP9+aij9JxzIsekp8VduZLTQwRVtDr4uDkbIXvRR/u8OYzo7cbrPb1nKDOOb +XUm4TOJXsZiKQlecdu/vvdFoqNL0Cbt3Nb4lggjEFixEIFapRBF37120Hapeaz6LMvYHL1cEksr1 +/p3C6eizjkxLAjHZ5DxIgif3GIJ2SDpxsROhOdUuxTTCHWKF3wP+TfSvPd9cW436cOGlfifHhi5q +jxLGhF5DUVCcGZt45vz27Ud+ez1m7xMTiF88oWP7+ayHNZ/zgp6kPwqcMWmLmaSISo5uZk3vFsQP +eSghYA2FFn3XVDjxklb9tTNMg9zXEJ9L/cb4Qr26fHMC4P99zVvh1Kxhe1fVSntb1IVYJ12/+Ctg +rKAmrhQhJ8Z3mjOAPF5GP/fDsaOGM8boXg25NSyqRsGFAnWAoOsk+xWq5Gd/bnc/9ASKL3x74xdh +8N0JqSDIvgmk0H5Ew7IwSjiqqewYmgeCK9u4nBit2uBGF6zPXQIDAQABo2MwYTAdBgNVHQ4EFgQU +2f4hQG6UnrybPZx9mCAZ5YwwYrIwHwYDVR0jBBgwFoAU2f4hQG6UnrybPZx9mCAZ5YwwYrIwDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBAJ5Fngw7tu/h +Osh80QA9z+LqBrWyOrsGS2h60COXdKcs8AjYeVrXWoSK2BKaG9l9XE1wxaX5q+WjiYndAfrs3fnp +kpfbsEZC89NiqpX+MWcUaViQCqoL7jcjx1BRtPV+nuN79+TMQjItSQzL/0kMmx40/W5ulop5A7Zv +2wnL/V9lFDfhOPXzYRZY5LVtDQsEGz9QLX+zx3oaFoBg+Iof6Rsqxvm6ARppv9JYx1RXCI/hOWB3 +S6xZhBqI8d3LT3jX5+EzLfzuQfogsL7L9ziUwOHQhQ+77Sxzq+3+knYaZH9bDTMJBzN7Bj8RpFxw +PIXAz+OQqIN3+tvmxYxoZxBnpVIt8MSZj3+/0WvitUfW2dCFmU2Umw9Lje4AWkcdEQOsQRivh7dv +DDqPys/cA8GiCcjl/YBeyGBCARsaU1q7N6a3vLqE6R5sGtRk2tRD/pOLS/IseRYQ1JMLiI+h2IYU +RpFHmygk71dSTlxCnKr3Sewn6EAes6aJInKc9Q0ztFijMDvd1GpUk74aTfOTlPf8hAs/hCBcNANE +xdqtvArBAs8e5ZTZ845b2EzwnexhF7sUMlQMAimTHpKG9n/v55IFDlndmQguLvqcAFLTxWYp5KeX +RKQOKIETNcX2b2TmQcTVL8w0RSXPQQCWPUouwpaYT05KnJe32x+SMsj/D1Fu1uwJ +-----END CERTIFICATE----- + +TrustCor ECA-1 +============== +-----BEGIN CERTIFICATE----- +MIIEIDCCAwigAwIBAgIJAISCLF8cYtBAMA0GCSqGSIb3DQEBCwUAMIGcMQswCQYDVQQGEwJQQTEP +MA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3Ig +U3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3Jp +dHkxFzAVBgNVBAMMDlRydXN0Q29yIEVDQS0xMB4XDTE2MDIwNDEyMzIzM1oXDTI5MTIzMTE3Mjgw +N1owgZwxCzAJBgNVBAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5 +MSQwIgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRydXN0Q29y +IENlcnRpZmljYXRlIEF1dGhvcml0eTEXMBUGA1UEAwwOVHJ1c3RDb3IgRUNBLTEwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPj+ARtZ+odnbb3w9U73NjKYKtR8aja+3+XzP4Q1HpGjOR +MRegdMTUpwHmspI+ap3tDvl0mEDTPwOABoJA6LHip1GnHYMma6ve+heRK9jGrB6xnhkB1Zem6g23 +xFUfJ3zSCNV2HykVh0A53ThFEXXQmqc04L/NyFIduUd+Dbi7xgz2c1cWWn5DkR9VOsZtRASqnKmc +p0yJF4OuowReUoCLHhIlERnXDH19MURB6tuvsBzvgdAsxZohmz3tQjtQJvLsznFhBmIhVE5/wZ0+ +fyCMgMsq2JdiyIMzkX2woloPV+g7zPIlstR8L+xNxqE6FXrntl019fZISjZFZtS6mFjBAgMBAAGj +YzBhMB0GA1UdDgQWBBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAfBgNVHSMEGDAWgBREnkj1zG1I1KBL +f/5ZJC+Dl5mahjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF +AAOCAQEABT41XBVwm8nHc2FvcivUwo/yQ10CzsSUuZQRg2dd4mdsdXa/uwyqNsatR5Nj3B5+1t4u +/ukZMjgDfxT2AHMsWbEhBuH7rBiVDKP/mZb3Kyeb1STMHd3BOuCYRLDE5D53sXOpZCz2HAF8P11F +hcCF5yWPldwX8zyfGm6wyuMdKulMY/okYWLW2n62HGz1Ah3UKt1VkOsqEUc8Ll50soIipX1TH0Xs +J5F95yIW6MBoNtjG8U+ARDL54dHRHareqKucBK+tIA5kmE2la8BIWJZpTdwHjFGTot+fDz2LYLSC +jaoITmJF4PkL0uDgPFveXHEnJcLmA4GLEFPjx1WitJ/X5g== +-----END CERTIFICATE----- + +SSL.com Root Certification Authority RSA +======================================== +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxDjAM +BgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24x +MTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYw +MjEyMTczOTM5WhcNNDEwMjEyMTczOTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx +EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NM +LmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2RxFdHaxh3a3by/ZPkPQ/C +Fp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aXqhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8 +P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcCC52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/ge +oeOy3ZExqysdBP+lSgQ36YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkp +k8zruFvh/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrFYD3Z +fBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93EJNyAKoFBbZQ+yODJ +gUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVcUS4cK38acijnALXRdMbX5J+tB5O2 +UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi8 +1xtZPCvM8hnIk2snYxnP/Okm+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4s +bE6x/c+cCbqiM+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGVcpNxJK1ok1iOMq8bs3AD/CUr +dIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBcHadm47GUBwwyOabqG7B52B2ccETjit3E+ZUf +ijhDPwGFpUenPUayvOUiaPd7nNgsPgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAsl +u1OJD7OAUN5F7kR/q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjq +erQ0cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jra6x+3uxj +MxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90IH37hVZkLId6Tngr75qNJ +vTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/YK9f1JmzJBjSWFupwWRoyeXkLtoh/D1JI +Pb9s2KJELtFOt3JY04kTlf5Eq/jXixtunLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406y +wKBjYZC6VWg3dGq2ktufoYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NI +WuuA8ShYIc2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- + +SSL.com Root Certification Authority ECC +======================================== +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMCVVMxDjAMBgNV +BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xMTAv +BgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEy +MTgxNDAzWhcNNDEwMjEyMTgxNDAzWjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAO +BgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuBBAAiA2IA +BEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI7Z4INcgn64mMU1jrYor+ +8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPgCemB+vNH06NjMGEwHQYDVR0OBBYEFILR +hXMw5zUE044CkvvlpNHEIejNMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTT +jgKS++Wk0cQh6M0wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCW +e+0F+S8Tkdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+gA0z +5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- + +SSL.com EV Root Certification Authority RSA R2 +============================================== +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNVBAYTAlVTMQ4w +DAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9u +MTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MB4XDTE3MDUzMTE4MTQzN1oXDTQyMDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQI +DAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYD +VQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvqM0fNTPl9fb69LT3w23jh +hqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssufOePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7w +cXHswxzpY6IXFJ3vG2fThVUCAtZJycxa4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTO +Zw+oz12WGQvE43LrrdF9HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+ +B6KjBSYRaZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcAb9Zh +CBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQGp8hLH94t2S42Oim +9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQVPWKchjgGAGYS5Fl2WlPAApiiECto +RHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMOpgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+Slm +JuwgUHfbSguPvuUCYHBBXtSuUDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48 ++qvWBkofZ6aYMBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa49QaAJadz20Zp +qJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBWs47LCp1Jjr+kxJG7ZhcFUZh1 +++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nx +Y/hoLVUE0fKNsKTPvDxeH3jnpaAgcLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2G +guDKBAdRUNf/ktUM79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDz +OFSz/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXtll9ldDz7 +CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEmKf7GUmG6sXP/wwyc5Wxq +lD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKKQbNmC1r7fSOl8hqw/96bg5Qu0T/fkreR +rwU7ZcegbLHNYhLDkBvjJc40vG93drEQw/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1 +hlMYegouCRw2n5H9gooiS9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX +9hwJ1C07mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- + +SSL.com EV Root Certification Authority ECC +=========================================== +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMCVVMxDjAMBgNV +BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xNDAy +BgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYw +MjEyMTgxNTIzWhcNNDEwMjEyMTgxNTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx +EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NM +LmNvbSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMAVIbc/R/fALhBYlzccBYy +3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1KthkuWnBaBu2+8KGwytAJKaNjMGEwHQYDVR0O +BBYEFFvKXuXe0oGqzagtZFG22XKbl+ZPMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe +5d7SgarNqC1kUbbZcpuX5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJ +N+vp1RPZytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZgh5Mm +m7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- + +GlobalSign Root CA - R6 +======================= +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEgMB4GA1UECxMX +R2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds +b2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQxMjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9i +YWxTaWduIFJvb3QgQ0EgLSBSNjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFs +U2lnbjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQss +grRIxutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1kZguSgMpE +3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxDaNc9PIrFsmbVkJq3MQbF +vuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJwLnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqM +PKq0pPbzlUoSB239jLKJz9CgYXfIWHSw1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+ +azayOeSsJDa38O+2HBNXk7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05O +WgtH8wY2SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/hbguy +CLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4nWUx2OVvq+aWh2IMP +0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpYrZxCRXluDocZXFSxZba/jJvcE+kN +b7gu3GduyYsRtYQUigAZcIN5kZeR1BonvzceMgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQE +AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNV +HSMEGDAWgBSubAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGtIxg93eFyRJa0 +lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr6155wsTLxDKZmOMNOsIeDjHfrY +BzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLjvUYAGm0CuiVdjaExUd1URhxN25mW7xocBFym +Fe944Hn+Xds+qkxV/ZoVqW/hpvvfcDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr +3TsTjxKM4kEaSHpzoHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB1 +0jZpnOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfspA9MRf/T +uTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+vJJUEeKgDu+6B5dpffItK +oZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+t +JDfLRVpOoERIyNiwmcUVhAn21klJwGW45hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- + +OISTE WISeKey Global Root GC CA +=============================== +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQswCQYDVQQGEwJD +SDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNlZDEo +MCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRa +Fw00MjA1MDkwOTU4MzNaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQL +ExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4nieUqjFqdr +VCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4Wp2OQ0jnUsYd4XxiWD1Ab +NTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7TrYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0E +AwMDaAAwZQIwJsdpW9zV57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtk +AjEA2zQgMgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- + +GTS Root R1 +=========== +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQbkepxUtHDA3sM9CJuRz04TANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQG +EwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJv +b3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAG +A1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx +9vaMf/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7wCl7r +aKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjwTcLCeoiKu7rPWRnW +r4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0PfyblqAj+lug8aJRT7oM6iCsVlgmy4HqM +LnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly +4cpk9+aCEI3oncKKiPo4Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr +06zqkUspzBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70paDPvOmbsB4om +3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrNVjzRlwW5y0vtOUucxD/SVRNu +JLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEM +BQADggIBADiWCu49tJYeX++dnAsznyvgyv3SjgofQXSlfKqE1OXyHuY3UjKcC9FhHb8owbZEKTV1 +d5iyfNm9dKyKaOOpMQkpAWBz40d8U6iQSifvS9efk+eCNs6aaAyC58/UEBZvXw6ZXPYfcX3v73sv +fuo21pdwCxXu11xWajOl40k4DLh9+42FpLFZXvRq4d2h9mREruZRgyFmxhE+885H7pwoHyXa/6xm +ld01D1zvICxi/ZG6qcz8WpyTgYMpl0p8WnK0OdC3d8t5/Wk6kjftbjhlRn7pYL15iJdfOBL07q9b +gsiG1eGZbYwE8na6SfZu6W0eX6DvJ4J2QPim01hcDyxC2kLGe4g0x8HYRZvBPsVhHdljUEn2NIVq +4BjFbkerQUIpm/ZgDdIx02OYI5NaAIFItO/Nis3Jz5nu2Z6qNuFoS3FJFDYoOj0dzpqPJeaAcWEr +tXvM+SUWgeExX6GjfhaknBZqlxi9dnKlC54dNuYvoS++cJEPqOba+MSSQGwlfnuzCdyyF62ARPBo +pY+Udf90WuioAnwMCeKpSwughQtiue+hMZL77/ZRBIls6Kl0obsXs7X9SQ98POyDGCBDTtWTurQ0 +sR8WNh8M5mQ5Fkzc4P4dyKliPUDqysU0ArSuiYgzNdwsE3PYJ/HQcu51OyLemGhmW/HGY0dVHLql +CFF1pkgl +-----END CERTIFICATE----- + +GTS Root R2 +=========== +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQbkepxlqz5yDFMJo/aFLybzANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQG +EwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJv +b3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAG +A1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTuk +k3LvCvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY6Dlo +7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAuMC6C/Pq8tBcKSOWI +m8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7kRXuJVfeKH2JShBKzwkCX44ofR5Gm +dFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWgf9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbu +ak7MkogwTZq9TwtImoS1mKPV+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscsz +cTJGr61K8YzodDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW +Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKaG73Vululycsl +aVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCqgc7dGtxRcw1PcOnlthYhGXmy +5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEM +BQADggIBALZp8KZ3/p7uC4Gt4cCpx/k1HUCCq+YEtN/L9x0Pg/B+E02NjO7jMyLDOfxA325BS0JT +vhaI8dI4XsRomRyYUpOM52jtG2pzegVATX9lO9ZY8c6DR2Dj/5epnGB3GFW1fgiTz9D2PGcDFWEJ ++YF59exTpJ/JjwGLc8R3dtyDovUMSRqodt6Sm2T4syzFJ9MHwAiApJiS4wGWAqoC7o87xdFtCjMw +c3i5T1QWvwsHoaRc5svJXISPD+AVdyx+Jn7axEvbpxZ3B7DNdehyQtaVhJ2Gg/LkkM0JR9SLA3Da +WsYDQvTtN6LwG1BUSw7YhN4ZKJmBR64JGz9I0cNv4rBgF/XuIwKl2gBbbZCr7qLpGzvpx0QnRY5r +n/WkhLx3+WuXrD5RRaIRpsyF7gpo8j5QOHokYh4XIDdtak23CZvJ/KRY9bb7nE4Yu5UC56Gtmwfu +Nmsk0jmGwZODUNKBRqhfYlcsu2xkiAhu7xNUX90txGdj08+JN7+dIPT7eoOboB6BAFDC5AwiWVIQ +7UNWhwD4FFKnHYuTjKJNRn8nxnGbJN7k2oaLDX5rIMHAnuFl2GqjpuiFizoHCBy69Y9Vmhh1fuXs +gWbRIXOhNUQLgD1bnF5vKheW0YMjiGZt5obicDIvUiLnyOd/xCxgXS/Dr55FBcOEArf9LAhST4Ld +o/DUhgkC +-----END CERTIFICATE----- + +GTS Root R3 +=========== +-----BEGIN CERTIFICATE----- +MIICDDCCAZGgAwIBAgIQbkepx2ypcyRAiQ8DVd2NHTAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJV +UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg +UjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE +ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUU +Rout736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL24Cej +QjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTB8Sa6oC2uhYHP +0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEAgFukfCPAlaUs3L6JbyO5o91lAFJekazInXJ0 +glMLfalAvWhgxeG4VDvBNhcl2MG9AjEAnjWSdIUlUfUk7GRSJFClH9voy8l27OyCbvWFGFPouOOa +KaqW04MjyaR7YbPMAuhd +-----END CERTIFICATE----- + +GTS Root R4 +=========== +-----BEGIN CERTIFICATE----- +MIICCjCCAZGgAwIBAgIQbkepyIuUtui7OyrYorLBmTAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJV +UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg +UjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE +ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa +6zzuhXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvRHYqj +QjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSATNbrdP9JNqPV +2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNnADBkAjBqUFJ0CMRw3J5QdCHojXohw0+WbhXRIjVhLfoI +N+4Zba3bssx9BzT1YBkstTTZbyACMANxsbqjYAuG7ZoIapVon+Kz4ZNkfF6Tpt95LY2F45TPI11x +zPKwTdb+mciUqXWi4w== +-----END CERTIFICATE----- + +UCA Global G2 Root +================== +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQG +EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBHbG9iYWwgRzIgUm9vdDAeFw0x +NjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0xCzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlU +cnVzdDEbMBkGA1UEAwwSVUNBIEdsb2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxeYrb3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmT +oni9kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzmVHqUwCoV +8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/RVogvGjqNO7uCEeBHANBS +h6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDcC/Vkw85DvG1xudLeJ1uK6NjGruFZfc8o +LTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIjtm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/ +R+zvWr9LesGtOxdQXGLYD0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBe +KW4bHAyvj5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6DlNaBa +4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6iIis7nCs+dwp4wwc +OxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznPO6Q0ibd5Ei9Hxeepl2n8pndntd97 +8XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFIHEjMz15DD/pQwIX4wVZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo +5sOASD0Ee/ojL3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 +1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl1qnN3e92mI0A +Ds0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oUb3n09tDh05S60FdRvScFDcH9 +yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LVPtateJLbXDzz2K36uGt/xDYotgIVilQsnLAX +c47QN6MUPJiVAAwpBVueSUmxX8fjy88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHo +jhJi6IjMtX9Gl8CbEGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZk +bxqgDMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI+Vg7RE+x +ygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGyYiGqhkCyLmTTX8jjfhFn +RR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bXUB+K+wb1whnw0A== +-----END CERTIFICATE----- + +UCA Extended Validation Root +============================ +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBHMQswCQYDVQQG +EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9u +IFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMxMDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8G +A1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrs +iWogD4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvSsPGP2KxF +Rv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aopO2z6+I9tTcg1367r3CTu +eUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dksHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR +59mzLC52LqGj3n5qiAno8geK+LLNEOfic0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH +0mK1lTnj8/FtDw5lhIpjVMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KR +el7sFsLzKuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/TuDv +B0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41Gsx2VYVdWf6/wFlth +WG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs1+lvK9JKBZP8nm9rZ/+I8U6laUpS +NwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQDfwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS +3H5aBZ8eNJr34RQwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQEL +BQADggIBADaNl8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR +ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQVBcZEhrxH9cM +aVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5c6sq1WnIeJEmMX3ixzDx/BR4 +dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb ++7lsq+KePRXBOy5nAliRn+/4Qh8st2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOW +F3sGPjLtx7dCvHaj2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwi +GpWOvpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2CxR9GUeOc +GMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmxcmtpzyKEC2IPrNkZAJSi +djzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbMfjKaiJUINlK73nZfdklJrX+9ZSCyycEr +dhh2n1ax +-----END CERTIFICATE----- + +Certigna Root CA +================ +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAwWjELMAkGA1UE +BhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAwMiA0ODE0NjMwODEwMDAzNjEZ +MBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0xMzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjda +MFoxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYz +MDgxMDAwMzYxGTAXBgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sOty3tRQgX +stmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9MCiBtnyN6tMbaLOQdLNyz +KNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPuI9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8 +JXrJhFwLrN1CTivngqIkicuQstDuI7pmTLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16 +XdG+RCYyKfHx9WzMfgIhC59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq +4NYKpkDfePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3YzIoej +wpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWTCo/1VTp2lc5ZmIoJ +lXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1kJWumIWmbat10TWuXekG9qxf5kBdI +jzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp/ +/TBt2dzhauH8XwIDAQABo4IBGjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +HQYDVR0OBBYEFBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczovL3d3d3cuY2Vy +dGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilodHRwOi8vY3JsLmNlcnRpZ25h +LmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYraHR0cDovL2NybC5kaGlteW90aXMuY29tL2Nl +cnRpZ25hcm9vdGNhLmNybDANBgkqhkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOIt +OoldaDgvUSILSo3L6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxP +TGRGHVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH60BGM+RFq +7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncBlA2c5uk5jR+mUYyZDDl3 +4bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdio2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd +8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS +6Cvu5zHbugRqh5jnxV/vfaci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaY +tlu3zM63Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayhjWZS +aX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw3kAP+HwV96LOPNde +E4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- + +emSign Root CA - G1 +=================== +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJJTjET +MBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl +ZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBHMTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgx +ODMwMDBaMGcxCzAJBgNVBAYTAklOMRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVk +aHJhIFRlY2hub2xvZ2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQzf2N4aLTN +LnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO8oG0x5ZOrRkVUkr+PHB1 +cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aqd7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHW +DV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhMtTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ +6DqS0hdW5TUaQBw+jSztOd9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrH +hQIDAQABo0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQDAgEG +MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31xPaOfG1vR2vjTnGs2 +vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjMwiI/aTvFthUvozXGaCocV685743Q +NcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6dGNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q ++Mri/Tm3R7nrft8EI6/6nAYH6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeih +U80Bv2noWgbyRQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- + +emSign ECC Root CA - G3 +======================= +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQGEwJJTjETMBEG +A1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg +MB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4 +MTgzMDAwWjBrMQswCQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11 +ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0WXTsuwYc +58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xySfvalY8L1X44uT6EYGQIr +MgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuBzhccLikenEhjQjAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+D +CBeQyh+KTOgNG3qxrdWBCUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7 +jHvrZQnD+JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- + +emSign Root CA - C1 +=================== +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkGA1UEBhMCVVMx +EzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNp +Z24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UE +BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQD +ExNlbVNpZ24gUm9vdCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+up +ufGZBczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZHdPIWoU/ +Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH3DspVpNqs8FqOp099cGX +OFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvHGPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4V +I5b2P/AgNBbeCsbEBEV5f6f9vtKppa+cxSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleooms +lMuoaJuvimUnzYnu3Yy1aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+ +XJGFehiqTbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD +ggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87/kOXSTKZEhVb3xEp +/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4kqNPEjE2NuLe/gDEo2APJ62gsIq1 +NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrGYQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9 +wC68AivTxEDkigcxHpvOJpkT+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQ +BmIMMMAVSKeoWXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE----- + +emSign ECC Root CA - C3 +======================= +-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQGEwJVUzETMBEG +A1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMxIDAeBgNVBAMTF2VtU2lnbiBF +Q0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UE +BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQD +ExdlbVNpZ24gRUNDIFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd +6bciMK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4OjavtisIGJAnB9 +SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0OBBYEFPtaSNCAIEDyqOkA +B2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gA +MGUCMQC02C8Cif22TGK6Q04ThHK1rt0c3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwU +ZOR8loMRnLDRWmFLpg9J0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE----- + +Hongkong Post Root CA 3 +======================= +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQELBQAwbzELMAkG +A1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJSG9uZyBLb25nMRYwFAYDVQQK +Ew1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25na29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2 +MDMwMjI5NDZaFw00MjA2MDMwMjI5NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtv +bmcxEjAQBgNVBAcTCUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMX +SG9uZ2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz +iNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFOdem1p+/l6TWZ5Mwc50tf +jTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mIVoBc+L0sPOFMV4i707mV78vH9toxdCim +5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOe +sL4jpNrcyCse2m5FHomY2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj +0mRiikKYvLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+TtbNe/ +JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZbx39ri1UbSsUgYT2u +y1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+l2oBlKN8W4UdKjk60FSh0Tlxnf0h ++bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YKTE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsG +xVd7GYYKecsAyVKvQv83j+GjHno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwID +AQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEwDQYJKoZIhvcN +AQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG7BJ8dNVI0lkUmcDrudHr9Egw +W62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCkMpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWld +y8joRTnU+kLBEUx3XZL7av9YROXrgZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov ++BS5gLNdTaqX4fnkGMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDc +eqFS3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJmOzj/2ZQw +9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+l6mc1X5VTMbeRRAc6uk7 +nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6cJfTzPV4e0hz5sy229zdcxsshTrD3mUcY +hcErulWuBurQB7Lcq9CClnXO0lD+mefPL5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB +60PZ2Pierc+xYw5F9KBaLJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fq +dBb9HxEGmpv0 +-----END CERTIFICATE----- + +Entrust Root Certification Authority - G4 +========================================= +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAwgb4xCzAJBgNV +BAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3Qu +bmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1 +dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0eSAtIEc0MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYT +AlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhv +cml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhv +cml0eSAtIEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3D +umSXbcr3DbVZwbPLqGgZ2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV +3imz/f3ET+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j5pds +8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAMC1rlLAHGVK/XqsEQ +e9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73TDtTUXm6Hnmo9RR3RXRv06QqsYJn7 +ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNXwbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5X +xNMhIWNlUpEbsZmOeX7m640A2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV +7rtNOzK+mndmnqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8 +dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwlN4y6mACXi0mW +Hv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNjc0kCAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9n +MA0GCSqGSIb3DQEBCwUAA4ICAQAS5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4Q +jbRaZIxowLByQzTSGwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht +7LGrhFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/B7NTeLUK +YvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uIAeV8KEsD+UmDfLJ/fOPt +jqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbwH5Lk6rWS02FREAutp9lfx1/cH6NcjKF+ +m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKW +RGhXxNUzzxkvFMSUHHuk2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjA +JOgc47OlIQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk5F6G ++TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuYn/PIjhs4ViFqUZPT +kcpG2om3PVODLAgfi49T3f+sHw== +-----END CERTIFICATE----- + +Microsoft ECC Root Certificate Authority 2017 +============================================= +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQgRUND +IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4 +MjMxNjA0WjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQ +BgcqhkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZRogPZnZH6 +thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYbhGBKia/teQ87zvH2RPUB +eMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTIy5lycFIM ++Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlf +Xu5gKcs68tvWMoQZP3zVL8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaR +eNtUjGUBiudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- + +Microsoft RSA Root Certificate Authority 2017 +============================================= +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQg +UlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIw +NzE4MjMwMDIzWjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u +MTYwNAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZNt9GkMml +7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0ZdDMbRnMlfl7rEqUrQ7e +S0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw7 +1VdyvD/IybLeS2v4I2wDwAW9lcfNcztmgGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+ +dkC0zVJhUXAoP8XFWvLJjEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49F +yGcohJUcaDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaGYaRS +MLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6W6IYZVcSn2i51BVr +lMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4KUGsTuqwPN1q3ErWQgR5WrlcihtnJ +0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJ +ClTUFLkqqNfs+avNJVgyeY+QW5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZCLgLNFgVZJ8og +6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OCgMNPOsduET/m4xaRhPtthH80 +dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk ++ONVFT24bcMKpBLBaYVu32TxU5nhSnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex +/2kskZGT4d9Mozd2TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDy +AmH3pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGRxpl/j8nW +ZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiAppGWSZI1b7rCoucL5mxAyE +7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKT +c0QWbej09+CVgI+WXTik9KveCjCHk9hNAHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D +5KbvtwEwXlGjefVwaaZBRA+GsCyRxj3qrg+E +-----END CERTIFICATE----- + +e-Szigno Root CA 2017 +===================== +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNVBAYTAkhVMREw +DwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUt +MjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJvb3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZa +Fw00MjA4MjIxMjA3MDZaMHExCzAJBgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UE +CgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3pp +Z25vIFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtvxie+RJCx +s1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+HWyx7xf58etqjYzBhMA8G +A1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSHERUI0arBeAyxr87GyZDv +vzAEwDAfBgNVHSMEGDAWgBSHERUI0arBeAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEA +tVfd14pVCzbhhkT61NlojbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxO +svxyqltZ+efcMQ== +-----END CERTIFICATE----- + +certSIGN Root CA G2 +=================== +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNVBAYTAlJPMRQw +EgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjAeFw0xNzAy +MDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJBgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lH +TiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAMDFdRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05 +N0IwvlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZuIt4Imfk +abBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhpn+Sc8CnTXPnGFiWeI8Mg +wT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKscpc/I1mbySKEwQdPzH/iV8oScLumZfNp +dWO9lfsbl83kqK/20U6o2YpxJM02PbyWxPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91Qqh +ngLjYl/rNUssuHLoPj1PrCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732 +jcZZroiFDsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fxDTvf +95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgyLcsUDFDYg2WD7rlc +z8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6CeWRgKRM+o/1Pcmqr4tTluCRVLERL +iohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1Ud +DgQWBBSCIS1mxteg4BXrzkwJd8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOB +ywaK8SJJ6ejqkX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQlqiCA2ClV9+BB +/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0OJD7uNGzcgbJceaBxXntC6Z5 +8hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+cNywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5 +BiKDUyUM/FHE5r7iOZULJK2v0ZXkltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklW +atKcsWMy5WHgUyIOpwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tU +Sxfj03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZkPuXaTH4M +NMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE1LlSVHJ7liXMvGnjSG4N +0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MXQRBdJ3NghVdJIgc= +-----END CERTIFICATE----- diff --git a/kirby/composer.json b/kirby/composer.json new file mode 100644 index 0000000..9ebc32b --- /dev/null +++ b/kirby/composer.json @@ -0,0 +1,53 @@ +{ + "name": "getkirby/cms", + "description": "The Kirby 3 core", + "version": "3.4.4", + "license": "proprietary", + "keywords": ["kirby", "cms", "core"], + "homepage": "https://getkirby.com", + "type": "kirby-cms", + "authors": [ + { + "name": "Kirby Team", + "email": "support@getkirby.com", + "homepage": "https://getkirby.com" + } + ], + "support": { + "email": "support@getkirby.com", + "issues": "https://github.com/getkirby/kirby/issues", + "forum": "https://forum.getkirby.com", + "source": "https://github.com/getkirby/kirby" + }, + "require": { + "php": ">=7.2.0 <7.5.0", + "ext-mbstring": "*", + "ext-ctype": "*", + "getkirby/composer-installer": "^1.0", + "mustangostang/spyc": "0.6.3", + "michelf/php-smartypants": "1.8.1", + "claviska/simpleimage": "3.3.4", + "phpmailer/phpmailer": "6.1.6", + "filp/whoops": "2.7.2", + "true/punycode": "2.1.1", + "laminas/laminas-escaper": "2.6.1" + }, + "autoload": { + "files": ["config/setup.php"], + "classmap": ["dependencies/"], + "psr-4": { + "Kirby\\": "src/" + } + }, + "scripts": { + "analyze": "phpstan analyse", + "build": "./scripts/build", + "fix": "php-cs-fixer fix --config .php_cs", + "post-update-cmd": "curl -o cacert.pem https://curl.haxx.se/ca/cacert.pem", + "test": "phpunit --stderr --coverage-html=tests/coverage", + "zip": "composer archive --format=zip --file=dist" + }, + "config": { + "optimize-autoloader": true + } +} diff --git a/kirby/composer.lock b/kirby/composer.lock new file mode 100644 index 0000000..9a3c059 --- /dev/null +++ b/kirby/composer.lock @@ -0,0 +1,629 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "b85ce02787ff0e052a3cc7a01c2daa91", + "packages": [ + { + "name": "claviska/simpleimage", + "version": "3.3.4", + "source": { + "type": "git", + "url": "https://github.com/claviska/SimpleImage.git", + "reference": "3786d80af8e6d05e5e42f0350e5e5da5b92041a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/claviska/SimpleImage/zipball/3786d80af8e6d05e5e42f0350e5e5da5b92041a0", + "reference": "3786d80af8e6d05e5e42f0350e5e5da5b92041a0", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "league/color-extractor": "0.3.*", + "php": ">=5.6.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "claviska": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Cory LaViska", + "homepage": "http://www.abeautifulsite.net/", + "role": "Developer" + } + ], + "description": "A PHP class that makes working with images as simple as possible.", + "time": "2019-09-26T01:22:02+00:00" + }, + { + "name": "filp/whoops", + "version": "2.7.2", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "17d0d3f266c8f925ebd035cd36f83cf802b47d4a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/17d0d3f266c8f925ebd035cd36f83cf802b47d4a", + "reference": "17d0d3f266c8f925ebd035cd36f83cf802b47d4a", + "shasum": "" + }, + "require": { + "php": "^5.5.9 || ^7.0", + "psr/log": "^1.0.1" + }, + "require-dev": { + "mockery/mockery": "^0.9 || ^1.0", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0", + "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "time": "2020-05-05T12:28:07+00:00" + }, + { + "name": "getkirby/composer-installer", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/getkirby/composer-installer.git", + "reference": "240a8b2c275d61b66601feb58222b7d34bc6cf1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getkirby/composer-installer/zipball/240a8b2c275d61b66601feb58222b7d34bc6cf1e", + "reference": "240a8b2c275d61b66601feb58222b7d34bc6cf1e", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0" + }, + "require-dev": { + "composer/composer": "^1.8 || 2.0.*@dev", + "phpunit/phpunit": "^7.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Kirby\\ComposerInstaller\\Plugin" + }, + "autoload": { + "psr-4": { + "Kirby\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Kirby's custom Composer installer for the Kirby CMS and for Kirby plugins", + "homepage": "https://getkirby.com", + "time": "2020-09-13T14:43:34+00:00" + }, + { + "name": "laminas/laminas-escaper", + "version": "2.6.1", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-escaper.git", + "reference": "25f2a053eadfa92ddacb609dcbbc39362610da70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/25f2a053eadfa92ddacb609dcbbc39362610da70", + "reference": "25f2a053eadfa92ddacb609dcbbc39362610da70", + "shasum": "" + }, + "require": { + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^5.6 || ^7.0" + }, + "replace": { + "zendframework/zend-escaper": "self.version" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~1.0.0", + "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6.x-dev", + "dev-develop": "2.7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laminas\\Escaper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs", + "homepage": "https://laminas.dev", + "keywords": [ + "escaper", + "laminas" + ], + "time": "2019-12-31T16:43:30+00:00" + }, + { + "name": "laminas/laminas-zendframework-bridge", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-zendframework-bridge.git", + "reference": "6ede70583e101030bcace4dcddd648f760ddf642" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/6ede70583e101030bcace4dcddd648f760ddf642", + "reference": "6ede70583e101030bcace4dcddd648f760ddf642", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1 || ^9.3", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "laminas": { + "module": "Laminas\\ZendFrameworkBridge" + } + }, + "autoload": { + "files": [ + "src/autoload.php" + ], + "psr-4": { + "Laminas\\ZendFrameworkBridge\\": "src//" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Alias legacy ZF class names to Laminas Project equivalents.", + "keywords": [ + "ZendFramework", + "autoloading", + "laminas", + "zf" + ], + "time": "2020-09-14T14:23:00+00:00" + }, + { + "name": "league/color-extractor", + "version": "0.3.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/color-extractor.git", + "reference": "837086ec60f50c84c611c613963e4ad2e2aec806" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/color-extractor/zipball/837086ec60f50c84c611c613963e4ad2e2aec806", + "reference": "837086ec60f50c84c611c613963e4ad2e2aec806", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "php": ">=5.4.0" + }, + "replace": { + "matthecat/colorextractor": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2", + "phpunit/phpunit": "~5" + }, + "type": "library", + "autoload": { + "psr-4": { + "": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathieu Lechat", + "email": "math.lechat@gmail.com", + "homepage": "http://matthecat.com", + "role": "Developer" + } + ], + "description": "Extract colors from an image as a human would do.", + "homepage": "https://github.com/thephpleague/color-extractor", + "keywords": [ + "color", + "extract", + "human", + "image", + "palette" + ], + "time": "2016-12-15T09:30:02+00:00" + }, + { + "name": "michelf/php-smartypants", + "version": "1.8.1", + "source": { + "type": "git", + "url": "https://github.com/michelf/php-smartypants.git", + "reference": "47d17c90a4dfd0ccf1f87e25c65e6c8012415aad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/michelf/php-smartypants/zipball/47d17c90a4dfd0ccf1f87e25c65e6c8012415aad", + "reference": "47d17c90a4dfd0ccf1f87e25c65e6c8012415aad", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Michelf": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Michel Fortin", + "email": "michel.fortin@michelf.ca", + "homepage": "https://michelf.ca/", + "role": "Developer" + }, + { + "name": "John Gruber", + "homepage": "https://daringfireball.net/" + } + ], + "description": "PHP SmartyPants", + "homepage": "https://michelf.ca/projects/php-smartypants/", + "keywords": [ + "dashes", + "quotes", + "spaces", + "typographer", + "typography" + ], + "time": "2016-12-13T01:01:17+00:00" + }, + { + "name": "mustangostang/spyc", + "version": "0.6.3", + "source": { + "type": "git", + "url": "git@github.com:mustangostang/spyc.git", + "reference": "4627c838b16550b666d15aeae1e5289dd5b77da0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mustangostang/spyc/zipball/4627c838b16550b666d15aeae1e5289dd5b77da0", + "reference": "4627c838b16550b666d15aeae1e5289dd5b77da0", + "shasum": "" + }, + "require": { + "php": ">=5.3.1" + }, + "require-dev": { + "phpunit/phpunit": "4.3.*@dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.5.x-dev" + } + }, + "autoload": { + "files": [ + "Spyc.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "mustangostang", + "email": "vlad.andersen@gmail.com" + } + ], + "description": "A simple YAML loader/dumper class for PHP", + "homepage": "https://github.com/mustangostang/spyc/", + "keywords": [ + "spyc", + "yaml", + "yml" + ], + "time": "2019-09-10T13:16:29+00:00" + }, + { + "name": "phpmailer/phpmailer", + "version": "v6.1.6", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "c2796cb1cb99d7717290b48c4e6f32cb6c60b7b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/c2796cb1cb99d7717290b48c4e6f32cb6c60b7b3", + "reference": "c2796cb1cb99d7717290b48c4e6f32cb6c60b7b3", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "doctrine/annotations": "^1.2", + "friendsofphp/php-cs-fixer": "^2.2", + "phpunit/phpunit": "^4.8 || ^5.7" + }, + "suggest": { + "ext-mbstring": "Needed to send email in multibyte encoding charset", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "stevenmaguire/oauth2-microsoft": "Needed for Microsoft XOAUTH2 authentication", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "authors": [ + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, + { + "name": "Brent R. Matzelle" + } + ], + "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "time": "2020-05-27T12:24:03+00:00" + }, + { + "name": "psr/log", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2020-03-23T09:12:05+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/a6977d63bf9a0ad4c65cd352709e230876f9904a", + "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2020-07-14T12:35:20+00:00" + }, + { + "name": "true/punycode", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/true/php-punycode.git", + "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/true/php-punycode/zipball/a4d0c11a36dd7f4e7cd7096076cab6d3378a071e", + "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.7", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "TrueBV\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Renan Gonçalves", + "email": "renan.saddam@gmail.com" + } + ], + "description": "A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA)", + "homepage": "https://github.com/true/php-punycode", + "keywords": [ + "idna", + "punycode" + ], + "time": "2016-11-16T10:37:54+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.2.0 <7.5.0", + "ext-mbstring": "*", + "ext-ctype": "*" + }, + "platform-dev": [] +} diff --git a/kirby/config/aliases.php b/kirby/config/aliases.php new file mode 100644 index 0000000..8c7016f --- /dev/null +++ b/kirby/config/aliases.php @@ -0,0 +1,62 @@ + 'Kirby\Cms\Asset', + 'collection' => 'Kirby\Cms\Collection', + 'dir' => 'Kirby\Cms\Dir', + 'field' => 'Kirby\Cms\Field', + 'file' => 'Kirby\Cms\File', + 'files' => 'Kirby\Cms\Files', + 'html' => 'Kirby\Cms\Html', + 'kirby' => 'Kirby\Cms\App', + 'page' => 'Kirby\Cms\Page', + 'pages' => 'Kirby\Cms\Pages', + 'pagination' => 'Kirby\Cms\Pagination', + 'r' => 'Kirby\Cms\R', + 'response' => 'Kirby\Cms\Response', + 's' => 'Kirby\Cms\S', + 'site' => 'Kirby\Cms\Site', + 'structure' => 'Kirby\Cms\Structure', + 'url' => 'Kirby\Cms\Url', + 'user' => 'Kirby\Cms\User', + 'users' => 'Kirby\Cms\Users', + 'visitor' => 'Kirby\Cms\Visitor', + + // data handler + 'data' => 'Kirby\Data\Data', + 'json' => 'Kirby\Data\Json', + 'yaml' => 'Kirby\Data\Yaml', + + // data classes + 'database' => 'Kirby\Database\Database', + 'db' => 'Kirby\Database\Db', + + // exceptions + 'errorpageexception' => 'Kirby\Exception\ErrorPageException', + + // http classes + 'cookie' => 'Kirby\Http\Cookie', + 'header' => 'Kirby\Http\Header', + 'remote' => 'Kirby\Http\Remote', + 'server' => 'Kirby\Http\Server', + + // image classes + 'dimensions' => 'Kirby\Image\Dimensions', + + // toolkit classes + 'a' => 'Kirby\Toolkit\A', + 'c' => 'Kirby\Toolkit\Config', + 'config' => 'Kirby\Toolkit\Config', + 'escape' => 'Kirby\Toolkit\Escape', + 'f' => 'Kirby\Toolkit\F', + 'i18n' => 'Kirby\Toolkit\I18n', + 'mime' => 'Kirby\Toolkit\Mime', + 'obj' => 'Kirby\Toolkit\Obj', + 'str' => 'Kirby\Toolkit\Str', + 'tpl' => 'Kirby\Toolkit\Tpl', + 'v' => 'Kirby\Toolkit\V', + 'xml' => 'Kirby\Toolkit\Xml' +]; +// @codeCoverageIgnoreEnd diff --git a/kirby/config/api/authentication.php b/kirby/config/api/authentication.php new file mode 100644 index 0000000..8dd5f33 --- /dev/null +++ b/kirby/config/api/authentication.php @@ -0,0 +1,24 @@ +kirby()->auth(); + $allowImpersonation = $this->kirby()->option('api.allowImpersonation') ?? false; + + // csrf token check + if ($auth->type($allowImpersonation) === 'session' && $auth->csrf() === false) { + throw new PermissionException('Unauthenticated'); + } + + // get user from session or basic auth + if ($user = $auth->user(null, $allowImpersonation)) { + if ($user->role()->permissions()->for('access', 'panel') === false) { + throw new PermissionException(['key' => 'access.panel']); + } + + return $user; + } + + throw new PermissionException('Unauthenticated'); +}; diff --git a/kirby/config/api/collections.php b/kirby/config/api/collections.php new file mode 100644 index 0000000..bd817e9 --- /dev/null +++ b/kirby/config/api/collections.php @@ -0,0 +1,72 @@ + [ + 'model' => 'page', + 'type' => 'Kirby\Cms\Pages', + 'view' => 'compact' + ], + + /** + * Files + */ + 'files' => [ + 'model' => 'file', + 'type' => 'Kirby\Cms\Files' + ], + + /** + * Languages + */ + 'languages' => [ + 'model' => 'language', + 'type' => 'Kirby\Cms\Languages' + ], + + /** + * Pages + */ + 'pages' => [ + 'model' => 'page', + 'type' => 'Kirby\Cms\Pages', + 'view' => 'compact' + ], + + /** + * Roles + */ + 'roles' => [ + 'model' => 'role', + 'type' => 'Kirby\Cms\Roles', + 'view' => 'compact' + ], + + /** + * Translations + */ + 'translations' => [ + 'model' => 'translation', + 'type' => 'Kirby\Cms\Translations', + 'view' => 'compact' + ], + + /** + * Users + */ + 'users' => [ + 'default' => function () { + return $this->users(); + }, + 'model' => 'user', + 'type' => 'Kirby\Cms\Users', + 'view' => 'compact' + ] + +]; diff --git a/kirby/config/api/models.php b/kirby/config/api/models.php new file mode 100644 index 0000000..51d19fb --- /dev/null +++ b/kirby/config/api/models.php @@ -0,0 +1,20 @@ + include __DIR__ . '/models/File.php', + 'FileBlueprint' => include __DIR__ . '/models/FileBlueprint.php', + 'FileVersion' => include __DIR__ . '/models/FileVersion.php', + 'Language' => include __DIR__ . '/models/Language.php', + 'Page' => include __DIR__ . '/models/Page.php', + 'PageBlueprint' => include __DIR__ . '/models/PageBlueprint.php', + 'Role' => include __DIR__ . '/models/Role.php', + 'Site' => include __DIR__ . '/models/Site.php', + 'SiteBlueprint' => include __DIR__ . '/models/SiteBlueprint.php', + 'System' => include __DIR__ . '/models/System.php', + 'Translation' => include __DIR__ . '/models/Translation.php', + 'User' => include __DIR__ . '/models/User.php', + 'UserBlueprint' => include __DIR__ . '/models/UserBlueprint.php', +]; diff --git a/kirby/config/api/models/File.php b/kirby/config/api/models/File.php new file mode 100644 index 0000000..bf5e573 --- /dev/null +++ b/kirby/config/api/models/File.php @@ -0,0 +1,166 @@ + [ + 'blueprint' => function (File $file) { + return $file->blueprint(); + }, + 'content' => function (File $file) { + return Form::for($file)->values(); + }, + 'dimensions' => function (File $file) { + return $file->dimensions()->toArray(); + }, + 'dragText' => function (File $file) { + return $file->dragText(); + }, + 'exists' => function (File $file) { + return $file->exists(); + }, + 'extension' => function (File $file) { + return $file->extension(); + }, + 'filename' => function (File $file) { + return $file->filename(); + }, + 'id' => function (File $file) { + return $file->id(); + }, + 'link' => function (File $file) { + return $file->panelUrl(true); + }, + 'mime' => function (File $file) { + return $file->mime(); + }, + 'modified' => function (File $file) { + return $file->modified('c'); + }, + 'name' => function (File $file) { + return $file->name(); + }, + 'next' => function (File $file) { + return $file->next(); + }, + 'nextWithTemplate' => function (File $file) { + $files = $file->templateSiblings()->sortBy('sort', 'asc', 'filename', 'asc'); + $index = $files->indexOf($file); + + return $files->nth($index + 1); + }, + 'niceSize' => function (File $file) { + return $file->niceSize(); + }, + 'options' => function (File $file) { + return $file->panelOptions(); + }, + 'panelIcon' => function (File $file) { + return $file->panelIcon(); + }, + 'panelImage' => function (File $file) { + return $file->panelImage(); + }, + 'panelUrl' => function (File $file) { + return $file->panelUrl(true); + }, + 'prev' => function (File $file) { + return $file->prev(); + }, + 'prevWithTemplate' => function (File $file) { + $files = $file->templateSiblings()->sortBy('sort', 'asc', 'filename', 'asc'); + $index = $files->indexOf($file); + + return $files->nth($index - 1); + }, + 'parent' => function (File $file) { + return $file->parent(); + }, + 'parents' => function (File $file) { + return $file->parents()->flip(); + }, + 'size' => function (File $file) { + return $file->size(); + }, + 'template' => function (File $file) { + return $file->template(); + }, + 'thumbs' => function ($file) { + if ($file->isResizable() === false) { + return null; + } + + return [ + 'tiny' => $file->resize(128)->url(), + 'small' => $file->resize(256)->url(), + 'medium' => $file->resize(512)->url(), + 'large' => $file->resize(768)->url(), + 'huge' => $file->resize(1024)->url(), + ]; + }, + 'type' => function (File $file) { + return $file->type(); + }, + 'url' => function (File $file) { + return $file->url(true); + }, + ], + 'type' => 'Kirby\Cms\File', + 'views' => [ + 'default' => [ + 'content', + 'dimensions', + 'exists', + 'extension', + 'filename', + 'id', + 'link', + 'mime', + 'modified', + 'name', + 'next' => 'compact', + 'niceSize', + 'parent' => 'compact', + 'options', + 'prev' => 'compact', + 'size', + 'template', + 'type', + 'url' + ], + 'compact' => [ + 'filename', + 'id', + 'link', + 'type', + 'url', + ], + 'panel' => [ + 'blueprint', + 'content', + 'dimensions', + 'extension', + 'filename', + 'id', + 'link', + 'mime', + 'modified', + 'name', + 'nextWithTemplate' => 'compact', + 'niceSize', + 'options', + 'panelIcon', + 'panelImage', + 'parent' => 'compact', + 'parents' => ['id', 'slug', 'title'], + 'prevWithTemplate' => 'compact', + 'template', + 'type', + 'url' + ] + ], +]; diff --git a/kirby/config/api/models/FileBlueprint.php b/kirby/config/api/models/FileBlueprint.php new file mode 100644 index 0000000..0ca7cc0 --- /dev/null +++ b/kirby/config/api/models/FileBlueprint.php @@ -0,0 +1,26 @@ + [ + 'name' => function (FileBlueprint $blueprint) { + return $blueprint->name(); + }, + 'options' => function (FileBlueprint $blueprint) { + return $blueprint->options(); + }, + 'tabs' => function (FileBlueprint $blueprint) { + return $blueprint->tabs(); + }, + 'title' => function (FileBlueprint $blueprint) { + return $blueprint->title(); + }, + ], + 'type' => 'Kirby\Cms\FileBlueprint', + 'views' => [ + ], +]; diff --git a/kirby/config/api/models/FileVersion.php b/kirby/config/api/models/FileVersion.php new file mode 100644 index 0000000..3d518b3 --- /dev/null +++ b/kirby/config/api/models/FileVersion.php @@ -0,0 +1,83 @@ + [ + 'dimensions' => function (FileVersion $file) { + return $file->dimensions()->toArray(); + }, + 'exists' => function (FileVersion $file) { + return $file->exists(); + }, + 'extension' => function (FileVersion $file) { + return $file->extension(); + }, + 'filename' => function (FileVersion $file) { + return $file->filename(); + }, + 'id' => function (FileVersion $file) { + return $file->id(); + }, + 'mime' => function (FileVersion $file) { + return $file->mime(); + }, + 'modified' => function (FileVersion $file) { + return $file->modified('c'); + }, + 'name' => function (FileVersion $file) { + return $file->name(); + }, + 'niceSize' => function (FileVersion $file) { + return $file->niceSize(); + }, + 'size' => function (FileVersion $file) { + return $file->size(); + }, + 'type' => function (FileVersion $file) { + return $file->type(); + }, + 'url' => function (FileVersion $file) { + return $file->url(true); + }, + ], + 'type' => 'Kirby\Cms\FileVersion', + 'views' => [ + 'default' => [ + 'dimensions', + 'exists', + 'extension', + 'filename', + 'id', + 'mime', + 'modified', + 'name', + 'niceSize', + 'size', + 'type', + 'url' + ], + 'compact' => [ + 'filename', + 'id', + 'type', + 'url', + ], + 'panel' => [ + 'dimensions', + 'extension', + 'filename', + 'id', + 'mime', + 'modified', + 'name', + 'niceSize', + 'template', + 'type', + 'url' + ] + ], +]; diff --git a/kirby/config/api/models/Language.php b/kirby/config/api/models/Language.php new file mode 100644 index 0000000..9350a89 --- /dev/null +++ b/kirby/config/api/models/Language.php @@ -0,0 +1,44 @@ + [ + 'code' => function (Language $language) { + return $language->code(); + }, + 'default' => function (Language $language) { + return $language->isDefault(); + }, + 'direction' => function (Language $language) { + return $language->direction(); + }, + 'locale' => function (Language $language) { + return $language->locale(); + }, + 'name' => function (Language $language) { + return $language->name(); + }, + 'rules' => function (Language $language) { + return $language->rules(); + }, + 'url' => function (Language $language) { + return $language->url(); + }, + ], + 'type' => 'Kirby\Cms\Language', + 'views' => [ + 'default' => [ + 'code', + 'default', + 'direction', + 'locale', + 'name', + 'rules', + 'url' + ] + ] +]; diff --git a/kirby/config/api/models/Page.php b/kirby/config/api/models/Page.php new file mode 100644 index 0000000..1f8467b --- /dev/null +++ b/kirby/config/api/models/Page.php @@ -0,0 +1,157 @@ + [ + 'blueprint' => function (Page $page) { + return $page->blueprint(); + }, + 'blueprints' => function (Page $page) { + return $page->blueprints(); + }, + 'children' => function (Page $page) { + return $page->children(); + }, + 'content' => function (Page $page) { + return Form::for($page)->values(); + }, + 'drafts' => function (Page $page) { + return $page->drafts(); + }, + 'errors' => function (Page $page) { + return $page->errors(); + }, + 'files' => function (Page $page) { + return $page->files()->sortBy('sort', 'asc', 'filename', 'asc'); + }, + 'hasChildren' => function (Page $page) { + return $page->hasChildren(); + }, + 'hasDrafts' => function (Page $page) { + return $page->hasDrafts(); + }, + 'hasFiles' => function (Page $page) { + return $page->hasFiles(); + }, + 'id' => function (Page $page) { + return $page->id(); + }, + 'isSortable' => function (Page $page) { + return $page->isSortable(); + }, + 'next' => function (Page $page) { + return $page + ->nextAll() + ->filterBy('intendedTemplate', $page->intendedTemplate()) + ->filterBy('status', $page->status()) + ->filterBy('isReadable', true) + ->first(); + }, + 'num' => function (Page $page) { + return $page->num(); + }, + 'options' => function (Page $page) { + return $page->panelOptions(['preview']); + }, + 'panelIcon' => function (Page $page) { + return $page->panelIcon(); + }, + 'panelImage' => function (Page $page) { + return $page->panelImage(); + }, + 'parent' => function (Page $page) { + return $page->parent(); + }, + 'parents' => function (Page $page) { + return $page->parents()->flip(); + }, + 'prev' => function (Page $page) { + return $page + ->prevAll() + ->filterBy('intendedTemplate', $page->intendedTemplate()) + ->filterBy('status', $page->status()) + ->filterBy('isReadable', true) + ->last(); + }, + 'previewUrl' => function (Page $page) { + return $page->previewUrl(); + }, + 'siblings' => function (Page $page) { + if ($page->isDraft() === true) { + return $page->parentModel()->children()->not($page); + } else { + return $page->siblings(); + } + }, + 'slug' => function (Page $page) { + return $page->slug(); + }, + 'status' => function (Page $page) { + return $page->status(); + }, + 'template' => function (Page $page) { + return $page->intendedTemplate()->name(); + }, + 'title' => function (Page $page) { + return $page->title()->value(); + }, + 'url' => function (Page $page) { + return $page->url(); + }, + ], + 'type' => 'Kirby\Cms\Page', + 'views' => [ + 'compact' => [ + 'id', + 'title', + 'url', + 'num' + ], + 'default' => [ + 'content', + 'id', + 'status', + 'num', + 'options', + 'parent' => 'compact', + 'slug', + 'template', + 'title', + 'url' + ], + 'panel' => [ + 'id', + 'blueprint', + 'content', + 'status', + 'options', + 'next' => ['id', 'slug', 'title'], + 'parents' => ['id', 'slug', 'title'], + 'prev' => ['id', 'slug', 'title'], + 'previewUrl', + 'slug', + 'title', + 'url' + ], + 'selector' => [ + 'id', + 'title', + 'parent' => [ + 'id', + 'title' + ], + 'children' => [ + 'hasChildren', + 'id', + 'panelIcon', + 'panelImage', + 'title', + ], + ] + ], +]; diff --git a/kirby/config/api/models/PageBlueprint.php b/kirby/config/api/models/PageBlueprint.php new file mode 100644 index 0000000..fd4cdef --- /dev/null +++ b/kirby/config/api/models/PageBlueprint.php @@ -0,0 +1,35 @@ + [ + 'name' => function (PageBlueprint $blueprint) { + return $blueprint->name(); + }, + 'num' => function (PageBlueprint $blueprint) { + return $blueprint->num(); + }, + 'options' => function (PageBlueprint $blueprint) { + return $blueprint->options(); + }, + 'preview' => function (PageBlueprint $blueprint) { + return $blueprint->preview(); + }, + 'status' => function (PageBlueprint $blueprint) { + return $blueprint->status(); + }, + 'tabs' => function (PageBlueprint $blueprint) { + return $blueprint->tabs(); + }, + 'title' => function (PageBlueprint $blueprint) { + return $blueprint->title(); + }, + ], + 'type' => 'Kirby\Cms\PageBlueprint', + 'views' => [ + ], +]; diff --git a/kirby/config/api/models/Role.php b/kirby/config/api/models/Role.php new file mode 100644 index 0000000..99aed16 --- /dev/null +++ b/kirby/config/api/models/Role.php @@ -0,0 +1,31 @@ + [ + 'description' => function (Role $role) { + return $role->description(); + }, + 'name' => function (Role $role) { + return $role->name(); + }, + 'permissions' => function (Role $role) { + return $role->permissions()->toArray(); + }, + 'title' => function (Role $role) { + return $role->title(); + }, + ], + 'type' => 'Kirby\Cms\Role', + 'views' => [ + 'compact' => [ + 'description', + 'name', + 'title' + ] + ] +]; diff --git a/kirby/config/api/models/Site.php b/kirby/config/api/models/Site.php new file mode 100644 index 0000000..ca5dfe0 --- /dev/null +++ b/kirby/config/api/models/Site.php @@ -0,0 +1,72 @@ + function () { + return $this->site(); + }, + 'fields' => [ + 'blueprint' => function (Site $site) { + return $site->blueprint(); + }, + 'children' => function (Site $site) { + return $site->children(); + }, + 'content' => function (Site $site) { + return Form::for($site)->values(); + }, + 'drafts' => function (Site $site) { + return $site->drafts(); + }, + 'files' => function (Site $site) { + return $site->files()->sortBy('sort', 'asc', 'filename', 'asc'); + }, + 'options' => function (Site $site) { + return $site->permissions()->toArray(); + }, + 'previewUrl' => function (Site $site) { + return $site->previewUrl(); + }, + 'title' => function (Site $site) { + return $site->title()->value(); + }, + 'url' => function (Site $site) { + return $site->url(); + }, + ], + 'type' => 'Kirby\Cms\Site', + 'views' => [ + 'compact' => [ + 'title', + 'url' + ], + 'default' => [ + 'content', + 'options', + 'title', + 'url' + ], + 'panel' => [ + 'title', + 'blueprint', + 'content', + 'options', + 'previewUrl', + 'url' + ], + 'selector' => [ + 'title', + 'children' => [ + 'id', + 'title', + 'panelIcon', + 'hasChildren' + ], + ] + ] +]; diff --git a/kirby/config/api/models/SiteBlueprint.php b/kirby/config/api/models/SiteBlueprint.php new file mode 100644 index 0000000..a3b0943 --- /dev/null +++ b/kirby/config/api/models/SiteBlueprint.php @@ -0,0 +1,26 @@ + [ + 'name' => function (SiteBlueprint $blueprint) { + return $blueprint->name(); + }, + 'options' => function (SiteBlueprint $blueprint) { + return $blueprint->options(); + }, + 'tabs' => function (SiteBlueprint $blueprint) { + return $blueprint->tabs(); + }, + 'title' => function (SiteBlueprint $blueprint) { + return $blueprint->title(); + }, + ], + 'type' => 'Kirby\Cms\SiteBlueprint', + 'views' => [ + ], +]; diff --git a/kirby/config/api/models/System.php b/kirby/config/api/models/System.php new file mode 100644 index 0000000..6e74756 --- /dev/null +++ b/kirby/config/api/models/System.php @@ -0,0 +1,119 @@ + [ + 'ascii' => function () { + return Str::$ascii; + }, + 'defaultLanguage' => function () { + return $this->kirby()->option('panel.language', 'en'); + }, + 'isOk' => function (System $system) { + return $system->isOk(); + }, + 'isInstallable' => function (System $system) { + return $system->isInstallable(); + }, + 'isInstalled' => function (System $system) { + return $system->isInstalled(); + }, + 'isLocal' => function (System $system) { + return $system->isLocal(); + }, + 'multilang' => function () { + return $this->kirby()->option('languages', false) !== false; + }, + 'languages' => function () { + return $this->kirby()->languages(); + }, + 'license' => function (System $system) { + return $system->license(); + }, + 'requirements' => function (System $system) { + return $system->toArray(); + }, + 'site' => function () { + try { + return $this->site()->blueprint()->title(); + } catch (Throwable $e) { + return $this->site()->title()->value(); + } + }, + 'slugs' => function () { + return Str::$language; + }, + 'title' => function () { + return $this->site()->title()->value(); + }, + 'translation' => function () { + if ($user = $this->user()) { + $translationCode = $user->language(); + } else { + $translationCode = $this->kirby()->option('panel.language', 'en'); + } + + if ($translation = $this->kirby()->translation($translationCode)) { + return $translation; + } else { + return $this->kirby()->translation('en'); + } + }, + 'kirbytext' => function () { + return $this->kirby()->option('panel.kirbytext') ?? true; + }, + 'user' => function () { + return $this->user(); + }, + 'version' => function () { + $user = $this->user(); + + if ($user && $user->role()->permissions()->for('access', 'settings') === true) { + return $this->kirby()->version(); + } else { + return null; + } + } + ], + 'type' => 'Kirby\Cms\System', + 'views' => [ + 'login' => [ + 'isOk', + 'isInstallable', + 'isInstalled', + 'title', + 'translation' + ], + 'troubleshooting' => [ + 'isOk', + 'isInstallable', + 'isInstalled', + 'title', + 'translation', + 'requirements' + ], + 'panel' => [ + 'ascii', + 'defaultLanguage', + 'isOk', + 'isInstalled', + 'isLocal', + 'kirbytext', + 'languages', + 'license', + 'multilang', + 'requirements', + 'site', + 'slugs', + 'title', + 'translation', + 'user' => 'auth', + 'version' + ] + ], +]; diff --git a/kirby/config/api/models/Translation.php b/kirby/config/api/models/Translation.php new file mode 100644 index 0000000..13be9a0 --- /dev/null +++ b/kirby/config/api/models/Translation.php @@ -0,0 +1,34 @@ + [ + 'author' => function (Translation $translation) { + return $translation->author(); + }, + 'data' => function (Translation $translation) { + return $translation->dataWithFallback(); + }, + 'direction' => function (Translation $translation) { + return $translation->direction(); + }, + 'id' => function (Translation $translation) { + return $translation->id(); + }, + 'name' => function (Translation $translation) { + return $translation->name(); + }, + ], + 'type' => 'Kirby\Cms\Translation', + 'views' => [ + 'compact' => [ + 'direction', + 'id', + 'name' + ] + ] +]; diff --git a/kirby/config/api/models/User.php b/kirby/config/api/models/User.php new file mode 100644 index 0000000..c4faab6 --- /dev/null +++ b/kirby/config/api/models/User.php @@ -0,0 +1,108 @@ + function () { + return $this->user(); + }, + 'fields' => [ + 'avatar' => function (User $user) { + return $user->avatar() ? $user->avatar()->crop(512) : null; + }, + 'blueprint' => function (User $user) { + return $user->blueprint(); + }, + 'content' => function (User $user) { + return Form::for($user)->values(); + }, + 'email' => function (User $user) { + return $user->email(); + }, + 'files' => function (User $user) { + return $user->files()->sortBy('sort', 'asc', 'filename', 'asc'); + }, + 'id' => function (User $user) { + return $user->id(); + }, + 'language' => function (User $user) { + return $user->language(); + }, + 'name' => function (User $user) { + return $user->name()->value(); + }, + 'next' => function (User $user) { + return $user->next(); + }, + 'options' => function (User $user) { + return $user->panelOptions(); + }, + 'permissions' => function (User $user) { + return $user->role()->permissions()->toArray(); + }, + 'prev' => function (User $user) { + return $user->prev(); + }, + 'role' => function (User $user) { + return $user->role(); + }, + 'roles' => function (User $user) { + return $user->roles(); + }, + 'username' => function (User $user) { + return $user->username(); + } + ], + 'type' => 'Kirby\Cms\User', + 'views' => [ + 'default' => [ + 'avatar', + 'content', + 'email', + 'id', + 'language', + 'name', + 'next' => 'compact', + 'options', + 'prev' => 'compact', + 'role', + 'username' + ], + 'compact' => [ + 'avatar' => 'compact', + 'id', + 'email', + 'language', + 'name', + 'role' => 'compact', + 'username' + ], + 'auth' => [ + 'avatar' => 'compact', + 'permissions', + 'email', + 'id', + 'name', + 'role', + 'language' + ], + 'panel' => [ + 'avatar' => 'compact', + 'blueprint', + 'content', + 'email', + 'id', + 'language', + 'name', + 'next' => ['id', 'name'], + 'options', + 'prev' => ['id', 'name'], + 'role', + 'username', + ], + ] +]; diff --git a/kirby/config/api/models/UserBlueprint.php b/kirby/config/api/models/UserBlueprint.php new file mode 100644 index 0000000..fdf63eb --- /dev/null +++ b/kirby/config/api/models/UserBlueprint.php @@ -0,0 +1,26 @@ + [ + 'name' => function (UserBlueprint $blueprint) { + return $blueprint->name(); + }, + 'options' => function (UserBlueprint $blueprint) { + return $blueprint->options(); + }, + 'tabs' => function (UserBlueprint $blueprint) { + return $blueprint->tabs(); + }, + 'title' => function (UserBlueprint $blueprint) { + return $blueprint->title(); + }, + ], + 'type' => 'Kirby\Cms\UserBlueprint', + 'views' => [ + ], +]; diff --git a/kirby/config/api/routes.php b/kirby/config/api/routes.php new file mode 100644 index 0000000..fd3449d --- /dev/null +++ b/kirby/config/api/routes.php @@ -0,0 +1,26 @@ +option('languages', false) !== false) { + $routes = array_merge($routes, include __DIR__ . '/routes/languages.php'); + } + + return $routes; +}; diff --git a/kirby/config/api/routes/auth.php b/kirby/config/api/routes/auth.php new file mode 100644 index 0000000..7e4dcb4 --- /dev/null +++ b/kirby/config/api/routes/auth.php @@ -0,0 +1,55 @@ + 'auth', + 'method' => 'GET', + 'action' => function () { + if ($user = $this->kirby()->auth()->user()) { + return $this->resolve($user)->view('auth'); + } + + throw new NotFoundException('The user cannot be found'); + } + ], + [ + 'pattern' => 'auth/login', + 'method' => 'POST', + 'auth' => false, + 'action' => function () { + $auth = $this->kirby()->auth(); + + // csrf token check + if ($auth->type() === 'session' && $auth->csrf() === false) { + throw new InvalidArgumentException('Invalid CSRF token'); + } + + $email = $this->requestBody('email'); + $long = $this->requestBody('long'); + $password = $this->requestBody('password'); + + $user = $this->kirby()->auth()->login($email, $password, $long); + + return [ + 'code' => 200, + 'status' => 'ok', + 'user' => $this->resolve($user)->view('auth')->toArray() + ]; + } + ], + [ + 'pattern' => 'auth/logout', + 'method' => 'POST', + 'auth' => false, + 'action' => function () { + $this->kirby()->auth()->logout(); + return true; + } + ], +]; diff --git a/kirby/config/api/routes/files.php b/kirby/config/api/routes/files.php new file mode 100644 index 0000000..4935d44 --- /dev/null +++ b/kirby/config/api/routes/files.php @@ -0,0 +1,123 @@ + '(:all)/files/(:any)/sections/(:any)', + 'method' => 'GET', + 'action' => function (string $path, string $filename, string $sectionName) { + if ($section = $this->file($path, $filename)->blueprint()->section($sectionName)) { + return $section->toResponse(); + } + } + ], + [ + 'pattern' => '(:all)/files/(:any)/fields/(:any)/(:all?)', + 'method' => 'ALL', + 'action' => function (string $parent, string $filename, string $fieldName, string $path = null) { + if ($file = $this->file($parent, $filename)) { + return $this->fieldApi($file, $fieldName, $path); + } + } + ], + [ + 'pattern' => '(:all)/files', + 'method' => 'GET', + 'action' => function (string $path) { + return $this->parent($path)->files()->sortBy('sort', 'asc', 'filename', 'asc'); + } + ], + [ + 'pattern' => '(:all)/files', + 'method' => 'POST', + 'action' => function (string $path) { + return $this->upload(function ($source, $filename) use ($path) { + return $this->parent($path)->createFile([ + 'source' => $source, + 'template' => $this->requestBody('template'), + 'filename' => $filename + ]); + }); + } + ], + [ + 'pattern' => '(:all)/files/search', + 'method' => 'GET|POST', + 'action' => function (string $path) { + $files = $this->parent($path)->files(); + + if ($this->requestMethod() === 'GET') { + return $files->search($this->requestQuery('q')); + } else { + return $files->query($this->requestBody()); + } + } + ], + [ + 'pattern' => '(:all)/files/sort', + 'method' => 'PATCH', + 'action' => function (string $path) { + return $this->parent($path)->files()->changeSort( + $this->requestBody('files'), + $this->requestBody('index') + ); + } + ], + [ + 'pattern' => '(:all)/files/(:any)', + 'method' => 'GET', + 'action' => function (string $path, string $filename) { + return $this->file($path, $filename); + } + ], + [ + 'pattern' => '(:all)/files/(:any)', + 'method' => 'PATCH', + 'action' => function (string $path, string $filename) { + return $this->file($path, $filename)->update($this->requestBody(), $this->language(), true); + } + ], + [ + 'pattern' => '(:all)/files/(:any)', + 'method' => 'POST', + 'action' => function (string $path, string $filename) { + return $this->upload(function ($source) use ($path, $filename) { + return $this->file($path, $filename)->replace($source); + }); + } + ], + [ + 'pattern' => '(:all)/files/(:any)', + 'method' => 'DELETE', + 'action' => function (string $path, string $filename) { + return $this->file($path, $filename)->delete(); + } + ], + [ + 'pattern' => '(:all)/files/(:any)/name', + 'method' => 'PATCH', + 'action' => function (string $path, string $filename) { + return $this->file($path, $filename)->changeName($this->requestBody('name')); + } + ], + [ + 'pattern' => 'files/search', + 'method' => 'GET|POST', + 'action' => function () { + $files = $this + ->site() + ->index(true) + ->filterBy('isReadable', true) + ->files(); + + if ($this->requestMethod() === 'GET') { + return $files->search($this->requestQuery('q')); + } else { + return $files->query($this->requestBody()); + } + } + ], +]; diff --git a/kirby/config/api/routes/languages.php b/kirby/config/api/routes/languages.php new file mode 100644 index 0000000..8d8829b --- /dev/null +++ b/kirby/config/api/routes/languages.php @@ -0,0 +1,46 @@ + 'languages', + 'method' => 'GET', + 'action' => function () { + return $this->kirby()->languages(); + } + ], + [ + 'pattern' => 'languages', + 'method' => 'POST', + 'action' => function () { + return $this->kirby()->languages()->create($this->requestBody()); + } + ], + [ + 'pattern' => 'languages/(:any)', + 'method' => 'GET', + 'action' => function (string $code) { + return $this->kirby()->languages()->find($code); + } + ], + [ + 'pattern' => 'languages/(:any)', + 'method' => 'PATCH', + 'action' => function (string $code) { + if ($language = $this->kirby()->languages()->find($code)) { + return $language->update($this->requestBody()); + } + } + ], + [ + 'pattern' => 'languages/(:any)', + 'method' => 'DELETE', + 'action' => function (string $code) { + if ($language = $this->kirby()->languages()->find($code)) { + return $language->delete(); + } + } + ] +]; diff --git a/kirby/config/api/routes/lock.php b/kirby/config/api/routes/lock.php new file mode 100644 index 0000000..302a945 --- /dev/null +++ b/kirby/config/api/routes/lock.php @@ -0,0 +1,99 @@ + '(:all)/lock', + 'method' => 'GET', + 'action' => function (string $path) { + if ($lock = $this->parent($path)->lock()) { + return [ + 'supported' => true, + 'locked' => $lock->get() + ]; + } + + return [ + 'supported' => false, + 'locked' => null + ]; + } + ], + [ + 'pattern' => '(:all)/lock', + 'method' => 'PATCH', + 'action' => function (string $path) { + if ($lock = $this->parent($path)->lock()) { + return $lock->create(); + } + + throw new Exception([ + 'key' => 'lock.notImplemented', + 'httpCode' => 501 + ]); + } + ], + [ + 'pattern' => '(:all)/lock', + 'method' => 'DELETE', + 'action' => function (string $path) { + if ($lock = $this->parent($path)->lock()) { + return $lock->remove(); + } + + throw new Exception([ + 'key' => 'lock.notImplemented', + 'httpCode' => 501 + ]); + } + ], + [ + 'pattern' => '(:all)/unlock', + 'method' => 'GET', + 'action' => function (string $path) { + if ($lock = $this->parent($path)->lock()) { + return [ + 'supported' => true, + 'unlocked' => $lock->isUnlocked() + ]; + } + + return [ + 'supported' => false, + 'unlocked' => null + ]; + } + ], + [ + 'pattern' => '(:all)/unlock', + 'method' => 'PATCH', + 'action' => function (string $path) { + if ($lock = $this->parent($path)->lock()) { + return $lock->unlock(); + } + + throw new Exception([ + 'key' => 'lock.notImplemented', + 'httpCode' => 501 + ]); + } + ], + [ + 'pattern' => '(:all)/unlock', + 'method' => 'DELETE', + 'action' => function (string $path) { + if ($lock = $this->parent($path)->lock()) { + return $lock->resolve(); + } + + throw new Exception([ + 'key' => 'lock.notImplemented', + 'httpCode' => 501 + ]); + } + ], +]; diff --git a/kirby/config/api/routes/pages.php b/kirby/config/api/routes/pages.php new file mode 100644 index 0000000..3cd4d6d --- /dev/null +++ b/kirby/config/api/routes/pages.php @@ -0,0 +1,124 @@ + 'pages/(:any)', + 'method' => 'GET', + 'action' => function (string $id) { + return $this->page($id); + } + ], + [ + 'pattern' => 'pages/(:any)', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->page($id)->update($this->requestBody(), $this->language(), true); + } + ], + [ + 'pattern' => 'pages/(:any)', + 'method' => 'DELETE', + 'action' => function (string $id) { + return $this->page($id)->delete($this->requestBody('force', false)); + } + ], + [ + 'pattern' => 'pages/(:any)/blueprint', + 'method' => 'GET', + 'action' => function (string $id) { + return $this->page($id)->blueprint(); + } + ], + [ + 'pattern' => [ + 'pages/(:any)/blueprints', + // Deprecated: remove in 3.6.0 + 'pages/(:any)/children/blueprints', + ], + 'method' => 'GET', + 'action' => function (string $id) { + return $this->page($id)->blueprints($this->requestQuery('section')); + } + ], + [ + 'pattern' => 'pages/(:any)/children', + 'method' => 'GET', + 'action' => function (string $id) { + return $this->pages($id, $this->requestQuery('status')); + } + ], + [ + 'pattern' => 'pages/(:any)/children', + 'method' => 'POST', + 'action' => function (string $id) { + return $this->page($id)->createChild($this->requestBody()); + } + ], + [ + 'pattern' => 'pages/(:any)/children/search', + 'method' => 'GET|POST', + 'action' => function (string $id) { + return $this->searchPages($id); + } + ], + [ + 'pattern' => 'pages/(:any)/duplicate', + 'method' => 'POST', + 'action' => function (string $id) { + return $this->page($id)->duplicate($this->requestBody('slug'), [ + 'children' => $this->requestBody('children'), + 'files' => $this->requestBody('files'), + ]); + } + ], + [ + 'pattern' => 'pages/(:any)/slug', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->page($id)->changeSlug($this->requestBody('slug')); + } + ], + [ + 'pattern' => 'pages/(:any)/status', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->page($id)->changeStatus($this->requestBody('status'), $this->requestBody('position')); + } + ], + [ + 'pattern' => 'pages/(:any)/template', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->page($id)->changeTemplate($this->requestBody('template')); + } + ], + [ + 'pattern' => 'pages/(:any)/title', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->page($id)->changeTitle($this->requestBody('title')); + } + ], + [ + 'pattern' => 'pages/(:any)/sections/(:any)', + 'method' => 'GET', + 'action' => function (string $id, string $sectionName) { + if ($section = $this->page($id)->blueprint()->section($sectionName)) { + return $section->toResponse(); + } + } + ], + [ + 'pattern' => 'pages/(:any)/fields/(:any)/(:all?)', + 'method' => 'ALL', + 'action' => function (string $id, string $fieldName, string $path = null) { + if ($page = $this->page($id)) { + return $this->fieldApi($page, $fieldName, $path); + } + } + ], +]; diff --git a/kirby/config/api/routes/roles.php b/kirby/config/api/routes/roles.php new file mode 100644 index 0000000..ab9505b --- /dev/null +++ b/kirby/config/api/routes/roles.php @@ -0,0 +1,28 @@ + 'roles', + 'method' => 'GET', + 'action' => function () { + switch (get('canBe')) { + case 'changed': + return $this->kirby()->roles()->canBeChanged(); + case 'created': + return $this->kirby()->roles()->canBeCreated(); + default: + return $this->kirby()->roles(); + } + } + ], + [ + 'pattern' => 'roles/(:any)', + 'method' => 'GET', + 'action' => function (string $name) { + return $this->kirby()->roles()->find($name); + } + ] +]; diff --git a/kirby/config/api/routes/site.php b/kirby/config/api/routes/site.php new file mode 100644 index 0000000..7e31908 --- /dev/null +++ b/kirby/config/api/routes/site.php @@ -0,0 +1,107 @@ + 'site', + 'action' => function () { + return $this->site(); + } + ], + [ + 'pattern' => 'site', + 'method' => 'PATCH', + 'action' => function () { + return $this->site()->update($this->requestBody(), $this->language(), true); + } + ], + [ + 'pattern' => 'site/children', + 'method' => 'GET', + 'action' => function () { + return $this->pages(null, $this->requestQuery('status')); + } + ], + [ + 'pattern' => 'site/children', + 'method' => 'POST', + 'action' => function () { + return $this->site()->createChild($this->requestBody()); + } + ], + [ + 'pattern' => 'site/children/search', + 'method' => 'GET|POST', + 'action' => function () { + return $this->searchPages(); + } + ], + [ + 'pattern' => 'site/blueprint', + 'method' => 'GET', + 'action' => function () { + return $this->site()->blueprint(); + } + ], + [ + 'pattern' => [ + 'site/blueprints', + // Deprecated: remove in 3.6.0 + 'site/children/blueprints', + ], + 'method' => 'GET', + 'action' => function () { + return $this->site()->blueprints($this->requestQuery('section')); + } + ], + [ + 'pattern' => 'site/find', + 'method' => 'POST', + 'action' => function () { + return $this->site()->find(false, ...$this->requestBody()); + } + ], + [ + 'pattern' => 'site/title', + 'method' => 'PATCH', + 'action' => function () { + return $this->site()->changeTitle($this->requestBody('title')); + } + ], + [ + 'pattern' => 'site/search', + 'method' => 'GET|POST', + 'action' => function () { + $pages = $this + ->site() + ->index(true) + ->filterBy('isReadable', true); + + if ($this->requestMethod() === 'GET') { + return $pages->search($this->requestQuery('q')); + } else { + return $pages->query($this->requestBody()); + } + } + ], + [ + 'pattern' => 'site/sections/(:any)', + 'method' => 'GET', + 'action' => function (string $sectionName) { + if ($section = $this->site()->blueprint()->section($sectionName)) { + return $section->toResponse(); + } + } + ], + [ + 'pattern' => 'site/fields/(:any)/(:all?)', + 'method' => 'ALL', + 'action' => function (string $fieldName, string $path = null) { + return $this->fieldApi($this->site(), $fieldName, $path); + } + ] + +]; diff --git a/kirby/config/api/routes/system.php b/kirby/config/api/routes/system.php new file mode 100644 index 0000000..44c8807 --- /dev/null +++ b/kirby/config/api/routes/system.php @@ -0,0 +1,79 @@ + 'system', + 'method' => 'GET', + 'auth' => false, + 'action' => function () { + $system = $this->kirby()->system(); + + if ($this->kirby()->user()) { + return $system; + } else { + if ($system->isOk() === true) { + $info = $this->resolve($system)->view('login')->toArray(); + } else { + $info = $this->resolve($system)->view('troubleshooting')->toArray(); + } + + return [ + 'status' => 'ok', + 'data' => $info, + 'type' => 'model' + ]; + } + } + ], + [ + 'pattern' => 'system/register', + 'method' => 'POST', + 'action' => function () { + return $this->kirby()->system()->register($this->requestBody('license'), $this->requestBody('email')); + } + ], + [ + 'pattern' => 'system/install', + 'method' => 'POST', + 'auth' => false, + 'action' => function () { + $system = $this->kirby()->system(); + $auth = $this->kirby()->auth(); + + // csrf token check + if ($auth->type() === 'session' && $auth->csrf() === false) { + throw new InvalidArgumentException('Invalid CSRF token'); + } + + if ($system->isOk() === false) { + throw new Exception('The server is not setup correctly'); + } + + if ($system->isInstallable() === false) { + throw new Exception('The Panel cannot be installed'); + } + + if ($system->isInstalled() === true) { + throw new Exception('The Panel is already installed'); + } + + // create the first user + $user = $this->users()->create($this->requestBody()); + $token = $user->login($this->requestBody('password')); + + return [ + 'status' => 'ok', + 'token' => $token, + 'user' => $this->resolve($user)->view('auth')->toArray() + ]; + } + ] + +]; diff --git a/kirby/config/api/routes/translations.php b/kirby/config/api/routes/translations.php new file mode 100644 index 0000000..db7faca --- /dev/null +++ b/kirby/config/api/routes/translations.php @@ -0,0 +1,24 @@ + 'translations', + 'method' => 'GET', + 'auth' => false, + 'action' => function () { + return $this->kirby()->translations(); + } + ], + [ + 'pattern' => 'translations/(:any)', + 'method' => 'GET', + 'auth' => false, + 'action' => function (string $code) { + return $this->kirby()->translations()->find($code); + } + ] + +]; diff --git a/kirby/config/api/routes/users.php b/kirby/config/api/routes/users.php new file mode 100644 index 0000000..a5c41d4 --- /dev/null +++ b/kirby/config/api/routes/users.php @@ -0,0 +1,160 @@ + 'users', + 'method' => 'GET', + 'action' => function () { + return $this->users(); + } + ], + [ + 'pattern' => 'users', + 'method' => 'POST', + 'action' => function () { + return $this->users()->create($this->requestBody()); + } + ], + [ + 'pattern' => 'users/search', + 'method' => 'GET|POST', + 'action' => function () { + if ($this->requestMethod() === 'GET') { + return $this->users()->search($this->requestQuery('q')); + } else { + return $this->users()->query($this->requestBody()); + } + } + ], + [ + 'pattern' => 'users/(:any)', + 'method' => 'GET', + 'action' => function (string $id) { + return $this->user($id); + } + ], + [ + 'pattern' => 'users/(:any)', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->user($id)->update($this->requestBody(), $this->language(), true); + } + ], + [ + 'pattern' => 'users/(:any)', + 'method' => 'DELETE', + 'action' => function (string $id) { + return $this->user($id)->delete(); + } + ], + [ + 'pattern' => 'users/(:any)/avatar', + 'method' => 'GET', + 'action' => function (string $id) { + return $this->user($id)->avatar(); + } + ], + [ + 'pattern' => 'users/(:any)/avatar', + 'method' => 'POST', + 'action' => function (string $id) { + if ($avatar = $this->user($id)->avatar()) { + $avatar->delete(); + } + + return $this->upload(function ($source, $filename) use ($id) { + return $this->user($id)->createFile([ + 'filename' => 'profile.' . F::extension($filename), + 'template' => 'avatar', + 'source' => $source + ]); + }, $single = true); + } + ], + [ + 'pattern' => 'users/(:any)/avatar', + 'method' => 'DELETE', + 'action' => function (string $id) { + return $this->user($id)->avatar()->delete(); + } + ], + [ + 'pattern' => 'users/(:any)/blueprint', + 'method' => 'GET', + 'action' => function (string $id) { + return $this->user($id)->blueprint(); + } + ], + [ + 'pattern' => 'users/(:any)/blueprints', + 'method' => 'GET', + 'action' => function (string $id) { + return $this->user($id)->blueprints($this->requestQuery('section')); + } + ], + [ + 'pattern' => 'users/(:any)/email', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->user($id)->changeEmail($this->requestBody('email')); + } + ], + [ + 'pattern' => 'users/(:any)/fields/(:any)/(:all?)', + 'method' => 'ALL', + 'action' => function (string $id, string $fieldName, string $path = null) { + if ($user = $this->user($id)) { + return $this->fieldApi($user, $fieldName, $path); + } + } + ], + [ + 'pattern' => 'users/(:any)/language', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->user($id)->changeLanguage($this->requestBody('language')); + } + ], + [ + 'pattern' => 'users/(:any)/name', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->user($id)->changeName($this->requestBody('name')); + } + ], + [ + 'pattern' => 'users/(:any)/password', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->user($id)->changePassword($this->requestBody('password')); + } + ], + [ + 'pattern' => 'users/(:any)/role', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->user($id)->changeRole($this->requestBody('role')); + } + ], + [ + 'pattern' => 'users/(:any)/roles', + 'action' => function (string $id) { + return $this->user($id)->roles(); + } + ], + [ + 'pattern' => 'users/(:any)/sections/(:any)', + 'method' => 'GET', + 'action' => function (string $id, string $sectionName) { + if ($section = $this->user($id)->blueprint()->section($sectionName)) { + return $section->toResponse(); + } + } + ], +]; diff --git a/kirby/config/blueprints.php b/kirby/config/blueprints.php new file mode 100644 index 0000000..84792fb --- /dev/null +++ b/kirby/config/blueprints.php @@ -0,0 +1,7 @@ + __DIR__ . '/blueprints/file.yml', + 'pages/default' => __DIR__ . '/blueprints/page.yml', + 'site' => __DIR__ . '/blueprints/site.yml' +]; diff --git a/kirby/config/blueprints/file.yml b/kirby/config/blueprints/file.yml new file mode 100644 index 0000000..d5ef1df --- /dev/null +++ b/kirby/config/blueprints/file.yml @@ -0,0 +1,2 @@ +name: File +title: file diff --git a/kirby/config/blueprints/page.yml b/kirby/config/blueprints/page.yml new file mode 100644 index 0000000..ceb895a --- /dev/null +++ b/kirby/config/blueprints/page.yml @@ -0,0 +1,3 @@ +name: Page +title: Page + diff --git a/kirby/config/blueprints/site.yml b/kirby/config/blueprints/site.yml new file mode 100644 index 0000000..04718f3 --- /dev/null +++ b/kirby/config/blueprints/site.yml @@ -0,0 +1,7 @@ +name: Site +title: Site +sections: + pages: + headline: Pages + type: pages + diff --git a/kirby/config/components.php b/kirby/config/components.php new file mode 100755 index 0000000..132aa21 --- /dev/null +++ b/kirby/config/components.php @@ -0,0 +1,376 @@ + function (App $kirby, string $url, $options = null): string { + return $url; + }, + + + /** + * Object and variable dumper + * to help with debugging. + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param mixed $variable + * @param bool $echo + * @return string + */ + 'dump' => function (App $kirby, $variable, bool $echo = true) { + if (Server::cli() === true) { + $output = print_r($variable, true) . PHP_EOL; + } else { + $output = '
' . print_r($variable, true) . '
'; + } + + if ($echo === true) { + echo $output; + } + + return $output; + }, + + /** + * Modify URLs for file objects + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param \Kirby\Cms\File $file The original file object + * @return string + */ + 'file::url' => function (App $kirby, File $file): string { + return $file->mediaUrl(); + }, + + /** + * Adapt file characteristics + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param \Kirby\Cms\File|\Kirby\Cms\FileModifications $file The file object + * @param array $options All thumb options (width, height, crop, blur, grayscale) + * @return \Kirby\Cms\File|\Kirby\Cms\FileVersion + */ + 'file::version' => function (App $kirby, $file, array $options = []) { + if ($file->isResizable() === false) { + return $file; + } + + // create url and root + $mediaRoot = dirname($file->mediaRoot()); + $template = $mediaRoot . '/{{ name }}{{ attributes }}.{{ extension }}'; + $thumbRoot = (new Filename($file->root(), $template, $options))->toString(); + $thumbName = basename($thumbRoot); + $job = $mediaRoot . '/.jobs/' . $thumbName . '.json'; + + if (file_exists($thumbRoot) === false) { + try { + Data::write($job, array_merge($options, [ + 'filename' => $file->filename() + ])); + } catch (Throwable $e) { + return $file; + } + } + + return new FileVersion([ + 'modifications' => $options, + 'original' => $file, + 'root' => $thumbRoot, + 'url' => dirname($file->mediaUrl()) . '/' . $thumbName, + ]); + }, + + /** + * Used by the `js()` helper + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param string $url Relative or absolute URL + * @param string|array $options An array of attributes for the link tag or a media attribute string + */ + 'js' => function (App $kirby, string $url, $options = null): string { + return $url; + }, + + /** + * Add your own Markdown parser + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param string $text Text to parse + * @param array $options Markdown options + * @param bool $inline Whether to wrap the text in `

` tags + * @return string + */ + 'markdown' => function (App $kirby, string $text = null, array $options = [], bool $inline = false): string { + static $markdown; + static $config; + + // if the config options have changed or the component is called for the first time, + // (re-)initialize the parser object + if ($config !== $options) { + $markdown = new Markdown($options); + $config = $options; + } + + return $markdown->parse($text, $inline); + }, + + /** + * Add your own search engine + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param \Kirby\Cms\Collection $collection Collection of searchable models + * @param string $query + * @param mixed $params + * @return \Kirby\Cms\Collection|bool + */ + 'search' => function (App $kirby, Collection $collection, string $query = null, $params = []) { + if (empty(trim($query)) === true) { + return $collection->limit(0); + } + + if (is_string($params) === true) { + $params = ['fields' => Str::split($params, '|')]; + } + + $defaults = [ + 'fields' => [], + 'minlength' => 2, + 'score' => [], + 'words' => false, + ]; + + $options = array_merge($defaults, $params); + $collection = clone $collection; + $searchWords = preg_replace('/(\s)/u', ',', $query); + $searchWords = Str::split($searchWords, ',', $options['minlength']); + $lowerQuery = Str::lower($query); + $exactQuery = $options['words'] ? '(\b' . preg_quote($query) . '\b)' : preg_quote($query); + + if (empty($options['stopwords']) === false) { + $searchWords = array_diff($searchWords, $options['stopwords']); + } + + $searchWords = array_map(function ($value) use ($options) { + return $options['words'] ? '\b' . preg_quote($value) . '\b' : preg_quote($value); + }, $searchWords); + + $preg = '!(' . implode('|', $searchWords) . ')!i'; + $results = $collection->filter(function ($item) use ($query, $preg, $options, $lowerQuery, $exactQuery) { + $data = $item->content()->toArray(); + $keys = array_keys($data); + $keys[] = 'id'; + + if (is_a($item, 'Kirby\Cms\User') === true) { + $keys[] = 'name'; + $keys[] = 'email'; + $keys[] = 'role'; + } elseif (is_a($item, 'Kirby\Cms\Page') === true) { + // apply the default score for pages + $options['score'] = array_merge([ + 'id' => 64, + 'title' => 64, + ], $options['score']); + } + + if (empty($options['fields']) === false) { + $fields = array_map('strtolower', $options['fields']); + $keys = array_intersect($keys, $fields); + } + + $item->searchHits = 0; + $item->searchScore = 0; + + foreach ($keys as $key) { + $score = $options['score'][$key] ?? 1; + $value = $data[$key] ?? (string)$item->$key(); + + $lowerValue = Str::lower($value); + + // check for exact matches + if ($lowerQuery == $lowerValue) { + $item->searchScore += 16 * $score; + $item->searchHits += 1; + + // check for exact beginning matches + } elseif ($options['words'] === false && Str::startsWith($lowerValue, $lowerQuery) === true) { + $item->searchScore += 8 * $score; + $item->searchHits += 1; + + // check for exact query matches + } elseif ($matches = preg_match_all('!' . $exactQuery . '!i', $value, $r)) { + $item->searchScore += 2 * $score; + $item->searchHits += $matches; + } + + // check for any match + if ($matches = preg_match_all($preg, $value, $r)) { + $item->searchHits += $matches; + $item->searchScore += $matches * $score; + } + } + + return $item->searchHits > 0 ? true : false; + }); + + return $results->sortBy('searchScore', 'desc'); + }, + + /** + * Add your own SmartyPants parser + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param string $text Text to parse + * @param array $options SmartyPants options + * @return string + */ + 'smartypants' => function (App $kirby, string $text = null, array $options = []): string { + static $smartypants; + static $config; + + // if the config options have changed or the component is called for the first time, + // (re-)initialize the parser object + if ($config !== $options) { + $smartypants = new Smartypants($options); + $config = $options; + } + + return $smartypants->parse($text); + }, + + /** + * Add your own snippet loader + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param string|array $name Snippet name + * @param array $data Data array for the snippet + * @return string|null + */ + 'snippet' => function (App $kirby, $name, array $data = []): ?string { + $snippets = A::wrap($name); + + foreach ($snippets as $name) { + $name = (string)$name; + $file = $kirby->root('snippets') . '/' . $name . '.php'; + + if (file_exists($file) === false) { + $file = $kirby->extensions('snippets')[$name] ?? null; + } + + if ($file) { + break; + } + } + + return Snippet::load($file, $data); + }, + + /** + * Add your own template engine + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param string $name Template name + * @param string $type Extension type + * @param string $defaultType Default extension type + * @return \Kirby\Cms\Template + */ + 'template' => function (App $kirby, string $name, string $type = 'html', string $defaultType = 'html') { + return new Template($name, $type, $defaultType); + }, + + /** + * Add your own thumb generator + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param string $src The root of the original file + * @param string $template The template for the root to the desired destination + * @param array $options All thumb options that should be applied: `width`, `height`, `crop`, `blur`, `grayscale` + * @return string + */ + 'thumb' => function (App $kirby, string $src, string $template, array $options): string { + $darkroom = Darkroom::factory(option('thumbs.driver', 'gd'), option('thumbs', [])); + $options = $darkroom->preprocess($src, $options); + $root = (new Filename($src, $template, $options))->toString(); + + F::copy($src, $root, true); + $darkroom->process($root, $options); + + return $root; + }, + + /** + * Modify all URLs + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param string $path URL path + * @param array|string|null $options Array of options for the Uri class + * @param Closure $originalHandler Deprecated: Callback function to the original URL handler with `$path` and `$options` as parameters + * Use `$kirby->nativeComponent('url')` inside your URL component instead. + * @return string + */ + 'url' => function (App $kirby, string $path = null, $options = null, Closure $originalHandler = null): string { + $language = null; + + // get language from simple string option + if (is_string($options) === true) { + $language = $options; + $options = null; + } + + // get language from array + if (is_array($options) === true && isset($options['language']) === true) { + $language = $options['language']; + unset($options['language']); + } + + // get a language url for the linked page, if the page can be found + if ($kirby->multilang() === true) { + $parts = Str::split($path, '#'); + + if ($page = page($parts[0] ?? null)) { + $path = $page->url($language); + + if (isset($parts[1]) === true) { + $path .= '#' . $parts[1]; + } + } + } + + // keep relative urls + if (substr($path, 0, 2) === './' || substr($path, 0, 3) === '../') { + return $path; + } + + $url = Url::makeAbsolute($path, $kirby->url()); + + if ($options === null) { + return $url; + } + + return (new Uri($url, $options))->toString(); + }, + +]; diff --git a/kirby/config/fields.php b/kirby/config/fields.php new file mode 100644 index 0000000..6b82d09 --- /dev/null +++ b/kirby/config/fields.php @@ -0,0 +1,28 @@ + __DIR__ . '/fields/checkboxes.php', + 'date' => __DIR__ . '/fields/date.php', + 'email' => __DIR__ . '/fields/email.php', + 'files' => __DIR__ . '/fields/files.php', + 'gap' => __DIR__ . '/fields/gap.php', + 'headline' => __DIR__ . '/fields/headline.php', + 'hidden' => __DIR__ . '/fields/hidden.php', + 'info' => __DIR__ . '/fields/info.php', + 'line' => __DIR__ . '/fields/line.php', + 'multiselect' => __DIR__ . '/fields/multiselect.php', + 'number' => __DIR__ . '/fields/number.php', + 'pages' => __DIR__ . '/fields/pages.php', + 'radio' => __DIR__ . '/fields/radio.php', + 'range' => __DIR__ . '/fields/range.php', + 'select' => __DIR__ . '/fields/select.php', + 'structure' => __DIR__ . '/fields/structure.php', + 'tags' => __DIR__ . '/fields/tags.php', + 'tel' => __DIR__ . '/fields/tel.php', + 'text' => __DIR__ . '/fields/text.php', + 'textarea' => __DIR__ . '/fields/textarea.php', + 'time' => __DIR__ . '/fields/time.php', + 'toggle' => __DIR__ . '/fields/toggle.php', + 'url' => __DIR__ . '/fields/url.php', + 'users' => __DIR__ . '/fields/users.php' +]; diff --git a/kirby/config/fields/checkboxes.php b/kirby/config/fields/checkboxes.php new file mode 100644 index 0000000..6837b45 --- /dev/null +++ b/kirby/config/fields/checkboxes.php @@ -0,0 +1,61 @@ + ['min', 'options'], + 'props' => [ + /** + * Unset inherited props + */ + 'after' => null, + 'before' => null, + 'icon' => null, + 'placeholder' => null, + + /** + * Arranges the checkboxes in the given number of columns + */ + 'columns' => function (int $columns = 1) { + return $columns; + }, + /** + * Default value for the field, which will be used when a page/file/user is created + */ + 'default' => function ($default = null) { + return Str::split($default, ','); + }, + /** + * Maximum number of checked boxes + */ + 'max' => function (int $max = null) { + return $max; + }, + /** + * Minimum number of checked boxes + */ + 'min' => function (int $min = null) { + return $min; + }, + 'value' => function ($value = null) { + return Str::split($value, ','); + }, + ], + 'computed' => [ + 'default' => function () { + return $this->sanitizeOptions($this->default); + }, + 'value' => function () { + return $this->sanitizeOptions($this->value); + }, + ], + 'save' => function ($value): string { + return A::join($value, ', '); + }, + 'validations' => [ + 'options', + 'max', + 'min' + ] +]; diff --git a/kirby/config/fields/date.php b/kirby/config/fields/date.php new file mode 100644 index 0000000..cbf4c55 --- /dev/null +++ b/kirby/config/fields/date.php @@ -0,0 +1,129 @@ + [ + /** + * Default date when a new page/file/user gets created + */ + 'default' => function ($default = null) { + return $default; + }, + + /** + * Defines a custom format that is used when the field is saved + */ + 'format' => function (string $format = null) { + return $format; + }, + + /** + * Changes the calendar icon to something custom + */ + 'icon' => function (string $icon = 'calendar') { + return $icon; + }, + /** + * Youngest date, which can be selected/saved + */ + 'max' => function (string $max = null) { + return $this->toDate($max); + }, + /** + * Oldest date, which can be selected/saved + */ + 'min' => function (string $min = null) { + return $this->toDate($min); + }, + /** + * The placeholder is not available + */ + 'placeholder' => null, + /** + * Pass `true` or an array of time field options to show the time selector. + */ + 'time' => function ($time = false) { + return $time; + }, + /** + * Must be a parseable date string + */ + 'value' => function ($value = null) { + return $value; + }, + ], + 'computed' => [ + 'default' => function () { + return $this->toDate($this->default); + }, + 'format' => function () { + return $this->props['format'] ?? ($this->time() === false ? 'Y-m-d' : 'Y-m-d H:i'); + }, + 'value' => function () { + return $this->toDate($this->value); + }, + ], + 'methods' => [ + 'toDate' => function ($value) { + if ($timestamp = timestamp($value, $this->time['step'] ?? 5)) { + return date('Y-m-d H:i:s', $timestamp); + } + + return null; + } + ], + 'save' => function ($value) { + if ($value !== null && $date = strtotime($value)) { + return date($this->format(), $date); + } + + return ''; + }, + 'validations' => [ + 'date', + 'minMax' => function ($value) { + $min = $this->min ? strtotime($this->min) : null; + $max = $this->max ? strtotime($this->max) : null; + $value = strtotime($this->value()); + $format = 'd.m.Y'; + $errors = []; + + if ($value && $min && $value < $min) { + $errors['min'] = $min; + } + + if ($value && $max && $value > $max) { + $errors['max'] = $max; + } + + if (empty($errors) === false) { + if ($min && $max) { + throw new Exception([ + 'key' => 'validation.date.between', + 'data' => [ + 'min' => date($format, $min), + 'max' => date($format, $max) + ] + ]); + } elseif ($min) { + throw new Exception([ + 'key' => 'validation.date.after', + 'data' => [ + 'date' => date($format, $min), + ] + ]); + } else { + throw new Exception([ + 'key' => 'validation.date.before', + 'data' => [ + 'date' => date($format, $max), + ] + ]); + } + } + + return true; + }, + ] +]; diff --git a/kirby/config/fields/email.php b/kirby/config/fields/email.php new file mode 100644 index 0000000..e7892b8 --- /dev/null +++ b/kirby/config/fields/email.php @@ -0,0 +1,40 @@ + 'text', + 'props' => [ + /** + * Unset inherited props + */ + 'converter' => null, + 'counter' => null, + + /** + * Sets the HTML5 autocomplete mode for the input + */ + 'autocomplete' => function (string $autocomplete = 'email') { + return $autocomplete; + }, + + /** + * Changes the email icon to something custom + */ + 'icon' => function (string $icon = 'email') { + return $icon; + }, + + /** + * Custom placeholder text, when the field is empty. + */ + 'placeholder' => function ($value = null) { + return I18n::translate($value, $value) ?? I18n::translate('email.placeholder'); + } + ], + 'validations' => [ + 'minlength', + 'maxlength', + 'email' + ] +]; diff --git a/kirby/config/fields/files.php b/kirby/config/fields/files.php new file mode 100644 index 0000000..e4cb1f4 --- /dev/null +++ b/kirby/config/fields/files.php @@ -0,0 +1,138 @@ + [ + 'picker', + 'filepicker', + 'min', + 'upload' + ], + 'props' => [ + /** + * Unset inherited props + */ + 'after' => null, + 'before' => null, + 'autofocus' => null, + 'icon' => null, + 'placeholder' => null, + + /** + * Sets the file(s), which are selected by default when a new page is created + */ + 'default' => function ($default = null) { + return $default; + }, + + /** + * Changes the layout of the selected files. Available layouts: `list`, `cards` + */ + 'layout' => function (string $layout = 'list') { + return $layout; + }, + + /** + * Layout size for cards: `tiny`, `small`, `medium`, `large` or `huge` + */ + 'size' => function (string $size = 'auto') { + return $size; + }, + + 'value' => function ($value = null) { + return $value; + } + ], + 'computed' => [ + 'parentModel' => function () { + if (is_string($this->parent) === true && $model = $this->model()->query($this->parent, 'Kirby\Cms\Model')) { + return $model; + } + + return $this->model(); + }, + 'parent' => function () { + return $this->parentModel->apiUrl(true); + }, + 'query' => function () { + return $this->query ?? $this->parentModel::CLASS_ALIAS . '.files'; + }, + 'default' => function () { + return $this->toFiles($this->default); + }, + 'value' => function () { + return $this->toFiles($this->value); + }, + ], + 'methods' => [ + 'fileResponse' => function ($file) { + return $file->panelPickerData([ + 'image' => $this->image, + 'info' => $this->info ?? false, + 'model' => $this->model(), + 'text' => $this->text, + ]); + }, + 'toFiles' => function ($value = null) { + $files = []; + + foreach (Data::decode($value, 'yaml') as $id) { + if (is_array($id) === true) { + $id = $id['id'] ?? null; + } + + if ($id !== null && ($file = $this->kirby()->file($id, $this->model()))) { + $files[] = $this->fileResponse($file); + } + } + + return $files; + } + ], + 'api' => function () { + return [ + [ + 'pattern' => '/', + 'action' => function () { + $field = $this->field(); + + return $field->filepicker([ + 'image' => $field->image(), + 'info' => $field->info(), + 'limit' => $field->limit(), + 'page' => $this->requestQuery('page'), + 'query' => $field->query(), + 'search' => $this->requestQuery('search'), + 'text' => $field->text() + ]); + } + ], + [ + 'pattern' => 'upload', + 'method' => 'POST', + 'action' => function () { + $field = $this->field(); + $uploads = $field->uploads(); + + return $field->upload($this, $uploads, function ($file, $parent) use ($field) { + return $file->panelPickerData([ + 'image' => $field->image(), + 'info' => $field->info(), + 'model' => $field->model(), + 'text' => $field->text(), + ]); + }); + } + ] + ]; + }, + 'save' => function ($value = null) { + return A::pluck($value, 'uuid'); + }, + 'validations' => [ + 'max', + 'min' + ] +]; diff --git a/kirby/config/fields/gap.php b/kirby/config/fields/gap.php new file mode 100644 index 0000000..6844d6c --- /dev/null +++ b/kirby/config/fields/gap.php @@ -0,0 +1,5 @@ + false +]; diff --git a/kirby/config/fields/headline.php b/kirby/config/fields/headline.php new file mode 100644 index 0000000..9b52938 --- /dev/null +++ b/kirby/config/fields/headline.php @@ -0,0 +1,27 @@ + false, + 'props' => [ + /** + * Unset inherited props + */ + 'after' => null, + 'autofocus' => null, + 'before' => null, + 'default' => null, + 'disabled' => null, + 'help' => null, + 'icon' => null, + 'placeholder' => null, + 'required' => null, + 'translate' => null, + + /** + * If `false`, the prepended number will be hidden + */ + 'numbered' => function (bool $numbered = true) { + return $numbered; + } + ] +]; diff --git a/kirby/config/fields/hidden.php b/kirby/config/fields/hidden.php new file mode 100644 index 0000000..0b67a5f --- /dev/null +++ b/kirby/config/fields/hidden.php @@ -0,0 +1,3 @@ + [ + /** + * Text to be displayed + */ + 'text' => function ($value = null) { + return I18n::translate($value, $value); + }, + ], + 'computed' => [ + 'text' => function () { + if ($text = $this->text) { + $text = $this->model()->toString($text); + $text = $this->kirby()->kirbytext($text); + return $text; + } + } + ], + 'save' => false, +]; diff --git a/kirby/config/fields/line.php b/kirby/config/fields/line.php new file mode 100644 index 0000000..6844d6c --- /dev/null +++ b/kirby/config/fields/line.php @@ -0,0 +1,5 @@ + false +]; diff --git a/kirby/config/fields/mixins/filepicker.php b/kirby/config/fields/mixins/filepicker.php new file mode 100644 index 0000000..ba81230 --- /dev/null +++ b/kirby/config/fields/mixins/filepicker.php @@ -0,0 +1,14 @@ + [ + 'filepicker' => function (array $params = []) { + // fetch the parent model + $params['model'] = $this->model(); + + return (new FilePicker($params))->toArray(); + } + ] +]; diff --git a/kirby/config/fields/mixins/min.php b/kirby/config/fields/mixins/min.php new file mode 100644 index 0000000..33e24d4 --- /dev/null +++ b/kirby/config/fields/mixins/min.php @@ -0,0 +1,22 @@ + [ + 'min' => function () { + // set min to at least 1, if required + if ($this->required === true) { + return $this->min ?? 1; + } + + return $this->min; + }, + 'required' => function () { + // set required to true if min is set + if ($this->min) { + return true; + } + + return $this->required; + } + ] +]; diff --git a/kirby/config/fields/mixins/options.php b/kirby/config/fields/mixins/options.php new file mode 100644 index 0000000..170761a --- /dev/null +++ b/kirby/config/fields/mixins/options.php @@ -0,0 +1,48 @@ + [ + /** + * API settings for options requests. This will only take affect when `options` is set to `api`. + */ + 'api' => function ($api = null) { + return $api; + }, + /** + * An array with options + */ + 'options' => function ($options = []) { + return $options; + }, + /** + * Query settings for options queries. This will only take affect when `options` is set to `query`. + */ + 'query' => function ($query = null) { + return $query; + }, + ], + 'computed' => [ + 'options' => function (): array { + return $this->getOptions(); + } + ], + 'methods' => [ + 'getOptions' => function () { + return Options::factory( + $this->options(), + $this->props, + $this->model() + ); + }, + 'sanitizeOption' => function ($option) { + $allowed = array_column($this->options(), 'value'); + return in_array($option, $allowed, true) === true ? $option : null; + }, + 'sanitizeOptions' => function ($options) { + $allowed = array_column($this->options(), 'value'); + return array_intersect($options, $allowed); + }, + ] +]; diff --git a/kirby/config/fields/mixins/pagepicker.php b/kirby/config/fields/mixins/pagepicker.php new file mode 100644 index 0000000..bbdc86e --- /dev/null +++ b/kirby/config/fields/mixins/pagepicker.php @@ -0,0 +1,14 @@ + [ + 'pagepicker' => function (array $params = []) { + // inject the current model + $params['model'] = $this->model(); + + return (new PagePicker($params))->toArray(); + } + ] +]; diff --git a/kirby/config/fields/mixins/picker.php b/kirby/config/fields/mixins/picker.php new file mode 100644 index 0000000..c2660ad --- /dev/null +++ b/kirby/config/fields/mixins/picker.php @@ -0,0 +1,78 @@ + [ + /** + * The placeholder text if none have been selected yet + */ + 'empty' => function ($empty = null) { + return I18n::translate($empty, $empty); + }, + + /** + * Image settings for each item + */ + 'image' => function ($image = null) { + return $image; + }, + + /** + * Info text for each item + */ + 'info' => function (string $info = null) { + return $info; + }, + + /** + * Whether each item should be clickable + */ + 'link' => function (bool $link = true) { + return $link; + }, + + /** + * The minimum number of required selected + */ + 'min' => function (int $min = null) { + return $min; + }, + + /** + * The maximum number of allowed selected + */ + 'max' => function (int $max = null) { + return $max; + }, + + /** + * If `false`, only a single one can be selected + */ + 'multiple' => function (bool $multiple = true) { + return $multiple; + }, + + /** + * Query for the items to be included in the picker + */ + 'query' => function (string $query = null) { + return $query; + }, + + /** + * Enable/disable the search field in the picker + */ + 'search' => function (bool $search = true) { + return $search; + }, + + /** + * Main text for each item + */ + 'text' => function (string $text = null) { + return $text; + }, + + ], +]; diff --git a/kirby/config/fields/mixins/upload.php b/kirby/config/fields/mixins/upload.php new file mode 100644 index 0000000..ce5bd4c --- /dev/null +++ b/kirby/config/fields/mixins/upload.php @@ -0,0 +1,72 @@ + [ + /** + * Sets the upload options for linked files (since 3.2.0) + */ + 'uploads' => function ($uploads = []) { + if ($uploads === false) { + return false; + } + + if (is_string($uploads) === true) { + $uploads = ['template' => $uploads]; + } + + if (is_array($uploads) === false) { + $uploads = []; + } + + $template = $uploads['template'] ?? null; + + if ($template) { + $file = new File([ + 'filename' => 'tmp', + 'template' => $template + ]); + + $uploads['accept'] = $file->blueprint()->accept()['mime'] ?? '*'; + } else { + $uploads['accept'] = '*'; + } + + return $uploads; + }, + ], + 'methods' => [ + 'upload' => function (Api $api, $params, Closure $map) { + if ($params === false) { + throw new Exception('Uploads are disabled for this field'); + } + + if ($parentQuery = ($params['parent'] ?? null)) { + $parent = $this->model()->query($parentQuery); + } else { + $parent = $this->model(); + } + + if (is_a($parent, 'Kirby\Cms\File') === true) { + $parent = $parent->parent(); + } + + return $api->upload(function ($source, $filename) use ($parent, $params, $map) { + $file = $parent->createFile([ + 'source' => $source, + 'template' => $params['template'] ?? null, + 'filename' => $filename, + ]); + + if (is_a($file, 'Kirby\Cms\File') === false) { + throw new Exception('The file could not be uploaded'); + } + + return $map($file, $parent); + }); + } + ] +]; diff --git a/kirby/config/fields/mixins/userpicker.php b/kirby/config/fields/mixins/userpicker.php new file mode 100644 index 0000000..41c2b62 --- /dev/null +++ b/kirby/config/fields/mixins/userpicker.php @@ -0,0 +1,13 @@ + [ + 'userpicker' => function (array $params = []) { + $params['model'] = $this->model(); + + return (new UserPicker($params))->toArray(); + } + ] +]; diff --git a/kirby/config/fields/multiselect.php b/kirby/config/fields/multiselect.php new file mode 100644 index 0000000..4ed2422 --- /dev/null +++ b/kirby/config/fields/multiselect.php @@ -0,0 +1,32 @@ + 'tags', + 'props' => [ + /** + * Unset inherited props + */ + 'accept' => null, + /** + * Custom icon to replace the arrow down. + */ + 'icon' => function (string $icon = null) { + return $icon; + }, + /** + * Enable/disable the search in the dropdown + * Also limit displayed items (display: 20) + * and set minimum number of characters to search (min: 3) + */ + 'search' => function ($search = true) { + return $search; + }, + /** + * If `true`, selected entries will be sorted + * according to their position in the dropdown + */ + 'sort' => function (bool $sort = false) { + return $sort; + }, + ] +]; diff --git a/kirby/config/fields/number.php b/kirby/config/fields/number.php new file mode 100644 index 0000000..92a23ee --- /dev/null +++ b/kirby/config/fields/number.php @@ -0,0 +1,48 @@ + [ + /** + * Default number that will be saved when a new page/user/file is created + */ + 'default' => function ($default = null) { + return $this->toNumber($default); + }, + /** + * The lowest allowed number + */ + 'min' => function (float $min = null) { + return $min; + }, + /** + * The highest allowed number + */ + 'max' => function (float $max = null) { + return $max; + }, + /** + * Allowed incremental steps between numbers (i.e `0.5`) + */ + 'step' => function ($step = null) { + return $this->toNumber($step); + }, + 'value' => function ($value = null) { + return $this->toNumber($value); + } + ], + 'methods' => [ + 'toNumber' => function ($value) { + if ($this->isEmpty($value) === true) { + return null; + } + + return is_float($value) === true ? $value : (float)Str::float($value); + } + ], + 'validations' => [ + 'min', + 'max' + ] +]; diff --git a/kirby/config/fields/pages.php b/kirby/config/fields/pages.php new file mode 100644 index 0000000..471f8b1 --- /dev/null +++ b/kirby/config/fields/pages.php @@ -0,0 +1,117 @@ + ['min', 'pagepicker', 'picker'], + 'props' => [ + /** + * Unset inherited props + */ + 'after' => null, + 'autofocus' => null, + 'before' => null, + 'icon' => null, + 'placeholder' => null, + + /** + * Default selected page(s) when a new page/file/user is created + */ + 'default' => function ($default = null) { + return $this->toPages($default); + }, + + /** + * Changes the layout of the selected files. Available layouts: `list`, `cards` + */ + 'layout' => function (string $layout = 'list') { + return $layout; + }, + + /** + * Optional query to select a specific set of pages + */ + 'query' => function (string $query = null) { + return $query; + }, + + /** + * Layout size for cards: `tiny`, `small`, `medium`, `large` or `huge` + */ + 'size' => function (string $size = 'auto') { + return $size; + }, + + /** + * Optionally include subpages of pages + */ + 'subpages' => function (bool $subpages = true) { + return $subpages; + }, + + 'value' => function ($value = null) { + return $this->toPages($value); + }, + ], + 'computed' => [ + /** + * Unset inherited computed + */ + 'default' => null + ], + 'methods' => [ + 'pageResponse' => function ($page) { + return $page->panelPickerData([ + 'image' => $this->image, + 'info' => $this->info, + 'text' => $this->text, + ]); + }, + 'toPages' => function ($value = null) { + $pages = []; + $kirby = kirby(); + + foreach (Data::decode($value, 'yaml') as $id) { + if (is_array($id) === true) { + $id = $id['id'] ?? null; + } + + if ($id !== null && ($page = $kirby->page($id))) { + $pages[] = $this->pageResponse($page); + } + } + + return $pages; + } + ], + 'api' => function () { + return [ + [ + 'pattern' => '/', + 'action' => function () { + $field = $this->field(); + + return $field->pagepicker([ + 'image' => $field->image(), + 'info' => $field->info(), + 'limit' => $field->limit(), + 'page' => $this->requestQuery('page'), + 'parent' => $this->requestQuery('parent'), + 'query' => $field->query(), + 'search' => $this->requestQuery('search'), + 'subpages' => $field->subpages(), + 'text' => $field->text() + ]); + } + ] + ]; + }, + 'save' => function ($value = null) { + return A::pluck($value, 'id'); + }, + 'validations' => [ + 'max', + 'min' + ] +]; diff --git a/kirby/config/fields/radio.php b/kirby/config/fields/radio.php new file mode 100644 index 0000000..dd9ffc3 --- /dev/null +++ b/kirby/config/fields/radio.php @@ -0,0 +1,29 @@ + ['options'], + 'props' => [ + /** + * Unset inherited props + */ + 'after' => null, + 'before' => null, + 'icon' => null, + 'placeholder' => null, + + /** + * Arranges the radio buttons in the given number of columns + */ + 'columns' => function (int $columns = 1) { + return $columns; + }, + ], + 'computed' => [ + 'default' => function () { + return $this->sanitizeOption($this->default); + }, + 'value' => function () { + return $this->sanitizeOption($this->value) ?? ''; + } + ] +]; diff --git a/kirby/config/fields/range.php b/kirby/config/fields/range.php new file mode 100644 index 0000000..5f14388 --- /dev/null +++ b/kirby/config/fields/range.php @@ -0,0 +1,24 @@ + 'number', + 'props' => [ + /** + * Unset inherited props + */ + 'placeholder' => null, + + /** + * The maximum value on the slider + */ + 'max' => function (float $max = 100) { + return $max; + }, + /** + * Enables/disables the tooltip and set the before and after values + */ + 'tooltip' => function ($tooltip = true) { + return $tooltip; + }, + ] +]; diff --git a/kirby/config/fields/select.php b/kirby/config/fields/select.php new file mode 100644 index 0000000..24b14b6 --- /dev/null +++ b/kirby/config/fields/select.php @@ -0,0 +1,24 @@ + 'radio', + 'props' => [ + /** + * Unset inherited props + */ + 'columns' => null, + + /** + * Custom icon to replace the arrow down. + */ + 'icon' => function (string $icon = null) { + return $icon; + }, + /** + * Custom placeholder string for empty option. + */ + 'placeholder' => function (string $placeholder = '—') { + return $placeholder; + }, + ] +]; diff --git a/kirby/config/fields/structure.php b/kirby/config/fields/structure.php new file mode 100644 index 0000000..b834b2c --- /dev/null +++ b/kirby/config/fields/structure.php @@ -0,0 +1,193 @@ + ['min'], + 'props' => [ + /** + * Unset inherited props + */ + 'after' => null, + 'before' => null, + 'autofocus' => null, + 'icon' => null, + 'placeholder' => null, + + /** + * Optional columns definition to only show selected fields in the structure table. + */ + 'columns' => function (array $columns = []) { + // lower case all keys, because field names will + // be lowercase as well. + return array_change_key_case($columns); + }, + + /** + * Toggles duplicating rows for the structure + */ + 'duplicate' => function (bool $duplicate = true) { + return $duplicate; + }, + + /** + * The placeholder text if no items have been added yet + */ + 'empty' => function ($empty = null) { + return I18n::translate($empty, $empty); + }, + + /** + * Set the default rows for the structure + */ + 'default' => function (array $default = null) { + return $default; + }, + + /** + * Fields setup for the structure form. Works just like fields in regular forms. + */ + 'fields' => function (array $fields) { + return $fields; + }, + /** + * The number of entries that will be displayed on a single page. Afterwards pagination kicks in. + */ + 'limit' => function (int $limit = null) { + return $limit; + }, + /** + * Maximum allowed entries in the structure. Afterwards the "Add" button will be switched off. + */ + 'max' => function (int $max = null) { + return $max; + }, + /** + * Minimum required entries in the structure + */ + 'min' => function (int $min = null) { + return $min; + }, + /** + * Toggles adding to the top or bottom of the list + */ + 'prepend' => function (bool $prepend = null) { + return $prepend; + }, + /** + * Toggles drag & drop sorting + */ + 'sortable' => function (bool $sortable = null) { + return $sortable; + }, + /** + * Sorts the entries by the given field and order (i.e. `title desc`) + * Drag & drop is disabled in this case + */ + 'sortBy' => function (string $sort = null) { + return $sort; + } + ], + 'computed' => [ + 'default' => function () { + return $this->rows($this->default); + }, + 'value' => function () { + return $this->rows($this->value); + }, + 'fields' => function () { + if (empty($this->fields) === true) { + throw new Exception('Please provide some fields for the structure'); + } + + return $this->form()->fields()->toArray(); + }, + 'columns' => function () { + $columns = []; + + if (empty($this->columns)) { + foreach ($this->fields as $field) { + + // Skip hidden and unsaveable fields + // They should never be included as column + if ($field['type'] === 'hidden' || $field['saveable'] === false) { + continue; + } + + $columns[$field['name']] = [ + 'type' => $field['type'], + 'label' => $field['label'] ?? $field['name'] + ]; + } + } else { + foreach ($this->columns as $columnName => $columnProps) { + if (is_array($columnProps) === false) { + $columnProps = []; + } + + $field = $this->fields[$columnName] ?? null; + + if (empty($field) === true || $field['saveable'] === false) { + continue; + } + + $columns[$columnName] = array_merge($columnProps, [ + 'type' => $field['type'], + 'label' => $field['label'] ?? $field['name'] + ]); + } + } + + return $columns; + } + ], + 'methods' => [ + 'rows' => function ($value) { + $rows = Data::decode($value, 'yaml'); + $value = []; + + foreach ($rows as $index => $row) { + if (is_array($row) === false) { + continue; + } + + $value[] = $this->form($row)->values(); + } + + return $value; + }, + 'form' => function (array $values = []) { + return new Form([ + 'fields' => $this->attrs['fields'], + 'values' => $values, + 'model' => $this->model + ]); + }, + ], + 'api' => function () { + return [ + [ + 'pattern' => 'validate', + 'method' => 'ALL', + 'action' => function () { + return array_values($this->field()->form($this->requestBody())->errors()); + } + ] + ]; + }, + 'save' => function ($value) { + $data = []; + + foreach ($value as $row) { + $data[] = $this->form($row)->content(); + } + + return $data; + }, + 'validations' => [ + 'min', + 'max' + ] +]; diff --git a/kirby/config/fields/tags.php b/kirby/config/fields/tags.php new file mode 100644 index 0000000..93c29bd --- /dev/null +++ b/kirby/config/fields/tags.php @@ -0,0 +1,96 @@ + ['min', 'options'], + 'props' => [ + + /** + * Unset inherited props + */ + 'after' => null, + 'before' => null, + 'placeholder' => null, + + /** + * If set to `all`, any type of input is accepted. If set to `options` only the predefined options are accepted as input. + */ + 'accept' => function ($value = 'all') { + return V::in($value, ['all', 'options']) ? $value : 'all'; + }, + /** + * Changes the tag icon + */ + 'icon' => function ($icon = 'tag') { + return $icon; + }, + /** + * Minimum number of required entries/tags + */ + 'min' => function (int $min = null) { + return $min; + }, + /** + * Maximum number of allowed entries/tags + */ + 'max' => function (int $max = null) { + return $max; + }, + /** + * Custom tags separator, which will be used to store tags in the content file + */ + 'separator' => function (string $separator = ',') { + return $separator; + }, + ], + 'computed' => [ + 'default' => function (): array { + return $this->toTags($this->default); + }, + 'value' => function (): array { + return $this->toTags($this->value); + } + ], + 'methods' => [ + 'toTags' => function ($value) { + if (is_null($value) === true) { + return []; + } + + $options = $this->options(); + + // transform into value-text objects + return array_map(function ($option) use ($options) { + + // already a valid object + if (is_array($option) === true && isset($option['value'], $option['text']) === true) { + return $option; + } + + $index = array_search($option, array_column($options, 'value')); + + if ($index !== false) { + return $options[$index]; + } + + return [ + 'value' => $option, + 'text' => $option, + ]; + }, Str::split($value, $this->separator())); + } + ], + 'save' => function (array $value = null): string { + return A::join( + A::pluck($value, 'value'), + $this->separator() . ' ' + ); + }, + 'validations' => [ + 'min', + 'max' + ] +]; diff --git a/kirby/config/fields/tel.php b/kirby/config/fields/tel.php new file mode 100644 index 0000000..3d73430 --- /dev/null +++ b/kirby/config/fields/tel.php @@ -0,0 +1,27 @@ + 'text', + 'props' => [ + /** + * Unset inherited props + */ + 'converter' => null, + 'counter' => null, + 'spellcheck' => null, + + /** + * Sets the HTML5 autocomplete attribute + */ + 'autocomplete' => function (string $autocomplete = 'tel') { + return $autocomplete; + }, + + /** + * Changes the phone icon + */ + 'icon' => function (string $icon = 'phone') { + return $icon; + } + ] +]; diff --git a/kirby/config/fields/text.php b/kirby/config/fields/text.php new file mode 100644 index 0000000..c32a037 --- /dev/null +++ b/kirby/config/fields/text.php @@ -0,0 +1,103 @@ + [ + + /** + * The field value will be converted with the selected converter before the value gets saved. Available converters: `lower`, `upper`, `ucfirst`, `slug` + */ + 'converter' => function ($value = null) { + if ($value !== null && in_array($value, array_keys($this->converters())) === false) { + throw new InvalidArgumentException([ + 'key' => 'field.converter.invalid', + 'data' => ['converter' => $value] + ]); + } + + return $value; + }, + + /** + * Shows or hides the character counter in the top right corner + */ + 'counter' => function (bool $counter = true) { + return $counter; + }, + + /** + * Maximum number of allowed characters + */ + 'maxlength' => function (int $maxlength = null) { + return $maxlength; + }, + + /** + * Minimum number of required characters + */ + 'minlength' => function (int $minlength = null) { + return $minlength; + }, + + /** + * A regular expression, which will be used to validate the input + */ + 'pattern' => function (string $pattern = null) { + return $pattern; + }, + + /** + * If `false`, spellcheck will be switched off + */ + 'spellcheck' => function (bool $spellcheck = false) { + return $spellcheck; + }, + ], + 'computed' => [ + 'default' => function () { + return $this->convert($this->default); + }, + 'value' => function () { + return (string)$this->convert($this->value); + } + ], + 'methods' => [ + 'convert' => function ($value) { + if ($this->converter() === null) { + return $value; + } + + $value = trim($value); + $converter = $this->converters()[$this->converter()]; + + if (is_array($value) === true) { + return array_map($converter, $value); + } + + return call_user_func($converter, $value); + }, + 'converters' => function (): array { + return [ + 'lower' => function ($value) { + return Str::lower($value); + }, + 'slug' => function ($value) { + return Str::slug($value); + }, + 'ucfirst' => function ($value) { + return Str::ucfirst($value); + }, + 'upper' => function ($value) { + return Str::upper($value); + }, + ]; + }, + ], + 'validations' => [ + 'minlength', + 'maxlength', + 'pattern' + ] +]; diff --git a/kirby/config/fields/textarea.php b/kirby/config/fields/textarea.php new file mode 100644 index 0000000..cd23ddf --- /dev/null +++ b/kirby/config/fields/textarea.php @@ -0,0 +1,122 @@ + ['filepicker', 'upload'], + 'props' => [ + /** + * Unset inherited props + */ + 'after' => null, + 'before' => null, + + /** + * Enables/disables the format buttons. Can either be `true`/`false` or a list of allowed buttons. Available buttons: `headlines`, `italic`, `bold`, `link`, `email`, `file`, `code`, `ul`, `ol` (as well as `|` for a divider) + */ + 'buttons' => function ($buttons = true) { + return $buttons; + }, + + /** + * Enables/disables the character counter in the top right corner + */ + 'counter' => function (bool $counter = true) { + return $counter; + }, + + /** + * Sets the default text when a new page/file/user is created + */ + 'default' => function (string $default = null) { + return trim($default); + }, + + /** + * Sets the options for the files picker + */ + 'files' => function ($files = []) { + if (is_string($files) === true) { + return ['query' => $files]; + } + + if (is_array($files) === false) { + $files = []; + } + + return $files; + }, + + /** + * Sets the font family (sans or monospace) + */ + 'font' => function (string $font = null) { + return $font === 'monospace' ? 'monospace' : 'sans-serif'; + }, + + /** + * Maximum number of allowed characters + */ + 'maxlength' => function (int $maxlength = null) { + return $maxlength; + }, + + /** + * Minimum number of required characters + */ + 'minlength' => function (int $minlength = null) { + return $minlength; + }, + + /** + * Changes the size of the textarea. Available sizes: `small`, `medium`, `large`, `huge` + */ + 'size' => function (string $size = null) { + return $size; + }, + + /** + * If `false`, spellcheck will be switched off + */ + 'spellcheck' => function (bool $spellcheck = true) { + return $spellcheck; + }, + + 'value' => function (string $value = null) { + return trim($value); + } + ], + 'api' => function () { + return [ + [ + 'pattern' => 'files', + 'action' => function () { + $params = array_merge($this->field()->files(), [ + 'page' => $this->requestQuery('page'), + 'search' => $this->requestQuery('search') + ]); + + return $this->field()->filepicker($params); + } + ], + [ + 'pattern' => 'upload', + 'action' => function () { + $field = $this->field(); + $uploads = $field->uploads(); + + return $this->field()->upload($this, $uploads, function ($file, $parent) use ($field) { + $absolute = $field->model()->is($parent) === false; + + return [ + 'filename' => $file->filename(), + 'dragText' => $file->dragText('auto', $absolute), + ]; + }); + } + ] + ]; + }, + 'validations' => [ + 'minlength', + 'maxlength' + ] +]; diff --git a/kirby/config/fields/time.php b/kirby/config/fields/time.php new file mode 100644 index 0000000..ef2fa23 --- /dev/null +++ b/kirby/config/fields/time.php @@ -0,0 +1,68 @@ + [ + /** + * Unset inherited props + */ + 'placeholder' => null, + + /** + * Sets the default time when a new page/file/user is created + */ + 'default' => function ($default = null) { + return $default; + }, + /** + * Changes the clock icon + */ + 'icon' => function (string $icon = 'clock') { + return $icon; + }, + /** + * `12` or `24` hour notation. If `12`, an AM/PM selector will be shown. + */ + 'notation' => function (int $value = 24) { + return $value === 24 ? 24 : 12; + }, + /** + * The interval between minutes in the minutes select dropdown. + */ + 'step' => function (int $step = 5) { + return $step; + }, + 'value' => function ($value = null) { + return $value; + } + ], + 'computed' => [ + 'default' => function () { + return $this->toTime($this->default); + }, + 'format' => function () { + return $this->notation === 24 ? 'H:i' : 'h:i a'; + }, + 'value' => function () { + return $this->toTime($this->value); + } + ], + 'methods' => [ + 'toTime' => function ($value) { + if ($timestamp = timestamp($value, $this->step)) { + return date('H:i', $timestamp); + } + + return null; + } + ], + 'save' => function ($value): string { + if ($timestamp = strtotime($value)) { + return date($this->format, $timestamp); + } + + return ''; + }, + 'validations' => [ + 'time', + ] +]; diff --git a/kirby/config/fields/toggle.php b/kirby/config/fields/toggle.php new file mode 100644 index 0000000..a43c613 --- /dev/null +++ b/kirby/config/fields/toggle.php @@ -0,0 +1,68 @@ + [ + /** + * Unset inherited props + */ + 'placeholder' => null, + + /** + * Default value which will be saved when a new page/user/file is created + */ + 'default' => function ($default = null) { + return $this->default = $default; + }, + /** + * Sets the text next to the toggle. The text can be a string or an array of two options. The first one is the negative text and the second one the positive. The text will automatically switch when the toggle is triggered. + */ + 'text' => function ($value = null) { + if (is_array($value) === true) { + if (A::isAssociative($value) === true) { + return I18n::translate($value, $value); + } + + foreach ($value as $key => $val) { + $value[$key] = I18n::translate($val, $val); + } + + return $value; + } + + return I18n::translate($value, $value); + }, + ], + 'computed' => [ + 'default' => function () { + return $this->toBool($this->default); + }, + 'value' => function () { + if ($this->props['value'] === null) { + return $this->default(); + } else { + return $this->toBool($this->props['value']); + } + } + ], + 'methods' => [ + 'toBool' => function ($value) { + return in_array($value, [true, 'true', 1, '1', 'on'], true) === true; + } + ], + 'save' => function (): string { + return $this->value() === true ? 'true' : 'false'; + }, + 'validations' => [ + 'boolean', + 'required' => function ($value) { + if ($this->isRequired() && ($value === false || $this->isEmpty($value))) { + throw new InvalidArgumentException([ + 'key' => 'form.field.required' + ]); + } + }, + ] +]; diff --git a/kirby/config/fields/url.php b/kirby/config/fields/url.php new file mode 100644 index 0000000..f92dd2c --- /dev/null +++ b/kirby/config/fields/url.php @@ -0,0 +1,41 @@ + 'text', + 'props' => [ + /** + * Unset inherited props + */ + 'converter' => null, + 'counter' => null, + 'spellcheck' => null, + + /** + * Sets the HTML5 autocomplete attribute + */ + 'autocomplete' => function (string $autocomplete = 'url') { + return $autocomplete; + }, + + /** + * Changes the link icon + */ + 'icon' => function (string $icon = 'url') { + return $icon; + }, + + /** + * Sets custom placeholder text, when the field is empty + */ + 'placeholder' => function ($value = null) { + return I18n::translate($value, $value) ?? 'https://example.com'; + } + ], + 'validations' => [ + 'minlength', + 'maxlength', + 'url' + ], +]; diff --git a/kirby/config/fields/users.php b/kirby/config/fields/users.php new file mode 100644 index 0000000..6da224a --- /dev/null +++ b/kirby/config/fields/users.php @@ -0,0 +1,97 @@ + ['min', 'picker', 'userpicker'], + 'props' => [ + /** + * Unset inherited props + */ + 'after' => null, + 'autofocus' => null, + 'before' => null, + 'icon' => null, + 'placeholder' => null, + + /** + * Default selected user(s) when a new page/file/user is created + */ + 'default' => function ($default = null) { + if ($default === false) { + return []; + } + + if ($default === null && $user = $this->kirby()->user()) { + return [ + $this->userResponse($user) + ]; + } + + return $this->toUsers($default); + }, + + 'value' => function ($value = null) { + return $this->toUsers($value); + }, + ], + 'computed' => [ + /** + * Unset inherited computed + */ + 'default' => null + ], + 'methods' => [ + 'userResponse' => function ($user) { + return $user->panelPickerData([ + 'info' => $this->info, + 'image' => $this->image, + 'text' => $this->text, + ]); + }, + 'toUsers' => function ($value = null) { + $users = []; + $kirby = kirby(); + + foreach (Data::decode($value, 'yaml') as $email) { + if (is_array($email) === true) { + $email = $email['email'] ?? null; + } + + if ($email !== null && ($user = $kirby->user($email))) { + $users[] = $this->userResponse($user); + } + } + + return $users; + } + ], + 'api' => function () { + return [ + [ + 'pattern' => '/', + 'action' => function () { + $field = $this->field(); + + return $field->userpicker([ + 'image' => $field->image(), + 'info' => $field->info(), + 'limit' => $field->limit(), + 'page' => $this->requestQuery('page'), + 'query' => $field->query(), + 'search' => $this->requestQuery('search'), + 'text' => $field->text() + ]); + } + ] + ]; + }, + 'save' => function ($value = null) { + return A::pluck($value, 'id'); + }, + 'validations' => [ + 'max', + 'min' + ] +]; diff --git a/kirby/config/helpers.php b/kirby/config/helpers.php new file mode 100755 index 0000000..b68104d --- /dev/null +++ b/kirby/config/helpers.php @@ -0,0 +1,884 @@ +collection($name); +} + +/** + * Checks / returns a CSRF token + * + * @param string $check Pass a token here to compare it to the one in the session + * @return string|bool Either the token or a boolean check result + */ +function csrf(string $check = null) +{ + $session = App::instance()->session(); + + // check explicitly if there have been no arguments at all; + // checking for null introduces a security issue because null could come + // from user input or bugs in the calling code! + if (func_num_args() === 0) { + // no arguments, generate/return a token + + $token = $session->get('csrf'); + if (is_string($token) !== true) { + $token = bin2hex(random_bytes(32)); + $session->set('csrf', $token); + } + + return $token; + } elseif (is_string($check) === true && is_string($session->get('csrf')) === true) { + // argument has been passed, check the token + return hash_equals($session->get('csrf'), $check) === true; + } + + return false; +} + +/** + * Creates one or multiple CSS link tags + * + * @param string|array $url Relative or absolute URLs, an array of URLs or `@auto` for automatic template css loading + * @param string|array $options Pass an array of attributes for the link tag or a media attribute string + * @return string|null + */ +function css($url, $options = null): ?string +{ + if (is_array($url) === true) { + $links = array_map(function ($url) use ($options) { + return css($url, $options); + }, $url); + + return implode(PHP_EOL, $links); + } + + if (is_string($options) === true) { + $options = ['media' => $options]; + } + + $kirby = App::instance(); + + if ($url === '@auto') { + if (!$url = Url::toTemplateAsset('css/templates', 'css')) { + return null; + } + } + + $url = $kirby->component('css')($kirby, $url, $options); + $url = Url::to($url); + $attr = array_merge((array)$options, [ + 'href' => $url, + 'rel' => 'stylesheet' + ]); + + return ''; +} + +/** + * Triggers a deprecation warning if debug mode is active + * @since 3.3.0 + * + * @param string $message + * @return bool Whether the warning was triggered + */ +function deprecated(string $message): bool +{ + if (App::instance()->option('debug') === true) { + return trigger_error($message, E_USER_DEPRECATED) === true; + } + + return false; +} + +if (function_exists('dump') === false) { + /** + * Simple object and variable dumper + * to help with debugging. + * + * @param mixed $variable + * @param bool $echo + * @return string + */ + function dump($variable, bool $echo = true): string + { + $kirby = App::instance(); + return $kirby->component('dump')($kirby, $variable, $echo); + } +} + +if (function_exists('e') === false) { + /** + * Smart version of echo with an if condition as first argument + * + * @param mixed $condition + * @param mixed $value The string to be echoed if the condition is true + * @param mixed $alternative An alternative string which should be echoed when the condition is false + */ + function e($condition, $value, $alternative = null) + { + echo r($condition, $value, $alternative); + } +} + +/** + * Escape context specific output + * + * @param string $string Untrusted data + * @param string $context Location of output + * @param bool $strict Whether to escape an extended set of characters (HTML attributes only) + * @return string Escaped data + */ +function esc($string, $context = 'html', $strict = false) +{ + if (method_exists('Kirby\Toolkit\Escape', $context) === true) { + return Escape::$context($string, $strict); + } + + return $string; +} + + +/** + * Shortcut for $kirby->request()->get() + * + * @param mixed $key The key to look for. Pass false or null to return the entire request array. + * @param mixed $default Optional default value, which should be returned if no element has been found + * @return mixed + */ +function get($key = null, $default = null) +{ + return App::instance()->request()->get($key, $default); +} + +/** + * Embeds a Github Gist + * + * @param string $url + * @param string $file + * @return string + */ +function gist(string $url, string $file = null): string +{ + return kirbytag([ + 'gist' => $url, + 'file' => $file, + ]); +} + +/** + * Redirects to the given Urls + * Urls can be relative or absolute. + * + * @param string $url + * @param int $code + * @return void + */ +function go(string $url = null, int $code = 302) +{ + die(Response::redirect($url, $code)); +} + +/** + * Shortcut for html() + * + * @param string|null $string unencoded text + * @param bool $keepTags + * @return string + */ +function h(?string $string, bool $keepTags = false) +{ + return Html::encode($string, $keepTags); +} + +/** + * Creates safe html by encoding special characters + * + * @param string|null $string unencoded text + * @param bool $keepTags + * @return string + */ +function html(?string $string, bool $keepTags = false) +{ + return Html::encode($string, $keepTags); +} + +/** + * Return an image from any page + * specified by the path + * + * Example: + * + * + * @param string $path + * @return \Kirby\Cms\File|null + */ +function image(string $path = null) +{ + if ($path === null) { + return page()->image(); + } + + $uri = dirname($path); + $filename = basename($path); + + if ($uri === '.') { + $uri = null; + } + + switch ($uri) { + case '/': + $parent = site(); + break; + case null: + $parent = page(); + break; + default: + $parent = page($uri); + break; + } + + if ($parent) { + return $parent->image($filename); + } else { + return null; + } +} + +/** + * Runs a number of validators on a set of data and checks if the data is invalid + * + * @param array $data + * @param array $rules + * @param array $messages + * @return false|array + */ +function invalid(array $data = [], array $rules = [], array $messages = []) +{ + $errors = []; + + foreach ($rules as $field => $validations) { + $validationIndex = -1; + + // See: http://php.net/manual/en/types.comparisons.php + // only false for: null, undefined variable, '', [] + $filled = isset($data[$field]) && $data[$field] !== '' && $data[$field] !== []; + $message = $messages[$field] ?? $field; + + // True if there is an error message for each validation method. + $messageArray = is_array($message); + + foreach ($validations as $method => $options) { + if (is_numeric($method) === true) { + $method = $options; + } + + $validationIndex++; + + if ($method === 'required') { + if ($filled) { + // Field is required and filled. + continue; + } + } elseif ($filled) { + if (is_array($options) === false) { + $options = [$options]; + } + + array_unshift($options, $data[$field] ?? null); + + if (V::$method(...$options) === true) { + // Field is filled and passes validation method. + continue; + } + } else { + // If a field is not required and not filled, no validation should be done. + continue; + } + + // If no continue was called we have a failed validation. + if ($messageArray) { + $errors[$field][] = $message[$validationIndex] ?? $field; + } else { + $errors[$field] = $message; + } + } + } + + return $errors; +} + +/** + * Creates a script tag to load a javascript file + * + * @param string|array $url + * @param string|array $options + * @return string|null + */ +function js($url, $options = null): ?string +{ + if (is_array($url) === true) { + $scripts = array_map(function ($url) use ($options) { + return js($url, $options); + }, $url); + + return implode(PHP_EOL, $scripts); + } + + if (is_bool($options) === true) { + $options = ['async' => $options]; + } + + $kirby = App::instance(); + + if ($url === '@auto') { + if (!$url = Url::toTemplateAsset('js/templates', 'js')) { + return null; + } + } + + $url = $kirby->component('js')($kirby, $url, $options); + $url = Url::to($url); + $attr = array_merge((array)$options, ['src' => $url]); + + return ''; +} + +/** + * Returns the Kirby object in any situation + * + * @return \Kirby\Cms\App + */ +function kirby() +{ + return App::instance(); +} + +/** + * Makes it possible to use any defined Kirbytag as standalone function + * + * @param string|array $type + * @param string $value + * @param array $attr + * @param array $data + * @return string + */ +function kirbytag($type, string $value = null, array $attr = [], array $data = []): string +{ + if (is_array($type) === true) { + $kirbytag = $type; + $type = key($kirbytag); + $value = current($kirbytag); + $attr = $kirbytag; + + // check data attribute and separate from attr data if exists + if (isset($attr['data']) === true) { + $data = $attr['data']; + unset($attr['data']); + } + } + + return App::instance()->kirbytag($type, $value, $attr, $data); +} + +/** + * Parses KirbyTags in the given string. Shortcut + * for `$kirby->kirbytags($text, $data)` + * + * @param string $text + * @param array $data + * @return string + */ +function kirbytags(string $text = null, array $data = []): string +{ + return App::instance()->kirbytags($text, $data); +} + +/** + * Parses KirbyTags and Markdown in the + * given string. Shortcut for `$kirby->kirbytext()` + * + * @param string $text + * @param array $data + * @return string + */ +function kirbytext(string $text = null, array $data = []): string +{ + return App::instance()->kirbytext($text, $data); +} + +/** + * Parses KirbyTags and inline Markdown in the + * given string. + * @since 3.1.0 + * + * @param string $text + * @param array $data + * @return string + */ +function kirbytextinline(string $text = null, array $data = []): string +{ + return App::instance()->kirbytext($text, $data, true); +} + +/** + * Shortcut for `kirbytext()` helper + * + * @param string $text + * @param array $data + * @return string + */ +function kt(string $text = null, array $data = []): string +{ + return kirbytext($text, $data); +} + +/** + * Shortcut for `kirbytextinline()` helper + * @since 3.1.0 + * + * @param string $text + * @param array $data + * @return string + */ +function kti(string $text = null, array $data = []): string +{ + return kirbytextinline($text, $data); +} + +/** + * A super simple class autoloader + * + * @param array $classmap + * @param string $base + * @return void + */ +function load(array $classmap, string $base = null) +{ + // convert all classnames to lowercase + $classmap = array_change_key_case($classmap); + + spl_autoload_register(function ($class) use ($classmap, $base) { + $class = strtolower($class); + + if (!isset($classmap[$class])) { + return false; + } + + if ($base) { + include $base . '/' . $classmap[$class]; + } else { + include $classmap[$class]; + } + }); +} + +/** + * Parses markdown in the given string. Shortcut for + * `$kirby->markdown($text)` + * + * @param string $text + * @return string + */ +function markdown(string $text = null): string +{ + return App::instance()->markdown($text); +} + +/** + * Shortcut for `$kirby->option($key, $default)` + * + * @param string $key + * @param mixed $default + * @return mixed + */ +function option(string $key, $default = null) +{ + return App::instance()->option($key, $default); +} + +/** + * Fetches a single page or multiple pages by + * id or the current page when no id is specified + * + * @param string|array ...$id + * @return \Kirby\Cms\Page|null + */ +function page(...$id) +{ + if (empty($id) === true) { + return App::instance()->site()->page(); + } + + return App::instance()->site()->find(...$id); +} + +/** + * Helper to build page collections + * + * @param string|array ...$id + * @return \Kirby\Cms\Pages + */ +function pages(...$id) +{ + return App::instance()->site()->find(...$id); +} + +/** + * Returns a single param from the URL + * + * @param string $key + * @param string $fallback + * @return string|null + */ +function param(string $key, string $fallback = null): ?string +{ + return App::instance()->request()->url()->params()->$key ?? $fallback; +} + +/** + * Returns all params from the current Url + * + * @return array + */ +function params(): array +{ + return App::instance()->request()->url()->params()->toArray(); +} + +/** + * Smart version of return with an if condition as first argument + * + * @param mixed $condition + * @param mixed $value The string to be returned if the condition is true + * @param mixed $alternative An alternative string which should be returned when the condition is false + * @return mixed + */ +function r($condition, $value, $alternative = null) +{ + return $condition ? $value : $alternative; +} + +/** + * Returns the current site object + * + * @return \Kirby\Cms\Site + */ +function site() +{ + return App::instance()->site(); +} + +/** + * Determines the size/length of numbers, strings, arrays and countable objects + * + * @param mixed $value + * @return int + */ +function size($value): int +{ + if (is_numeric($value)) { + return $value; + } + + if (is_string($value)) { + return Str::length(trim($value)); + } + + if (is_array($value)) { + return count($value); + } + + if (is_object($value)) { + if (is_a($value, 'Countable') === true) { + return count($value); + } + + if (is_a($value, 'Kirby\Toolkit\Collection') === true) { + return $value->count(); + } + } +} + +/** + * Enhances the given string with + * smartypants. Shortcut for `$kirby->smartypants($text)` + * + * @param string $text + * @return string + */ +function smartypants(string $text = null): string +{ + return App::instance()->smartypants($text); +} + +/** + * Embeds a snippet from the snippet folder + * + * @param string|array $name + * @param array|object $data + * @param bool $return + * @return string + */ +function snippet($name, $data = [], bool $return = false) +{ + if (is_object($data) === true) { + $data = ['item' => $data]; + } + + $snippet = App::instance()->snippet($name, $data); + + if ($return === true) { + return $snippet; + } + + echo $snippet; +} + +/** + * Includes an SVG file by absolute or + * relative file path. + * + * @param string|\Kirby\Cms\File $file + * @return string|false + */ +function svg($file) +{ + // support for Kirby's file objects + if (is_a($file, 'Kirby\Cms\File') === true && $file->extension() === 'svg') { + return $file->read(); + } + + if (is_string($file) === false) { + return false; + } + + $extension = F::extension($file); + + // check for valid svg files + if ($extension !== 'svg') { + return false; + } + + // try to convert relative paths to absolute + if (file_exists($file) === false) { + $root = App::instance()->root(); + $file = realpath($root . '/' . $file); + + if (file_exists($file) === false) { + return false; + } + } + + return F::read($file); +} + +/** + * Returns translate string for key from translation file + * + * @param string|array $key + * @param string|null $fallback + * @return mixed + */ +function t($key, string $fallback = null) +{ + return I18n::translate($key, $fallback); +} + +/** + * Translates a count + * + * @param string|array $key + * @param int $count + * @return mixed + */ +function tc($key, int $count) +{ + return I18n::translateCount($key, $count); +} + +/** + * Rounds the minutes of the given date + * by the defined step + * + * @param string $date + * @param int $step + * @return string|null + */ +function timestamp(string $date = null, int $step = null): ?string +{ + if (V::date($date) === false) { + return null; + } + + $date = strtotime($date); + + if ($step === null) { + return $date; + } + + $hours = date('H', $date); + $minutes = date('i', $date); + $minutes = floor($minutes / $step) * $step; + $minutes = str_pad($minutes, 2, 0, STR_PAD_LEFT); + $date = date('Y-m-d', $date) . ' ' . $hours . ':' . $minutes; + + return strtotime($date); +} + +/** + * Translate by key and then replace + * placeholders in the text + * + * @param string $key + * @param string $fallback + * @param array $replace + * @param string $locale + * @return string + */ +function tt(string $key, $fallback = null, array $replace = null, string $locale = null) +{ + return I18n::template($key, $fallback, $replace, $locale); +} + +/** + * Builds a Twitter link + * + * @param string $username + * @param string $text + * @param string $title + * @param string $class + * @return string + */ +function twitter(string $username, string $text = null, string $title = null, string $class = null): string +{ + return kirbytag([ + 'twitter' => $username, + 'text' => $text, + 'title' => $title, + 'class' => $class + ]); +} + +/** + * Shortcut for url() + * + * @param string $path + * @param array|string|null $options + * @return string + */ +function u(string $path = null, $options = null): string +{ + return Url::to($path, $options); +} + +/** + * Builds an absolute URL for a given path + * + * @param string $path + * @param array|string|null $options + * @return string + */ +function url(string $path = null, $options = null): string +{ + return Url::to($path, $options); +} + +/** + * Creates a video embed via iframe for Youtube or Vimeo + * videos. The embed Urls are automatically detected from + * the given Url. + * + * @param string $url + * @param array $options + * @param array $attr + * @return string + */ +function video(string $url, array $options = [], array $attr = []): string +{ + return Html::video($url, $options, $attr); +} + +/** + * Embeds a Vimeo video by URL in an iframe + * + * @param string $url + * @param array $options + * @param array $attr + * @return string + */ +function vimeo(string $url, array $options = [], array $attr = []): string +{ + return Html::vimeo($url, $options, $attr); +} + +/** + * The widont function makes sure that there are no + * typographical widows at the end of a paragraph – + * that's a single word in the last line + * + * @param string|null $string + * @return string + */ +function widont(string $string = null): string +{ + return Str::widont($string); +} + +/** + * Embeds a Youtube video by URL in an iframe + * + * @param string $url + * @param array $options + * @param array $attr + * @return string + */ +function youtube(string $url, array $options = [], array $attr = []): string +{ + return Html::youtube($url, $options, $attr); +} diff --git a/kirby/config/methods.php b/kirby/config/methods.php new file mode 100644 index 0000000..f506fad --- /dev/null +++ b/kirby/config/methods.php @@ -0,0 +1,568 @@ + function (Field $field): bool { + return $field->toBool() === false; + }, + + /** + * Converts the field value into a proper boolean + * + * @param \Kirby\Cms\Field $field + * @return bool + */ + 'isTrue' => function (Field $field): bool { + return $field->toBool() === true; + }, + + /** + * Validates the field content with the given validator and parameters + * + * @param string $validator + * @param mixed ...$arguments A list of optional validator arguments + * @return bool + */ + 'isValid' => function (Field $field, string $validator, ...$arguments): bool { + return V::$validator($field->value, ...$arguments); + }, + + // converters + + /** + * Parses the field value with the given method + * + * @param \Kirby\Cms\Field $field + * @param string $method [',', 'yaml', 'json'] + * @return array + */ + 'toData' => function (Field $field, string $method = ',') { + switch ($method) { + case 'yaml': + case 'json': + return Data::decode($field->value, $method); + default: + return $field->split($method); + } + }, + + /** + * Converts the field value into a proper boolean + * + * @param \Kirby\Cms\Field $field + * @param bool $default Default value if the field is empty + * @return bool + */ + 'toBool' => function (Field $field, $default = false): bool { + $value = $field->isEmpty() ? $default : $field->value; + return filter_var($value, FILTER_VALIDATE_BOOLEAN); + }, + + /** + * Converts the field value to a timestamp or a formatted date + * + * @param \Kirby\Cms\Field $field + * @param string|null $format PHP date formatting string + * @param string|null $fallback Fallback string for `strtotime` (since 3.2) + * @return string|int + */ + 'toDate' => function (Field $field, string $format = null, string $fallback = null) use ($app) { + if (empty($field->value) === true && $fallback === null) { + return null; + } + + $time = empty($field->value) === true ? strtotime($fallback) : $field->toTimestamp(); + + if ($format === null) { + return $time; + } + + return $app->option('date.handler', 'date')($format, $time); + }, + + /** + * Returns a file object from a filename in the field + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\File|null + */ + 'toFile' => function (Field $field) { + return $field->toFiles()->first(); + }, + + /** + * Returns a file collection from a yaml list of filenames in the field + * + * @param \Kirby\Cms\Field $field + * @param string $separator + * @return \Kirby\Cms\Files + */ + 'toFiles' => function (Field $field, string $separator = 'yaml') { + $parent = $field->parent(); + $files = new Files([]); + + foreach ($field->toData($separator) as $id) { + if ($file = $parent->kirby()->file($id, $parent)) { + $files->add($file); + } + } + + return $files; + }, + + /** + * Converts the field value into a proper float + * + * @param \Kirby\Cms\Field $field + * @param float $default Default value if the field is empty + * @return float + */ + 'toFloat' => function (Field $field, float $default = 0) { + $value = $field->isEmpty() ? $default : $field->value; + return (float)$value; + }, + + /** + * Converts the field value into a proper integer + * + * @param \Kirby\Cms\Field $field + * @param int $default Default value if the field is empty + * @return int + */ + 'toInt' => function (Field $field, int $default = 0) { + $value = $field->isEmpty() ? $default : $field->value; + return (int)$value; + }, + + /** + * Wraps a link tag around the field value. The field value is used as the link text + * + * @param \Kirby\Cms\Field $field + * @param mixed $attr1 Can be an optional Url. If no Url is set, the Url of the Page, File or Site will be used. Can also be an array of link attributes + * @param mixed $attr2 If `$attr1` is used to set the Url, you can use `$attr2` to pass an array of additional attributes. + * @return string + */ + 'toLink' => function (Field $field, $attr1 = null, $attr2 = null) { + if (is_string($attr1) === true) { + $href = $attr1; + $attr = $attr2; + } else { + $href = $field->parent()->url(); + $attr = $attr1; + } + + if ($field->parent()->isActive()) { + $attr['aria-current'] = 'page'; + } + + return Html::a($href, $field->value, $attr ?? []); + }, + + /** + * Returns a page object from a page id in the field + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Page|null + */ + 'toPage' => function (Field $field) { + return $field->toPages()->first(); + }, + + /** + * Returns a pages collection from a yaml list of page ids in the field + * + * @param \Kirby\Cms\Field $field + * @param string $separator Can be any other separator to split the field value by + * @return \Kirby\Cms\Pages + */ + 'toPages' => function (Field $field, string $separator = 'yaml') use ($app) { + return $app->site()->find(false, false, ...$field->toData($separator)); + }, + + /** + * Converts a yaml field to a Structure object + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Structure + */ + 'toStructure' => function (Field $field) { + try { + return new Structure(Data::decode($field->value, 'yaml'), $field->parent()); + } catch (Exception $e) { + if ($field->parent() === null) { + $message = 'Invalid structure data for "' . $field->key() . '" field'; + } else { + $message = 'Invalid structure data for "' . $field->key() . '" field on parent "' . $field->parent()->title() . '"'; + } + + throw new InvalidArgumentException($message); + } + }, + + /** + * Converts the field value to a Unix timestamp + * + * @param \Kirby\Cms\Field $field + * @return int + */ + 'toTimestamp' => function (Field $field): int { + return strtotime($field->value); + }, + + /** + * Turns the field value into an absolute Url + * + * @param \Kirby\Cms\Field $field + * @return string + */ + 'toUrl' => function (Field $field): string { + return Url::to($field->value); + }, + + /** + * Converts a user email address to a user object + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\User|null + */ + 'toUser' => function (Field $field) { + return $field->toUsers()->first(); + }, + + /** + * Returns a users collection from a yaml list of user email addresses in the field + * + * @param \Kirby\Cms\Field $field + * @param string $separator + * @return \Kirby\Cms\Users + */ + 'toUsers' => function (Field $field, string $separator = 'yaml') use ($app) { + return $app->users()->find(false, false, ...$field->toData($separator)); + }, + + // inspectors + + /** + * Returns the length of the field content + */ + 'length' => function (Field $field) { + return Str::length($field->value); + }, + + /** + * Returns the number of words in the text + */ + 'words' => function (Field $field) { + return str_word_count(strip_tags($field->value)); + }, + + // manipulators + + /** + * Applies the callback function to the field + * @since 3.4.0 + * + * @param \Kirby\Cms\Field $field + * @param Closure $callback + */ + 'callback' => function (Field $field, Closure $callback) { + return $callback($field); + }, + + /** + * Escapes the field value to be safely used in HTML + * templates without the risk of XSS attacks + * + * @param \Kirby\Cms\Field $field + * @param string $context html, attr, js or css + */ + 'escape' => function (Field $field, string $context = 'html') { + $field->value = esc($field->value, $context); + return $field; + }, + + /** + * Creates an excerpt of the field value without html + * or any other formatting. + * + * @param \Kirby\Cms\Field $field + * @param int $cahrs + * @param bool $strip + * @param string $rep + * @return \Kirby\Cms\Field + */ + 'excerpt' => function (Field $field, int $chars = 0, bool $strip = true, string $rep = ' …') { + $field->value = Str::excerpt($field->kirbytext()->value(), $chars, $strip, $rep); + return $field; + }, + + /** + * Converts the field content to valid HTML + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'html' => function (Field $field) { + $field->value = htmlentities($field->value, ENT_COMPAT, 'utf-8'); + return $field; + }, + + /** + * Strips all block-level HTML elements from the field value, + * it can be safely placed inside of other inline elements + * without the risk of breaking the HTML structure. + * @since 3.3.0 + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'inline' => function (Field $field) { + // List of valid inline elements taken from: https://developer.mozilla.org/de/docs/Web/HTML/Inline_elemente + // Obsolete elements, script tags, image maps and form elements have + // been excluded for safety reasons and as they are most likely not + // needed in most cases. + $field->value = strip_tags($field->value, '
'); + return $field; + }, + + /** + * Converts the field content from Markdown/Kirbytext to valid HTML + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'kirbytext' => function (Field $field) use ($app) { + $field->value = $app->kirbytext($field->value, [ + 'parent' => $field->parent(), + 'field' => $field + ]); + + return $field; + }, + + /** + * Converts the field content from inline Markdown/Kirbytext + * to valid HTML + * @since 3.1.0 + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'kirbytextinline' => function (Field $field) use ($app) { + $field->value = $app->kirbytext($field->value, [ + 'parent' => $field->parent(), + 'field' => $field + ], true); + + return $field; + }, + + /** + * Parses all KirbyTags without also parsing Markdown + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'kirbytags' => function (Field $field) use ($app) { + $field->value = $app->kirbytags($field->value, [ + 'parent' => $field->parent(), + 'field' => $field + ]); + + return $field; + }, + + /** + * Converts the field content to lowercase + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'lower' => function (Field $field) { + $field->value = Str::lower($field->value); + return $field; + }, + + /** + * Converts markdown to valid HTML + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'markdown' => function (Field $field) use ($app) { + $field->value = $app->markdown($field->value); + return $field; + }, + + /** + * Converts all line breaks in the field content to `
` tags. + * @since 3.3.0 + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'nl2br' => function (Field $field) { + $field->value = nl2br($field->value, false); + return $field; + }, + + /** + * Uses the field value as Kirby query + * + * @param \Kirby\Cms\Field $field + * @param string|null $expect + * @return mixed + */ + 'query' => function (Field $field, string $expect = null) use ($app) { + if ($parent = $field->parent()) { + return $parent->query($field->value, $expect); + } + + return Str::query($field->value, [ + 'kirby' => $app, + 'site' => $app->site(), + 'page' => $app->page() + ]); + }, + + /** + * It parses any queries found in the field value. + * + * @param \Kirby\Cms\Field $field + * @param array $data + * @param string $fallback Fallback for tokens in the template that cannot be replaced + * @return \Kirby\Cms\Field + */ + 'replace' => function (Field $field, array $data = [], string $fallback = '') use ($app) { + if ($parent = $field->parent()) { + $field->value = $field->parent()->toString($field->value, $data, $fallback); + } else { + $field->value = Str::template($field->value, array_replace([ + 'kirby' => $app, + 'site' => $app->site(), + 'page' => $app->page() + ], $data), $fallback); + } + + return $field; + }, + + /** + * Cuts the string after the given length and + * adds "…" if it is longer + * + * @param \Kirby\Cms\Field $field + * @param int $length The number of characters in the string + * @param string $appendix An optional replacement for the missing rest + * @return \Kirby\Cms\Field + */ + 'short' => function (Field $field, int $length, string $appendix = '…') { + $field->value = Str::short($field->value, $length, $appendix); + return $field; + }, + + /** + * Converts the field content to a slug + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\cms\Field + */ + 'slug' => function (Field $field) { + $field->value = Str::slug($field->value); + return $field; + }, + + /** + * Applies SmartyPants to the field + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\cms\Field + */ + 'smartypants' => function (Field $field) use ($app) { + $field->value = $app->smartypants($field->value); + return $field; + }, + + /** + * Splits the field content into an array + * + * @param \Kirby\Cms\Field $field + * @return array + */ + 'split' => function (Field $field, $separator = ',') { + return Str::split((string)$field->value, $separator); + }, + + /** + * Converts the field content to uppercase + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\cms\Field + */ + 'upper' => function (Field $field) { + $field->value = Str::upper($field->value); + return $field; + }, + + /** + * Avoids typographical widows in strings by replacing + * the last space with ` ` + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\cms\Field + */ + 'widont' => function (Field $field) { + $field->value = Str::widont($field->value); + return $field; + }, + + /** + * Converts the field content to valid XML + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'xml' => function (Field $field) { + $field->value = Xml::encode($field->value); + return $field; + }, + + // aliases + + /** + * Parses yaml in the field content and returns an array + * + * @param \Kirby\Cms\Field $field + * @return array + */ + 'yaml' => function (Field $field): array { + return $field->toData('yaml'); + }, + + ]; +}; diff --git a/kirby/config/presets/files.php b/kirby/config/presets/files.php new file mode 100644 index 0000000..fe0a7e5 --- /dev/null +++ b/kirby/config/presets/files.php @@ -0,0 +1,24 @@ + [ + 'headline' => $props['headline'] ?? t('files'), + 'type' => 'files', + 'layout' => $props['layout'] ?? 'cards', + 'template' => $props['template'] ?? null, + 'image' => $props['image'] ?? null, + 'info' => '{{ file.dimensions }}' + ] + ]; + + // remove global options + unset( + $props['headline'], + $props['layout'], + $props['template'], + $props['image'] + ); + + return $props; +}; diff --git a/kirby/config/presets/page.php b/kirby/config/presets/page.php new file mode 100644 index 0000000..62df5de --- /dev/null +++ b/kirby/config/presets/page.php @@ -0,0 +1,72 @@ + $props + ]; + } + + return array_replace_recursive($defaults, $props); + }; + + if (empty($props['sidebar']) === false) { + $sidebar = $props['sidebar']; + } else { + $sidebar = []; + + $pages = $props['pages'] ?? []; + $files = $props['files'] ?? []; + + if ($pages !== false) { + $sidebar['pages'] = $section([ + 'headline' => t('pages'), + 'type' => 'pages', + 'status' => 'all', + 'layout' => 'list', + ], $pages); + } + + if ($files !== false) { + $sidebar['files'] = $section([ + 'headline' => t('files'), + 'type' => 'files', + 'layout' => 'list' + ], $files); + } + } + + if (empty($sidebar) === true) { + $props['fields'] = $props['fields'] ?? []; + + unset( + $props['files'], + $props['pages'] + ); + } else { + $props['columns'] = [ + [ + 'width' => '2/3', + 'fields' => $props['fields'] ?? [] + ], + [ + 'width' => '1/3', + 'sections' => $sidebar + ], + ]; + + unset( + $props['fields'], + $props['files'], + $props['pages'], + $props['sidebar'] + ); + } + + return $props; +}; diff --git a/kirby/config/presets/pages.php b/kirby/config/presets/pages.php new file mode 100644 index 0000000..5bba76b --- /dev/null +++ b/kirby/config/presets/pages.php @@ -0,0 +1,57 @@ + $headline, + 'type' => 'pages', + 'layout' => 'list', + 'status' => $status + ]; + + if ($props === true) { + $props = []; + } + + if (is_string($props) === true) { + $props = [ + 'headline' => $props + ]; + } + + // inject the global templates definition + if (empty($templates) === false) { + $props['templates'] = $props['templates'] ?? $templates; + } + + return array_replace_recursive($defaults, $props); + }; + + $sections = []; + + $drafts = $props['drafts'] ?? []; + $unlisted = $props['unlisted'] ?? false; + $listed = $props['listed'] ?? []; + + + if ($drafts !== false) { + $sections['drafts'] = $section(t('pages.status.draft'), 'drafts', $drafts); + } + + if ($unlisted !== false) { + $sections['unlisted'] = $section(t('pages.status.unlisted'), 'unlisted', $unlisted); + } + + if ($listed !== false) { + $sections['listed'] = $section(t('pages.status.listed'), 'listed', $listed); + } + + // cleaning up + unset($props['drafts'], $props['unlisted'], $props['listed'], $props['templates']); + + return array_merge($props, ['sections' => $sections]); +}; diff --git a/kirby/config/roots.php b/kirby/config/roots.php new file mode 100644 index 0000000..c473763 --- /dev/null +++ b/kirby/config/roots.php @@ -0,0 +1,90 @@ + function (array $roots) { + return realpath(__DIR__ . '/../'); + }, + + // i18n + 'i18n' => function (array $roots) { + return $roots['kirby'] . '/i18n'; + }, + 'i18n:translations' => function (array $roots) { + return $roots['i18n'] . '/translations'; + }, + 'i18n:rules' => function (array $roots) { + return $roots['i18n'] . '/rules'; + }, + + // index + 'index' => function (array $roots) { + return realpath(__DIR__ . '/../../'); + }, + + // assets + 'assets' => function (array $roots) { + return $roots['index'] . '/assets'; + }, + + // content + 'content' => function (array $roots) { + return $roots['index'] . '/content'; + }, + + // media + 'media' => function (array $roots) { + return $roots['index'] . '/media'; + }, + + // panel + 'panel' => function (array $roots) { + return $roots['kirby'] . '/panel'; + }, + + // site + 'site' => function (array $roots) { + return $roots['index'] . '/site'; + }, + 'accounts' => function (array $roots) { + return $roots['site'] . '/accounts'; + }, + 'blueprints' => function (array $roots) { + return $roots['site'] . '/blueprints'; + }, + 'cache' => function (array $roots) { + return $roots['site'] . '/cache'; + }, + 'collections' => function (array $roots) { + return $roots['site'] . '/collections'; + }, + 'config' => function (array $roots) { + return $roots['site'] . '/config'; + }, + 'controllers' => function (array $roots) { + return $roots['site'] . '/controllers'; + }, + 'languages' => function (array $roots) { + return $roots['site'] . '/languages'; + }, + 'models' => function (array $roots) { + return $roots['site'] . '/models'; + }, + 'plugins' => function (array $roots) { + return $roots['site'] . '/plugins'; + }, + 'sessions' => function (array $roots) { + return $roots['site'] . '/sessions'; + }, + 'snippets' => function (array $roots) { + return $roots['site'] . '/snippets'; + }, + 'templates' => function (array $roots) { + return $roots['site'] . '/templates'; + }, + + // blueprints + 'roles' => function (array $roots) { + return $roots['blueprints'] . '/users'; + }, +]; diff --git a/kirby/config/routes.php b/kirby/config/routes.php new file mode 100644 index 0000000..6c4a02b --- /dev/null +++ b/kirby/config/routes.php @@ -0,0 +1,151 @@ +option('api.slug', 'api'); + $panel = $kirby->option('panel.slug', 'panel'); + $index = $kirby->url('index'); + $media = $kirby->url('media'); + + if (Str::startsWith($media, $index) === true) { + $media = Str::after($media, $index); + } else { + // media URL is outside of the site, we can't make routing work; + // fall back to the standard media route + $media = 'media'; + } + + /** + * Before routes are running before the + * plugin routes and cannot be overwritten by + * plugins. + */ + $before = [ + [ + 'pattern' => $api . '/(:all)', + 'method' => 'ALL', + 'env' => 'api', + 'action' => function ($path = null) use ($kirby) { + if ($kirby->option('api') === false) { + return null; + } + + $request = $kirby->request(); + + return $kirby->api()->render($path, $this->method(), [ + 'body' => $request->body()->toArray(), + 'files' => $request->files()->toArray(), + 'headers' => $request->headers(), + 'query' => $request->query()->toArray(), + ]); + } + ], + [ + 'pattern' => $media . '/plugins/index.(css|js)', + 'env' => 'media', + 'action' => function (string $type) use ($kirby) { + $plugins = new PanelPlugins(); + + return $kirby + ->response() + ->type($type) + ->body($plugins->read($type)); + } + ], + [ + 'pattern' => $media . '/plugins/(:any)/(:any)/(:all).(css|gif|js|jpg|png|svg|webp|woff2|woff)', + 'env' => 'media', + 'action' => function (string $provider, string $pluginName, string $filename, string $extension) { + return PluginAssets::resolve($provider . '/' . $pluginName, $filename . '.' . $extension); + } + ], + [ + 'pattern' => $panel . '/(:all?)', + 'env' => 'panel', + 'action' => function () use ($kirby) { + if ($kirby->option('panel') === false) { + return null; + } + + return Panel::render($kirby); + } + ], + [ + 'pattern' => $media . '/pages/(:all)/(:any)/(:any)', + 'env' => 'media', + 'action' => function ($path, $hash, $filename) use ($kirby) { + return Media::link($kirby->page($path), $hash, $filename); + } + ], + [ + 'pattern' => $media . '/site/(:any)/(:any)', + 'env' => 'media', + 'action' => function ($hash, $filename) use ($kirby) { + return Media::link($kirby->site(), $hash, $filename); + } + ], + [ + 'pattern' => $media . '/users/(:any)/(:any)/(:any)', + 'env' => 'media', + 'action' => function ($id, $hash, $filename) use ($kirby) { + return Media::link($kirby->user($id), $hash, $filename); + } + ], + [ + 'pattern' => $media . '/assets/(:all)/(:any)/(:any)', + 'env' => 'media', + 'action' => function ($path, $hash, $filename) { + return Media::thumb($path, $hash, $filename); + } + ] + ]; + + // Multi-language setup + if ($kirby->multilang() === true) { + $after = LanguageRoutes::create($kirby); + } else { + + // Single-language home + $after[] = [ + 'pattern' => '', + 'method' => 'ALL', + 'env' => 'site', + 'action' => function () use ($kirby) { + return $kirby->resolve(); + } + ]; + + // redirect the home page folder to the real homepage + $after[] = [ + 'pattern' => $kirby->option('home', 'home'), + 'method' => 'ALL', + 'env' => 'site', + 'action' => function () use ($kirby) { + return $kirby + ->response() + ->redirect($kirby->site()->url()); + } + ]; + + // Single-language subpages + $after[] = [ + 'pattern' => '(:all)', + 'method' => 'ALL', + 'env' => 'site', + 'action' => function (string $path) use ($kirby) { + return $kirby->resolve($path); + } + ]; + } + + return [ + 'before' => $before, + 'after' => $after + ]; +}; diff --git a/kirby/config/sections/fields.php b/kirby/config/sections/fields.php new file mode 100644 index 0000000..84c9276 --- /dev/null +++ b/kirby/config/sections/fields.php @@ -0,0 +1,66 @@ + [ + 'fields' => function (array $fields = []) { + return $fields; + } + ], + 'computed' => [ + 'form' => function () { + $fields = $this->fields; + $disabled = $this->model->permissions()->update() === false; + $content = $this->model->content()->toArray(); + + if ($disabled === true) { + foreach ($fields as $key => $props) { + $fields[$key]['disabled'] = true; + } + } + + return new Form([ + 'fields' => $fields, + 'values' => $content, + 'model' => $this->model, + 'strict' => true + ]); + }, + 'fields' => function () { + $fields = $this->form->fields()->toArray(); + + if (is_a($this->model, 'Kirby\Cms\Page') === true || is_a($this->model, 'Kirby\Cms\Site') === true) { + // the title should never be updated directly via + // fields section to avoid conflicts with the rename dialog + unset($fields['title']); + } + + foreach ($fields as $index => $props) { + unset($fields[$index]['value']); + } + + return $fields; + }, + 'errors' => function () { + return $this->form->errors(); + }, + 'data' => function () { + $values = $this->form->values(); + + if (is_a($this->model, 'Kirby\Cms\Page') === true || is_a($this->model, 'Kirby\Cms\Site') === true) { + // the title should never be updated directly via + // fields section to avoid conflicts with the rename dialog + unset($values['title']); + } + + return $values; + } + ], + 'toArray' => function () { + return [ + 'errors' => $this->errors, + 'fields' => $this->fields, + ]; + } +]; diff --git a/kirby/config/sections/files.php b/kirby/config/sections/files.php new file mode 100644 index 0000000..378b9f8 --- /dev/null +++ b/kirby/config/sections/files.php @@ -0,0 +1,241 @@ + [ + 'empty', + 'headline', + 'help', + 'layout', + 'min', + 'max', + 'pagination', + 'parent', + ], + 'props' => [ + /** + * Enables/disables reverse sorting + */ + 'flip' => function (bool $flip = false) { + return $flip; + }, + /** + * Image options to control the source and look of file previews + */ + 'image' => function ($image = null) { + return $image ?? []; + }, + /** + * Optional info text setup. Info text is shown on the right (lists) or below (cards) the filename. + */ + 'info' => function (string $info = null) { + return $info; + }, + /** + * The size option controls the size of cards. By default cards are auto-sized and the cards grid will always fill the full width. With a size you can disable auto-sizing. Available sizes: `tiny`, `small`, `medium`, `large`, `huge` + */ + 'size' => function (string $size = 'auto') { + return $size; + }, + /** + * Enables/disables manual sorting + */ + 'sortable' => function (bool $sortable = true) { + return $sortable; + }, + /** + * Overwrites manual sorting and sorts by the given field and sorting direction (i.e. `filename desc`) + */ + 'sortBy' => function (string $sortBy = null) { + return $sortBy; + }, + /** + * Filters all files by template and also sets the template, which will be used for all uploads + */ + 'template' => function (string $template = null) { + return $template; + }, + /** + * Setup for the main text in the list or cards. By default this will display the filename. + */ + 'text' => function (string $text = '{{ file.filename }}') { + return $text; + } + ], + 'computed' => [ + 'accept' => function () { + if ($this->template) { + $file = new File([ + 'filename' => 'tmp', + 'template' => $this->template + ]); + + return $file->blueprint()->accept()['mime'] ?? '*'; + } + + return null; + }, + 'parent' => function () { + return $this->parentModel(); + }, + 'files' => function () { + $files = $this->parent->files()->template($this->template); + + // filter out all protected files + $files = $files->filterBy('isReadable', true); + + if ($this->sortBy) { + $files = $files->sortBy(...$files::sortArgs($this->sortBy)); + } elseif ($this->sortable === true) { + $files = $files->sortBy('sort', 'asc', 'filename', 'asc'); + } + + // flip + if ($this->flip === true) { + $files = $files->flip(); + } + + // apply the default pagination + $files = $files->paginate([ + 'page' => $this->page, + 'limit' => $this->limit, + 'method' => 'none' // the page is manually provided + ]); + + return $files; + }, + 'data' => function () { + $data = []; + + // the drag text needs to be absolute when the files come from + // a different parent model + $dragTextAbsolute = $this->model->is($this->parent) === false; + + foreach ($this->files as $file) { + $image = $file->panelImage($this->image); + + $data[] = [ + 'dragText' => $file->dragText('auto', $dragTextAbsolute), + 'extension' => $file->extension(), + 'filename' => $file->filename(), + 'id' => $file->id(), + 'icon' => $file->panelIcon($image), + 'image' => $image, + 'info' => $file->toString($this->info ?? false), + 'link' => $file->panelUrl(true), + 'mime' => $file->mime(), + 'parent' => $file->parent()->panelPath(), + 'text' => $file->toString($this->text), + 'url' => $file->url(), + ]; + } + + return $data; + }, + 'total' => function () { + return $this->files->pagination()->total(); + }, + 'errors' => function () { + $errors = []; + + if ($this->validateMax() === false) { + $errors['max'] = I18n::template('error.section.files.max.' . I18n::form($this->max), [ + 'max' => $this->max, + 'section' => $this->headline + ]); + } + + if ($this->validateMin() === false) { + $errors['min'] = I18n::template('error.section.files.min.' . I18n::form($this->min), [ + 'min' => $this->min, + 'section' => $this->headline + ]); + } + + if (empty($errors) === true) { + return []; + } + + return [ + $this->name => [ + 'label' => $this->headline, + 'message' => $errors, + ] + ]; + }, + 'link' => function () { + $modelLink = $this->model->panelUrl(true); + $parentLink = $this->parent->panelUrl(true); + + if ($modelLink !== $parentLink) { + return $parentLink; + } + }, + 'pagination' => function () { + return $this->pagination(); + }, + 'sortable' => function () { + if ($this->sortable === false) { + return false; + } + + if ($this->sortBy !== null) { + return false; + } + + if ($this->flip === true) { + return false; + } + + return true; + }, + 'upload' => function () { + if ($this->isFull() === true) { + return false; + } + + // count all uploaded files + $total = count($this->data); + $max = $this->max ? $this->max - $total : null; + + if ($this->max && $total === $this->max - 1) { + $multiple = false; + } else { + $multiple = true; + } + + return [ + 'accept' => $this->accept, + 'multiple' => $multiple, + 'max' => $max, + 'api' => $this->parent->apiUrl(true) . '/files', + 'attributes' => array_filter([ + 'template' => $this->template + ]) + ]; + } + ], + 'toArray' => function () { + return [ + 'data' => $this->data, + 'errors' => $this->errors, + 'options' => [ + 'accept' => $this->accept, + 'apiUrl' => $this->parent->apiUrl(true), + 'empty' => $this->empty, + 'headline' => $this->headline, + 'help' => $this->help, + 'layout' => $this->layout, + 'link' => $this->link, + 'max' => $this->max, + 'min' => $this->min, + 'size' => $this->size, + 'sortable' => $this->sortable, + 'upload' => $this->upload + ], + 'pagination' => $this->pagination + ]; + } +]; diff --git a/kirby/config/sections/info.php b/kirby/config/sections/info.php new file mode 100644 index 0000000..254a76b --- /dev/null +++ b/kirby/config/sections/info.php @@ -0,0 +1,35 @@ + [ + 'headline', + ], + 'props' => [ + 'text' => function ($text = null) { + return I18n::translate($text, $text); + }, + 'theme' => function (string $theme = null) { + return $theme; + } + ], + 'computed' => [ + 'text' => function () { + if ($this->text) { + $text = $this->model()->toString($this->text); + $text = $this->kirby()->kirbytext($text); + return $text; + } + }, + ], + 'toArray' => function () { + return [ + 'options' => [ + 'headline' => $this->headline, + 'text' => $this->text, + 'theme' => $this->theme + ] + ]; + } +]; diff --git a/kirby/config/sections/mixins/empty.php b/kirby/config/sections/mixins/empty.php new file mode 100644 index 0000000..1c58194 --- /dev/null +++ b/kirby/config/sections/mixins/empty.php @@ -0,0 +1,21 @@ + [ + /** + * Sets the text for the empty state box + */ + 'empty' => function ($empty = null) { + return I18n::translate($empty, $empty); + } + ], + 'computed' => [ + 'empty' => function () { + if ($this->empty) { + return $this->model()->toString($this->empty); + } + } + ] +]; diff --git a/kirby/config/sections/mixins/headline.php b/kirby/config/sections/mixins/headline.php new file mode 100644 index 0000000..f4bb7e1 --- /dev/null +++ b/kirby/config/sections/mixins/headline.php @@ -0,0 +1,23 @@ + [ + /** + * The headline for the section. This can be a simple string or a template with additional info from the parent page. + */ + 'headline' => function ($headline = null) { + return I18n::translate($headline, $headline); + } + ], + 'computed' => [ + 'headline' => function () { + if ($this->headline) { + return $this->model()->toString($this->headline); + } + + return ucfirst($this->name); + } + ] +]; diff --git a/kirby/config/sections/mixins/help.php b/kirby/config/sections/mixins/help.php new file mode 100644 index 0000000..80f42ee --- /dev/null +++ b/kirby/config/sections/mixins/help.php @@ -0,0 +1,23 @@ + [ + /** + * Sets the help text + */ + 'help' => function ($help = null) { + return I18n::translate($help, $help); + } + ], + 'computed' => [ + 'help' => function () { + if ($this->help) { + $help = $this->model()->toString($this->help); + $help = $this->kirby()->kirbytext($help); + return $help; + } + } + ] +]; diff --git a/kirby/config/sections/mixins/layout.php b/kirby/config/sections/mixins/layout.php new file mode 100644 index 0000000..4a7a621 --- /dev/null +++ b/kirby/config/sections/mixins/layout.php @@ -0,0 +1,12 @@ + [ + /** + * Section layout. Available layout methods: `list`, `cards`. + */ + 'layout' => function (string $layout = 'list') { + return $layout === 'cards' ? 'cards' : 'list'; + } + ] +]; diff --git a/kirby/config/sections/mixins/max.php b/kirby/config/sections/mixins/max.php new file mode 100644 index 0000000..5ce303c --- /dev/null +++ b/kirby/config/sections/mixins/max.php @@ -0,0 +1,28 @@ + [ + /** + * Sets the maximum number of allowed entries in the section + */ + 'max' => function (int $max = null) { + return $max; + } + ], + 'methods' => [ + 'isFull' => function () { + if ($this->max) { + return $this->total >= $this->max; + } + + return false; + }, + 'validateMax' => function () { + if ($this->max && $this->total > $this->max) { + return false; + } + + return true; + } + ] +]; diff --git a/kirby/config/sections/mixins/min.php b/kirby/config/sections/mixins/min.php new file mode 100644 index 0000000..bfc495d --- /dev/null +++ b/kirby/config/sections/mixins/min.php @@ -0,0 +1,21 @@ + [ + /** + * Sets the minimum number of required entries in the section + */ + 'min' => function (int $min = null) { + return $min; + } + ], + 'methods' => [ + 'validateMin' => function () { + if ($this->min && $this->min > $this->total) { + return false; + } + + return true; + } + ] +]; diff --git a/kirby/config/sections/mixins/pagination.php b/kirby/config/sections/mixins/pagination.php new file mode 100644 index 0000000..8bf3dee --- /dev/null +++ b/kirby/config/sections/mixins/pagination.php @@ -0,0 +1,36 @@ + [ + /** + * Sets the number of items per page. If there are more items the pagination navigation will be shown at the bottom of the section. + */ + 'limit' => function (int $limit = 20) { + return $limit; + }, + /** + * Sets the default page for the pagination. This will overwrite default pagination. + */ + 'page' => function (int $page = null) { + return get('page', $page); + }, + ], + 'methods' => [ + 'pagination' => function () { + $pagination = new Pagination([ + 'limit' => $this->limit, + 'page' => $this->page, + 'total' => $this->total + ]); + + return [ + 'limit' => $pagination->limit(), + 'offset' => $pagination->offset(), + 'page' => $pagination->page(), + 'total' => $pagination->total(), + ]; + }, + ] +]; diff --git a/kirby/config/sections/mixins/parent.php b/kirby/config/sections/mixins/parent.php new file mode 100644 index 0000000..3534acf --- /dev/null +++ b/kirby/config/sections/mixins/parent.php @@ -0,0 +1,43 @@ + [ + /** + * Sets the query to a parent to find items for the list + */ + 'parent' => function (string $parent = null) { + return $parent; + } + ], + 'methods' => [ + 'parentModel' => function () { + $parent = $this->parent; + + if (is_string($parent) === true) { + $query = $parent; + $parent = $this->model->query($query); + + if (!$parent) { + throw new Exception('The parent for the query "' . $query . '" cannot be found in the section "' . $this->name() . '"'); + } + + if ( + is_a($parent, 'Kirby\Cms\Page') === false && + is_a($parent, 'Kirby\Cms\Site') === false && + is_a($parent, 'Kirby\Cms\File') === false && + is_a($parent, 'Kirby\Cms\User') === false + ) { + throw new Exception('The parent for the section "' . $this->name() . '" has to be a page, site or user object'); + } + } + + if ($parent === null) { + return $this->model; + } + + return $parent; + } + ] +]; diff --git a/kirby/config/sections/pages.php b/kirby/config/sections/pages.php new file mode 100644 index 0000000..db51d99 --- /dev/null +++ b/kirby/config/sections/pages.php @@ -0,0 +1,298 @@ + [ + 'empty', + 'headline', + 'help', + 'layout', + 'min', + 'max', + 'pagination', + 'parent' + ], + 'props' => [ + /** + * Optional array of templates that should only be allowed to add + * or `false` to completely disable page creation + */ + 'create' => function ($create = null) { + return $create; + }, + /** + * Enables/disables reverse sorting + */ + 'flip' => function (bool $flip = false) { + return $flip; + }, + /** + * Image options to control the source and look of page previews + */ + 'image' => function ($image = null) { + return $image ?? []; + }, + /** + * Optional info text setup. Info text is shown on the right (lists) or below (cards) the page title. + */ + 'info' => function (string $info = null) { + return $info; + }, + /** + * The size option controls the size of cards. By default cards are auto-sized and the cards grid will always fill the full width. With a size you can disable auto-sizing. Available sizes: `tiny`, `small`, `medium`, `large`, `huge` + */ + 'size' => function (string $size = 'auto') { + return $size; + }, + /** + * Enables/disables manual sorting + */ + 'sortable' => function (bool $sortable = true) { + return $sortable; + }, + /** + * Overwrites manual sorting and sorts by the given field and sorting direction (i.e. `date desc`) + */ + 'sortBy' => function (string $sortBy = null) { + return $sortBy; + }, + /** + * Filters pages by their status. Available status settings: `draft`, `unlisted`, `listed`, `published`, `all`. + */ + 'status' => function (string $status = '') { + if ($status === 'drafts') { + $status = 'draft'; + } + + if (in_array($status, ['all', 'draft', 'published', 'listed', 'unlisted']) === false) { + $status = 'all'; + } + + return $status; + }, + /** + * Filters the list by templates and sets template options when adding new pages to the section. + */ + 'templates' => function ($templates = null) { + return A::wrap($templates ?? $this->template); + }, + /** + * Setup for the main text in the list or cards. By default this will display the page title. + */ + 'text' => function (string $text = '{{ page.title }}') { + return $text; + } + ], + 'computed' => [ + 'parent' => function () { + return $this->parentModel(); + }, + 'pages' => function () { + switch ($this->status) { + case 'draft': + $pages = $this->parent->drafts(); + break; + case 'listed': + $pages = $this->parent->children()->listed(); + break; + case 'published': + $pages = $this->parent->children(); + break; + case 'unlisted': + $pages = $this->parent->children()->unlisted(); + break; + default: + $pages = $this->parent->childrenAndDrafts(); + } + + // loop for the best performance + foreach ($pages->data as $id => $page) { + + // remove all protected pages + if ($page->isReadable() === false) { + unset($pages->data[$id]); + continue; + } + + // filter by all set templates + if ($this->templates && in_array($page->intendedTemplate()->name(), $this->templates) === false) { + unset($pages->data[$id]); + continue; + } + } + + // sort + if ($this->sortBy) { + $pages = $pages->sortBy(...$pages::sortArgs($this->sortBy)); + } + + // flip + if ($this->flip === true) { + $pages = $pages->flip(); + } + + // pagination + $pages = $pages->paginate([ + 'page' => $this->page, + 'limit' => $this->limit, + 'method' => 'none' // the page is manually provided + ]); + + return $pages; + }, + 'total' => function () { + return $this->pages->pagination()->total(); + }, + 'data' => function () { + $data = []; + + foreach ($this->pages as $item) { + $permissions = $item->permissions(); + $image = $item->panelImage($this->image); + + $data[] = [ + 'id' => $item->id(), + 'dragText' => $item->dragText(), + 'text' => $item->toString($this->text), + 'info' => $item->toString($this->info ?? false), + 'parent' => $item->parentId(), + 'icon' => $item->panelIcon($image), + 'image' => $image, + 'link' => $item->panelUrl(true), + 'status' => $item->status(), + 'permissions' => [ + 'sort' => $permissions->can('sort'), + 'changeStatus' => $permissions->can('changeStatus') + ] + ]; + } + + return $data; + }, + 'errors' => function () { + $errors = []; + + if ($this->validateMax() === false) { + $errors['max'] = I18n::template('error.section.pages.max.' . I18n::form($this->max), [ + 'max' => $this->max, + 'section' => $this->headline + ]); + } + + if ($this->validateMin() === false) { + $errors['min'] = I18n::template('error.section.pages.min.' . I18n::form($this->min), [ + 'min' => $this->min, + 'section' => $this->headline + ]); + } + + if (empty($errors) === true) { + return []; + } + + return [ + $this->name => [ + 'label' => $this->headline, + 'message' => $errors, + ] + ]; + }, + 'add' => function () { + if ($this->create === false) { + return false; + } + + if (in_array($this->status, ['draft', 'all']) === false) { + return false; + } + + if ($this->isFull() === true) { + return false; + } + + return true; + }, + 'link' => function () { + $modelLink = $this->model->panelUrl(true); + $parentLink = $this->parent->panelUrl(true); + + if ($modelLink !== $parentLink) { + return $parentLink; + } + }, + 'pagination' => function () { + return $this->pagination(); + }, + 'sortable' => function () { + if (in_array($this->status, ['listed', 'published', 'all']) === false) { + return false; + } + + if ($this->sortable === false) { + return false; + } + + if ($this->sortBy !== null) { + return false; + } + + if ($this->flip === true) { + return false; + } + + return true; + } + ], + 'methods' => [ + 'blueprints' => function () { + $blueprints = []; + $templates = empty($this->create) === false ? A::wrap($this->create) : $this->templates; + + if (empty($templates) === true) { + $templates = $this->kirby()->blueprints(); + } + + // convert every template to a usable option array + // for the template select box + foreach ($templates as $template) { + try { + $props = Blueprint::load('pages/' . $template); + + $blueprints[] = [ + 'name' => basename($props['name']), + 'title' => $props['title'], + ]; + } catch (Throwable $e) { + $blueprints[] = [ + 'name' => basename($template), + 'title' => ucfirst($template), + ]; + } + } + + return $blueprints; + } + ], + 'toArray' => function () { + return [ + 'data' => $this->data, + 'errors' => $this->errors, + 'options' => [ + 'add' => $this->add, + 'empty' => $this->empty, + 'headline' => $this->headline, + 'help' => $this->help, + 'layout' => $this->layout, + 'link' => $this->link, + 'max' => $this->max, + 'min' => $this->min, + 'size' => $this->size, + 'sortable' => $this->sortable + ], + 'pagination' => $this->pagination, + ]; + } +]; diff --git a/kirby/config/setup.php b/kirby/config/setup.php new file mode 100644 index 0000000..acbc2df --- /dev/null +++ b/kirby/config/setup.php @@ -0,0 +1,41 @@ + [ + 'attr' => [], + 'html' => function ($tag) { + return strtolower($tag->date) === 'year' ? date('Y') : date($tag->date); + } + ], + + /** + * Email + */ + 'email' => [ + 'attr' => [ + 'class', + 'rel', + 'target', + 'text', + 'title' + ], + 'html' => function ($tag) { + return Html::email($tag->value, $tag->text, [ + 'class' => $tag->class, + 'rel' => $tag->rel, + 'target' => $tag->target, + 'title' => $tag->title, + ]); + } + ], + + /** + * File + */ + 'file' => [ + 'attr' => [ + 'class', + 'download', + 'rel', + 'target', + 'text', + 'title' + ], + 'html' => function ($tag) { + if (!$file = $tag->file($tag->value)) { + return $tag->text; + } + + // use filename if the text is empty and make sure to + // ignore markdown italic underscores in filenames + if (empty($tag->text) === true) { + $tag->text = str_replace('_', '\_', $file->filename()); + } + + return Html::a($file->url(), $tag->text, [ + 'class' => $tag->class, + 'download' => $tag->download !== 'false', + 'rel' => $tag->rel, + 'target' => $tag->target, + 'title' => $tag->title, + ]); + } + ], + + /** + * Gist + */ + 'gist' => [ + 'attr' => [ + 'file' + ], + 'html' => function ($tag) { + return Html::gist($tag->value, $tag->file); + } + ], + + /** + * Image + */ + 'image' => [ + 'attr' => [ + 'alt', + 'caption', + 'class', + 'height', + 'imgclass', + 'link', + 'linkclass', + 'rel', + 'target', + 'title', + 'width' + ], + 'html' => function ($tag) { + if ($tag->file = $tag->file($tag->value)) { + $tag->src = $tag->file->url(); + $tag->alt = $tag->alt ?? $tag->file->alt()->or(' ')->value(); + $tag->title = $tag->title ?? $tag->file->title()->value(); + $tag->caption = $tag->caption ?? $tag->file->caption()->value(); + } else { + $tag->src = Url::to($tag->value); + } + + $link = function ($img) use ($tag) { + if (empty($tag->link) === true) { + return $img; + } + + if ($link = $tag->file($tag->link)) { + $link = $link->url(); + } else { + $link = $tag->link === 'self' ? $tag->src : $tag->link; + } + + return Html::a($link, [$img], [ + 'rel' => $tag->rel, + 'class' => $tag->linkclass, + 'target' => $tag->target + ]); + }; + + $image = Html::img($tag->src, [ + 'width' => $tag->width, + 'height' => $tag->height, + 'class' => $tag->imgclass, + 'title' => $tag->title, + 'alt' => $tag->alt ?? ' ' + ]); + + if ($tag->kirby()->option('kirbytext.image.figure', true) === false) { + return $link($image); + } + + // render KirbyText in caption + if ($tag->caption) { + $tag->caption = [$tag->kirby()->kirbytext($tag->caption, [], true)]; + } + + return Html::figure([ $link($image) ], $tag->caption, [ + 'class' => $tag->class + ]); + } + ], + + /** + * Link + */ + 'link' => [ + 'attr' => [ + 'class', + 'lang', + 'rel', + 'role', + 'target', + 'title', + 'text', + ], + 'html' => function ($tag) { + if (empty($tag->lang) === false) { + $tag->value = Url::to($tag->value, $tag->lang); + } + + return Html::a($tag->value, $tag->text, [ + 'rel' => $tag->rel, + 'class' => $tag->class, + 'role' => $tag->role, + 'title' => $tag->title, + 'target' => $tag->target, + ]); + } + ], + + /** + * Tel + */ + 'tel' => [ + 'attr' => [ + 'class', + 'rel', + 'text', + 'title' + ], + 'html' => function ($tag) { + return Html::tel($tag->value, $tag->text, [ + 'class' => $tag->class, + 'rel' => $tag->rel, + 'title' => $tag->title + ]); + } + ], + + /** + * Twitter + */ + 'twitter' => [ + 'attr' => [ + 'class', + 'rel', + 'target', + 'text', + 'title' + ], + 'html' => function ($tag) { + + // get and sanitize the username + $username = str_replace('@', '', $tag->value); + + // build the profile url + $url = 'https://twitter.com/' . $username; + + // sanitize the link text + $text = $tag->text ?? '@' . $username; + + // build the final link + return Html::a($url, $text, [ + 'class' => $tag->class, + 'rel' => $tag->rel, + 'target' => $tag->target, + 'title' => $tag->title, + ]); + } + ], + + /** + * Video + */ + 'video' => [ + 'attr' => [ + 'class', + 'caption', + 'height', + 'width' + ], + 'html' => function ($tag) { + $video = Html::video( + $tag->value, + $tag->kirby()->option('kirbytext.video.options', []), + [ + 'height' => $tag->height ?? $tag->kirby()->option('kirbytext.video.height'), + 'width' => $tag->width ?? $tag->kirby()->option('kirbytext.video.width'), + ] + ); + + return Html::figure([$video], $tag->caption, [ + 'class' => $tag->class ?? $tag->kirby()->option('kirbytext.video.class', 'video'), + ]); + } + ], + +]; diff --git a/kirby/config/urls.php b/kirby/config/urls.php new file mode 100644 index 0000000..1bfe0fd --- /dev/null +++ b/kirby/config/urls.php @@ -0,0 +1,33 @@ + function () { + return Url::index(); + }, + 'base' => function (array $urls) { + return rtrim($urls['index'], '/'); + }, + 'current' => function (array $urls) { + $path = trim($this->path(), '/'); + + if (empty($path) === true) { + return $urls['index']; + } else { + return $urls['base'] . '/' . $path; + } + }, + 'assets' => function (array $urls) { + return $urls['base'] . '/assets'; + }, + 'api' => function (array $urls) { + return $urls['base'] . '/' . ($this->options['api']['slug'] ?? 'api'); + }, + 'media' => function (array $urls) { + return $urls['base'] . '/media'; + }, + 'panel' => function (array $urls) { + return $urls['base'] . '/' . ($this->options['panel']['slug'] ?? 'panel'); + } +]; diff --git a/kirby/dependencies/parsedown-extra/ParsedownExtra.php b/kirby/dependencies/parsedown-extra/ParsedownExtra.php new file mode 100644 index 0000000..c3db03a --- /dev/null +++ b/kirby/dependencies/parsedown-extra/ParsedownExtra.php @@ -0,0 +1,627 @@ +BlockTypes[':'] []= 'DefinitionList'; + $this->BlockTypes['*'] []= 'Abbreviation'; + + # identify footnote definitions before reference definitions + array_unshift($this->BlockTypes['['], 'Footnote'); + + # identify footnote markers before before links + array_unshift($this->InlineTypes['['], 'FootnoteMarker'); + } + + # + # ~ + + public function text($text) + { + $Elements = $this->textElements($text); + + # convert to markup + $markup = $this->elements($Elements); + + # trim line breaks + $markup = trim($markup, "\n"); + + # merge consecutive dl elements + + $markup = preg_replace('/<\/dl>\s+

\s+/', '', $markup); + + # add footnotes + + if (isset($this->DefinitionData['Footnote'])) { + $Element = $this->buildFootnoteElement(); + + $markup .= "\n" . $this->element($Element); + } + + return $markup; + } + + # + # Blocks + # + + # + # Abbreviation + + protected function blockAbbreviation($Line) + { + if (preg_match('/^\*\[(.+?)\]:[ ]*(.+?)[ ]*$/', $Line['text'], $matches)) { + $this->DefinitionData['Abbreviation'][$matches[1]] = $matches[2]; + + $Block = array( + 'hidden' => true, + ); + + return $Block; + } + } + + # + # Footnote + + protected function blockFootnote($Line) + { + if (preg_match('/^\[\^(.+?)\]:[ ]?(.*)$/', $Line['text'], $matches)) { + $Block = array( + 'label' => $matches[1], + 'text' => $matches[2], + 'hidden' => true, + ); + + return $Block; + } + } + + protected function blockFootnoteContinue($Line, $Block) + { + if ($Line['text'][0] === '[' and preg_match('/^\[\^(.+?)\]:/', $Line['text'])) { + return; + } + + if (isset($Block['interrupted'])) { + if ($Line['indent'] >= 4) { + $Block['text'] .= "\n\n" . $Line['text']; + + return $Block; + } + } else { + $Block['text'] .= "\n" . $Line['text']; + + return $Block; + } + } + + protected function blockFootnoteComplete($Block) + { + $this->DefinitionData['Footnote'][$Block['label']] = array( + 'text' => $Block['text'], + 'count' => null, + 'number' => null, + ); + + return $Block; + } + + # + # Definition List + + protected function blockDefinitionList($Line, $Block) + { + if (! isset($Block) or $Block['type'] !== 'Paragraph') { + return; + } + + $Element = array( + 'name' => 'dl', + 'elements' => array(), + ); + + $terms = explode("\n", $Block['element']['handler']['argument']); + + foreach ($terms as $term) { + $Element['elements'] []= array( + 'name' => 'dt', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $term, + 'destination' => 'elements' + ), + ); + } + + $Block['element'] = $Element; + + $Block = $this->addDdElement($Line, $Block); + + return $Block; + } + + protected function blockDefinitionListContinue($Line, array $Block) + { + if ($Line['text'][0] === ':') { + $Block = $this->addDdElement($Line, $Block); + + return $Block; + } else { + if (isset($Block['interrupted']) and $Line['indent'] === 0) { + return; + } + + if (isset($Block['interrupted'])) { + $Block['dd']['handler']['function'] = 'textElements'; + $Block['dd']['handler']['argument'] .= "\n\n"; + + $Block['dd']['handler']['destination'] = 'elements'; + + unset($Block['interrupted']); + } + + $text = substr($Line['body'], min($Line['indent'], 4)); + + $Block['dd']['handler']['argument'] .= "\n" . $text; + + return $Block; + } + } + + # + # Header + + protected function blockHeader($Line) + { + $Block = parent::blockHeader($Line); + + if (preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE)) { + $attributeString = $matches[1][0]; + + $Block['element']['attributes'] = $this->parseAttributeData($attributeString); + + $Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]); + } + + return $Block; + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped or $this->safeMode) { + return; + } + + if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) { + return; + } + + $Block = array( + 'name' => $matches[1], + 'depth' => 0, + 'element' => array( + 'rawHtml' => $Line['text'], + 'autobreak' => true, + ), + ); + + $length = strlen($matches[0]); + $remainder = substr($Line['text'], $length); + + if (trim($remainder) === '') { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) { + $Block['closed'] = true; + $Block['void'] = true; + } + } else { + if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) { + return; + } + if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) { + $Block['closed'] = true; + } + } + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed'])) { + return; + } + + if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) { # open + $Block['depth'] ++; + } + + if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) { # close + if ($Block['depth'] > 0) { + $Block['depth'] --; + } else { + $Block['closed'] = true; + } + } + + if (isset($Block['interrupted'])) { + $Block['element']['rawHtml'] .= "\n"; + unset($Block['interrupted']); + } + + $Block['element']['rawHtml'] .= "\n".$Line['body']; + + return $Block; + } + + protected function blockMarkupComplete($Block) + { + if (! isset($Block['void'])) { + $Block['element']['rawHtml'] = $this->processTag($Block['element']['rawHtml']); + } + + return $Block; + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + $Block = parent::blockSetextHeader($Line, $Block); + + if (preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE)) { + $attributeString = $matches[1][0]; + + $Block['element']['attributes'] = $this->parseAttributeData($attributeString); + + $Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]); + } + + return $Block; + } + + # + # Inline Elements + # + + # + # Footnote Marker + + protected function inlineFootnoteMarker($Excerpt) + { + if (preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches)) { + $name = $matches[1]; + + if (! isset($this->DefinitionData['Footnote'][$name])) { + return; + } + + $this->DefinitionData['Footnote'][$name]['count'] ++; + + if (! isset($this->DefinitionData['Footnote'][$name]['number'])) { + $this->DefinitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount; # » & + } + + $Element = array( + 'name' => 'sup', + 'attributes' => array('id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name), + 'element' => array( + 'name' => 'a', + 'attributes' => array('href' => '#fn:'.$name, 'class' => 'footnote-ref'), + 'text' => $this->DefinitionData['Footnote'][$name]['number'], + ), + ); + + return array( + 'extent' => strlen($matches[0]), + 'element' => $Element, + ); + } + } + + private $footnoteCount = 0; + + # + # Link + + protected function inlineLink($Excerpt) + { + $Link = parent::inlineLink($Excerpt); + + $remainder = substr($Excerpt['text'], $Link['extent']); + + if (preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches)) { + $Link['element']['attributes'] += $this->parseAttributeData($matches[1]); + + $Link['extent'] += strlen($matches[0]); + } + + return $Link; + } + + # + # ~ + # + + private $currentAbreviation; + private $currentMeaning; + + protected function insertAbreviation(array $Element) + { + if (isset($Element['text'])) { + $Element['elements'] = self::pregReplaceElements( + '/\b'.preg_quote($this->currentAbreviation, '/').'\b/', + array( + array( + 'name' => 'abbr', + 'attributes' => array( + 'title' => $this->currentMeaning, + ), + 'text' => $this->currentAbreviation, + ) + ), + $Element['text'] + ); + + unset($Element['text']); + } + + return $Element; + } + + protected function inlineText($text) + { + $Inline = parent::inlineText($text); + + if (isset($this->DefinitionData['Abbreviation'])) { + foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning) { + $this->currentAbreviation = $abbreviation; + $this->currentMeaning = $meaning; + + $Inline['element'] = $this->elementApplyRecursiveDepthFirst( + array($this, 'insertAbreviation'), + $Inline['element'] + ); + } + } + + return $Inline; + } + + # + # Util Methods + # + + protected function addDdElement(array $Line, array $Block) + { + $text = substr($Line['text'], 1); + $text = trim($text); + + unset($Block['dd']); + + $Block['dd'] = array( + 'name' => 'dd', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $text, + 'destination' => 'elements' + ), + ); + + if (isset($Block['interrupted'])) { + $Block['dd']['handler']['function'] = 'textElements'; + + unset($Block['interrupted']); + } + + $Block['element']['elements'] []= & $Block['dd']; + + return $Block; + } + + protected function buildFootnoteElement() + { + $Element = array( + 'name' => 'div', + 'attributes' => array('class' => 'footnotes'), + 'elements' => array( + array('name' => 'hr'), + array( + 'name' => 'ol', + 'elements' => array(), + ), + ), + ); + + uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes'); + + foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData) { + if (! isset($DefinitionData['number'])) { + continue; + } + + $text = $DefinitionData['text']; + + $textElements = parent::textElements($text); + + $numbers = range(1, $DefinitionData['count']); + + $backLinkElements = array(); + + foreach ($numbers as $number) { + $backLinkElements[] = array('text' => ' '); + $backLinkElements[] = array( + 'name' => 'a', + 'attributes' => array( + 'href' => "#fnref$number:$definitionId", + 'rev' => 'footnote', + 'class' => 'footnote-backref', + ), + 'rawHtml' => '↩', + 'allowRawHtmlInSafeMode' => true, + 'autobreak' => false, + ); + } + + unset($backLinkElements[0]); + + $n = count($textElements) -1; + + if ($textElements[$n]['name'] === 'p') { + $backLinkElements = array_merge( + array( + array( + 'rawHtml' => ' ', + 'allowRawHtmlInSafeMode' => true, + ), + ), + $backLinkElements + ); + + unset($textElements[$n]['name']); + + $textElements[$n] = array( + 'name' => 'p', + 'elements' => array_merge( + array($textElements[$n]), + $backLinkElements + ), + ); + } else { + $textElements[] = array( + 'name' => 'p', + 'elements' => $backLinkElements + ); + } + + $Element['elements'][1]['elements'] []= array( + 'name' => 'li', + 'attributes' => array('id' => 'fn:'.$definitionId), + 'elements' => array_merge( + $textElements + ), + ); + } + + return $Element; + } + + # ~ + + protected function parseAttributeData($attributeString) + { + $Data = array(); + + $attributes = preg_split('/[ ]+/', $attributeString, - 1, PREG_SPLIT_NO_EMPTY); + + foreach ($attributes as $attribute) { + if ($attribute[0] === '#') { + $Data['id'] = substr($attribute, 1); + } else { # "." + $classes []= substr($attribute, 1); + } + } + + if (isset($classes)) { + $Data['class'] = implode(' ', $classes); + } + + return $Data; + } + + # ~ + + protected function processTag($elementMarkup) # recursive + { + # http://stackoverflow.com/q/1148928/200145 + libxml_use_internal_errors(true); + + $DOMDocument = new DOMDocument; + + # http://stackoverflow.com/q/11309194/200145 + $elementMarkup = mb_convert_encoding($elementMarkup, 'HTML-ENTITIES', 'UTF-8'); + + # Ensure that saveHTML() is not remove new line characters. New lines will be split by this character. + $DOMDocument->formatOutput = true; + + # http://stackoverflow.com/q/4879946/200145 + $DOMDocument->loadHTML($elementMarkup); + $DOMDocument->removeChild($DOMDocument->doctype); + $DOMDocument->replaceChild($DOMDocument->firstChild->firstChild->firstChild, $DOMDocument->firstChild); + + $elementText = ''; + + if ($DOMDocument->documentElement->getAttribute('markdown') === '1') { + foreach ($DOMDocument->documentElement->childNodes as $Node) { + $elementText .= $DOMDocument->saveHTML($Node); + } + + $DOMDocument->documentElement->removeAttribute('markdown'); + + $elementText = "\n".$this->text($elementText)."\n"; + } else { + foreach ($DOMDocument->documentElement->childNodes as $Node) { + $nodeMarkup = $DOMDocument->saveHTML($Node); + + if ($Node instanceof DOMElement and ! in_array($Node->nodeName, $this->textLevelElements)) { + $elementText .= $this->processTag($nodeMarkup); + } else { + $elementText .= $nodeMarkup; + } + } + } + + # because we don't want for markup to get encoded + $DOMDocument->documentElement->nodeValue = 'placeholder\x1A'; + + $markup = $DOMDocument->saveHTML($DOMDocument->documentElement); + $markup = str_replace('placeholder\x1A', $elementText, $markup); + + return $markup; + } + + # ~ + + protected function sortFootnotes($A, $B) # callback + { + return $A['number'] - $B['number']; + } + + # + # Fields + # + + protected $regexAttribute = '(?:[#.][-\w]+[ ]*)'; +} diff --git a/kirby/dependencies/parsedown/Parsedown.php b/kirby/dependencies/parsedown/Parsedown.php new file mode 100755 index 0000000..6552c0c --- /dev/null +++ b/kirby/dependencies/parsedown/Parsedown.php @@ -0,0 +1,1822 @@ +textElements($text); + + # convert to markup + $markup = $this->elements($Elements); + + # trim line breaks + $markup = trim($markup, "\n"); + + return $markup; + } + + protected function textElements($text) + { + # make sure no definitions are set + $this->DefinitionData = array(); + + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + # remove surrounding line breaks + $text = trim($text, "\n"); + + # split text into lines + $lines = explode("\n", $text); + + # iterate through lines to identify blocks + return $this->linesElements($lines); + } + + # + # Setters + # + + public function setBreaksEnabled($breaksEnabled) + { + $this->breaksEnabled = $breaksEnabled; + + return $this; + } + + protected $breaksEnabled; + + public function setMarkupEscaped($markupEscaped) + { + $this->markupEscaped = $markupEscaped; + + return $this; + } + + protected $markupEscaped; + + public function setUrlsLinked($urlsLinked) + { + $this->urlsLinked = $urlsLinked; + + return $this; + } + + protected $urlsLinked = true; + + public function setSafeMode($safeMode) + { + $this->safeMode = (bool) $safeMode; + + return $this; + } + + protected $safeMode; + + public function setStrictMode($strictMode) + { + $this->strictMode = (bool) $strictMode; + + return $this; + } + + protected $strictMode; + + protected $safeLinksWhitelist = array( + 'http://', + 'https://', + 'ftp://', + 'ftps://', + 'mailto:', + 'data:image/png;base64,', + 'data:image/gif;base64,', + 'data:image/jpeg;base64,', + 'irc:', + 'ircs:', + 'git:', + 'ssh:', + 'news:', + 'steam:', + ); + + # + # Lines + # + + protected $BlockTypes = array( + '#' => array('Header'), + '*' => array('Rule', 'List'), + '+' => array('List'), + '-' => array('SetextHeader', 'Table', 'Rule', 'List'), + '0' => array('List'), + '1' => array('List'), + '2' => array('List'), + '3' => array('List'), + '4' => array('List'), + '5' => array('List'), + '6' => array('List'), + '7' => array('List'), + '8' => array('List'), + '9' => array('List'), + ':' => array('Table'), + '<' => array('Comment', 'Markup'), + '=' => array('SetextHeader'), + '>' => array('Quote'), + '[' => array('Reference'), + '_' => array('Rule'), + '`' => array('FencedCode'), + '|' => array('Table'), + '~' => array('FencedCode'), + ); + + # ~ + + protected $unmarkedBlockTypes = array( + 'Code', + ); + + # + # Blocks + # + + protected function lines(array $lines) + { + return $this->elements($this->linesElements($lines)); + } + + protected function linesElements(array $lines) + { + $Elements = array(); + $CurrentBlock = null; + + foreach ($lines as $line) { + if (chop($line) === '') { + if (isset($CurrentBlock)) { + $CurrentBlock['interrupted'] = ( + isset($CurrentBlock['interrupted']) + ? $CurrentBlock['interrupted'] + 1 : 1 + ); + } + + continue; + } + + while (($beforeTab = strstr($line, "\t", true)) !== false) { + $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4; + + $line = $beforeTab + . str_repeat(' ', $shortage) + . substr($line, strlen($beforeTab) + 1) + ; + } + + $indent = strspn($line, ' '); + + $text = $indent > 0 ? substr($line, $indent) : $line; + + # ~ + + $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); + + # ~ + + if (isset($CurrentBlock['continuable'])) { + $methodName = 'block' . $CurrentBlock['type'] . 'Continue'; + $Block = $this->$methodName($Line, $CurrentBlock); + + if (isset($Block)) { + $CurrentBlock = $Block; + + continue; + } else { + if ($this->isBlockCompletable($CurrentBlock['type'])) { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + } + } + + # ~ + + $marker = $text[0]; + + # ~ + + $blockTypes = $this->unmarkedBlockTypes; + + if (isset($this->BlockTypes[$marker])) { + foreach ($this->BlockTypes[$marker] as $blockType) { + $blockTypes []= $blockType; + } + } + + # + # ~ + + foreach ($blockTypes as $blockType) { + $Block = $this->{"block$blockType"}($Line, $CurrentBlock); + + if (isset($Block)) { + $Block['type'] = $blockType; + + if (! isset($Block['identified'])) { + if (isset($CurrentBlock)) { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $Block['identified'] = true; + } + + if ($this->isBlockContinuable($blockType)) { + $Block['continuable'] = true; + } + + $CurrentBlock = $Block; + + continue 2; + } + } + + # ~ + + if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph') { + $Block = $this->paragraphContinue($Line, $CurrentBlock); + } + + if (isset($Block)) { + $CurrentBlock = $Block; + } else { + if (isset($CurrentBlock)) { + $Elements[] = $this->extractElement($CurrentBlock); + } + + $CurrentBlock = $this->paragraph($Line); + + $CurrentBlock['identified'] = true; + } + } + + # ~ + + if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) { + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); + } + + # ~ + + if (isset($CurrentBlock)) { + $Elements[] = $this->extractElement($CurrentBlock); + } + + # ~ + + return $Elements; + } + + protected function extractElement(array $Component) + { + if (! isset($Component['element'])) { + if (isset($Component['markup'])) { + $Component['element'] = array('rawHtml' => $Component['markup']); + } elseif (isset($Component['hidden'])) { + $Component['element'] = array(); + } + } + + return $Component['element']; + } + + protected function isBlockContinuable($Type) + { + return method_exists($this, 'block' . $Type . 'Continue'); + } + + protected function isBlockCompletable($Type) + { + return method_exists($this, 'block' . $Type . 'Complete'); + } + + # + # Code + + protected function blockCode($Line, $Block = null) + { + if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted'])) { + return; + } + + if ($Line['indent'] >= 4) { + $text = substr($Line['body'], 4); + + $Block = array( + 'element' => array( + 'name' => 'pre', + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ), + ); + + return $Block; + } + } + + protected function blockCodeContinue($Line, $Block) + { + if ($Line['indent'] >= 4) { + if (isset($Block['interrupted'])) { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + $Block['element']['element']['text'] .= "\n"; + + $text = substr($Line['body'], 4); + + $Block['element']['element']['text'] .= $text; + + return $Block; + } + } + + protected function blockCodeComplete($Block) + { + return $Block; + } + + # + # Comment + + protected function blockComment($Line) + { + if ($this->markupEscaped or $this->safeMode) { + return; + } + + if (strpos($Line['text'], '') !== false) { + $Block['closed'] = true; + } + + return $Block; + } + } + + protected function blockCommentContinue($Line, array $Block) + { + if (isset($Block['closed'])) { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + if (strpos($Line['text'], '-->') !== false) { + $Block['closed'] = true; + } + + return $Block; + } + + # + # Fenced Code + + protected function blockFencedCode($Line) + { + $marker = $Line['text'][0]; + + $openerLength = strspn($Line['text'], $marker); + + if ($openerLength < 3) { + return; + } + + $infostring = trim(substr($Line['text'], $openerLength), "\t "); + + if (strpos($infostring, '`') !== false) { + return; + } + + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if ($infostring !== '') { + /** + * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes + * Every HTML element may have a class attribute specified. + * The attribute, if specified, must have a value that is a set + * of space-separated tokens representing the various classes + * that the element belongs to. + * [...] + * The space characters, for the purposes of this specification, + * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), + * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and + * U+000D CARRIAGE RETURN (CR). + */ + $language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r")); + + $Element['attributes'] = array('class' => "language-$language"); + } + + $Block = array( + 'char' => $marker, + 'openerLength' => $openerLength, + 'element' => array( + 'name' => 'pre', + 'element' => $Element, + ), + ); + + return $Block; + } + + protected function blockFencedCodeContinue($Line, $Block) + { + if (isset($Block['complete'])) { + return; + } + + if (isset($Block['interrupted'])) { + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); + + unset($Block['interrupted']); + } + + if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength'] + and chop(substr($Line['text'], $len), ' ') === '' + ) { + $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1); + + $Block['complete'] = true; + + return $Block; + } + + $Block['element']['element']['text'] .= "\n" . $Line['body']; + + return $Block; + } + + protected function blockFencedCodeComplete($Block) + { + return $Block; + } + + # + # Header + + protected function blockHeader($Line) + { + $level = strspn($Line['text'], '#'); + + if ($level > 6) { + return; + } + + $text = trim($Line['text'], '#'); + + if ($this->strictMode and isset($text[0]) and $text[0] !== ' ') { + return; + } + + $text = trim($text, ' '); + + $Block = array( + 'element' => array( + 'name' => 'h' . min(6, $level), + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $text, + 'destination' => 'elements', + ) + ), + ); + + return $Block; + } + + # + # List + + protected function blockList($Line, array $CurrentBlock = null) + { + list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]'); + + if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches)) { + $contentIndent = strlen($matches[2]); + + if ($contentIndent >= 5) { + $contentIndent -= 1; + $matches[1] = substr($matches[1], 0, -$contentIndent); + $matches[3] = str_repeat(' ', $contentIndent) . $matches[3]; + } elseif ($contentIndent === 0) { + $matches[1] .= ' '; + } + + $markerWithoutWhitespace = strstr($matches[1], ' ', true); + + $Block = array( + 'indent' => $Line['indent'], + 'pattern' => $pattern, + 'data' => array( + 'type' => $name, + 'marker' => $matches[1], + 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)), + ), + 'element' => array( + 'name' => $name, + 'elements' => array(), + ), + ); + $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/'); + + if ($name === 'ol') { + $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0'; + + if ($listStart !== '1') { + if ( + isset($CurrentBlock) + and $CurrentBlock['type'] === 'Paragraph' + and ! isset($CurrentBlock['interrupted']) + ) { + return; + } + + $Block['element']['attributes'] = array('start' => $listStart); + } + } + + $Block['li'] = array( + 'name' => 'li', + 'handler' => array( + 'function' => 'li', + 'argument' => !empty($matches[3]) ? array($matches[3]) : array(), + 'destination' => 'elements' + ) + ); + + $Block['element']['elements'] []= & $Block['li']; + + return $Block; + } + } + + protected function blockListContinue($Line, array $Block) + { + if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument'])) { + return null; + } + + $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker'])); + + if ($Line['indent'] < $requiredIndent + and ( + ( + $Block['data']['type'] === 'ol' + and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) or ( + $Block['data']['type'] === 'ul' + and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) + ) + ) { + if (isset($Block['interrupted'])) { + $Block['li']['handler']['argument'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + unset($Block['li']); + + $text = isset($matches[1]) ? $matches[1] : ''; + + $Block['indent'] = $Line['indent']; + + $Block['li'] = array( + 'name' => 'li', + 'handler' => array( + 'function' => 'li', + 'argument' => array($text), + 'destination' => 'elements' + ) + ); + + $Block['element']['elements'] []= & $Block['li']; + + return $Block; + } elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line)) { + return null; + } + + if ($Line['text'][0] === '[' and $this->blockReference($Line)) { + return $Block; + } + + if ($Line['indent'] >= $requiredIndent) { + if (isset($Block['interrupted'])) { + $Block['li']['handler']['argument'] []= ''; + + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + $text = substr($Line['body'], $requiredIndent); + + $Block['li']['handler']['argument'] []= $text; + + return $Block; + } + + if (! isset($Block['interrupted'])) { + $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']); + + $Block['li']['handler']['argument'] []= $text; + + return $Block; + } + } + + protected function blockListComplete(array $Block) + { + if (isset($Block['loose'])) { + foreach ($Block['element']['elements'] as &$li) { + if (end($li['handler']['argument']) !== '') { + $li['handler']['argument'] []= ''; + } + } + } + + return $Block; + } + + # + # Quote + + protected function blockQuote($Line) + { + if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) { + $Block = array( + 'element' => array( + 'name' => 'blockquote', + 'handler' => array( + 'function' => 'linesElements', + 'argument' => (array) $matches[1], + 'destination' => 'elements', + ) + ), + ); + + return $Block; + } + } + + protected function blockQuoteContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) { + return; + } + + if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) { + $Block['element']['handler']['argument'] []= $matches[1]; + + return $Block; + } + + if (! isset($Block['interrupted'])) { + $Block['element']['handler']['argument'] []= $Line['text']; + + return $Block; + } + } + + # + # Rule + + protected function blockRule($Line) + { + $marker = $Line['text'][0]; + + if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '') { + $Block = array( + 'element' => array( + 'name' => 'hr', + ), + ); + + return $Block; + } + } + + # + # Setext + + protected function blockSetextHeader($Line, array $Block = null) + { + if (! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) { + return; + } + + if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '') { + $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; + + return $Block; + } + } + + # + # Markup + + protected function blockMarkup($Line) + { + if ($this->markupEscaped or $this->safeMode) { + return; + } + + if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches)) { + $element = strtolower($matches[1]); + + if (in_array($element, $this->textLevelElements)) { + return; + } + + $Block = array( + 'name' => $matches[1], + 'element' => array( + 'rawHtml' => $Line['text'], + 'autobreak' => true, + ), + ); + + return $Block; + } + } + + protected function blockMarkupContinue($Line, array $Block) + { + if (isset($Block['closed']) or isset($Block['interrupted'])) { + return; + } + + $Block['element']['rawHtml'] .= "\n" . $Line['body']; + + return $Block; + } + + # + # Reference + + protected function blockReference($Line) + { + if (strpos($Line['text'], ']') !== false + and preg_match('/^\[(.+?)\]:[ ]*+?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches) + ) { + $id = strtolower($matches[1]); + + $Data = array( + 'url' => $matches[2], + 'title' => isset($matches[3]) ? $matches[3] : null, + ); + + $this->DefinitionData['Reference'][$id] = $Data; + + $Block = array( + 'element' => array(), + ); + + return $Block; + } + } + + # + # Table + + protected function blockTable($Line, array $Block = null) + { + if (! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) { + return; + } + + if ( + strpos($Block['element']['handler']['argument'], '|') === false + and strpos($Line['text'], '|') === false + and strpos($Line['text'], ':') === false + or strpos($Block['element']['handler']['argument'], "\n") !== false + ) { + return; + } + + if (chop($Line['text'], ' -:|') !== '') { + return; + } + + $alignments = array(); + + $divider = $Line['text']; + + $divider = trim($divider); + $divider = trim($divider, '|'); + + $dividerCells = explode('|', $divider); + + foreach ($dividerCells as $dividerCell) { + $dividerCell = trim($dividerCell); + + if ($dividerCell === '') { + return; + } + + $alignment = null; + + if ($dividerCell[0] === ':') { + $alignment = 'left'; + } + + if (substr($dividerCell, - 1) === ':') { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments []= $alignment; + } + + # ~ + + $HeaderElements = array(); + + $header = $Block['element']['handler']['argument']; + + $header = trim($header); + $header = trim($header, '|'); + + $headerCells = explode('|', $header); + + if (count($headerCells) !== count($alignments)) { + return; + } + + foreach ($headerCells as $index => $headerCell) { + $headerCell = trim($headerCell); + + $HeaderElement = array( + 'name' => 'th', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $headerCell, + 'destination' => 'elements', + ) + ); + + if (isset($alignments[$index])) { + $alignment = $alignments[$index]; + + $HeaderElement['attributes'] = array( + 'style' => "text-align: $alignment;", + ); + } + + $HeaderElements []= $HeaderElement; + } + + # ~ + + $Block = array( + 'alignments' => $alignments, + 'identified' => true, + 'element' => array( + 'name' => 'table', + 'elements' => array(), + ), + ); + + $Block['element']['elements'] []= array( + 'name' => 'thead', + ); + + $Block['element']['elements'] []= array( + 'name' => 'tbody', + 'elements' => array(), + ); + + $Block['element']['elements'][0]['elements'] []= array( + 'name' => 'tr', + 'elements' => $HeaderElements, + ); + + return $Block; + } + + protected function blockTableContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) { + return; + } + + if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|')) { + $Elements = array(); + + $row = $Line['text']; + + $row = trim($row); + $row = trim($row, '|'); + + preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches); + + $cells = array_slice($matches[0], 0, count($Block['alignments'])); + + foreach ($cells as $index => $cell) { + $cell = trim($cell); + + $Element = array( + 'name' => 'td', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $cell, + 'destination' => 'elements', + ) + ); + + if (isset($Block['alignments'][$index])) { + $Element['attributes'] = array( + 'style' => 'text-align: ' . $Block['alignments'][$index] . ';', + ); + } + + $Elements []= $Element; + } + + $Element = array( + 'name' => 'tr', + 'elements' => $Elements, + ); + + $Block['element']['elements'][1]['elements'] []= $Element; + + return $Block; + } + } + + # + # ~ + # + + protected function paragraph($Line) + { + return array( + 'type' => 'Paragraph', + 'element' => array( + 'name' => 'p', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $Line['text'], + 'destination' => 'elements', + ), + ), + ); + } + + protected function paragraphContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) { + return; + } + + $Block['element']['handler']['argument'] .= "\n".$Line['text']; + + return $Block; + } + + # + # Inline Elements + # + + protected $InlineTypes = array( + '!' => array('Image'), + '&' => array('SpecialCharacter'), + '*' => array('Emphasis'), + ':' => array('Url'), + '<' => array('UrlTag', 'EmailTag', 'Markup'), + '[' => array('Link'), + '_' => array('Emphasis'), + '`' => array('Code'), + '~' => array('Strikethrough'), + '\\' => array('EscapeSequence'), + ); + + # ~ + + protected $inlineMarkerList = '!*_&[:<`~\\'; + + # + # ~ + # + + public function line($text, $nonNestables = array()) + { + return $this->elements($this->lineElements($text, $nonNestables)); + } + + protected function lineElements($text, $nonNestables = array()) + { + $Elements = array(); + + $nonNestables = ( + empty($nonNestables) + ? array() + : array_combine($nonNestables, $nonNestables) + ); + + # $excerpt is based on the first occurrence of a marker + + while ($excerpt = strpbrk($text, $this->inlineMarkerList)) { + $marker = $excerpt[0]; + + $markerPosition = strlen($text) - strlen($excerpt); + + $Excerpt = array('text' => $excerpt, 'context' => $text); + + foreach ($this->InlineTypes[$marker] as $inlineType) { + # check to see if the current inline type is nestable in the current context + + if (isset($nonNestables[$inlineType])) { + continue; + } + + $Inline = $this->{"inline$inlineType"}($Excerpt); + + if (! isset($Inline)) { + continue; + } + + # makes sure that the inline belongs to "our" marker + + if (isset($Inline['position']) and $Inline['position'] > $markerPosition) { + continue; + } + + # sets a default inline position + + if (! isset($Inline['position'])) { + $Inline['position'] = $markerPosition; + } + + # cause the new element to 'inherit' our non nestables + + + $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables']) + ? array_merge($Inline['element']['nonNestables'], $nonNestables) + : $nonNestables + ; + + # the text that comes before the inline + $unmarkedText = substr($text, 0, $Inline['position']); + + # compile the unmarked text + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + # compile the inline + $Elements[] = $this->extractElement($Inline); + + # remove the examined text + $text = substr($text, $Inline['position'] + $Inline['extent']); + + continue 2; + } + + # the marker does not belong to an inline + + $unmarkedText = substr($text, 0, $markerPosition + 1); + + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; + + $text = substr($text, $markerPosition + 1); + } + + $InlineText = $this->inlineText($text); + $Elements[] = $InlineText['element']; + + foreach ($Elements as &$Element) { + if (! isset($Element['autobreak'])) { + $Element['autobreak'] = false; + } + } + + return $Elements; + } + + # + # ~ + # + + protected function inlineText($text) + { + $Inline = array( + 'extent' => strlen($text), + 'element' => array(), + ); + + $Inline['element']['elements'] = self::pregReplaceElements( + $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/', + array( + array('name' => 'br'), + array('text' => "\n"), + ), + $text + ); + + return $Inline; + } + + protected function inlineCode($Excerpt) + { + $marker = $Excerpt['text'][0]; + + if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(? strlen($matches[0]), + 'element' => array( + 'name' => 'code', + 'text' => $text, + ), + ); + } + } + + protected function inlineEmailTag($Excerpt) + { + $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'; + + $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@' + . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*'; + + if (strpos($Excerpt['text'], '>') !== false + and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches) + ) { + $url = $matches[1]; + + if (! isset($matches[2])) { + $url = "mailto:$url"; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[1], + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + protected function inlineEmphasis($Excerpt) + { + if (! isset($Excerpt['text'][1])) { + return; + } + + $marker = $Excerpt['text'][0]; + + if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) { + $emphasis = 'strong'; + } elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) { + $emphasis = 'em'; + } else { + return; + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => $emphasis, + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) + ), + ); + } + + protected function inlineEscapeSequence($Excerpt) + { + if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) { + return array( + 'element' => array('rawHtml' => $Excerpt['text'][1]), + 'extent' => 2, + ); + } + } + + protected function inlineImage($Excerpt) + { + if (! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') { + return; + } + + $Excerpt['text']= substr($Excerpt['text'], 1); + + $Link = $this->inlineLink($Excerpt); + + if ($Link === null) { + return; + } + + $Inline = array( + 'extent' => $Link['extent'] + 1, + 'element' => array( + 'name' => 'img', + 'attributes' => array( + 'src' => $Link['element']['attributes']['href'], + 'alt' => $Link['element']['handler']['argument'], + ), + 'autobreak' => true, + ), + ); + + $Inline['element']['attributes'] += $Link['element']['attributes']; + + unset($Inline['element']['attributes']['href']); + + return $Inline; + } + + protected function inlineLink($Excerpt) + { + $Element = array( + 'name' => 'a', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => null, + 'destination' => 'elements', + ), + 'nonNestables' => array('Url', 'Link'), + 'attributes' => array( + 'href' => null, + 'title' => null, + ), + ); + + $extent = 0; + + $remainder = $Excerpt['text']; + + if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) { + $Element['handler']['argument'] = $matches[1]; + + $extent += strlen($matches[0]); + + $remainder = substr($remainder, $extent); + } else { + return; + } + + if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) { + $Element['attributes']['href'] = $matches[1]; + + if (isset($matches[2])) { + $Element['attributes']['title'] = substr($matches[2], 1, - 1); + } + + $extent += strlen($matches[0]); + } else { + if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) { + $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument']; + $definition = strtolower($definition); + + $extent += strlen($matches[0]); + } else { + $definition = strtolower($Element['handler']['argument']); + } + + if (! isset($this->DefinitionData['Reference'][$definition])) { + return; + } + + $Definition = $this->DefinitionData['Reference'][$definition]; + + $Element['attributes']['href'] = $Definition['url']; + $Element['attributes']['title'] = $Definition['title']; + } + + return array( + 'extent' => $extent, + 'element' => $Element, + ); + } + + protected function inlineMarkup($Excerpt) + { + if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) { + return; + } + + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) { + return array( + 'element' => array('rawHtml' => $matches[0]), + 'extent' => strlen($matches[0]), + ); + } + } + + protected function inlineSpecialCharacter($Excerpt) + { + if ($Excerpt['text'][1] !== ' ' and strpos($Excerpt['text'], ';') !== false + and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches) + ) { + return array( + 'element' => array('rawHtml' => '&' . $matches[1] . ';'), + 'extent' => strlen($matches[0]), + ); + } + + return; + } + + protected function inlineStrikethrough($Excerpt) + { + if (! isset($Excerpt['text'][1])) { + return; + } + + if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) { + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'del', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) + ), + ); + } + } + + protected function inlineUrl($Excerpt) + { + if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') { + return; + } + + if (strpos($Excerpt['context'], 'http') !== false + and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE) + ) { + $url = $matches[0][0]; + + $Inline = array( + 'extent' => strlen($matches[0][0]), + 'position' => $matches[0][1], + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + + return $Inline; + } + } + + protected function inlineUrlTag($Excerpt) + { + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) { + $url = $matches[1]; + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $url, + 'attributes' => array( + 'href' => $url, + ), + ), + ); + } + } + + # ~ + + protected function unmarkedText($text) + { + $Inline = $this->inlineText($text); + return $this->element($Inline['element']); + } + + # + # Handlers + # + + protected function handle(array $Element) + { + if (isset($Element['handler'])) { + if (!isset($Element['nonNestables'])) { + $Element['nonNestables'] = array(); + } + + if (is_string($Element['handler'])) { + $function = $Element['handler']; + $argument = $Element['text']; + unset($Element['text']); + $destination = 'rawHtml'; + } else { + $function = $Element['handler']['function']; + $argument = $Element['handler']['argument']; + $destination = $Element['handler']['destination']; + } + + $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']); + + if ($destination === 'handler') { + $Element = $this->handle($Element); + } + + unset($Element['handler']); + } + + return $Element; + } + + protected function handleElementRecursive(array $Element) + { + return $this->elementApplyRecursive(array($this, 'handle'), $Element); + } + + protected function handleElementsRecursive(array $Elements) + { + return $this->elementsApplyRecursive(array($this, 'handle'), $Elements); + } + + protected function elementApplyRecursive($closure, array $Element) + { + $Element = call_user_func($closure, $Element); + + if (isset($Element['elements'])) { + $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']); + } elseif (isset($Element['element'])) { + $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']); + } + + return $Element; + } + + protected function elementApplyRecursiveDepthFirst($closure, array $Element) + { + if (isset($Element['elements'])) { + $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']); + } elseif (isset($Element['element'])) { + $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']); + } + + $Element = call_user_func($closure, $Element); + + return $Element; + } + + protected function elementsApplyRecursive($closure, array $Elements) + { + foreach ($Elements as &$Element) { + $Element = $this->elementApplyRecursive($closure, $Element); + } + + return $Elements; + } + + protected function elementsApplyRecursiveDepthFirst($closure, array $Elements) + { + foreach ($Elements as &$Element) { + $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element); + } + + return $Elements; + } + + protected function element(array $Element) + { + if ($this->safeMode) { + $Element = $this->sanitiseElement($Element); + } + + # identity map if element has no handler + $Element = $this->handle($Element); + + $hasName = isset($Element['name']); + + $markup = ''; + + if ($hasName) { + $markup .= '<' . $Element['name']; + + if (isset($Element['attributes'])) { + foreach ($Element['attributes'] as $name => $value) { + if ($value === null) { + continue; + } + + $markup .= " $name=\"".self::escape($value).'"'; + } + } + } + + $permitRawHtml = false; + + if (isset($Element['text'])) { + $text = $Element['text']; + } + // very strongly consider an alternative if you're writing an + // extension + elseif (isset($Element['rawHtml'])) { + $text = $Element['rawHtml']; + + $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; + $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; + } + + $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']); + + if ($hasContent) { + $markup .= $hasName ? '>' : ''; + + if (isset($Element['elements'])) { + $markup .= $this->elements($Element['elements']); + } elseif (isset($Element['element'])) { + $markup .= $this->element($Element['element']); + } else { + if (!$permitRawHtml) { + $markup .= self::escape($text, true); + } else { + $markup .= $text; + } + } + + $markup .= $hasName ? '' : ''; + } elseif ($hasName) { + $markup .= ' />'; + } + + return $markup; + } + + protected function elements(array $Elements) + { + $markup = ''; + + $autoBreak = true; + + foreach ($Elements as $Element) { + if (empty($Element)) { + continue; + } + + $autoBreakNext = ( + isset($Element['autobreak']) + ? $Element['autobreak'] : isset($Element['name']) + ); + // (autobreak === false) covers both sides of an element + $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext; + + $markup .= ($autoBreak ? "\n" : '') . $this->element($Element); + $autoBreak = $autoBreakNext; + } + + $markup .= $autoBreak ? "\n" : ''; + + return $markup; + } + + # ~ + + protected function li($lines) + { + $Elements = $this->linesElements($lines); + + if (! in_array('', $lines) + and isset($Elements[0]) and isset($Elements[0]['name']) + and $Elements[0]['name'] === 'p' + ) { + unset($Elements[0]['name']); + } + + return $Elements; + } + + # + # AST Convenience + # + + /** + * Replace occurrences $regexp with $Elements in $text. Return an array of + * elements representing the replacement. + */ + protected static function pregReplaceElements($regexp, $Elements, $text) + { + $newElements = array(); + + while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) { + $offset = $matches[0][1]; + $before = substr($text, 0, $offset); + $after = substr($text, $offset + strlen($matches[0][0])); + + $newElements[] = array('text' => $before); + + foreach ($Elements as $Element) { + $newElements[] = $Element; + } + + $text = $after; + } + + $newElements[] = array('text' => $text); + + return $newElements; + } + + # + # Deprecated Methods + # + + public function parse($text) + { + $markup = $this->text($text); + + return $markup; + } + + protected function sanitiseElement(array $Element) + { + static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; + static $safeUrlNameToAtt = array( + 'a' => 'href', + 'img' => 'src', + ); + + if (! isset($Element['name'])) { + unset($Element['attributes']); + return $Element; + } + + if (isset($safeUrlNameToAtt[$Element['name']])) { + $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); + } + + if (! empty($Element['attributes'])) { + foreach ($Element['attributes'] as $att => $val) { + # filter out badly parsed attribute + if (! preg_match($goodAttribute, $att)) { + unset($Element['attributes'][$att]); + } + # dump onevent attribute + elseif (self::striAtStart($att, 'on')) { + unset($Element['attributes'][$att]); + } + } + } + + return $Element; + } + + protected function filterUnsafeUrlInAttribute(array $Element, $attribute) + { + foreach ($this->safeLinksWhitelist as $scheme) { + if (self::striAtStart($Element['attributes'][$attribute], $scheme)) { + return $Element; + } + } + + $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); + + return $Element; + } + + # + # Static Methods + # + + protected static function escape($text, $allowQuotes = false) + { + return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); + } + + protected static function striAtStart($string, $needle) + { + $len = strlen($needle); + + if ($len > strlen($string)) { + return false; + } else { + return strtolower(substr($string, 0, $len)) === strtolower($needle); + } + } + + public static function instance($name = 'default') + { + if (isset(self::$instances[$name])) { + return self::$instances[$name]; + } + + $instance = new static(); + + self::$instances[$name] = $instance; + + return $instance; + } + + private static $instances = array(); + + # + # Fields + # + + protected $DefinitionData; + + # + # Read-Only + + protected $specialCharacters = array( + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~' + ); + + protected $StrongRegex = array( + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us', + ); + + protected $EmRegex = array( + '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', + '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', + ); + + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+'; + + protected $voidElements = array( + 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', + ); + + protected $textLevelElements = array( + 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', + 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', + 'i', 'rp', 'del', 'code', 'strike', 'marquee', + 'q', 'rt', 'ins', 'font', 'strong', + 's', 'tt', 'kbd', 'mark', + 'u', 'xm', 'sub', 'nobr', + 'sup', 'ruby', + 'var', 'span', + 'wbr', 'time', + ); +} diff --git a/kirby/i18n/rules/LICENSE b/kirby/i18n/rules/LICENSE new file mode 100644 index 0000000..36c3036 --- /dev/null +++ b/kirby/i18n/rules/LICENSE @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2012-217 Florian Eckerstorfer + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/kirby/i18n/rules/ar.json b/kirby/i18n/rules/ar.json new file mode 100755 index 0000000..e46915f --- /dev/null +++ b/kirby/i18n/rules/ar.json @@ -0,0 +1,30 @@ +{ + "أ" : "a", + "ب" : "b", + "ت" : "t", + "ث" : "th", + "ج" : "g", + "ح" : "h", + "خ" : "kh", + "د" : "d", + "ذ" : "th", + "ر" : "r", + "ز" : "z", + "س" : "s", + "ش" : "sh", + "ص" : "s", + "ض" : "d", + "ط" : "t", + "ظ" : "th", + "ع" : "aa", + "غ" : "gh", + "ف" : "f", + "ق" : "k", + "ك" : "k", + "ل" : "l", + "م" : "m", + "ن" : "n", + "ه" : "h", + "و" : "o", + "ي" : "y" +} diff --git a/kirby/i18n/rules/az.json b/kirby/i18n/rules/az.json new file mode 100755 index 0000000..ad6e2a9 --- /dev/null +++ b/kirby/i18n/rules/az.json @@ -0,0 +1,16 @@ +{ + "Ə": "E", + "Ç": "C", + "Ğ": "G", + "İ": "I", + "Ş": "S", + "Ö": "O", + "Ü": "U", + "ə": "e", + "ç": "c", + "ğ": "g", + "ı": "i", + "ş": "s", + "ö": "o", + "ü": "u" +} diff --git a/kirby/i18n/rules/bg.json b/kirby/i18n/rules/bg.json new file mode 100755 index 0000000..4c45ca1 --- /dev/null +++ b/kirby/i18n/rules/bg.json @@ -0,0 +1,65 @@ +{ + "А": "A", + "Б": "B", + "В": "V", + "Г": "G", + "Д": "D", + "Е": "E", + "Ж": "J", + "З": "Z", + "И": "I", + "Й": "Y", + "К": "K", + "Л": "L", + "М": "M", + "Н": "N", + "О": "O", + "П": "P", + "Р": "R", + "С": "S", + "Т": "T", + "У": "U", + "Ф": "F", + "Х": "H", + "Ц": "Ts", + "Ч": "Ch", + "Ш": "Sh", + "Щ": "Sht", + "Ъ": "A", + "Ь": "I", + "Ю": "Iu", + "Я": "Ia", + "а": "a", + "б": "b", + "в": "v", + "г": "g", + "д": "d", + "е": "e", + "ж": "j", + "з": "z", + "и": "i", + "й": "y", + "к": "k", + "л": "l", + "м": "m", + "н": "n", + "о": "o", + "п": "p", + "р": "r", + "с": "s", + "т": "t", + "у": "u", + "ф": "f", + "х": "h", + "ц": "ts", + "ч": "ch", + "ш": "sh", + "щ": "sht", + "ъ": "a", + "ь": "i", + "ю": "iu", + "я": "ia", + "ия": "ia", + "йо": "iо", + "ьо": "io" +} diff --git a/kirby/i18n/rules/cs.json b/kirby/i18n/rules/cs.json new file mode 100755 index 0000000..549f805 --- /dev/null +++ b/kirby/i18n/rules/cs.json @@ -0,0 +1,20 @@ +{ + "Č": "C", + "Ď": "D", + "Ě": "E", + "Ň": "N", + "Ř": "R", + "Š": "S", + "Ť": "T", + "Ů": "U", + "Ž": "Z", + "č": "c", + "ď": "d", + "ě": "e", + "ň": "n", + "ř": "r", + "š": "s", + "ť": "t", + "ů": "u", + "ž": "z" +} diff --git a/kirby/i18n/rules/da.json b/kirby/i18n/rules/da.json new file mode 100755 index 0000000..b88c17c --- /dev/null +++ b/kirby/i18n/rules/da.json @@ -0,0 +1,10 @@ +{ + "Æ": "Ae", + "æ": "ae", + "Ø": "Oe", + "ø": "oe", + "Å": "Aa", + "å": "aa", + "É": "E", + "é": "e" +} diff --git a/kirby/i18n/rules/de.json b/kirby/i18n/rules/de.json new file mode 100755 index 0000000..881b68c --- /dev/null +++ b/kirby/i18n/rules/de.json @@ -0,0 +1,9 @@ +{ + "Ä": "AE", + "Ö": "OE", + "Ü": "UE", + "ß": "ss", + "ä": "ae", + "ö": "oe", + "ü": "ue" +} diff --git a/kirby/i18n/rules/el.json b/kirby/i18n/rules/el.json new file mode 100755 index 0000000..767a223 --- /dev/null +++ b/kirby/i18n/rules/el.json @@ -0,0 +1,111 @@ +{ + "ΑΥ": "AU", + "Αυ": "Au", + "ΟΥ": "OU", + "Ου": "Ou", + "ΕΥ": "EU", + "Ευ": "Eu", + "ΕΙ": "I", + "Ει": "I", + "ΟΙ": "I", + "Οι": "I", + "ΥΙ": "I", + "Υι": "I", + "ΑΎ": "AU", + "Αύ": "Au", + "ΟΎ": "OU", + "Ού": "Ou", + "ΕΎ": "EU", + "Εύ": "Eu", + "ΕΊ": "I", + "Εί": "I", + "ΟΊ": "I", + "Οί": "I", + "ΎΙ": "I", + "Ύι": "I", + "ΥΊ": "I", + "Υί": "I", + "αυ": "au", + "ου": "ou", + "ευ": "eu", + "ει": "i", + "οι": "i", + "υι": "i", + "αύ": "au", + "ού": "ou", + "εύ": "eu", + "εί": "i", + "οί": "i", + "ύι": "i", + "υί": "i", + "Α": "A", + "Β": "V", + "Γ": "G", + "Δ": "D", + "Ε": "E", + "Ζ": "Z", + "Η": "I", + "Θ": "Th", + "Ι": "I", + "Κ": "K", + "Λ": "L", + "Μ": "M", + "Ν": "N", + "Ξ": "X", + "Ο": "O", + "Π": "P", + "Ρ": "R", + "Σ": "S", + "Τ": "T", + "Υ": "I", + "Φ": "F", + "Χ": "Ch", + "Ψ": "Ps", + "Ω": "O", + "Ά": "A", + "Έ": "E", + "Ή": "I", + "Ί": "I", + "Ό": "O", + "Ύ": "I", + "Ϊ": "I", + "Ϋ": "I", + "ϒ": "I", + "α": "a", + "β": "v", + "γ": "g", + "δ": "d", + "ε": "e", + "ζ": "z", + "η": "i", + "θ": "th", + "ι": "i", + "κ": "k", + "λ": "l", + "μ": "m", + "ν": "n", + "ξ": "x", + "ο": "o", + "π": "p", + "ρ": "r", + "ς": "s", + "σ": "s", + "τ": "t", + "υ": "i", + "φ": "f", + "χ": "ch", + "ψ": "ps", + "ω": "o", + "ά": "a", + "έ": "e", + "ή": "i", + "ί": "i", + "ό": "o", + "ύ": "i", + "ϊ": "i", + "ϋ": "i", + "ΰ": "i", + "ώ": "o", + "ϐ": "v", + "ϑ": "th" +} diff --git a/kirby/i18n/rules/eo.json b/kirby/i18n/rules/eo.json new file mode 100755 index 0000000..9a4e658 --- /dev/null +++ b/kirby/i18n/rules/eo.json @@ -0,0 +1,14 @@ +{ + "ĉ": "cx", + "ĝ": "gx", + "ĥ": "hx", + "ĵ": "jx", + "ŝ": "sx", + "ŭ": "ux", + "Ĉ": "CX", + "Ĝ": "GX", + "Ĥ": "HX", + "Ĵ": "JX", + "Ŝ": "SX", + "Ŭ": "UX" +} diff --git a/kirby/i18n/rules/et.json b/kirby/i18n/rules/et.json new file mode 100755 index 0000000..fcea469 --- /dev/null +++ b/kirby/i18n/rules/et.json @@ -0,0 +1,14 @@ +{ + "Š": "S", + "Ž": "Z", + "Õ": "O", + "Ä": "A", + "Ö": "O", + "Ü": "U", + "š": "s", + "ž": "z", + "õ": "o", + "ä": "a", + "ö": "o", + "ü": "u" +} \ No newline at end of file diff --git a/kirby/i18n/rules/fa.json b/kirby/i18n/rules/fa.json new file mode 100755 index 0000000..0448016 --- /dev/null +++ b/kirby/i18n/rules/fa.json @@ -0,0 +1,36 @@ +{ + "آ" : "A", + "ا" : "a", + "ب" : "b", + "پ" : "p", + "ت" : "t", + "ث" : "th", + "ج" : "j", + "چ" : "ch", + "ح" : "h", + "خ" : "kh", + "د" : "d", + "ذ" : "th", + "ر" : "r", + "ز" : "z", + "ژ" : "zh", + "س" : "s", + "ش" : "sh", + "ص" : "s", + "ض" : "z", + "ط" : "t", + "ظ" : "z", + "ع" : "a", + "غ" : "gh", + "ف" : "f", + "ق" : "g", + "ك" : "k", + "ک" : "k", + "گ" : "g", + "ل" : "l", + "م" : "m", + "ن" : "n", + "و" : "o", + "ه" : "h", + "ی" : "y" +} diff --git a/kirby/i18n/rules/fi.json b/kirby/i18n/rules/fi.json new file mode 100755 index 0000000..fd35423 --- /dev/null +++ b/kirby/i18n/rules/fi.json @@ -0,0 +1,6 @@ +{ + "Ä": "A", + "Ö": "O", + "ä": "a", + "ö": "o" +} diff --git a/kirby/i18n/rules/fr.json b/kirby/i18n/rules/fr.json new file mode 100755 index 0000000..29c94b9 --- /dev/null +++ b/kirby/i18n/rules/fr.json @@ -0,0 +1,34 @@ +{ + "À": "A", + "Â": "A", + "Æ": "AE", + "Ç": "C", + "É": "E", + "È": "E", + "Ê": "E", + "Ë": "E", + "Ï": "I", + "Î": "I", + "Ô": "O", + "Œ": "OE", + "Ù": "U", + "Û": "U", + "Ü": "U", + "à": "a", + "â": "a", + "æ": "ae", + "ç": "c", + "é": "e", + "è": "e", + "ê": "e", + "ë": "e", + "ï": "i", + "î": "i", + "ô": "o", + "œ": "oe", + "ù": "u", + "û": "u", + "ü": "u", + "ÿ": "y", + "Ÿ": "Y" +} diff --git a/kirby/i18n/rules/hi.json b/kirby/i18n/rules/hi.json new file mode 100755 index 0000000..f653f15 --- /dev/null +++ b/kirby/i18n/rules/hi.json @@ -0,0 +1,66 @@ +{ + "अ": "a", + "आ": "aa", + "ए": "e", + "ई": "ii", + "ऍ": "ei", + "ऎ": "ae", + "ऐ": "ai", + "इ": "i", + "ओ": "o", + "ऑ": "oi", + "ऒ": "oii", + "ऊ": "uu", + "औ": "ou", + "उ": "u", + "ब": "B", + "भ": "Bha", + "च": "Ca", + "छ": "Chha", + "ड": "Da", + "ढ": "Dha", + "फ": "Fa", + "फ़": "Fi", + "ग": "Ga", + "घ": "Gha", + "ग़": "Ghi", + "ह": "Ha", + "ज": "Ja", + "झ": "Jha", + "क": "Ka", + "ख": "Kha", + "ख़": "Khi", + "ल": "L", + "ळ": "Li", + "ऌ": "Li", + "ऴ": "Lii", + "ॡ": "Lii", + "म": "Ma", + "न": "Na", + "ङ": "Na", + "ञ": "Nia", + "ण": "Nae", + "ऩ": "Ni", + "ॐ": "oms", + "प": "Pa", + "क़": "Qi", + "र": "Ra", + "ऋ": "Ri", + "ॠ": "Ri", + "ऱ": "Ri", + "स": "Sa", + "श": "Sha", + "ष": "Shha", + "ट": "Ta", + "त": "Ta", + "ठ": "Tha", + "द": "Tha", + "थ": "Tha", + "ध": "Thha", + "ड़": "ugDha", + "ढ़": "ugDhha", + "व": "Va", + "य": "Ya", + "य़": "Yi", + "ज़": "Za" +} diff --git a/kirby/i18n/rules/hr.json b/kirby/i18n/rules/hr.json new file mode 100755 index 0000000..bf2b10d --- /dev/null +++ b/kirby/i18n/rules/hr.json @@ -0,0 +1,12 @@ +{ + "Č": "C", + "Ć": "C", + "Ž": "Z", + "Š": "S", + "Đ": "Dj", + "č": "c", + "ć": "c", + "ž": "z", + "š": "s", + "đ": "dj" +} \ No newline at end of file diff --git a/kirby/i18n/rules/hu.json b/kirby/i18n/rules/hu.json new file mode 100755 index 0000000..2bb2f3a --- /dev/null +++ b/kirby/i18n/rules/hu.json @@ -0,0 +1,20 @@ +{ + "Á": "a", + "É": "e", + "Í": "i", + "Ó": "o", + "Ö": "o", + "Ő": "o", + "Ú": "u", + "Ü": "u", + "Ű": "u", + "á": "a", + "é": "e", + "í": "i", + "ó": "o", + "ö": "o", + "ő": "o", + "ú": "u", + "ü": "u", + "ű": "u" +} diff --git a/kirby/i18n/rules/hy.json b/kirby/i18n/rules/hy.json new file mode 100755 index 0000000..08188e6 --- /dev/null +++ b/kirby/i18n/rules/hy.json @@ -0,0 +1,79 @@ +{ + "Ա": "A", + "Բ": "B", + "Գ": "G", + "Դ": "D", + "Ե": "E", + "Զ": "Z", + "Է": "E", + "Ը": "Y", + "Թ": "Th", + "Ժ": "Zh", + "Ի": "I", + "Լ": "L", + "Խ": "Kh", + "Ծ": "Ts", + "Կ": "K", + "Հ": "H", + "Ձ": "Dz", + "Ղ": "Gh", + "Ճ": "Tch", + "Մ": "M", + "Յ": "Y", + "Ն": "N", + "Շ": "Sh", + "Ո": "Vo", + "Չ": "Ch", + "Պ": "P", + "Ջ": "J", + "Ռ": "R", + "Ս": "S", + "Վ": "V", + "Տ": "T", + "Ր": "R", + "Ց": "C", + "Ւ": "u", + "Փ": "Ph", + "Ք": "Q", + "և": "ev", + "Օ": "O", + "Ֆ": "F", + "ա": "a", + "բ": "b", + "գ": "g", + "դ": "d", + "ե": "e", + "զ": "z", + "է": "e", + "ը": "y", + "թ": "th", + "ժ": "zh", + "ի": "i", + "լ": "l", + "խ": "kh", + "ծ": "ts", + "կ": "k", + "հ": "h", + "ձ": "dz", + "ղ": "gh", + "ճ": "tch", + "մ": "m", + "յ": "y", + "ն": "n", + "շ": "sh", + "ո": "vo", + "չ": "ch", + "պ": "p", + "ջ": "j", + "ռ": "r", + "ս": "s", + "վ": "v", + "տ": "t", + "ր": "r", + "ց": "c", + "ւ": "u", + "փ": "ph", + "ք": "q", + "օ": "o", + "ֆ": "f" +} diff --git a/kirby/i18n/rules/it.json b/kirby/i18n/rules/it.json new file mode 100755 index 0000000..647c2cf --- /dev/null +++ b/kirby/i18n/rules/it.json @@ -0,0 +1,13 @@ +{ + "À": "a", + "È": "e", + "Ì": "i", + "Ò": "o", + "Ù": "u", + "à": "a", + "é": "e", + "è": "e", + "ì": "i", + "ò": "o", + "ù": "u" +} diff --git a/kirby/i18n/rules/ja.json b/kirby/i18n/rules/ja.json new file mode 100644 index 0000000..b313813 --- /dev/null +++ b/kirby/i18n/rules/ja.json @@ -0,0 +1,172 @@ +{ + "きゃ": "kya", + "しゃ": "sha", + "ちゃ": "cha", + "にゃ": "nya", + "ひゃ": "hya", + "みゃ": "mya", + "りゃ": "rya", + "ぎゃ": "gya", + "じゃ": "ja", + "ぢゃ": "ja", + "びゃ": "bya", + "ぴゃ": "pya", + + "きゅ": "kyu", + "しゅ": "shu", + "ちゅ": "chu", + "にゅ": "nyu", + "ひゅ": "hyu", + "みゅ": "myu", + "りゅ": "ryu", + "ぎゅ": "gyu", + "じゅ": "ju", + "ぢゅ": "ju", + "びゅ": "byu", + "ぴゅ": "pyu", + + "きょ": "kyo", + "しょ": "sho", + "ちょ": "cho", + "にょ": "nyo", + "ひょ": "hyo", + "みょ": "myo", + "りょ": "ryo", + "ぎょ": "gyo", + "じょ": "jo", + "ぢょ": "jo", + "びょ": "byo", + "ぴょ": "pyo", + + "あ": "a", + "ア": "a", + "か": "ka", + "カ": "ka", + "さ": "sa", + "サ": "sa", + "た": "ta", + "タ": "ta", + "な": "na", + "ナ": "na", + "は": "ha", + "ハ": "ha", + "ま": "ma", + "マ": "ma", + "や": "ya", + "ヤ": "ya", + "ら": "ra", + "ラ": "ra", + "わ": "wa", + "ワ": "wa", + "が": "ga", + "ざ": "za", + "ザ": "za", + "だ": "da", + "ば": "ba", + "ぱ": "pa", + + "い": "i", + "イ": "i", + "き": "ki", + "キ": "ki", + "し": "shi", + "シ": "shi", + "ち": "chi", + "チ": "chi", + "に": "ni", + "ニ": "ni", + "ひ": "hi", + "ヒ": "hi", + "み": "mi", + "ミ": "mi", + "り": "ri", + "リ": "ri", + "ゐ": "wi", + "ヰ": "wi", + "ぎ": "gi", + "じ": "dji", + "ぢ": "ji", + "び": "bi", + "ぴ": "pi", + + "う": "u", + "ウ": "u", + "く": "ku", + "ク": "ku", + "す": "su", + "ス": "su", + "つ": "tsu", + "ツ": "tsu", + "ぬ": "nu", + "ヌ": "nu", + "ふ": "fu", + "フ": "fu", + "む": "mu", + "ム": "mu", + "ゆ": "yu", + "ユ": "yu", + "る": "ru", + "ル": "ru", + "ぐ": "gu", + "ず": "zu", + "づ": "dzu", + "ぶ": "bu", + "ぷ": "pu", + "プ": "pu", + "ズ": "zu", + "グ": "gu", + + "え": "e", + "エ": "e", + "け": "ke", + "ケ": "ke", + "せ": "se", + "セ": "se", + "て": "te", + "テ": "te", + "ね": "ne", + "ネ": "ne", + "へ": "he", + "ヘ": "he", + "め": "me", + "メ": "me", + "れ": "re", + "レ": "re", + "ゑ": "we", + "ヱ": "we", + "げ": "ge", + "ぜ": "ze", + "で": "de", + "べ": "be", + "ぺ": "pe", + + "お": "o", + "オ": "o", + "こ": "ko", + "コ": "ko", + "そ": "so", + "ソ": "so", + "と": "to", + "ト": "to", + "の": "no", + "ノ": "no", + "ほ": "ho", + "ホ": "ho", + "も": "mo", + "モ": "mo", + "よ": "yo", + "ヨ": "yo", + "ろ": "ro", + "ロ": "ro", + "を": "wo", + "ヲ": "wo", + "ん": "n", + "ン": "n", + "ご": "go", + "ぞ": "zo", + "ど": "do", + "ド": "do", + "ぼ": "bo", + "ポ": "po", + "ぽ": "po" +} diff --git a/kirby/i18n/rules/ka.json b/kirby/i18n/rules/ka.json new file mode 100755 index 0000000..2c63573 --- /dev/null +++ b/kirby/i18n/rules/ka.json @@ -0,0 +1,35 @@ +{ + "ა": "a", + "ბ": "b", + "გ": "g", + "დ": "d", + "ე": "e", + "ვ": "v", + "ზ": "z", + "თ": "t", + "ი": "i", + "კ": "k", + "ლ": "l", + "მ": "m", + "ნ": "n", + "ო": "o", + "პ": "p", + "ჟ": "zh", + "რ": "r", + "ს": "s", + "ტ": "t", + "უ": "u", + "ფ": "f", + "ქ": "k", + "ღ": "gh", + "ყ": "q", + "შ": "sh", + "ჩ": "ch", + "ც": "ts", + "ძ": "dz", + "წ": "ts", + "ჭ": "ch", + "ხ": "kh", + "ჯ": "j", + "ჰ": "h" +} diff --git a/kirby/i18n/rules/ko.json b/kirby/i18n/rules/ko.json new file mode 100644 index 0000000..8dad2c0 --- /dev/null +++ b/kirby/i18n/rules/ko.json @@ -0,0 +1,11174 @@ +{ + "가": "ga", + "각": "gak", + "갂": "gakk", + "갃": "gak", + "간": "gan", + "갅": "gan", + "갆": "gan", + "갇": "gat", + "갈": "gal", + "갉": "gak", + "갊": "gam", + "갋": "gap", + "갌": "gat", + "갍": "gat", + "갎": "gap", + "갏": "gal", + "감": "gam", + "갑": "gap", + "값": "gap", + "갓": "gat", + "갔": "gat", + "강": "gang", + "갖": "gat", + "갗": "gat", + "갘": "gak", + "같": "gat", + "갚": "gap", + "갛": "gat", + "개": "gae", + "객": "gaek", + "갞": "gaekk", + "갟": "gaek", + "갠": "gaen", + "갡": "gaen", + "갢": "gaen", + "갣": "gaet", + "갤": "gael", + "갥": "gaek", + "갦": "gaem", + "갧": "gaep", + "갨": "gaet", + "갩": "gaet", + "갪": "gaep", + "갫": "gael", + "갬": "gaem", + "갭": "gaep", + "갮": "gaep", + "갯": "gaet", + "갰": "gaet", + "갱": "gaeng", + "갲": "gaet", + "갳": "gaet", + "갴": "gaek", + "갵": "gaet", + "갶": "gaep", + "갷": "gaet", + "갸": "gya", + "갹": "gyak", + "갺": "gyakk", + "갻": "gyak", + "갼": "gyan", + "갽": "gyan", + "갾": "gyan", + "갿": "gyat", + "걀": "gyal", + "걁": "gyak", + "걂": "gyam", + "걃": "gyap", + "걄": "gyat", + "걅": "gyat", + "걆": "gyap", + "걇": "gyal", + "걈": "gyam", + "걉": "gyap", + "걊": "gyap", + "걋": "gyat", + "걌": "gyat", + "걍": "gyang", + "걎": "gyat", + "걏": "gyat", + "걐": "gyak", + "걑": "gyat", + "걒": "gyap", + "걓": "gyat", + "걔": "gyae", + "걕": "gyaek", + "걖": "gyaekk", + "걗": "gyaek", + "걘": "gyaen", + "걙": "gyaen", + "걚": "gyaen", + "걛": "gyaet", + "걜": "gyael", + "걝": "gyaek", + "걞": "gyaem", + "걟": "gyaep", + "걠": "gyaet", + "걡": "gyaet", + "걢": "gyaep", + "걣": "gyael", + "걤": "gyaem", + "걥": "gyaep", + "걦": "gyaep", + "걧": "gyaet", + "걨": "gyaet", + "걩": "gyaeng", + "걪": "gyaet", + "걫": "gyaet", + "걬": "gyaek", + "걭": "gyaet", + "걮": "gyaep", + "걯": "gyaet", + "거": "geo", + "걱": "geok", + "걲": "geokk", + "걳": "geok", + "건": "geon", + "걵": "geon", + "걶": "geon", + "걷": "geot", + "걸": "geol", + "걹": "geok", + "걺": "geom", + "걻": "geop", + "걼": "geot", + "걽": "geot", + "걾": "geop", + "걿": "geol", + "검": "geom", + "겁": "geop", + "겂": "geop", + "것": "geot", + "겄": "geot", + "겅": "geong", + "겆": "geot", + "겇": "geot", + "겈": "geok", + "겉": "geot", + "겊": "geop", + "겋": "geot", + "게": "ge", + "겍": "gek", + "겎": "gekk", + "겏": "gek", + "겐": "gen", + "겑": "gen", + "겒": "gen", + "겓": "get", + "겔": "gel", + "겕": "gek", + "겖": "gem", + "겗": "gep", + "겘": "get", + "겙": "get", + "겚": "gep", + "겛": "gel", + "겜": "gem", + "겝": "gep", + "겞": "gep", + "겟": "get", + "겠": "get", + "겡": "geng", + "겢": "get", + "겣": "get", + "겤": "gek", + "겥": "get", + "겦": "gep", + "겧": "get", + "겨": "gyeo", + "격": "gyeok", + "겪": "gyeokk", + "겫": "gyeok", + "견": "gyeon", + "겭": "gyeon", + "겮": "gyeon", + "겯": "gyeot", + "결": "gyeol", + "겱": "gyeok", + "겲": "gyeom", + "겳": "gyeop", + "겴": "gyeot", + "겵": "gyeot", + "겶": "gyeop", + "겷": "gyeol", + "겸": "gyeom", + "겹": "gyeop", + "겺": "gyeop", + "겻": "gyeot", + "겼": "gyeot", + "경": "gyeong", + "겾": "gyeot", + "겿": "gyeot", + "곀": "gyeok", + "곁": "gyeot", + "곂": "gyeop", + "곃": "gyeot", + "계": "gye", + "곅": "gyek", + "곆": "gyekk", + "곇": "gyek", + "곈": "gyen", + "곉": "gyen", + "곊": "gyen", + "곋": "gyet", + "곌": "gyel", + "곍": "gyek", + "곎": "gyem", + "곏": "gyep", + "곐": "gyet", + "곑": "gyet", + "곒": "gyep", + "곓": "gyel", + "곔": "gyem", + "곕": "gyep", + "곖": "gyep", + "곗": "gyet", + "곘": "gyet", + "곙": "gyeng", + "곚": "gyet", + "곛": "gyet", + "곜": "gyek", + "곝": "gyet", + "곞": "gyep", + "곟": "gyet", + "고": "go", + "곡": "gok", + "곢": "gokk", + "곣": "gok", + "곤": "gon", + "곥": "gon", + "곦": "gon", + "곧": "got", + "골": "gol", + "곩": "gok", + "곪": "gom", + "곫": "gop", + "곬": "got", + "곭": "got", + "곮": "gop", + "곯": "gol", + "곰": "gom", + "곱": "gop", + "곲": "gop", + "곳": "got", + "곴": "got", + "공": "gong", + "곶": "got", + "곷": "got", + "곸": "gok", + "곹": "got", + "곺": "gop", + "곻": "got", + "과": "gwa", + "곽": "gwak", + "곾": "gwakk", + "곿": "gwak", + "관": "gwan", + "괁": "gwan", + "괂": "gwan", + "괃": "gwat", + "괄": "gwal", + "괅": "gwak", + "괆": "gwam", + "괇": "gwap", + "괈": "gwat", + "괉": "gwat", + "괊": "gwap", + "괋": "gwal", + "괌": "gwam", + "괍": "gwap", + "괎": "gwap", + "괏": "gwat", + "괐": "gwat", + "광": "gwang", + "괒": "gwat", + "괓": "gwat", + "괔": "gwak", + "괕": "gwat", + "괖": "gwap", + "괗": "gwat", + "괘": "gwae", + "괙": "gwaek", + "괚": "gwaekk", + "괛": "gwaek", + "괜": "gwaen", + "괝": "gwaen", + "괞": "gwaen", + "괟": "gwaet", + "괠": "gwael", + "괡": "gwaek", + "괢": "gwaem", + "괣": "gwaep", + "괤": "gwaet", + "괥": "gwaet", + "괦": "gwaep", + "괧": "gwael", + "괨": "gwaem", + "괩": "gwaep", + "괪": "gwaep", + "괫": "gwaet", + "괬": "gwaet", + "괭": "gwaeng", + "괮": "gwaet", + "괯": "gwaet", + "괰": "gwaek", + "괱": "gwaet", + "괲": "gwaep", + "괳": "gwaet", + "괴": "goe", + "괵": "goek", + "괶": "goekk", + "괷": "goek", + "괸": "goen", + "괹": "goen", + "괺": "goen", + "괻": "goet", + "괼": "goel", + "괽": "goek", + "괾": "goem", + "괿": "goep", + "굀": "goet", + "굁": "goet", + "굂": "goep", + "굃": "goel", + "굄": "goem", + "굅": "goep", + "굆": "goep", + "굇": "goet", + "굈": "goet", + "굉": "goeng", + "굊": "goet", + "굋": "goet", + "굌": "goek", + "굍": "goet", + "굎": "goep", + "굏": "goet", + "교": "gyo", + "굑": "gyok", + "굒": "gyokk", + "굓": "gyok", + "굔": "gyon", + "굕": "gyon", + "굖": "gyon", + "굗": "gyot", + "굘": "gyol", + "굙": "gyok", + "굚": "gyom", + "굛": "gyop", + "굜": "gyot", + "굝": "gyot", + "굞": "gyop", + "굟": "gyol", + "굠": "gyom", + "굡": "gyop", + "굢": "gyop", + "굣": "gyot", + "굤": "gyot", + "굥": "gyong", + "굦": "gyot", + "굧": "gyot", + "굨": "gyok", + "굩": "gyot", + "굪": "gyop", + "굫": "gyot", + "구": "gu", + "국": "guk", + "굮": "gukk", + "굯": "guk", + "군": "gun", + "굱": "gun", + "굲": "gun", + "굳": "gut", + "굴": "gul", + "굵": "guk", + "굶": "gum", + "굷": "gup", + "굸": "gut", + "굹": "gut", + "굺": "gup", + "굻": "gul", + "굼": "gum", + "굽": "gup", + "굾": "gup", + "굿": "gut", + "궀": "gut", + "궁": "gung", + "궂": "gut", + "궃": "gut", + "궄": "guk", + "궅": "gut", + "궆": "gup", + "궇": "gut", + "궈": "gwo", + "궉": "gwok", + "궊": "gwokk", + "궋": "gwok", + "권": "gwon", + "궍": "gwon", + "궎": "gwon", + "궏": "gwot", + "궐": "gwol", + "궑": "gwok", + "궒": "gwom", + "궓": "gwop", + "궔": "gwot", + "궕": "gwot", + "궖": "gwop", + "궗": "gwol", + "궘": "gwom", + "궙": "gwop", + "궚": "gwop", + "궛": "gwot", + "궜": "gwot", + "궝": "gwong", + "궞": "gwot", + "궟": "gwot", + "궠": "gwok", + "궡": "gwot", + "궢": "gwop", + "궣": "gwot", + "궤": "gwe", + "궥": "gwek", + "궦": "gwekk", + "궧": "gwek", + "궨": "gwen", + "궩": "gwen", + "궪": "gwen", + "궫": "gwet", + "궬": "gwel", + "궭": "gwek", + "궮": "gwem", + "궯": "gwep", + "궰": "gwet", + "궱": "gwet", + "궲": "gwep", + "궳": "gwel", + "궴": "gwem", + "궵": "gwep", + "궶": "gwep", + "궷": "gwet", + "궸": "gwet", + "궹": "gweng", + "궺": "gwet", + "궻": "gwet", + "궼": "gwek", + "궽": "gwet", + "궾": "gwep", + "궿": "gwet", + "귀": "gwi", + "귁": "gwik", + "귂": "gwikk", + "귃": "gwik", + "귄": "gwin", + "귅": "gwin", + "귆": "gwin", + "귇": "gwit", + "귈": "gwil", + "귉": "gwik", + "귊": "gwim", + "귋": "gwip", + "귌": "gwit", + "귍": "gwit", + "귎": "gwip", + "귏": "gwil", + "귐": "gwim", + "귑": "gwip", + "귒": "gwip", + "귓": "gwit", + "귔": "gwit", + "귕": "gwing", + "귖": "gwit", + "귗": "gwit", + "귘": "gwik", + "귙": "gwit", + "귚": "gwip", + "귛": "gwit", + "규": "gyu", + "귝": "gyuk", + "귞": "gyukk", + "귟": "gyuk", + "균": "gyun", + "귡": "gyun", + "귢": "gyun", + "귣": "gyut", + "귤": "gyul", + "귥": "gyuk", + "귦": "gyum", + "귧": "gyup", + "귨": "gyut", + "귩": "gyut", + "귪": "gyup", + "귫": "gyul", + "귬": "gyum", + "귭": "gyup", + "귮": "gyup", + "귯": "gyut", + "귰": "gyut", + "귱": "gyung", + "귲": "gyut", + "귳": "gyut", + "귴": "gyuk", + "귵": "gyut", + "귶": "gyup", + "귷": "gyut", + "그": "geu", + "극": "geuk", + "귺": "geukk", + "귻": "geuk", + "근": "geun", + "귽": "geun", + "귾": "geun", + "귿": "geut", + "글": "geul", + "긁": "geuk", + "긂": "geum", + "긃": "geup", + "긄": "geut", + "긅": "geut", + "긆": "geup", + "긇": "geul", + "금": "geum", + "급": "geup", + "긊": "geup", + "긋": "geut", + "긌": "geut", + "긍": "geung", + "긎": "geut", + "긏": "geut", + "긐": "geuk", + "긑": "geut", + "긒": "geup", + "긓": "geut", + "긔": "geui", + "긕": "geuik", + "긖": "geuikk", + "긗": "geuik", + "긘": "geuin", + "긙": "geuin", + "긚": "geuin", + "긛": "geuit", + "긜": "geuil", + "긝": "geuik", + "긞": "geuim", + "긟": "geuip", + "긠": "geuit", + "긡": "geuit", + "긢": "geuip", + "긣": "geuil", + "긤": "geuim", + "긥": "geuip", + "긦": "geuip", + "긧": "geuit", + "긨": "geuit", + "긩": "geuing", + "긪": "geuit", + "긫": "geuit", + "긬": "geuik", + "긭": "geuit", + "긮": "geuip", + "긯": "geuit", + "기": "gi", + "긱": "gik", + "긲": "gikk", + "긳": "gik", + "긴": "gin", + "긵": "gin", + "긶": "gin", + "긷": "git", + "길": "gil", + "긹": "gik", + "긺": "gim", + "긻": "gip", + "긼": "git", + "긽": "git", + "긾": "gip", + "긿": "gil", + "김": "gim", + "깁": "gip", + "깂": "gip", + "깃": "git", + "깄": "git", + "깅": "ging", + "깆": "git", + "깇": "git", + "깈": "gik", + "깉": "git", + "깊": "gip", + "깋": "git", + "까": "kka", + "깍": "kkak", + "깎": "kkakk", + "깏": "kkak", + "깐": "kkan", + "깑": "kkan", + "깒": "kkan", + "깓": "kkat", + "깔": "kkal", + "깕": "kkak", + "깖": "kkam", + "깗": "kkap", + "깘": "kkat", + "깙": "kkat", + "깚": "kkap", + "깛": "kkal", + "깜": "kkam", + "깝": "kkap", + "깞": "kkap", + "깟": "kkat", + "깠": "kkat", + "깡": "kkang", + "깢": "kkat", + "깣": "kkat", + "깤": "kkak", + "깥": "kkat", + "깦": "kkap", + "깧": "kkat", + "깨": "kkae", + "깩": "kkaek", + "깪": "kkaekk", + "깫": "kkaek", + "깬": "kkaen", + "깭": "kkaen", + "깮": "kkaen", + "깯": "kkaet", + "깰": "kkael", + "깱": "kkaek", + "깲": "kkaem", + "깳": "kkaep", + "깴": "kkaet", + "깵": "kkaet", + "깶": "kkaep", + "깷": "kkael", + "깸": "kkaem", + "깹": "kkaep", + "깺": "kkaep", + "깻": "kkaet", + "깼": "kkaet", + "깽": "kkaeng", + "깾": "kkaet", + "깿": "kkaet", + "꺀": "kkaek", + "꺁": "kkaet", + "꺂": "kkaep", + "꺃": "kkaet", + "꺄": "kkya", + "꺅": "kkyak", + "꺆": "kkyakk", + "꺇": "kkyak", + "꺈": "kkyan", + "꺉": "kkyan", + "꺊": "kkyan", + "꺋": "kkyat", + "꺌": "kkyal", + "꺍": "kkyak", + "꺎": "kkyam", + "꺏": "kkyap", + "꺐": "kkyat", + "꺑": "kkyat", + "꺒": "kkyap", + "꺓": "kkyal", + "꺔": "kkyam", + "꺕": "kkyap", + "꺖": "kkyap", + "꺗": "kkyat", + "꺘": "kkyat", + "꺙": "kkyang", + "꺚": "kkyat", + "꺛": "kkyat", + "꺜": "kkyak", + "꺝": "kkyat", + "꺞": "kkyap", + "꺟": "kkyat", + "꺠": "kkyae", + "꺡": "kkyaek", + "꺢": "kkyaekk", + "꺣": "kkyaek", + "꺤": "kkyaen", + "꺥": "kkyaen", + "꺦": "kkyaen", + "꺧": "kkyaet", + "꺨": "kkyael", + "꺩": "kkyaek", + "꺪": "kkyaem", + "꺫": "kkyaep", + "꺬": "kkyaet", + "꺭": "kkyaet", + "꺮": "kkyaep", + "꺯": "kkyael", + "꺰": "kkyaem", + "꺱": "kkyaep", + "꺲": "kkyaep", + "꺳": "kkyaet", + "꺴": "kkyaet", + "꺵": "kkyaeng", + "꺶": "kkyaet", + "꺷": "kkyaet", + "꺸": "kkyaek", + "꺹": "kkyaet", + "꺺": "kkyaep", + "꺻": "kkyaet", + "꺼": "kkeo", + "꺽": "kkeok", + "꺾": "kkeokk", + "꺿": "kkeok", + "껀": "kkeon", + "껁": "kkeon", + "껂": "kkeon", + "껃": "kkeot", + "껄": "kkeol", + "껅": "kkeok", + "껆": "kkeom", + "껇": "kkeop", + "껈": "kkeot", + "껉": "kkeot", + "껊": "kkeop", + "껋": "kkeol", + "껌": "kkeom", + "껍": "kkeop", + "껎": "kkeop", + "껏": "kkeot", + "껐": "kkeot", + "껑": "kkeong", + "껒": "kkeot", + "껓": "kkeot", + "껔": "kkeok", + "껕": "kkeot", + "껖": "kkeop", + "껗": "kkeot", + "께": "kke", + "껙": "kkek", + "껚": "kkekk", + "껛": "kkek", + "껜": "kken", + "껝": "kken", + "껞": "kken", + "껟": "kket", + "껠": "kkel", + "껡": "kkek", + "껢": "kkem", + "껣": "kkep", + "껤": "kket", + "껥": "kket", + "껦": "kkep", + "껧": "kkel", + "껨": "kkem", + "껩": "kkep", + "껪": "kkep", + "껫": "kket", + "껬": "kket", + "껭": "kkeng", + "껮": "kket", + "껯": "kket", + "껰": "kkek", + "껱": "kket", + "껲": "kkep", + "껳": "kket", + "껴": "kkyeo", + "껵": "kkyeok", + "껶": "kkyeokk", + "껷": "kkyeok", + "껸": "kkyeon", + "껹": "kkyeon", + "껺": "kkyeon", + "껻": "kkyeot", + "껼": "kkyeol", + "껽": "kkyeok", + "껾": "kkyeom", + "껿": "kkyeop", + "꼀": "kkyeot", + "꼁": "kkyeot", + "꼂": "kkyeop", + "꼃": "kkyeol", + "꼄": "kkyeom", + "꼅": "kkyeop", + "꼆": "kkyeop", + "꼇": "kkyeot", + "꼈": "kkyeot", + "꼉": "kkyeong", + "꼊": "kkyeot", + "꼋": "kkyeot", + "꼌": "kkyeok", + "꼍": "kkyeot", + "꼎": "kkyeop", + "꼏": "kkyeot", + "꼐": "kkye", + "꼑": "kkyek", + "꼒": "kkyekk", + "꼓": "kkyek", + "꼔": "kkyen", + "꼕": "kkyen", + "꼖": "kkyen", + "꼗": "kkyet", + "꼘": "kkyel", + "꼙": "kkyek", + "꼚": "kkyem", + "꼛": "kkyep", + "꼜": "kkyet", + "꼝": "kkyet", + "꼞": "kkyep", + "꼟": "kkyel", + "꼠": "kkyem", + "꼡": "kkyep", + "꼢": "kkyep", + "꼣": "kkyet", + "꼤": "kkyet", + "꼥": "kkyeng", + "꼦": "kkyet", + "꼧": "kkyet", + "꼨": "kkyek", + "꼩": "kkyet", + "꼪": "kkyep", + "꼫": "kkyet", + "꼬": "kko", + "꼭": "kkok", + "꼮": "kkokk", + "꼯": "kkok", + "꼰": "kkon", + "꼱": "kkon", + "꼲": "kkon", + "꼳": "kkot", + "꼴": "kkol", + "꼵": "kkok", + "꼶": "kkom", + "꼷": "kkop", + "꼸": "kkot", + "꼹": "kkot", + "꼺": "kkop", + "꼻": "kkol", + "꼼": "kkom", + "꼽": "kkop", + "꼾": "kkop", + "꼿": "kkot", + "꽀": "kkot", + "꽁": "kkong", + "꽂": "kkot", + "꽃": "kkot", + "꽄": "kkok", + "꽅": "kkot", + "꽆": "kkop", + "꽇": "kkot", + "꽈": "kkwa", + "꽉": "kkwak", + "꽊": "kkwakk", + "꽋": "kkwak", + "꽌": "kkwan", + "꽍": "kkwan", + "꽎": "kkwan", + "꽏": "kkwat", + "꽐": "kkwal", + "꽑": "kkwak", + "꽒": "kkwam", + "꽓": "kkwap", + "꽔": "kkwat", + "꽕": "kkwat", + "꽖": "kkwap", + "꽗": "kkwal", + "꽘": "kkwam", + "꽙": "kkwap", + "꽚": "kkwap", + "꽛": "kkwat", + "꽜": "kkwat", + "꽝": "kkwang", + "꽞": "kkwat", + "꽟": "kkwat", + "꽠": "kkwak", + "꽡": "kkwat", + "꽢": "kkwap", + "꽣": "kkwat", + "꽤": "kkwae", + "꽥": "kkwaek", + "꽦": "kkwaekk", + "꽧": "kkwaek", + "꽨": "kkwaen", + "꽩": "kkwaen", + "꽪": "kkwaen", + "꽫": "kkwaet", + "꽬": "kkwael", + "꽭": "kkwaek", + "꽮": "kkwaem", + "꽯": "kkwaep", + "꽰": "kkwaet", + "꽱": "kkwaet", + "꽲": "kkwaep", + "꽳": "kkwael", + "꽴": "kkwaem", + "꽵": "kkwaep", + "꽶": "kkwaep", + "꽷": "kkwaet", + "꽸": "kkwaet", + "꽹": "kkwaeng", + "꽺": "kkwaet", + "꽻": "kkwaet", + "꽼": "kkwaek", + "꽽": "kkwaet", + "꽾": "kkwaep", + "꽿": "kkwaet", + "꾀": "kkoe", + "꾁": "kkoek", + "꾂": "kkoekk", + "꾃": "kkoek", + "꾄": "kkoen", + "꾅": "kkoen", + "꾆": "kkoen", + "꾇": "kkoet", + "꾈": "kkoel", + "꾉": "kkoek", + "꾊": "kkoem", + "꾋": "kkoep", + "꾌": "kkoet", + "꾍": "kkoet", + "꾎": "kkoep", + "꾏": "kkoel", + "꾐": "kkoem", + "꾑": "kkoep", + "꾒": "kkoep", + "꾓": "kkoet", + "꾔": "kkoet", + "꾕": "kkoeng", + "꾖": "kkoet", + "꾗": "kkoet", + "꾘": "kkoek", + "꾙": "kkoet", + "꾚": "kkoep", + "꾛": "kkoet", + "꾜": "kkyo", + "꾝": "kkyok", + "꾞": "kkyokk", + "꾟": "kkyok", + "꾠": "kkyon", + "꾡": "kkyon", + "꾢": "kkyon", + "꾣": "kkyot", + "꾤": "kkyol", + "꾥": "kkyok", + "꾦": "kkyom", + "꾧": "kkyop", + "꾨": "kkyot", + "꾩": "kkyot", + "꾪": "kkyop", + "꾫": "kkyol", + "꾬": "kkyom", + "꾭": "kkyop", + "꾮": "kkyop", + "꾯": "kkyot", + "꾰": "kkyot", + "꾱": "kkyong", + "꾲": "kkyot", + "꾳": "kkyot", + "꾴": "kkyok", + "꾵": "kkyot", + "꾶": "kkyop", + "꾷": "kkyot", + "꾸": "kku", + "꾹": "kkuk", + "꾺": "kkukk", + "꾻": "kkuk", + "꾼": "kkun", + "꾽": "kkun", + "꾾": "kkun", + "꾿": "kkut", + "꿀": "kkul", + "꿁": "kkuk", + "꿂": "kkum", + "꿃": "kkup", + "꿄": "kkut", + "꿅": "kkut", + "꿆": "kkup", + "꿇": "kkul", + "꿈": "kkum", + "꿉": "kkup", + "꿊": "kkup", + "꿋": "kkut", + "꿌": "kkut", + "꿍": "kkung", + "꿎": "kkut", + "꿏": "kkut", + "꿐": "kkuk", + "꿑": "kkut", + "꿒": "kkup", + "꿓": "kkut", + "꿔": "kkwo", + "꿕": "kkwok", + "꿖": "kkwokk", + "꿗": "kkwok", + "꿘": "kkwon", + "꿙": "kkwon", + "꿚": "kkwon", + "꿛": "kkwot", + "꿜": "kkwol", + "꿝": "kkwok", + "꿞": "kkwom", + "꿟": "kkwop", + "꿠": "kkwot", + "꿡": "kkwot", + "꿢": "kkwop", + "꿣": "kkwol", + "꿤": "kkwom", + "꿥": "kkwop", + "꿦": "kkwop", + "꿧": "kkwot", + "꿨": "kkwot", + "꿩": "kkwong", + "꿪": "kkwot", + "꿫": "kkwot", + "꿬": "kkwok", + "꿭": "kkwot", + "꿮": "kkwop", + "꿯": "kkwot", + "꿰": "kkwe", + "꿱": "kkwek", + "꿲": "kkwekk", + "꿳": "kkwek", + "꿴": "kkwen", + "꿵": "kkwen", + "꿶": "kkwen", + "꿷": "kkwet", + "꿸": "kkwel", + "꿹": "kkwek", + "꿺": "kkwem", + "꿻": "kkwep", + "꿼": "kkwet", + "꿽": "kkwet", + "꿾": "kkwep", + "꿿": "kkwel", + "뀀": "kkwem", + "뀁": "kkwep", + "뀂": "kkwep", + "뀃": "kkwet", + "뀄": "kkwet", + "뀅": "kkweng", + "뀆": "kkwet", + "뀇": "kkwet", + "뀈": "kkwek", + "뀉": "kkwet", + "뀊": "kkwep", + "뀋": "kkwet", + "뀌": "kkwi", + "뀍": "kkwik", + "뀎": "kkwikk", + "뀏": "kkwik", + "뀐": "kkwin", + "뀑": "kkwin", + "뀒": "kkwin", + "뀓": "kkwit", + "뀔": "kkwil", + "뀕": "kkwik", + "뀖": "kkwim", + "뀗": "kkwip", + "뀘": "kkwit", + "뀙": "kkwit", + "뀚": "kkwip", + "뀛": "kkwil", + "뀜": "kkwim", + "뀝": "kkwip", + "뀞": "kkwip", + "뀟": "kkwit", + "뀠": "kkwit", + "뀡": "kkwing", + "뀢": "kkwit", + "뀣": "kkwit", + "뀤": "kkwik", + "뀥": "kkwit", + "뀦": "kkwip", + "뀧": "kkwit", + "뀨": "kkyu", + "뀩": "kkyuk", + "뀪": "kkyukk", + "뀫": "kkyuk", + "뀬": "kkyun", + "뀭": "kkyun", + "뀮": "kkyun", + "뀯": "kkyut", + "뀰": "kkyul", + "뀱": "kkyuk", + "뀲": "kkyum", + "뀳": "kkyup", + "뀴": "kkyut", + "뀵": "kkyut", + "뀶": "kkyup", + "뀷": "kkyul", + "뀸": "kkyum", + "뀹": "kkyup", + "뀺": "kkyup", + "뀻": "kkyut", + "뀼": "kkyut", + "뀽": "kkyung", + "뀾": "kkyut", + "뀿": "kkyut", + "끀": "kkyuk", + "끁": "kkyut", + "끂": "kkyup", + "끃": "kkyut", + "끄": "kkeu", + "끅": "kkeuk", + "끆": "kkeukk", + "끇": "kkeuk", + "끈": "kkeun", + "끉": "kkeun", + "끊": "kkeun", + "끋": "kkeut", + "끌": "kkeul", + "끍": "kkeuk", + "끎": "kkeum", + "끏": "kkeup", + "끐": "kkeut", + "끑": "kkeut", + "끒": "kkeup", + "끓": "kkeul", + "끔": "kkeum", + "끕": "kkeup", + "끖": "kkeup", + "끗": "kkeut", + "끘": "kkeut", + "끙": "kkeung", + "끚": "kkeut", + "끛": "kkeut", + "끜": "kkeuk", + "끝": "kkeut", + "끞": "kkeup", + "끟": "kkeut", + "끠": "kkeui", + "끡": "kkeuik", + "끢": "kkeuikk", + "끣": "kkeuik", + "끤": "kkeuin", + "끥": "kkeuin", + "끦": "kkeuin", + "끧": "kkeuit", + "끨": "kkeuil", + "끩": "kkeuik", + "끪": "kkeuim", + "끫": "kkeuip", + "끬": "kkeuit", + "끭": "kkeuit", + "끮": "kkeuip", + "끯": "kkeuil", + "끰": "kkeuim", + "끱": "kkeuip", + "끲": "kkeuip", + "끳": "kkeuit", + "끴": "kkeuit", + "끵": "kkeuing", + "끶": "kkeuit", + "끷": "kkeuit", + "끸": "kkeuik", + "끹": "kkeuit", + "끺": "kkeuip", + "끻": "kkeuit", + "끼": "kki", + "끽": "kkik", + "끾": "kkikk", + "끿": "kkik", + "낀": "kkin", + "낁": "kkin", + "낂": "kkin", + "낃": "kkit", + "낄": "kkil", + "낅": "kkik", + "낆": "kkim", + "낇": "kkip", + "낈": "kkit", + "낉": "kkit", + "낊": "kkip", + "낋": "kkil", + "낌": "kkim", + "낍": "kkip", + "낎": "kkip", + "낏": "kkit", + "낐": "kkit", + "낑": "kking", + "낒": "kkit", + "낓": "kkit", + "낔": "kkik", + "낕": "kkit", + "낖": "kkip", + "낗": "kkit", + "나": "na", + "낙": "nak", + "낚": "nakk", + "낛": "nak", + "난": "nan", + "낝": "nan", + "낞": "nan", + "낟": "nat", + "날": "nal", + "낡": "nak", + "낢": "nam", + "낣": "nap", + "낤": "nat", + "낥": "nat", + "낦": "nap", + "낧": "nal", + "남": "nam", + "납": "nap", + "낪": "nap", + "낫": "nat", + "났": "nat", + "낭": "nang", + "낮": "nat", + "낯": "nat", + "낰": "nak", + "낱": "nat", + "낲": "nap", + "낳": "nat", + "내": "nae", + "낵": "naek", + "낶": "naekk", + "낷": "naek", + "낸": "naen", + "낹": "naen", + "낺": "naen", + "낻": "naet", + "낼": "nael", + "낽": "naek", + "낾": "naem", + "낿": "naep", + "냀": "naet", + "냁": "naet", + "냂": "naep", + "냃": "nael", + "냄": "naem", + "냅": "naep", + "냆": "naep", + "냇": "naet", + "냈": "naet", + "냉": "naeng", + "냊": "naet", + "냋": "naet", + "냌": "naek", + "냍": "naet", + "냎": "naep", + "냏": "naet", + "냐": "nya", + "냑": "nyak", + "냒": "nyakk", + "냓": "nyak", + "냔": "nyan", + "냕": "nyan", + "냖": "nyan", + "냗": "nyat", + "냘": "nyal", + "냙": "nyak", + "냚": "nyam", + "냛": "nyap", + "냜": "nyat", + "냝": "nyat", + "냞": "nyap", + "냟": "nyal", + "냠": "nyam", + "냡": "nyap", + "냢": "nyap", + "냣": "nyat", + "냤": "nyat", + "냥": "nyang", + "냦": "nyat", + "냧": "nyat", + "냨": "nyak", + "냩": "nyat", + "냪": "nyap", + "냫": "nyat", + "냬": "nyae", + "냭": "nyaek", + "냮": "nyaekk", + "냯": "nyaek", + "냰": "nyaen", + "냱": "nyaen", + "냲": "nyaen", + "냳": "nyaet", + "냴": "nyael", + "냵": "nyaek", + "냶": "nyaem", + "냷": "nyaep", + "냸": "nyaet", + "냹": "nyaet", + "냺": "nyaep", + "냻": "nyael", + "냼": "nyaem", + "냽": "nyaep", + "냾": "nyaep", + "냿": "nyaet", + "넀": "nyaet", + "넁": "nyaeng", + "넂": "nyaet", + "넃": "nyaet", + "넄": "nyaek", + "넅": "nyaet", + "넆": "nyaep", + "넇": "nyaet", + "너": "neo", + "넉": "neok", + "넊": "neokk", + "넋": "neok", + "넌": "neon", + "넍": "neon", + "넎": "neon", + "넏": "neot", + "널": "neol", + "넑": "neok", + "넒": "neom", + "넓": "neop", + "넔": "neot", + "넕": "neot", + "넖": "neop", + "넗": "neol", + "넘": "neom", + "넙": "neop", + "넚": "neop", + "넛": "neot", + "넜": "neot", + "넝": "neong", + "넞": "neot", + "넟": "neot", + "넠": "neok", + "넡": "neot", + "넢": "neop", + "넣": "neot", + "네": "ne", + "넥": "nek", + "넦": "nekk", + "넧": "nek", + "넨": "nen", + "넩": "nen", + "넪": "nen", + "넫": "net", + "넬": "nel", + "넭": "nek", + "넮": "nem", + "넯": "nep", + "넰": "net", + "넱": "net", + "넲": "nep", + "넳": "nel", + "넴": "nem", + "넵": "nep", + "넶": "nep", + "넷": "net", + "넸": "net", + "넹": "neng", + "넺": "net", + "넻": "net", + "넼": "nek", + "넽": "net", + "넾": "nep", + "넿": "net", + "녀": "nyeo", + "녁": "nyeok", + "녂": "nyeokk", + "녃": "nyeok", + "년": "nyeon", + "녅": "nyeon", + "녆": "nyeon", + "녇": "nyeot", + "녈": "nyeol", + "녉": "nyeok", + "녊": "nyeom", + "녋": "nyeop", + "녌": "nyeot", + "녍": "nyeot", + "녎": "nyeop", + "녏": "nyeol", + "념": "nyeom", + "녑": "nyeop", + "녒": "nyeop", + "녓": "nyeot", + "녔": "nyeot", + "녕": "nyeong", + "녖": "nyeot", + "녗": "nyeot", + "녘": "nyeok", + "녙": "nyeot", + "녚": "nyeop", + "녛": "nyeot", + "녜": "nye", + "녝": "nyek", + "녞": "nyekk", + "녟": "nyek", + "녠": "nyen", + "녡": "nyen", + "녢": "nyen", + "녣": "nyet", + "녤": "nyel", + "녥": "nyek", + "녦": "nyem", + "녧": "nyep", + "녨": "nyet", + "녩": "nyet", + "녪": "nyep", + "녫": "nyel", + "녬": "nyem", + "녭": "nyep", + "녮": "nyep", + "녯": "nyet", + "녰": "nyet", + "녱": "nyeng", + "녲": "nyet", + "녳": "nyet", + "녴": "nyek", + "녵": "nyet", + "녶": "nyep", + "녷": "nyet", + "노": "no", + "녹": "nok", + "녺": "nokk", + "녻": "nok", + "논": "non", + "녽": "non", + "녾": "non", + "녿": "not", + "놀": "nol", + "놁": "nok", + "놂": "nom", + "놃": "nop", + "놄": "not", + "놅": "not", + "놆": "nop", + "놇": "nol", + "놈": "nom", + "놉": "nop", + "놊": "nop", + "놋": "not", + "놌": "not", + "농": "nong", + "놎": "not", + "놏": "not", + "놐": "nok", + "놑": "not", + "높": "nop", + "놓": "not", + "놔": "nwa", + "놕": "nwak", + "놖": "nwakk", + "놗": "nwak", + "놘": "nwan", + "놙": "nwan", + "놚": "nwan", + "놛": "nwat", + "놜": "nwal", + "놝": "nwak", + "놞": "nwam", + "놟": "nwap", + "놠": "nwat", + "놡": "nwat", + "놢": "nwap", + "놣": "nwal", + "놤": "nwam", + "놥": "nwap", + "놦": "nwap", + "놧": "nwat", + "놨": "nwat", + "놩": "nwang", + "놪": "nwat", + "놫": "nwat", + "놬": "nwak", + "놭": "nwat", + "놮": "nwap", + "놯": "nwat", + "놰": "nwae", + "놱": "nwaek", + "놲": "nwaekk", + "놳": "nwaek", + "놴": "nwaen", + "놵": "nwaen", + "놶": "nwaen", + "놷": "nwaet", + "놸": "nwael", + "놹": "nwaek", + "놺": "nwaem", + "놻": "nwaep", + "놼": "nwaet", + "놽": "nwaet", + "놾": "nwaep", + "놿": "nwael", + "뇀": "nwaem", + "뇁": "nwaep", + "뇂": "nwaep", + "뇃": "nwaet", + "뇄": "nwaet", + "뇅": "nwaeng", + "뇆": "nwaet", + "뇇": "nwaet", + "뇈": "nwaek", + "뇉": "nwaet", + "뇊": "nwaep", + "뇋": "nwaet", + "뇌": "noe", + "뇍": "noek", + "뇎": "noekk", + "뇏": "noek", + "뇐": "noen", + "뇑": "noen", + "뇒": "noen", + "뇓": "noet", + "뇔": "noel", + "뇕": "noek", + "뇖": "noem", + "뇗": "noep", + "뇘": "noet", + "뇙": "noet", + "뇚": "noep", + "뇛": "noel", + "뇜": "noem", + "뇝": "noep", + "뇞": "noep", + "뇟": "noet", + "뇠": "noet", + "뇡": "noeng", + "뇢": "noet", + "뇣": "noet", + "뇤": "noek", + "뇥": "noet", + "뇦": "noep", + "뇧": "noet", + "뇨": "nyo", + "뇩": "nyok", + "뇪": "nyokk", + "뇫": "nyok", + "뇬": "nyon", + "뇭": "nyon", + "뇮": "nyon", + "뇯": "nyot", + "뇰": "nyol", + "뇱": "nyok", + "뇲": "nyom", + "뇳": "nyop", + "뇴": "nyot", + "뇵": "nyot", + "뇶": "nyop", + "뇷": "nyol", + "뇸": "nyom", + "뇹": "nyop", + "뇺": "nyop", + "뇻": "nyot", + "뇼": "nyot", + "뇽": "nyong", + "뇾": "nyot", + "뇿": "nyot", + "눀": "nyok", + "눁": "nyot", + "눂": "nyop", + "눃": "nyot", + "누": "nu", + "눅": "nuk", + "눆": "nukk", + "눇": "nuk", + "눈": "nun", + "눉": "nun", + "눊": "nun", + "눋": "nut", + "눌": "nul", + "눍": "nuk", + "눎": "num", + "눏": "nup", + "눐": "nut", + "눑": "nut", + "눒": "nup", + "눓": "nul", + "눔": "num", + "눕": "nup", + "눖": "nup", + "눗": "nut", + "눘": "nut", + "눙": "nung", + "눚": "nut", + "눛": "nut", + "눜": "nuk", + "눝": "nut", + "눞": "nup", + "눟": "nut", + "눠": "nwo", + "눡": "nwok", + "눢": "nwokk", + "눣": "nwok", + "눤": "nwon", + "눥": "nwon", + "눦": "nwon", + "눧": "nwot", + "눨": "nwol", + "눩": "nwok", + "눪": "nwom", + "눫": "nwop", + "눬": "nwot", + "눭": "nwot", + "눮": "nwop", + "눯": "nwol", + "눰": "nwom", + "눱": "nwop", + "눲": "nwop", + "눳": "nwot", + "눴": "nwot", + "눵": "nwong", + "눶": "nwot", + "눷": "nwot", + "눸": "nwok", + "눹": "nwot", + "눺": "nwop", + "눻": "nwot", + "눼": "nwe", + "눽": "nwek", + "눾": "nwekk", + "눿": "nwek", + "뉀": "nwen", + "뉁": "nwen", + "뉂": "nwen", + "뉃": "nwet", + "뉄": "nwel", + "뉅": "nwek", + "뉆": "nwem", + "뉇": "nwep", + "뉈": "nwet", + "뉉": "nwet", + "뉊": "nwep", + "뉋": "nwel", + "뉌": "nwem", + "뉍": "nwep", + "뉎": "nwep", + "뉏": "nwet", + "뉐": "nwet", + "뉑": "nweng", + "뉒": "nwet", + "뉓": "nwet", + "뉔": "nwek", + "뉕": "nwet", + "뉖": "nwep", + "뉗": "nwet", + "뉘": "nwi", + "뉙": "nwik", + "뉚": "nwikk", + "뉛": "nwik", + "뉜": "nwin", + "뉝": "nwin", + "뉞": "nwin", + "뉟": "nwit", + "뉠": "nwil", + "뉡": "nwik", + "뉢": "nwim", + "뉣": "nwip", + "뉤": "nwit", + "뉥": "nwit", + "뉦": "nwip", + "뉧": "nwil", + "뉨": "nwim", + "뉩": "nwip", + "뉪": "nwip", + "뉫": "nwit", + "뉬": "nwit", + "뉭": "nwing", + "뉮": "nwit", + "뉯": "nwit", + "뉰": "nwik", + "뉱": "nwit", + "뉲": "nwip", + "뉳": "nwit", + "뉴": "nyu", + "뉵": "nyuk", + "뉶": "nyukk", + "뉷": "nyuk", + "뉸": "nyun", + "뉹": "nyun", + "뉺": "nyun", + "뉻": "nyut", + "뉼": "nyul", + "뉽": "nyuk", + "뉾": "nyum", + "뉿": "nyup", + "늀": "nyut", + "늁": "nyut", + "늂": "nyup", + "늃": "nyul", + "늄": "nyum", + "늅": "nyup", + "늆": "nyup", + "늇": "nyut", + "늈": "nyut", + "늉": "nyung", + "늊": "nyut", + "늋": "nyut", + "늌": "nyuk", + "늍": "nyut", + "늎": "nyup", + "늏": "nyut", + "느": "neu", + "늑": "neuk", + "늒": "neukk", + "늓": "neuk", + "는": "neun", + "늕": "neun", + "늖": "neun", + "늗": "neut", + "늘": "neul", + "늙": "neuk", + "늚": "neum", + "늛": "neup", + "늜": "neut", + "늝": "neut", + "늞": "neup", + "늟": "neul", + "늠": "neum", + "늡": "neup", + "늢": "neup", + "늣": "neut", + "늤": "neut", + "능": "neung", + "늦": "neut", + "늧": "neut", + "늨": "neuk", + "늩": "neut", + "늪": "neup", + "늫": "neut", + "늬": "neui", + "늭": "neuik", + "늮": "neuikk", + "늯": "neuik", + "늰": "neuin", + "늱": "neuin", + "늲": "neuin", + "늳": "neuit", + "늴": "neuil", + "늵": "neuik", + "늶": "neuim", + "늷": "neuip", + "늸": "neuit", + "늹": "neuit", + "늺": "neuip", + "늻": "neuil", + "늼": "neuim", + "늽": "neuip", + "늾": "neuip", + "늿": "neuit", + "닀": "neuit", + "닁": "neuing", + "닂": "neuit", + "닃": "neuit", + "닄": "neuik", + "닅": "neuit", + "닆": "neuip", + "닇": "neuit", + "니": "ni", + "닉": "nik", + "닊": "nikk", + "닋": "nik", + "닌": "nin", + "닍": "nin", + "닎": "nin", + "닏": "nit", + "닐": "nil", + "닑": "nik", + "닒": "nim", + "닓": "nip", + "닔": "nit", + "닕": "nit", + "닖": "nip", + "닗": "nil", + "님": "nim", + "닙": "nip", + "닚": "nip", + "닛": "nit", + "닜": "nit", + "닝": "ning", + "닞": "nit", + "닟": "nit", + "닠": "nik", + "닡": "nit", + "닢": "nip", + "닣": "nit", + "다": "da", + "닥": "dak", + "닦": "dakk", + "닧": "dak", + "단": "dan", + "닩": "dan", + "닪": "dan", + "닫": "dat", + "달": "dal", + "닭": "dak", + "닮": "dam", + "닯": "dap", + "닰": "dat", + "닱": "dat", + "닲": "dap", + "닳": "dal", + "담": "dam", + "답": "dap", + "닶": "dap", + "닷": "dat", + "닸": "dat", + "당": "dang", + "닺": "dat", + "닻": "dat", + "닼": "dak", + "닽": "dat", + "닾": "dap", + "닿": "dat", + "대": "dae", + "댁": "daek", + "댂": "daekk", + "댃": "daek", + "댄": "daen", + "댅": "daen", + "댆": "daen", + "댇": "daet", + "댈": "dael", + "댉": "daek", + "댊": "daem", + "댋": "daep", + "댌": "daet", + "댍": "daet", + "댎": "daep", + "댏": "dael", + "댐": "daem", + "댑": "daep", + "댒": "daep", + "댓": "daet", + "댔": "daet", + "댕": "daeng", + "댖": "daet", + "댗": "daet", + "댘": "daek", + "댙": "daet", + "댚": "daep", + "댛": "daet", + "댜": "dya", + "댝": "dyak", + "댞": "dyakk", + "댟": "dyak", + "댠": "dyan", + "댡": "dyan", + "댢": "dyan", + "댣": "dyat", + "댤": "dyal", + "댥": "dyak", + "댦": "dyam", + "댧": "dyap", + "댨": "dyat", + "댩": "dyat", + "댪": "dyap", + "댫": "dyal", + "댬": "dyam", + "댭": "dyap", + "댮": "dyap", + "댯": "dyat", + "댰": "dyat", + "댱": "dyang", + "댲": "dyat", + "댳": "dyat", + "댴": "dyak", + "댵": "dyat", + "댶": "dyap", + "댷": "dyat", + "댸": "dyae", + "댹": "dyaek", + "댺": "dyaekk", + "댻": "dyaek", + "댼": "dyaen", + "댽": "dyaen", + "댾": "dyaen", + "댿": "dyaet", + "덀": "dyael", + "덁": "dyaek", + "덂": "dyaem", + "덃": "dyaep", + "덄": "dyaet", + "덅": "dyaet", + "덆": "dyaep", + "덇": "dyael", + "덈": "dyaem", + "덉": "dyaep", + "덊": "dyaep", + "덋": "dyaet", + "덌": "dyaet", + "덍": "dyaeng", + "덎": "dyaet", + "덏": "dyaet", + "덐": "dyaek", + "덑": "dyaet", + "덒": "dyaep", + "덓": "dyaet", + "더": "deo", + "덕": "deok", + "덖": "deokk", + "덗": "deok", + "던": "deon", + "덙": "deon", + "덚": "deon", + "덛": "deot", + "덜": "deol", + "덝": "deok", + "덞": "deom", + "덟": "deop", + "덠": "deot", + "덡": "deot", + "덢": "deop", + "덣": "deol", + "덤": "deom", + "덥": "deop", + "덦": "deop", + "덧": "deot", + "덨": "deot", + "덩": "deong", + "덪": "deot", + "덫": "deot", + "덬": "deok", + "덭": "deot", + "덮": "deop", + "덯": "deot", + "데": "de", + "덱": "dek", + "덲": "dekk", + "덳": "dek", + "덴": "den", + "덵": "den", + "덶": "den", + "덷": "det", + "델": "del", + "덹": "dek", + "덺": "dem", + "덻": "dep", + "덼": "det", + "덽": "det", + "덾": "dep", + "덿": "del", + "뎀": "dem", + "뎁": "dep", + "뎂": "dep", + "뎃": "det", + "뎄": "det", + "뎅": "deng", + "뎆": "det", + "뎇": "det", + "뎈": "dek", + "뎉": "det", + "뎊": "dep", + "뎋": "det", + "뎌": "dyeo", + "뎍": "dyeok", + "뎎": "dyeokk", + "뎏": "dyeok", + "뎐": "dyeon", + "뎑": "dyeon", + "뎒": "dyeon", + "뎓": "dyeot", + "뎔": "dyeol", + "뎕": "dyeok", + "뎖": "dyeom", + "뎗": "dyeop", + "뎘": "dyeot", + "뎙": "dyeot", + "뎚": "dyeop", + "뎛": "dyeol", + "뎜": "dyeom", + "뎝": "dyeop", + "뎞": "dyeop", + "뎟": "dyeot", + "뎠": "dyeot", + "뎡": "dyeong", + "뎢": "dyeot", + "뎣": "dyeot", + "뎤": "dyeok", + "뎥": "dyeot", + "뎦": "dyeop", + "뎧": "dyeot", + "뎨": "dye", + "뎩": "dyek", + "뎪": "dyekk", + "뎫": "dyek", + "뎬": "dyen", + "뎭": "dyen", + "뎮": "dyen", + "뎯": "dyet", + "뎰": "dyel", + "뎱": "dyek", + "뎲": "dyem", + "뎳": "dyep", + "뎴": "dyet", + "뎵": "dyet", + "뎶": "dyep", + "뎷": "dyel", + "뎸": "dyem", + "뎹": "dyep", + "뎺": "dyep", + "뎻": "dyet", + "뎼": "dyet", + "뎽": "dyeng", + "뎾": "dyet", + "뎿": "dyet", + "돀": "dyek", + "돁": "dyet", + "돂": "dyep", + "돃": "dyet", + "도": "do", + "독": "dok", + "돆": "dokk", + "돇": "dok", + "돈": "don", + "돉": "don", + "돊": "don", + "돋": "dot", + "돌": "dol", + "돍": "dok", + "돎": "dom", + "돏": "dop", + "돐": "dot", + "돑": "dot", + "돒": "dop", + "돓": "dol", + "돔": "dom", + "돕": "dop", + "돖": "dop", + "돗": "dot", + "돘": "dot", + "동": "dong", + "돚": "dot", + "돛": "dot", + "돜": "dok", + "돝": "dot", + "돞": "dop", + "돟": "dot", + "돠": "dwa", + "돡": "dwak", + "돢": "dwakk", + "돣": "dwak", + "돤": "dwan", + "돥": "dwan", + "돦": "dwan", + "돧": "dwat", + "돨": "dwal", + "돩": "dwak", + "돪": "dwam", + "돫": "dwap", + "돬": "dwat", + "돭": "dwat", + "돮": "dwap", + "돯": "dwal", + "돰": "dwam", + "돱": "dwap", + "돲": "dwap", + "돳": "dwat", + "돴": "dwat", + "돵": "dwang", + "돶": "dwat", + "돷": "dwat", + "돸": "dwak", + "돹": "dwat", + "돺": "dwap", + "돻": "dwat", + "돼": "dwae", + "돽": "dwaek", + "돾": "dwaekk", + "돿": "dwaek", + "됀": "dwaen", + "됁": "dwaen", + "됂": "dwaen", + "됃": "dwaet", + "됄": "dwael", + "됅": "dwaek", + "됆": "dwaem", + "됇": "dwaep", + "됈": "dwaet", + "됉": "dwaet", + "됊": "dwaep", + "됋": "dwael", + "됌": "dwaem", + "됍": "dwaep", + "됎": "dwaep", + "됏": "dwaet", + "됐": "dwaet", + "됑": "dwaeng", + "됒": "dwaet", + "됓": "dwaet", + "됔": "dwaek", + "됕": "dwaet", + "됖": "dwaep", + "됗": "dwaet", + "되": "doe", + "됙": "doek", + "됚": "doekk", + "됛": "doek", + "된": "doen", + "됝": "doen", + "됞": "doen", + "됟": "doet", + "될": "doel", + "됡": "doek", + "됢": "doem", + "됣": "doep", + "됤": "doet", + "됥": "doet", + "됦": "doep", + "됧": "doel", + "됨": "doem", + "됩": "doep", + "됪": "doep", + "됫": "doet", + "됬": "doet", + "됭": "doeng", + "됮": "doet", + "됯": "doet", + "됰": "doek", + "됱": "doet", + "됲": "doep", + "됳": "doet", + "됴": "dyo", + "됵": "dyok", + "됶": "dyokk", + "됷": "dyok", + "됸": "dyon", + "됹": "dyon", + "됺": "dyon", + "됻": "dyot", + "됼": "dyol", + "됽": "dyok", + "됾": "dyom", + "됿": "dyop", + "둀": "dyot", + "둁": "dyot", + "둂": "dyop", + "둃": "dyol", + "둄": "dyom", + "둅": "dyop", + "둆": "dyop", + "둇": "dyot", + "둈": "dyot", + "둉": "dyong", + "둊": "dyot", + "둋": "dyot", + "둌": "dyok", + "둍": "dyot", + "둎": "dyop", + "둏": "dyot", + "두": "du", + "둑": "duk", + "둒": "dukk", + "둓": "duk", + "둔": "dun", + "둕": "dun", + "둖": "dun", + "둗": "dut", + "둘": "dul", + "둙": "duk", + "둚": "dum", + "둛": "dup", + "둜": "dut", + "둝": "dut", + "둞": "dup", + "둟": "dul", + "둠": "dum", + "둡": "dup", + "둢": "dup", + "둣": "dut", + "둤": "dut", + "둥": "dung", + "둦": "dut", + "둧": "dut", + "둨": "duk", + "둩": "dut", + "둪": "dup", + "둫": "dut", + "둬": "dwo", + "둭": "dwok", + "둮": "dwokk", + "둯": "dwok", + "둰": "dwon", + "둱": "dwon", + "둲": "dwon", + "둳": "dwot", + "둴": "dwol", + "둵": "dwok", + "둶": "dwom", + "둷": "dwop", + "둸": "dwot", + "둹": "dwot", + "둺": "dwop", + "둻": "dwol", + "둼": "dwom", + "둽": "dwop", + "둾": "dwop", + "둿": "dwot", + "뒀": "dwot", + "뒁": "dwong", + "뒂": "dwot", + "뒃": "dwot", + "뒄": "dwok", + "뒅": "dwot", + "뒆": "dwop", + "뒇": "dwot", + "뒈": "dwe", + "뒉": "dwek", + "뒊": "dwekk", + "뒋": "dwek", + "뒌": "dwen", + "뒍": "dwen", + "뒎": "dwen", + "뒏": "dwet", + "뒐": "dwel", + "뒑": "dwek", + "뒒": "dwem", + "뒓": "dwep", + "뒔": "dwet", + "뒕": "dwet", + "뒖": "dwep", + "뒗": "dwel", + "뒘": "dwem", + "뒙": "dwep", + "뒚": "dwep", + "뒛": "dwet", + "뒜": "dwet", + "뒝": "dweng", + "뒞": "dwet", + "뒟": "dwet", + "뒠": "dwek", + "뒡": "dwet", + "뒢": "dwep", + "뒣": "dwet", + "뒤": "dwi", + "뒥": "dwik", + "뒦": "dwikk", + "뒧": "dwik", + "뒨": "dwin", + "뒩": "dwin", + "뒪": "dwin", + "뒫": "dwit", + "뒬": "dwil", + "뒭": "dwik", + "뒮": "dwim", + "뒯": "dwip", + "뒰": "dwit", + "뒱": "dwit", + "뒲": "dwip", + "뒳": "dwil", + "뒴": "dwim", + "뒵": "dwip", + "뒶": "dwip", + "뒷": "dwit", + "뒸": "dwit", + "뒹": "dwing", + "뒺": "dwit", + "뒻": "dwit", + "뒼": "dwik", + "뒽": "dwit", + "뒾": "dwip", + "뒿": "dwit", + "듀": "dyu", + "듁": "dyuk", + "듂": "dyukk", + "듃": "dyuk", + "듄": "dyun", + "듅": "dyun", + "듆": "dyun", + "듇": "dyut", + "듈": "dyul", + "듉": "dyuk", + "듊": "dyum", + "듋": "dyup", + "듌": "dyut", + "듍": "dyut", + "듎": "dyup", + "듏": "dyul", + "듐": "dyum", + "듑": "dyup", + "듒": "dyup", + "듓": "dyut", + "듔": "dyut", + "듕": "dyung", + "듖": "dyut", + "듗": "dyut", + "듘": "dyuk", + "듙": "dyut", + "듚": "dyup", + "듛": "dyut", + "드": "deu", + "득": "deuk", + "듞": "deukk", + "듟": "deuk", + "든": "deun", + "듡": "deun", + "듢": "deun", + "듣": "deut", + "들": "deul", + "듥": "deuk", + "듦": "deum", + "듧": "deup", + "듨": "deut", + "듩": "deut", + "듪": "deup", + "듫": "deul", + "듬": "deum", + "듭": "deup", + "듮": "deup", + "듯": "deut", + "듰": "deut", + "등": "deung", + "듲": "deut", + "듳": "deut", + "듴": "deuk", + "듵": "deut", + "듶": "deup", + "듷": "deut", + "듸": "deui", + "듹": "deuik", + "듺": "deuikk", + "듻": "deuik", + "듼": "deuin", + "듽": "deuin", + "듾": "deuin", + "듿": "deuit", + "딀": "deuil", + "딁": "deuik", + "딂": "deuim", + "딃": "deuip", + "딄": "deuit", + "딅": "deuit", + "딆": "deuip", + "딇": "deuil", + "딈": "deuim", + "딉": "deuip", + "딊": "deuip", + "딋": "deuit", + "딌": "deuit", + "딍": "deuing", + "딎": "deuit", + "딏": "deuit", + "딐": "deuik", + "딑": "deuit", + "딒": "deuip", + "딓": "deuit", + "디": "di", + "딕": "dik", + "딖": "dikk", + "딗": "dik", + "딘": "din", + "딙": "din", + "딚": "din", + "딛": "dit", + "딜": "dil", + "딝": "dik", + "딞": "dim", + "딟": "dip", + "딠": "dit", + "딡": "dit", + "딢": "dip", + "딣": "dil", + "딤": "dim", + "딥": "dip", + "딦": "dip", + "딧": "dit", + "딨": "dit", + "딩": "ding", + "딪": "dit", + "딫": "dit", + "딬": "dik", + "딭": "dit", + "딮": "dip", + "딯": "dit", + "따": "tta", + "딱": "ttak", + "딲": "ttakk", + "딳": "ttak", + "딴": "ttan", + "딵": "ttan", + "딶": "ttan", + "딷": "ttat", + "딸": "ttal", + "딹": "ttak", + "딺": "ttam", + "딻": "ttap", + "딼": "ttat", + "딽": "ttat", + "딾": "ttap", + "딿": "ttal", + "땀": "ttam", + "땁": "ttap", + "땂": "ttap", + "땃": "ttat", + "땄": "ttat", + "땅": "ttang", + "땆": "ttat", + "땇": "ttat", + "땈": "ttak", + "땉": "ttat", + "땊": "ttap", + "땋": "ttat", + "때": "ttae", + "땍": "ttaek", + "땎": "ttaekk", + "땏": "ttaek", + "땐": "ttaen", + "땑": "ttaen", + "땒": "ttaen", + "땓": "ttaet", + "땔": "ttael", + "땕": "ttaek", + "땖": "ttaem", + "땗": "ttaep", + "땘": "ttaet", + "땙": "ttaet", + "땚": "ttaep", + "땛": "ttael", + "땜": "ttaem", + "땝": "ttaep", + "땞": "ttaep", + "땟": "ttaet", + "땠": "ttaet", + "땡": "ttaeng", + "땢": "ttaet", + "땣": "ttaet", + "땤": "ttaek", + "땥": "ttaet", + "땦": "ttaep", + "땧": "ttaet", + "땨": "ttya", + "땩": "ttyak", + "땪": "ttyakk", + "땫": "ttyak", + "땬": "ttyan", + "땭": "ttyan", + "땮": "ttyan", + "땯": "ttyat", + "땰": "ttyal", + "땱": "ttyak", + "땲": "ttyam", + "땳": "ttyap", + "땴": "ttyat", + "땵": "ttyat", + "땶": "ttyap", + "땷": "ttyal", + "땸": "ttyam", + "땹": "ttyap", + "땺": "ttyap", + "땻": "ttyat", + "땼": "ttyat", + "땽": "ttyang", + "땾": "ttyat", + "땿": "ttyat", + "떀": "ttyak", + "떁": "ttyat", + "떂": "ttyap", + "떃": "ttyat", + "떄": "ttyae", + "떅": "ttyaek", + "떆": "ttyaekk", + "떇": "ttyaek", + "떈": "ttyaen", + "떉": "ttyaen", + "떊": "ttyaen", + "떋": "ttyaet", + "떌": "ttyael", + "떍": "ttyaek", + "떎": "ttyaem", + "떏": "ttyaep", + "떐": "ttyaet", + "떑": "ttyaet", + "떒": "ttyaep", + "떓": "ttyael", + "떔": "ttyaem", + "떕": "ttyaep", + "떖": "ttyaep", + "떗": "ttyaet", + "떘": "ttyaet", + "떙": "ttyaeng", + "떚": "ttyaet", + "떛": "ttyaet", + "떜": "ttyaek", + "떝": "ttyaet", + "떞": "ttyaep", + "떟": "ttyaet", + "떠": "tteo", + "떡": "tteok", + "떢": "tteokk", + "떣": "tteok", + "떤": "tteon", + "떥": "tteon", + "떦": "tteon", + "떧": "tteot", + "떨": "tteol", + "떩": "tteok", + "떪": "tteom", + "떫": "tteop", + "떬": "tteot", + "떭": "tteot", + "떮": "tteop", + "떯": "tteol", + "떰": "tteom", + "떱": "tteop", + "떲": "tteop", + "떳": "tteot", + "떴": "tteot", + "떵": "tteong", + "떶": "tteot", + "떷": "tteot", + "떸": "tteok", + "떹": "tteot", + "떺": "tteop", + "떻": "tteot", + "떼": "tte", + "떽": "ttek", + "떾": "ttekk", + "떿": "ttek", + "뗀": "tten", + "뗁": "tten", + "뗂": "tten", + "뗃": "ttet", + "뗄": "ttel", + "뗅": "ttek", + "뗆": "ttem", + "뗇": "ttep", + "뗈": "ttet", + "뗉": "ttet", + "뗊": "ttep", + "뗋": "ttel", + "뗌": "ttem", + "뗍": "ttep", + "뗎": "ttep", + "뗏": "ttet", + "뗐": "ttet", + "뗑": "tteng", + "뗒": "ttet", + "뗓": "ttet", + "뗔": "ttek", + "뗕": "ttet", + "뗖": "ttep", + "뗗": "ttet", + "뗘": "ttyeo", + "뗙": "ttyeok", + "뗚": "ttyeokk", + "뗛": "ttyeok", + "뗜": "ttyeon", + "뗝": "ttyeon", + "뗞": "ttyeon", + "뗟": "ttyeot", + "뗠": "ttyeol", + "뗡": "ttyeok", + "뗢": "ttyeom", + "뗣": "ttyeop", + "뗤": "ttyeot", + "뗥": "ttyeot", + "뗦": "ttyeop", + "뗧": "ttyeol", + "뗨": "ttyeom", + "뗩": "ttyeop", + "뗪": "ttyeop", + "뗫": "ttyeot", + "뗬": "ttyeot", + "뗭": "ttyeong", + "뗮": "ttyeot", + "뗯": "ttyeot", + "뗰": "ttyeok", + "뗱": "ttyeot", + "뗲": "ttyeop", + "뗳": "ttyeot", + "뗴": "ttye", + "뗵": "ttyek", + "뗶": "ttyekk", + "뗷": "ttyek", + "뗸": "ttyen", + "뗹": "ttyen", + "뗺": "ttyen", + "뗻": "ttyet", + "뗼": "ttyel", + "뗽": "ttyek", + "뗾": "ttyem", + "뗿": "ttyep", + "똀": "ttyet", + "똁": "ttyet", + "똂": "ttyep", + "똃": "ttyel", + "똄": "ttyem", + "똅": "ttyep", + "똆": "ttyep", + "똇": "ttyet", + "똈": "ttyet", + "똉": "ttyeng", + "똊": "ttyet", + "똋": "ttyet", + "똌": "ttyek", + "똍": "ttyet", + "똎": "ttyep", + "똏": "ttyet", + "또": "tto", + "똑": "ttok", + "똒": "ttokk", + "똓": "ttok", + "똔": "tton", + "똕": "tton", + "똖": "tton", + "똗": "ttot", + "똘": "ttol", + "똙": "ttok", + "똚": "ttom", + "똛": "ttop", + "똜": "ttot", + "똝": "ttot", + "똞": "ttop", + "똟": "ttol", + "똠": "ttom", + "똡": "ttop", + "똢": "ttop", + "똣": "ttot", + "똤": "ttot", + "똥": "ttong", + "똦": "ttot", + "똧": "ttot", + "똨": "ttok", + "똩": "ttot", + "똪": "ttop", + "똫": "ttot", + "똬": "ttwa", + "똭": "ttwak", + "똮": "ttwakk", + "똯": "ttwak", + "똰": "ttwan", + "똱": "ttwan", + "똲": "ttwan", + "똳": "ttwat", + "똴": "ttwal", + "똵": "ttwak", + "똶": "ttwam", + "똷": "ttwap", + "똸": "ttwat", + "똹": "ttwat", + "똺": "ttwap", + "똻": "ttwal", + "똼": "ttwam", + "똽": "ttwap", + "똾": "ttwap", + "똿": "ttwat", + "뙀": "ttwat", + "뙁": "ttwang", + "뙂": "ttwat", + "뙃": "ttwat", + "뙄": "ttwak", + "뙅": "ttwat", + "뙆": "ttwap", + "뙇": "ttwat", + "뙈": "ttwae", + "뙉": "ttwaek", + "뙊": "ttwaekk", + "뙋": "ttwaek", + "뙌": "ttwaen", + "뙍": "ttwaen", + "뙎": "ttwaen", + "뙏": "ttwaet", + "뙐": "ttwael", + "뙑": "ttwaek", + "뙒": "ttwaem", + "뙓": "ttwaep", + "뙔": "ttwaet", + "뙕": "ttwaet", + "뙖": "ttwaep", + "뙗": "ttwael", + "뙘": "ttwaem", + "뙙": "ttwaep", + "뙚": "ttwaep", + "뙛": "ttwaet", + "뙜": "ttwaet", + "뙝": "ttwaeng", + "뙞": "ttwaet", + "뙟": "ttwaet", + "뙠": "ttwaek", + "뙡": "ttwaet", + "뙢": "ttwaep", + "뙣": "ttwaet", + "뙤": "ttoe", + "뙥": "ttoek", + "뙦": "ttoekk", + "뙧": "ttoek", + "뙨": "ttoen", + "뙩": "ttoen", + "뙪": "ttoen", + "뙫": "ttoet", + "뙬": "ttoel", + "뙭": "ttoek", + "뙮": "ttoem", + "뙯": "ttoep", + "뙰": "ttoet", + "뙱": "ttoet", + "뙲": "ttoep", + "뙳": "ttoel", + "뙴": "ttoem", + "뙵": "ttoep", + "뙶": "ttoep", + "뙷": "ttoet", + "뙸": "ttoet", + "뙹": "ttoeng", + "뙺": "ttoet", + "뙻": "ttoet", + "뙼": "ttoek", + "뙽": "ttoet", + "뙾": "ttoep", + "뙿": "ttoet", + "뚀": "ttyo", + "뚁": "ttyok", + "뚂": "ttyokk", + "뚃": "ttyok", + "뚄": "ttyon", + "뚅": "ttyon", + "뚆": "ttyon", + "뚇": "ttyot", + "뚈": "ttyol", + "뚉": "ttyok", + "뚊": "ttyom", + "뚋": "ttyop", + "뚌": "ttyot", + "뚍": "ttyot", + "뚎": "ttyop", + "뚏": "ttyol", + "뚐": "ttyom", + "뚑": "ttyop", + "뚒": "ttyop", + "뚓": "ttyot", + "뚔": "ttyot", + "뚕": "ttyong", + "뚖": "ttyot", + "뚗": "ttyot", + "뚘": "ttyok", + "뚙": "ttyot", + "뚚": "ttyop", + "뚛": "ttyot", + "뚜": "ttu", + "뚝": "ttuk", + "뚞": "ttukk", + "뚟": "ttuk", + "뚠": "ttun", + "뚡": "ttun", + "뚢": "ttun", + "뚣": "ttut", + "뚤": "ttul", + "뚥": "ttuk", + "뚦": "ttum", + "뚧": "ttup", + "뚨": "ttut", + "뚩": "ttut", + "뚪": "ttup", + "뚫": "ttul", + "뚬": "ttum", + "뚭": "ttup", + "뚮": "ttup", + "뚯": "ttut", + "뚰": "ttut", + "뚱": "ttung", + "뚲": "ttut", + "뚳": "ttut", + "뚴": "ttuk", + "뚵": "ttut", + "뚶": "ttup", + "뚷": "ttut", + "뚸": "ttwo", + "뚹": "ttwok", + "뚺": "ttwokk", + "뚻": "ttwok", + "뚼": "ttwon", + "뚽": "ttwon", + "뚾": "ttwon", + "뚿": "ttwot", + "뛀": "ttwol", + "뛁": "ttwok", + "뛂": "ttwom", + "뛃": "ttwop", + "뛄": "ttwot", + "뛅": "ttwot", + "뛆": "ttwop", + "뛇": "ttwol", + "뛈": "ttwom", + "뛉": "ttwop", + "뛊": "ttwop", + "뛋": "ttwot", + "뛌": "ttwot", + "뛍": "ttwong", + "뛎": "ttwot", + "뛏": "ttwot", + "뛐": "ttwok", + "뛑": "ttwot", + "뛒": "ttwop", + "뛓": "ttwot", + "뛔": "ttwe", + "뛕": "ttwek", + "뛖": "ttwekk", + "뛗": "ttwek", + "뛘": "ttwen", + "뛙": "ttwen", + "뛚": "ttwen", + "뛛": "ttwet", + "뛜": "ttwel", + "뛝": "ttwek", + "뛞": "ttwem", + "뛟": "ttwep", + "뛠": "ttwet", + "뛡": "ttwet", + "뛢": "ttwep", + "뛣": "ttwel", + "뛤": "ttwem", + "뛥": "ttwep", + "뛦": "ttwep", + "뛧": "ttwet", + "뛨": "ttwet", + "뛩": "ttweng", + "뛪": "ttwet", + "뛫": "ttwet", + "뛬": "ttwek", + "뛭": "ttwet", + "뛮": "ttwep", + "뛯": "ttwet", + "뛰": "ttwi", + "뛱": "ttwik", + "뛲": "ttwikk", + "뛳": "ttwik", + "뛴": "ttwin", + "뛵": "ttwin", + "뛶": "ttwin", + "뛷": "ttwit", + "뛸": "ttwil", + "뛹": "ttwik", + "뛺": "ttwim", + "뛻": "ttwip", + "뛼": "ttwit", + "뛽": "ttwit", + "뛾": "ttwip", + "뛿": "ttwil", + "뜀": "ttwim", + "뜁": "ttwip", + "뜂": "ttwip", + "뜃": "ttwit", + "뜄": "ttwit", + "뜅": "ttwing", + "뜆": "ttwit", + "뜇": "ttwit", + "뜈": "ttwik", + "뜉": "ttwit", + "뜊": "ttwip", + "뜋": "ttwit", + "뜌": "ttyu", + "뜍": "ttyuk", + "뜎": "ttyukk", + "뜏": "ttyuk", + "뜐": "ttyun", + "뜑": "ttyun", + "뜒": "ttyun", + "뜓": "ttyut", + "뜔": "ttyul", + "뜕": "ttyuk", + "뜖": "ttyum", + "뜗": "ttyup", + "뜘": "ttyut", + "뜙": "ttyut", + "뜚": "ttyup", + "뜛": "ttyul", + "뜜": "ttyum", + "뜝": "ttyup", + "뜞": "ttyup", + "뜟": "ttyut", + "뜠": "ttyut", + "뜡": "ttyung", + "뜢": "ttyut", + "뜣": "ttyut", + "뜤": "ttyuk", + "뜥": "ttyut", + "뜦": "ttyup", + "뜧": "ttyut", + "뜨": "tteu", + "뜩": "tteuk", + "뜪": "tteukk", + "뜫": "tteuk", + "뜬": "tteun", + "뜭": "tteun", + "뜮": "tteun", + "뜯": "tteut", + "뜰": "tteul", + "뜱": "tteuk", + "뜲": "tteum", + "뜳": "tteup", + "뜴": "tteut", + "뜵": "tteut", + "뜶": "tteup", + "뜷": "tteul", + "뜸": "tteum", + "뜹": "tteup", + "뜺": "tteup", + "뜻": "tteut", + "뜼": "tteut", + "뜽": "tteung", + "뜾": "tteut", + "뜿": "tteut", + "띀": "tteuk", + "띁": "tteut", + "띂": "tteup", + "띃": "tteut", + "띄": "tteui", + "띅": "tteuik", + "띆": "tteuikk", + "띇": "tteuik", + "띈": "tteuin", + "띉": "tteuin", + "띊": "tteuin", + "띋": "tteuit", + "띌": "tteuil", + "띍": "tteuik", + "띎": "tteuim", + "띏": "tteuip", + "띐": "tteuit", + "띑": "tteuit", + "띒": "tteuip", + "띓": "tteuil", + "띔": "tteuim", + "띕": "tteuip", + "띖": "tteuip", + "띗": "tteuit", + "띘": "tteuit", + "띙": "tteuing", + "띚": "tteuit", + "띛": "tteuit", + "띜": "tteuik", + "띝": "tteuit", + "띞": "tteuip", + "띟": "tteuit", + "띠": "tti", + "띡": "ttik", + "띢": "ttikk", + "띣": "ttik", + "띤": "ttin", + "띥": "ttin", + "띦": "ttin", + "띧": "ttit", + "띨": "ttil", + "띩": "ttik", + "띪": "ttim", + "띫": "ttip", + "띬": "ttit", + "띭": "ttit", + "띮": "ttip", + "띯": "ttil", + "띰": "ttim", + "띱": "ttip", + "띲": "ttip", + "띳": "ttit", + "띴": "ttit", + "띵": "tting", + "띶": "ttit", + "띷": "ttit", + "띸": "ttik", + "띹": "ttit", + "띺": "ttip", + "띻": "ttit", + "라": "ra", + "락": "rak", + "띾": "rakk", + "띿": "rak", + "란": "ran", + "랁": "ran", + "랂": "ran", + "랃": "rat", + "랄": "ral", + "랅": "rak", + "랆": "ram", + "랇": "rap", + "랈": "rat", + "랉": "rat", + "랊": "rap", + "랋": "ral", + "람": "ram", + "랍": "rap", + "랎": "rap", + "랏": "rat", + "랐": "rat", + "랑": "rang", + "랒": "rat", + "랓": "rat", + "랔": "rak", + "랕": "rat", + "랖": "rap", + "랗": "rat", + "래": "rae", + "랙": "raek", + "랚": "raekk", + "랛": "raek", + "랜": "raen", + "랝": "raen", + "랞": "raen", + "랟": "raet", + "랠": "rael", + "랡": "raek", + "랢": "raem", + "랣": "raep", + "랤": "raet", + "랥": "raet", + "랦": "raep", + "랧": "rael", + "램": "raem", + "랩": "raep", + "랪": "raep", + "랫": "raet", + "랬": "raet", + "랭": "raeng", + "랮": "raet", + "랯": "raet", + "랰": "raek", + "랱": "raet", + "랲": "raep", + "랳": "raet", + "랴": "rya", + "략": "ryak", + "랶": "ryakk", + "랷": "ryak", + "랸": "ryan", + "랹": "ryan", + "랺": "ryan", + "랻": "ryat", + "랼": "ryal", + "랽": "ryak", + "랾": "ryam", + "랿": "ryap", + "럀": "ryat", + "럁": "ryat", + "럂": "ryap", + "럃": "ryal", + "럄": "ryam", + "럅": "ryap", + "럆": "ryap", + "럇": "ryat", + "럈": "ryat", + "량": "ryang", + "럊": "ryat", + "럋": "ryat", + "럌": "ryak", + "럍": "ryat", + "럎": "ryap", + "럏": "ryat", + "럐": "ryae", + "럑": "ryaek", + "럒": "ryaekk", + "럓": "ryaek", + "럔": "ryaen", + "럕": "ryaen", + "럖": "ryaen", + "럗": "ryaet", + "럘": "ryael", + "럙": "ryaek", + "럚": "ryaem", + "럛": "ryaep", + "럜": "ryaet", + "럝": "ryaet", + "럞": "ryaep", + "럟": "ryael", + "럠": "ryaem", + "럡": "ryaep", + "럢": "ryaep", + "럣": "ryaet", + "럤": "ryaet", + "럥": "ryaeng", + "럦": "ryaet", + "럧": "ryaet", + "럨": "ryaek", + "럩": "ryaet", + "럪": "ryaep", + "럫": "ryaet", + "러": "reo", + "럭": "reok", + "럮": "reokk", + "럯": "reok", + "런": "reon", + "럱": "reon", + "럲": "reon", + "럳": "reot", + "럴": "reol", + "럵": "reok", + "럶": "reom", + "럷": "reop", + "럸": "reot", + "럹": "reot", + "럺": "reop", + "럻": "reol", + "럼": "reom", + "럽": "reop", + "럾": "reop", + "럿": "reot", + "렀": "reot", + "렁": "reong", + "렂": "reot", + "렃": "reot", + "렄": "reok", + "렅": "reot", + "렆": "reop", + "렇": "reot", + "레": "re", + "렉": "rek", + "렊": "rekk", + "렋": "rek", + "렌": "ren", + "렍": "ren", + "렎": "ren", + "렏": "ret", + "렐": "rel", + "렑": "rek", + "렒": "rem", + "렓": "rep", + "렔": "ret", + "렕": "ret", + "렖": "rep", + "렗": "rel", + "렘": "rem", + "렙": "rep", + "렚": "rep", + "렛": "ret", + "렜": "ret", + "렝": "reng", + "렞": "ret", + "렟": "ret", + "렠": "rek", + "렡": "ret", + "렢": "rep", + "렣": "ret", + "려": "ryeo", + "력": "ryeok", + "렦": "ryeokk", + "렧": "ryeok", + "련": "ryeon", + "렩": "ryeon", + "렪": "ryeon", + "렫": "ryeot", + "렬": "ryeol", + "렭": "ryeok", + "렮": "ryeom", + "렯": "ryeop", + "렰": "ryeot", + "렱": "ryeot", + "렲": "ryeop", + "렳": "ryeol", + "렴": "ryeom", + "렵": "ryeop", + "렶": "ryeop", + "렷": "ryeot", + "렸": "ryeot", + "령": "ryeong", + "렺": "ryeot", + "렻": "ryeot", + "렼": "ryeok", + "렽": "ryeot", + "렾": "ryeop", + "렿": "ryeot", + "례": "rye", + "롁": "ryek", + "롂": "ryekk", + "롃": "ryek", + "롄": "ryen", + "롅": "ryen", + "롆": "ryen", + "롇": "ryet", + "롈": "ryel", + "롉": "ryek", + "롊": "ryem", + "롋": "ryep", + "롌": "ryet", + "롍": "ryet", + "롎": "ryep", + "롏": "ryel", + "롐": "ryem", + "롑": "ryep", + "롒": "ryep", + "롓": "ryet", + "롔": "ryet", + "롕": "ryeng", + "롖": "ryet", + "롗": "ryet", + "롘": "ryek", + "롙": "ryet", + "롚": "ryep", + "롛": "ryet", + "로": "ro", + "록": "rok", + "롞": "rokk", + "롟": "rok", + "론": "ron", + "롡": "ron", + "롢": "ron", + "롣": "rot", + "롤": "rol", + "롥": "rok", + "롦": "rom", + "롧": "rop", + "롨": "rot", + "롩": "rot", + "롪": "rop", + "롫": "rol", + "롬": "rom", + "롭": "rop", + "롮": "rop", + "롯": "rot", + "롰": "rot", + "롱": "rong", + "롲": "rot", + "롳": "rot", + "롴": "rok", + "롵": "rot", + "롶": "rop", + "롷": "rot", + "롸": "rwa", + "롹": "rwak", + "롺": "rwakk", + "롻": "rwak", + "롼": "rwan", + "롽": "rwan", + "롾": "rwan", + "롿": "rwat", + "뢀": "rwal", + "뢁": "rwak", + "뢂": "rwam", + "뢃": "rwap", + "뢄": "rwat", + "뢅": "rwat", + "뢆": "rwap", + "뢇": "rwal", + "뢈": "rwam", + "뢉": "rwap", + "뢊": "rwap", + "뢋": "rwat", + "뢌": "rwat", + "뢍": "rwang", + "뢎": "rwat", + "뢏": "rwat", + "뢐": "rwak", + "뢑": "rwat", + "뢒": "rwap", + "뢓": "rwat", + "뢔": "rwae", + "뢕": "rwaek", + "뢖": "rwaekk", + "뢗": "rwaek", + "뢘": "rwaen", + "뢙": "rwaen", + "뢚": "rwaen", + "뢛": "rwaet", + "뢜": "rwael", + "뢝": "rwaek", + "뢞": "rwaem", + "뢟": "rwaep", + "뢠": "rwaet", + "뢡": "rwaet", + "뢢": "rwaep", + "뢣": "rwael", + "뢤": "rwaem", + "뢥": "rwaep", + "뢦": "rwaep", + "뢧": "rwaet", + "뢨": "rwaet", + "뢩": "rwaeng", + "뢪": "rwaet", + "뢫": "rwaet", + "뢬": "rwaek", + "뢭": "rwaet", + "뢮": "rwaep", + "뢯": "rwaet", + "뢰": "roe", + "뢱": "roek", + "뢲": "roekk", + "뢳": "roek", + "뢴": "roen", + "뢵": "roen", + "뢶": "roen", + "뢷": "roet", + "뢸": "roel", + "뢹": "roek", + "뢺": "roem", + "뢻": "roep", + "뢼": "roet", + "뢽": "roet", + "뢾": "roep", + "뢿": "roel", + "룀": "roem", + "룁": "roep", + "룂": "roep", + "룃": "roet", + "룄": "roet", + "룅": "roeng", + "룆": "roet", + "룇": "roet", + "룈": "roek", + "룉": "roet", + "룊": "roep", + "룋": "roet", + "료": "ryo", + "룍": "ryok", + "룎": "ryokk", + "룏": "ryok", + "룐": "ryon", + "룑": "ryon", + "룒": "ryon", + "룓": "ryot", + "룔": "ryol", + "룕": "ryok", + "룖": "ryom", + "룗": "ryop", + "룘": "ryot", + "룙": "ryot", + "룚": "ryop", + "룛": "ryol", + "룜": "ryom", + "룝": "ryop", + "룞": "ryop", + "룟": "ryot", + "룠": "ryot", + "룡": "ryong", + "룢": "ryot", + "룣": "ryot", + "룤": "ryok", + "룥": "ryot", + "룦": "ryop", + "룧": "ryot", + "루": "ru", + "룩": "ruk", + "룪": "rukk", + "룫": "ruk", + "룬": "run", + "룭": "run", + "룮": "run", + "룯": "rut", + "룰": "rul", + "룱": "ruk", + "룲": "rum", + "룳": "rup", + "룴": "rut", + "룵": "rut", + "룶": "rup", + "룷": "rul", + "룸": "rum", + "룹": "rup", + "룺": "rup", + "룻": "rut", + "룼": "rut", + "룽": "rung", + "룾": "rut", + "룿": "rut", + "뤀": "ruk", + "뤁": "rut", + "뤂": "rup", + "뤃": "rut", + "뤄": "rwo", + "뤅": "rwok", + "뤆": "rwokk", + "뤇": "rwok", + "뤈": "rwon", + "뤉": "rwon", + "뤊": "rwon", + "뤋": "rwot", + "뤌": "rwol", + "뤍": "rwok", + "뤎": "rwom", + "뤏": "rwop", + "뤐": "rwot", + "뤑": "rwot", + "뤒": "rwop", + "뤓": "rwol", + "뤔": "rwom", + "뤕": "rwop", + "뤖": "rwop", + "뤗": "rwot", + "뤘": "rwot", + "뤙": "rwong", + "뤚": "rwot", + "뤛": "rwot", + "뤜": "rwok", + "뤝": "rwot", + "뤞": "rwop", + "뤟": "rwot", + "뤠": "rwe", + "뤡": "rwek", + "뤢": "rwekk", + "뤣": "rwek", + "뤤": "rwen", + "뤥": "rwen", + "뤦": "rwen", + "뤧": "rwet", + "뤨": "rwel", + "뤩": "rwek", + "뤪": "rwem", + "뤫": "rwep", + "뤬": "rwet", + "뤭": "rwet", + "뤮": "rwep", + "뤯": "rwel", + "뤰": "rwem", + "뤱": "rwep", + "뤲": "rwep", + "뤳": "rwet", + "뤴": "rwet", + "뤵": "rweng", + "뤶": "rwet", + "뤷": "rwet", + "뤸": "rwek", + "뤹": "rwet", + "뤺": "rwep", + "뤻": "rwet", + "뤼": "rwi", + "뤽": "rwik", + "뤾": "rwikk", + "뤿": "rwik", + "륀": "rwin", + "륁": "rwin", + "륂": "rwin", + "륃": "rwit", + "륄": "rwil", + "륅": "rwik", + "륆": "rwim", + "륇": "rwip", + "륈": "rwit", + "륉": "rwit", + "륊": "rwip", + "륋": "rwil", + "륌": "rwim", + "륍": "rwip", + "륎": "rwip", + "륏": "rwit", + "륐": "rwit", + "륑": "rwing", + "륒": "rwit", + "륓": "rwit", + "륔": "rwik", + "륕": "rwit", + "륖": "rwip", + "륗": "rwit", + "류": "ryu", + "륙": "ryuk", + "륚": "ryukk", + "륛": "ryuk", + "륜": "ryun", + "륝": "ryun", + "륞": "ryun", + "륟": "ryut", + "률": "ryul", + "륡": "ryuk", + "륢": "ryum", + "륣": "ryup", + "륤": "ryut", + "륥": "ryut", + "륦": "ryup", + "륧": "ryul", + "륨": "ryum", + "륩": "ryup", + "륪": "ryup", + "륫": "ryut", + "륬": "ryut", + "륭": "ryung", + "륮": "ryut", + "륯": "ryut", + "륰": "ryuk", + "륱": "ryut", + "륲": "ryup", + "륳": "ryut", + "르": "reu", + "륵": "reuk", + "륶": "reukk", + "륷": "reuk", + "른": "reun", + "륹": "reun", + "륺": "reun", + "륻": "reut", + "를": "reul", + "륽": "reuk", + "륾": "reum", + "륿": "reup", + "릀": "reut", + "릁": "reut", + "릂": "reup", + "릃": "reul", + "름": "reum", + "릅": "reup", + "릆": "reup", + "릇": "reut", + "릈": "reut", + "릉": "reung", + "릊": "reut", + "릋": "reut", + "릌": "reuk", + "릍": "reut", + "릎": "reup", + "릏": "reut", + "릐": "reui", + "릑": "reuik", + "릒": "reuikk", + "릓": "reuik", + "릔": "reuin", + "릕": "reuin", + "릖": "reuin", + "릗": "reuit", + "릘": "reuil", + "릙": "reuik", + "릚": "reuim", + "릛": "reuip", + "릜": "reuit", + "릝": "reuit", + "릞": "reuip", + "릟": "reuil", + "릠": "reuim", + "릡": "reuip", + "릢": "reuip", + "릣": "reuit", + "릤": "reuit", + "릥": "reuing", + "릦": "reuit", + "릧": "reuit", + "릨": "reuik", + "릩": "reuit", + "릪": "reuip", + "릫": "reuit", + "리": "ri", + "릭": "rik", + "릮": "rikk", + "릯": "rik", + "린": "rin", + "릱": "rin", + "릲": "rin", + "릳": "rit", + "릴": "ril", + "릵": "rik", + "릶": "rim", + "릷": "rip", + "릸": "rit", + "릹": "rit", + "릺": "rip", + "릻": "ril", + "림": "rim", + "립": "rip", + "릾": "rip", + "릿": "rit", + "맀": "rit", + "링": "ring", + "맂": "rit", + "맃": "rit", + "맄": "rik", + "맅": "rit", + "맆": "rip", + "맇": "rit", + "마": "ma", + "막": "mak", + "맊": "makk", + "맋": "mak", + "만": "man", + "맍": "man", + "많": "man", + "맏": "mat", + "말": "mal", + "맑": "mak", + "맒": "mam", + "맓": "map", + "맔": "mat", + "맕": "mat", + "맖": "map", + "맗": "mal", + "맘": "mam", + "맙": "map", + "맚": "map", + "맛": "mat", + "맜": "mat", + "망": "mang", + "맞": "mat", + "맟": "mat", + "맠": "mak", + "맡": "mat", + "맢": "map", + "맣": "mat", + "매": "mae", + "맥": "maek", + "맦": "maekk", + "맧": "maek", + "맨": "maen", + "맩": "maen", + "맪": "maen", + "맫": "maet", + "맬": "mael", + "맭": "maek", + "맮": "maem", + "맯": "maep", + "맰": "maet", + "맱": "maet", + "맲": "maep", + "맳": "mael", + "맴": "maem", + "맵": "maep", + "맶": "maep", + "맷": "maet", + "맸": "maet", + "맹": "maeng", + "맺": "maet", + "맻": "maet", + "맼": "maek", + "맽": "maet", + "맾": "maep", + "맿": "maet", + "먀": "mya", + "먁": "myak", + "먂": "myakk", + "먃": "myak", + "먄": "myan", + "먅": "myan", + "먆": "myan", + "먇": "myat", + "먈": "myal", + "먉": "myak", + "먊": "myam", + "먋": "myap", + "먌": "myat", + "먍": "myat", + "먎": "myap", + "먏": "myal", + "먐": "myam", + "먑": "myap", + "먒": "myap", + "먓": "myat", + "먔": "myat", + "먕": "myang", + "먖": "myat", + "먗": "myat", + "먘": "myak", + "먙": "myat", + "먚": "myap", + "먛": "myat", + "먜": "myae", + "먝": "myaek", + "먞": "myaekk", + "먟": "myaek", + "먠": "myaen", + "먡": "myaen", + "먢": "myaen", + "먣": "myaet", + "먤": "myael", + "먥": "myaek", + "먦": "myaem", + "먧": "myaep", + "먨": "myaet", + "먩": "myaet", + "먪": "myaep", + "먫": "myael", + "먬": "myaem", + "먭": "myaep", + "먮": "myaep", + "먯": "myaet", + "먰": "myaet", + "먱": "myaeng", + "먲": "myaet", + "먳": "myaet", + "먴": "myaek", + "먵": "myaet", + "먶": "myaep", + "먷": "myaet", + "머": "meo", + "먹": "meok", + "먺": "meokk", + "먻": "meok", + "먼": "meon", + "먽": "meon", + "먾": "meon", + "먿": "meot", + "멀": "meol", + "멁": "meok", + "멂": "meom", + "멃": "meop", + "멄": "meot", + "멅": "meot", + "멆": "meop", + "멇": "meol", + "멈": "meom", + "멉": "meop", + "멊": "meop", + "멋": "meot", + "멌": "meot", + "멍": "meong", + "멎": "meot", + "멏": "meot", + "멐": "meok", + "멑": "meot", + "멒": "meop", + "멓": "meot", + "메": "me", + "멕": "mek", + "멖": "mekk", + "멗": "mek", + "멘": "men", + "멙": "men", + "멚": "men", + "멛": "met", + "멜": "mel", + "멝": "mek", + "멞": "mem", + "멟": "mep", + "멠": "met", + "멡": "met", + "멢": "mep", + "멣": "mel", + "멤": "mem", + "멥": "mep", + "멦": "mep", + "멧": "met", + "멨": "met", + "멩": "meng", + "멪": "met", + "멫": "met", + "멬": "mek", + "멭": "met", + "멮": "mep", + "멯": "met", + "며": "myeo", + "멱": "myeok", + "멲": "myeokk", + "멳": "myeok", + "면": "myeon", + "멵": "myeon", + "멶": "myeon", + "멷": "myeot", + "멸": "myeol", + "멹": "myeok", + "멺": "myeom", + "멻": "myeop", + "멼": "myeot", + "멽": "myeot", + "멾": "myeop", + "멿": "myeol", + "몀": "myeom", + "몁": "myeop", + "몂": "myeop", + "몃": "myeot", + "몄": "myeot", + "명": "myeong", + "몆": "myeot", + "몇": "myeot", + "몈": "myeok", + "몉": "myeot", + "몊": "myeop", + "몋": "myeot", + "몌": "mye", + "몍": "myek", + "몎": "myekk", + "몏": "myek", + "몐": "myen", + "몑": "myen", + "몒": "myen", + "몓": "myet", + "몔": "myel", + "몕": "myek", + "몖": "myem", + "몗": "myep", + "몘": "myet", + "몙": "myet", + "몚": "myep", + "몛": "myel", + "몜": "myem", + "몝": "myep", + "몞": "myep", + "몟": "myet", + "몠": "myet", + "몡": "myeng", + "몢": "myet", + "몣": "myet", + "몤": "myek", + "몥": "myet", + "몦": "myep", + "몧": "myet", + "모": "mo", + "목": "mok", + "몪": "mokk", + "몫": "mok", + "몬": "mon", + "몭": "mon", + "몮": "mon", + "몯": "mot", + "몰": "mol", + "몱": "mok", + "몲": "mom", + "몳": "mop", + "몴": "mot", + "몵": "mot", + "몶": "mop", + "몷": "mol", + "몸": "mom", + "몹": "mop", + "몺": "mop", + "못": "mot", + "몼": "mot", + "몽": "mong", + "몾": "mot", + "몿": "mot", + "뫀": "mok", + "뫁": "mot", + "뫂": "mop", + "뫃": "mot", + "뫄": "mwa", + "뫅": "mwak", + "뫆": "mwakk", + "뫇": "mwak", + "뫈": "mwan", + "뫉": "mwan", + "뫊": "mwan", + "뫋": "mwat", + "뫌": "mwal", + "뫍": "mwak", + "뫎": "mwam", + "뫏": "mwap", + "뫐": "mwat", + "뫑": "mwat", + "뫒": "mwap", + "뫓": "mwal", + "뫔": "mwam", + "뫕": "mwap", + "뫖": "mwap", + "뫗": "mwat", + "뫘": "mwat", + "뫙": "mwang", + "뫚": "mwat", + "뫛": "mwat", + "뫜": "mwak", + "뫝": "mwat", + "뫞": "mwap", + "뫟": "mwat", + "뫠": "mwae", + "뫡": "mwaek", + "뫢": "mwaekk", + "뫣": "mwaek", + "뫤": "mwaen", + "뫥": "mwaen", + "뫦": "mwaen", + "뫧": "mwaet", + "뫨": "mwael", + "뫩": "mwaek", + "뫪": "mwaem", + "뫫": "mwaep", + "뫬": "mwaet", + "뫭": "mwaet", + "뫮": "mwaep", + "뫯": "mwael", + "뫰": "mwaem", + "뫱": "mwaep", + "뫲": "mwaep", + "뫳": "mwaet", + "뫴": "mwaet", + "뫵": "mwaeng", + "뫶": "mwaet", + "뫷": "mwaet", + "뫸": "mwaek", + "뫹": "mwaet", + "뫺": "mwaep", + "뫻": "mwaet", + "뫼": "moe", + "뫽": "moek", + "뫾": "moekk", + "뫿": "moek", + "묀": "moen", + "묁": "moen", + "묂": "moen", + "묃": "moet", + "묄": "moel", + "묅": "moek", + "묆": "moem", + "묇": "moep", + "묈": "moet", + "묉": "moet", + "묊": "moep", + "묋": "moel", + "묌": "moem", + "묍": "moep", + "묎": "moep", + "묏": "moet", + "묐": "moet", + "묑": "moeng", + "묒": "moet", + "묓": "moet", + "묔": "moek", + "묕": "moet", + "묖": "moep", + "묗": "moet", + "묘": "myo", + "묙": "myok", + "묚": "myokk", + "묛": "myok", + "묜": "myon", + "묝": "myon", + "묞": "myon", + "묟": "myot", + "묠": "myol", + "묡": "myok", + "묢": "myom", + "묣": "myop", + "묤": "myot", + "묥": "myot", + "묦": "myop", + "묧": "myol", + "묨": "myom", + "묩": "myop", + "묪": "myop", + "묫": "myot", + "묬": "myot", + "묭": "myong", + "묮": "myot", + "묯": "myot", + "묰": "myok", + "묱": "myot", + "묲": "myop", + "묳": "myot", + "무": "mu", + "묵": "muk", + "묶": "mukk", + "묷": "muk", + "문": "mun", + "묹": "mun", + "묺": "mun", + "묻": "mut", + "물": "mul", + "묽": "muk", + "묾": "mum", + "묿": "mup", + "뭀": "mut", + "뭁": "mut", + "뭂": "mup", + "뭃": "mul", + "뭄": "mum", + "뭅": "mup", + "뭆": "mup", + "뭇": "mut", + "뭈": "mut", + "뭉": "mung", + "뭊": "mut", + "뭋": "mut", + "뭌": "muk", + "뭍": "mut", + "뭎": "mup", + "뭏": "mut", + "뭐": "mwo", + "뭑": "mwok", + "뭒": "mwokk", + "뭓": "mwok", + "뭔": "mwon", + "뭕": "mwon", + "뭖": "mwon", + "뭗": "mwot", + "뭘": "mwol", + "뭙": "mwok", + "뭚": "mwom", + "뭛": "mwop", + "뭜": "mwot", + "뭝": "mwot", + "뭞": "mwop", + "뭟": "mwol", + "뭠": "mwom", + "뭡": "mwop", + "뭢": "mwop", + "뭣": "mwot", + "뭤": "mwot", + "뭥": "mwong", + "뭦": "mwot", + "뭧": "mwot", + "뭨": "mwok", + "뭩": "mwot", + "뭪": "mwop", + "뭫": "mwot", + "뭬": "mwe", + "뭭": "mwek", + "뭮": "mwekk", + "뭯": "mwek", + "뭰": "mwen", + "뭱": "mwen", + "뭲": "mwen", + "뭳": "mwet", + "뭴": "mwel", + "뭵": "mwek", + "뭶": "mwem", + "뭷": "mwep", + "뭸": "mwet", + "뭹": "mwet", + "뭺": "mwep", + "뭻": "mwel", + "뭼": "mwem", + "뭽": "mwep", + "뭾": "mwep", + "뭿": "mwet", + "뮀": "mwet", + "뮁": "mweng", + "뮂": "mwet", + "뮃": "mwet", + "뮄": "mwek", + "뮅": "mwet", + "뮆": "mwep", + "뮇": "mwet", + "뮈": "mwi", + "뮉": "mwik", + "뮊": "mwikk", + "뮋": "mwik", + "뮌": "mwin", + "뮍": "mwin", + "뮎": "mwin", + "뮏": "mwit", + "뮐": "mwil", + "뮑": "mwik", + "뮒": "mwim", + "뮓": "mwip", + "뮔": "mwit", + "뮕": "mwit", + "뮖": "mwip", + "뮗": "mwil", + "뮘": "mwim", + "뮙": "mwip", + "뮚": "mwip", + "뮛": "mwit", + "뮜": "mwit", + "뮝": "mwing", + "뮞": "mwit", + "뮟": "mwit", + "뮠": "mwik", + "뮡": "mwit", + "뮢": "mwip", + "뮣": "mwit", + "뮤": "myu", + "뮥": "myuk", + "뮦": "myukk", + "뮧": "myuk", + "뮨": "myun", + "뮩": "myun", + "뮪": "myun", + "뮫": "myut", + "뮬": "myul", + "뮭": "myuk", + "뮮": "myum", + "뮯": "myup", + "뮰": "myut", + "뮱": "myut", + "뮲": "myup", + "뮳": "myul", + "뮴": "myum", + "뮵": "myup", + "뮶": "myup", + "뮷": "myut", + "뮸": "myut", + "뮹": "myung", + "뮺": "myut", + "뮻": "myut", + "뮼": "myuk", + "뮽": "myut", + "뮾": "myup", + "뮿": "myut", + "므": "meu", + "믁": "meuk", + "믂": "meukk", + "믃": "meuk", + "믄": "meun", + "믅": "meun", + "믆": "meun", + "믇": "meut", + "믈": "meul", + "믉": "meuk", + "믊": "meum", + "믋": "meup", + "믌": "meut", + "믍": "meut", + "믎": "meup", + "믏": "meul", + "믐": "meum", + "믑": "meup", + "믒": "meup", + "믓": "meut", + "믔": "meut", + "믕": "meung", + "믖": "meut", + "믗": "meut", + "믘": "meuk", + "믙": "meut", + "믚": "meup", + "믛": "meut", + "믜": "meui", + "믝": "meuik", + "믞": "meuikk", + "믟": "meuik", + "믠": "meuin", + "믡": "meuin", + "믢": "meuin", + "믣": "meuit", + "믤": "meuil", + "믥": "meuik", + "믦": "meuim", + "믧": "meuip", + "믨": "meuit", + "믩": "meuit", + "믪": "meuip", + "믫": "meuil", + "믬": "meuim", + "믭": "meuip", + "믮": "meuip", + "믯": "meuit", + "믰": "meuit", + "믱": "meuing", + "믲": "meuit", + "믳": "meuit", + "믴": "meuik", + "믵": "meuit", + "믶": "meuip", + "믷": "meuit", + "미": "mi", + "믹": "mik", + "믺": "mikk", + "믻": "mik", + "민": "min", + "믽": "min", + "믾": "min", + "믿": "mit", + "밀": "mil", + "밁": "mik", + "밂": "mim", + "밃": "mip", + "밄": "mit", + "밅": "mit", + "밆": "mip", + "밇": "mil", + "밈": "mim", + "밉": "mip", + "밊": "mip", + "밋": "mit", + "밌": "mit", + "밍": "ming", + "밎": "mit", + "및": "mit", + "밐": "mik", + "밑": "mit", + "밒": "mip", + "밓": "mit", + "바": "ba", + "박": "bak", + "밖": "bakk", + "밗": "bak", + "반": "ban", + "밙": "ban", + "밚": "ban", + "받": "bat", + "발": "bal", + "밝": "bak", + "밞": "bam", + "밟": "bap", + "밠": "bat", + "밡": "bat", + "밢": "bap", + "밣": "bal", + "밤": "bam", + "밥": "bap", + "밦": "bap", + "밧": "bat", + "밨": "bat", + "방": "bang", + "밪": "bat", + "밫": "bat", + "밬": "bak", + "밭": "bat", + "밮": "bap", + "밯": "bat", + "배": "bae", + "백": "baek", + "밲": "baekk", + "밳": "baek", + "밴": "baen", + "밵": "baen", + "밶": "baen", + "밷": "baet", + "밸": "bael", + "밹": "baek", + "밺": "baem", + "밻": "baep", + "밼": "baet", + "밽": "baet", + "밾": "baep", + "밿": "bael", + "뱀": "baem", + "뱁": "baep", + "뱂": "baep", + "뱃": "baet", + "뱄": "baet", + "뱅": "baeng", + "뱆": "baet", + "뱇": "baet", + "뱈": "baek", + "뱉": "baet", + "뱊": "baep", + "뱋": "baet", + "뱌": "bya", + "뱍": "byak", + "뱎": "byakk", + "뱏": "byak", + "뱐": "byan", + "뱑": "byan", + "뱒": "byan", + "뱓": "byat", + "뱔": "byal", + "뱕": "byak", + "뱖": "byam", + "뱗": "byap", + "뱘": "byat", + "뱙": "byat", + "뱚": "byap", + "뱛": "byal", + "뱜": "byam", + "뱝": "byap", + "뱞": "byap", + "뱟": "byat", + "뱠": "byat", + "뱡": "byang", + "뱢": "byat", + "뱣": "byat", + "뱤": "byak", + "뱥": "byat", + "뱦": "byap", + "뱧": "byat", + "뱨": "byae", + "뱩": "byaek", + "뱪": "byaekk", + "뱫": "byaek", + "뱬": "byaen", + "뱭": "byaen", + "뱮": "byaen", + "뱯": "byaet", + "뱰": "byael", + "뱱": "byaek", + "뱲": "byaem", + "뱳": "byaep", + "뱴": "byaet", + "뱵": "byaet", + "뱶": "byaep", + "뱷": "byael", + "뱸": "byaem", + "뱹": "byaep", + "뱺": "byaep", + "뱻": "byaet", + "뱼": "byaet", + "뱽": "byaeng", + "뱾": "byaet", + "뱿": "byaet", + "벀": "byaek", + "벁": "byaet", + "벂": "byaep", + "벃": "byaet", + "버": "beo", + "벅": "beok", + "벆": "beokk", + "벇": "beok", + "번": "beon", + "벉": "beon", + "벊": "beon", + "벋": "beot", + "벌": "beol", + "벍": "beok", + "벎": "beom", + "벏": "beop", + "벐": "beot", + "벑": "beot", + "벒": "beop", + "벓": "beol", + "범": "beom", + "법": "beop", + "벖": "beop", + "벗": "beot", + "벘": "beot", + "벙": "beong", + "벚": "beot", + "벛": "beot", + "벜": "beok", + "벝": "beot", + "벞": "beop", + "벟": "beot", + "베": "be", + "벡": "bek", + "벢": "bekk", + "벣": "bek", + "벤": "ben", + "벥": "ben", + "벦": "ben", + "벧": "bet", + "벨": "bel", + "벩": "bek", + "벪": "bem", + "벫": "bep", + "벬": "bet", + "벭": "bet", + "벮": "bep", + "벯": "bel", + "벰": "bem", + "벱": "bep", + "벲": "bep", + "벳": "bet", + "벴": "bet", + "벵": "beng", + "벶": "bet", + "벷": "bet", + "벸": "bek", + "벹": "bet", + "벺": "bep", + "벻": "bet", + "벼": "byeo", + "벽": "byeok", + "벾": "byeokk", + "벿": "byeok", + "변": "byeon", + "볁": "byeon", + "볂": "byeon", + "볃": "byeot", + "별": "byeol", + "볅": "byeok", + "볆": "byeom", + "볇": "byeop", + "볈": "byeot", + "볉": "byeot", + "볊": "byeop", + "볋": "byeol", + "볌": "byeom", + "볍": "byeop", + "볎": "byeop", + "볏": "byeot", + "볐": "byeot", + "병": "byeong", + "볒": "byeot", + "볓": "byeot", + "볔": "byeok", + "볕": "byeot", + "볖": "byeop", + "볗": "byeot", + "볘": "bye", + "볙": "byek", + "볚": "byekk", + "볛": "byek", + "볜": "byen", + "볝": "byen", + "볞": "byen", + "볟": "byet", + "볠": "byel", + "볡": "byek", + "볢": "byem", + "볣": "byep", + "볤": "byet", + "볥": "byet", + "볦": "byep", + "볧": "byel", + "볨": "byem", + "볩": "byep", + "볪": "byep", + "볫": "byet", + "볬": "byet", + "볭": "byeng", + "볮": "byet", + "볯": "byet", + "볰": "byek", + "볱": "byet", + "볲": "byep", + "볳": "byet", + "보": "bo", + "복": "bok", + "볶": "bokk", + "볷": "bok", + "본": "bon", + "볹": "bon", + "볺": "bon", + "볻": "bot", + "볼": "bol", + "볽": "bok", + "볾": "bom", + "볿": "bop", + "봀": "bot", + "봁": "bot", + "봂": "bop", + "봃": "bol", + "봄": "bom", + "봅": "bop", + "봆": "bop", + "봇": "bot", + "봈": "bot", + "봉": "bong", + "봊": "bot", + "봋": "bot", + "봌": "bok", + "봍": "bot", + "봎": "bop", + "봏": "bot", + "봐": "bwa", + "봑": "bwak", + "봒": "bwakk", + "봓": "bwak", + "봔": "bwan", + "봕": "bwan", + "봖": "bwan", + "봗": "bwat", + "봘": "bwal", + "봙": "bwak", + "봚": "bwam", + "봛": "bwap", + "봜": "bwat", + "봝": "bwat", + "봞": "bwap", + "봟": "bwal", + "봠": "bwam", + "봡": "bwap", + "봢": "bwap", + "봣": "bwat", + "봤": "bwat", + "봥": "bwang", + "봦": "bwat", + "봧": "bwat", + "봨": "bwak", + "봩": "bwat", + "봪": "bwap", + "봫": "bwat", + "봬": "bwae", + "봭": "bwaek", + "봮": "bwaekk", + "봯": "bwaek", + "봰": "bwaen", + "봱": "bwaen", + "봲": "bwaen", + "봳": "bwaet", + "봴": "bwael", + "봵": "bwaek", + "봶": "bwaem", + "봷": "bwaep", + "봸": "bwaet", + "봹": "bwaet", + "봺": "bwaep", + "봻": "bwael", + "봼": "bwaem", + "봽": "bwaep", + "봾": "bwaep", + "봿": "bwaet", + "뵀": "bwaet", + "뵁": "bwaeng", + "뵂": "bwaet", + "뵃": "bwaet", + "뵄": "bwaek", + "뵅": "bwaet", + "뵆": "bwaep", + "뵇": "bwaet", + "뵈": "boe", + "뵉": "boek", + "뵊": "boekk", + "뵋": "boek", + "뵌": "boen", + "뵍": "boen", + "뵎": "boen", + "뵏": "boet", + "뵐": "boel", + "뵑": "boek", + "뵒": "boem", + "뵓": "boep", + "뵔": "boet", + "뵕": "boet", + "뵖": "boep", + "뵗": "boel", + "뵘": "boem", + "뵙": "boep", + "뵚": "boep", + "뵛": "boet", + "뵜": "boet", + "뵝": "boeng", + "뵞": "boet", + "뵟": "boet", + "뵠": "boek", + "뵡": "boet", + "뵢": "boep", + "뵣": "boet", + "뵤": "byo", + "뵥": "byok", + "뵦": "byokk", + "뵧": "byok", + "뵨": "byon", + "뵩": "byon", + "뵪": "byon", + "뵫": "byot", + "뵬": "byol", + "뵭": "byok", + "뵮": "byom", + "뵯": "byop", + "뵰": "byot", + "뵱": "byot", + "뵲": "byop", + "뵳": "byol", + "뵴": "byom", + "뵵": "byop", + "뵶": "byop", + "뵷": "byot", + "뵸": "byot", + "뵹": "byong", + "뵺": "byot", + "뵻": "byot", + "뵼": "byok", + "뵽": "byot", + "뵾": "byop", + "뵿": "byot", + "부": "bu", + "북": "buk", + "붂": "bukk", + "붃": "buk", + "분": "bun", + "붅": "bun", + "붆": "bun", + "붇": "but", + "불": "bul", + "붉": "buk", + "붊": "bum", + "붋": "bup", + "붌": "but", + "붍": "but", + "붎": "bup", + "붏": "bul", + "붐": "bum", + "붑": "bup", + "붒": "bup", + "붓": "but", + "붔": "but", + "붕": "bung", + "붖": "but", + "붗": "but", + "붘": "buk", + "붙": "but", + "붚": "bup", + "붛": "but", + "붜": "bwo", + "붝": "bwok", + "붞": "bwokk", + "붟": "bwok", + "붠": "bwon", + "붡": "bwon", + "붢": "bwon", + "붣": "bwot", + "붤": "bwol", + "붥": "bwok", + "붦": "bwom", + "붧": "bwop", + "붨": "bwot", + "붩": "bwot", + "붪": "bwop", + "붫": "bwol", + "붬": "bwom", + "붭": "bwop", + "붮": "bwop", + "붯": "bwot", + "붰": "bwot", + "붱": "bwong", + "붲": "bwot", + "붳": "bwot", + "붴": "bwok", + "붵": "bwot", + "붶": "bwop", + "붷": "bwot", + "붸": "bwe", + "붹": "bwek", + "붺": "bwekk", + "붻": "bwek", + "붼": "bwen", + "붽": "bwen", + "붾": "bwen", + "붿": "bwet", + "뷀": "bwel", + "뷁": "bwek", + "뷂": "bwem", + "뷃": "bwep", + "뷄": "bwet", + "뷅": "bwet", + "뷆": "bwep", + "뷇": "bwel", + "뷈": "bwem", + "뷉": "bwep", + "뷊": "bwep", + "뷋": "bwet", + "뷌": "bwet", + "뷍": "bweng", + "뷎": "bwet", + "뷏": "bwet", + "뷐": "bwek", + "뷑": "bwet", + "뷒": "bwep", + "뷓": "bwet", + "뷔": "bwi", + "뷕": "bwik", + "뷖": "bwikk", + "뷗": "bwik", + "뷘": "bwin", + "뷙": "bwin", + "뷚": "bwin", + "뷛": "bwit", + "뷜": "bwil", + "뷝": "bwik", + "뷞": "bwim", + "뷟": "bwip", + "뷠": "bwit", + "뷡": "bwit", + "뷢": "bwip", + "뷣": "bwil", + "뷤": "bwim", + "뷥": "bwip", + "뷦": "bwip", + "뷧": "bwit", + "뷨": "bwit", + "뷩": "bwing", + "뷪": "bwit", + "뷫": "bwit", + "뷬": "bwik", + "뷭": "bwit", + "뷮": "bwip", + "뷯": "bwit", + "뷰": "byu", + "뷱": "byuk", + "뷲": "byukk", + "뷳": "byuk", + "뷴": "byun", + "뷵": "byun", + "뷶": "byun", + "뷷": "byut", + "뷸": "byul", + "뷹": "byuk", + "뷺": "byum", + "뷻": "byup", + "뷼": "byut", + "뷽": "byut", + "뷾": "byup", + "뷿": "byul", + "븀": "byum", + "븁": "byup", + "븂": "byup", + "븃": "byut", + "븄": "byut", + "븅": "byung", + "븆": "byut", + "븇": "byut", + "븈": "byuk", + "븉": "byut", + "븊": "byup", + "븋": "byut", + "브": "beu", + "븍": "beuk", + "븎": "beukk", + "븏": "beuk", + "븐": "beun", + "븑": "beun", + "븒": "beun", + "븓": "beut", + "블": "beul", + "븕": "beuk", + "븖": "beum", + "븗": "beup", + "븘": "beut", + "븙": "beut", + "븚": "beup", + "븛": "beul", + "븜": "beum", + "븝": "beup", + "븞": "beup", + "븟": "beut", + "븠": "beut", + "븡": "beung", + "븢": "beut", + "븣": "beut", + "븤": "beuk", + "븥": "beut", + "븦": "beup", + "븧": "beut", + "븨": "beui", + "븩": "beuik", + "븪": "beuikk", + "븫": "beuik", + "븬": "beuin", + "븭": "beuin", + "븮": "beuin", + "븯": "beuit", + "븰": "beuil", + "븱": "beuik", + "븲": "beuim", + "븳": "beuip", + "븴": "beuit", + "븵": "beuit", + "븶": "beuip", + "븷": "beuil", + "븸": "beuim", + "븹": "beuip", + "븺": "beuip", + "븻": "beuit", + "븼": "beuit", + "븽": "beuing", + "븾": "beuit", + "븿": "beuit", + "빀": "beuik", + "빁": "beuit", + "빂": "beuip", + "빃": "beuit", + "비": "bi", + "빅": "bik", + "빆": "bikk", + "빇": "bik", + "빈": "bin", + "빉": "bin", + "빊": "bin", + "빋": "bit", + "빌": "bil", + "빍": "bik", + "빎": "bim", + "빏": "bip", + "빐": "bit", + "빑": "bit", + "빒": "bip", + "빓": "bil", + "빔": "bim", + "빕": "bip", + "빖": "bip", + "빗": "bit", + "빘": "bit", + "빙": "bing", + "빚": "bit", + "빛": "bit", + "빜": "bik", + "빝": "bit", + "빞": "bip", + "빟": "bit", + "빠": "ppa", + "빡": "ppak", + "빢": "ppakk", + "빣": "ppak", + "빤": "ppan", + "빥": "ppan", + "빦": "ppan", + "빧": "ppat", + "빨": "ppal", + "빩": "ppak", + "빪": "ppam", + "빫": "ppap", + "빬": "ppat", + "빭": "ppat", + "빮": "ppap", + "빯": "ppal", + "빰": "ppam", + "빱": "ppap", + "빲": "ppap", + "빳": "ppat", + "빴": "ppat", + "빵": "ppang", + "빶": "ppat", + "빷": "ppat", + "빸": "ppak", + "빹": "ppat", + "빺": "ppap", + "빻": "ppat", + "빼": "ppae", + "빽": "ppaek", + "빾": "ppaekk", + "빿": "ppaek", + "뺀": "ppaen", + "뺁": "ppaen", + "뺂": "ppaen", + "뺃": "ppaet", + "뺄": "ppael", + "뺅": "ppaek", + "뺆": "ppaem", + "뺇": "ppaep", + "뺈": "ppaet", + "뺉": "ppaet", + "뺊": "ppaep", + "뺋": "ppael", + "뺌": "ppaem", + "뺍": "ppaep", + "뺎": "ppaep", + "뺏": "ppaet", + "뺐": "ppaet", + "뺑": "ppaeng", + "뺒": "ppaet", + "뺓": "ppaet", + "뺔": "ppaek", + "뺕": "ppaet", + "뺖": "ppaep", + "뺗": "ppaet", + "뺘": "ppya", + "뺙": "ppyak", + "뺚": "ppyakk", + "뺛": "ppyak", + "뺜": "ppyan", + "뺝": "ppyan", + "뺞": "ppyan", + "뺟": "ppyat", + "뺠": "ppyal", + "뺡": "ppyak", + "뺢": "ppyam", + "뺣": "ppyap", + "뺤": "ppyat", + "뺥": "ppyat", + "뺦": "ppyap", + "뺧": "ppyal", + "뺨": "ppyam", + "뺩": "ppyap", + "뺪": "ppyap", + "뺫": "ppyat", + "뺬": "ppyat", + "뺭": "ppyang", + "뺮": "ppyat", + "뺯": "ppyat", + "뺰": "ppyak", + "뺱": "ppyat", + "뺲": "ppyap", + "뺳": "ppyat", + "뺴": "ppyae", + "뺵": "ppyaek", + "뺶": "ppyaekk", + "뺷": "ppyaek", + "뺸": "ppyaen", + "뺹": "ppyaen", + "뺺": "ppyaen", + "뺻": "ppyaet", + "뺼": "ppyael", + "뺽": "ppyaek", + "뺾": "ppyaem", + "뺿": "ppyaep", + "뻀": "ppyaet", + "뻁": "ppyaet", + "뻂": "ppyaep", + "뻃": "ppyael", + "뻄": "ppyaem", + "뻅": "ppyaep", + "뻆": "ppyaep", + "뻇": "ppyaet", + "뻈": "ppyaet", + "뻉": "ppyaeng", + "뻊": "ppyaet", + "뻋": "ppyaet", + "뻌": "ppyaek", + "뻍": "ppyaet", + "뻎": "ppyaep", + "뻏": "ppyaet", + "뻐": "ppeo", + "뻑": "ppeok", + "뻒": "ppeokk", + "뻓": "ppeok", + "뻔": "ppeon", + "뻕": "ppeon", + "뻖": "ppeon", + "뻗": "ppeot", + "뻘": "ppeol", + "뻙": "ppeok", + "뻚": "ppeom", + "뻛": "ppeop", + "뻜": "ppeot", + "뻝": "ppeot", + "뻞": "ppeop", + "뻟": "ppeol", + "뻠": "ppeom", + "뻡": "ppeop", + "뻢": "ppeop", + "뻣": "ppeot", + "뻤": "ppeot", + "뻥": "ppeong", + "뻦": "ppeot", + "뻧": "ppeot", + "뻨": "ppeok", + "뻩": "ppeot", + "뻪": "ppeop", + "뻫": "ppeot", + "뻬": "ppe", + "뻭": "ppek", + "뻮": "ppekk", + "뻯": "ppek", + "뻰": "ppen", + "뻱": "ppen", + "뻲": "ppen", + "뻳": "ppet", + "뻴": "ppel", + "뻵": "ppek", + "뻶": "ppem", + "뻷": "ppep", + "뻸": "ppet", + "뻹": "ppet", + "뻺": "ppep", + "뻻": "ppel", + "뻼": "ppem", + "뻽": "ppep", + "뻾": "ppep", + "뻿": "ppet", + "뼀": "ppet", + "뼁": "ppeng", + "뼂": "ppet", + "뼃": "ppet", + "뼄": "ppek", + "뼅": "ppet", + "뼆": "ppep", + "뼇": "ppet", + "뼈": "ppyeo", + "뼉": "ppyeok", + "뼊": "ppyeokk", + "뼋": "ppyeok", + "뼌": "ppyeon", + "뼍": "ppyeon", + "뼎": "ppyeon", + "뼏": "ppyeot", + "뼐": "ppyeol", + "뼑": "ppyeok", + "뼒": "ppyeom", + "뼓": "ppyeop", + "뼔": "ppyeot", + "뼕": "ppyeot", + "뼖": "ppyeop", + "뼗": "ppyeol", + "뼘": "ppyeom", + "뼙": "ppyeop", + "뼚": "ppyeop", + "뼛": "ppyeot", + "뼜": "ppyeot", + "뼝": "ppyeong", + "뼞": "ppyeot", + "뼟": "ppyeot", + "뼠": "ppyeok", + "뼡": "ppyeot", + "뼢": "ppyeop", + "뼣": "ppyeot", + "뼤": "ppye", + "뼥": "ppyek", + "뼦": "ppyekk", + "뼧": "ppyek", + "뼨": "ppyen", + "뼩": "ppyen", + "뼪": "ppyen", + "뼫": "ppyet", + "뼬": "ppyel", + "뼭": "ppyek", + "뼮": "ppyem", + "뼯": "ppyep", + "뼰": "ppyet", + "뼱": "ppyet", + "뼲": "ppyep", + "뼳": "ppyel", + "뼴": "ppyem", + "뼵": "ppyep", + "뼶": "ppyep", + "뼷": "ppyet", + "뼸": "ppyet", + "뼹": "ppyeng", + "뼺": "ppyet", + "뼻": "ppyet", + "뼼": "ppyek", + "뼽": "ppyet", + "뼾": "ppyep", + "뼿": "ppyet", + "뽀": "ppo", + "뽁": "ppok", + "뽂": "ppokk", + "뽃": "ppok", + "뽄": "ppon", + "뽅": "ppon", + "뽆": "ppon", + "뽇": "ppot", + "뽈": "ppol", + "뽉": "ppok", + "뽊": "ppom", + "뽋": "ppop", + "뽌": "ppot", + "뽍": "ppot", + "뽎": "ppop", + "뽏": "ppol", + "뽐": "ppom", + "뽑": "ppop", + "뽒": "ppop", + "뽓": "ppot", + "뽔": "ppot", + "뽕": "ppong", + "뽖": "ppot", + "뽗": "ppot", + "뽘": "ppok", + "뽙": "ppot", + "뽚": "ppop", + "뽛": "ppot", + "뽜": "ppwa", + "뽝": "ppwak", + "뽞": "ppwakk", + "뽟": "ppwak", + "뽠": "ppwan", + "뽡": "ppwan", + "뽢": "ppwan", + "뽣": "ppwat", + "뽤": "ppwal", + "뽥": "ppwak", + "뽦": "ppwam", + "뽧": "ppwap", + "뽨": "ppwat", + "뽩": "ppwat", + "뽪": "ppwap", + "뽫": "ppwal", + "뽬": "ppwam", + "뽭": "ppwap", + "뽮": "ppwap", + "뽯": "ppwat", + "뽰": "ppwat", + "뽱": "ppwang", + "뽲": "ppwat", + "뽳": "ppwat", + "뽴": "ppwak", + "뽵": "ppwat", + "뽶": "ppwap", + "뽷": "ppwat", + "뽸": "ppwae", + "뽹": "ppwaek", + "뽺": "ppwaekk", + "뽻": "ppwaek", + "뽼": "ppwaen", + "뽽": "ppwaen", + "뽾": "ppwaen", + "뽿": "ppwaet", + "뾀": "ppwael", + "뾁": "ppwaek", + "뾂": "ppwaem", + "뾃": "ppwaep", + "뾄": "ppwaet", + "뾅": "ppwaet", + "뾆": "ppwaep", + "뾇": "ppwael", + "뾈": "ppwaem", + "뾉": "ppwaep", + "뾊": "ppwaep", + "뾋": "ppwaet", + "뾌": "ppwaet", + "뾍": "ppwaeng", + "뾎": "ppwaet", + "뾏": "ppwaet", + "뾐": "ppwaek", + "뾑": "ppwaet", + "뾒": "ppwaep", + "뾓": "ppwaet", + "뾔": "ppoe", + "뾕": "ppoek", + "뾖": "ppoekk", + "뾗": "ppoek", + "뾘": "ppoen", + "뾙": "ppoen", + "뾚": "ppoen", + "뾛": "ppoet", + "뾜": "ppoel", + "뾝": "ppoek", + "뾞": "ppoem", + "뾟": "ppoep", + "뾠": "ppoet", + "뾡": "ppoet", + "뾢": "ppoep", + "뾣": "ppoel", + "뾤": "ppoem", + "뾥": "ppoep", + "뾦": "ppoep", + "뾧": "ppoet", + "뾨": "ppoet", + "뾩": "ppoeng", + "뾪": "ppoet", + "뾫": "ppoet", + "뾬": "ppoek", + "뾭": "ppoet", + "뾮": "ppoep", + "뾯": "ppoet", + "뾰": "ppyo", + "뾱": "ppyok", + "뾲": "ppyokk", + "뾳": "ppyok", + "뾴": "ppyon", + "뾵": "ppyon", + "뾶": "ppyon", + "뾷": "ppyot", + "뾸": "ppyol", + "뾹": "ppyok", + "뾺": "ppyom", + "뾻": "ppyop", + "뾼": "ppyot", + "뾽": "ppyot", + "뾾": "ppyop", + "뾿": "ppyol", + "뿀": "ppyom", + "뿁": "ppyop", + "뿂": "ppyop", + "뿃": "ppyot", + "뿄": "ppyot", + "뿅": "ppyong", + "뿆": "ppyot", + "뿇": "ppyot", + "뿈": "ppyok", + "뿉": "ppyot", + "뿊": "ppyop", + "뿋": "ppyot", + "뿌": "ppu", + "뿍": "ppuk", + "뿎": "ppukk", + "뿏": "ppuk", + "뿐": "ppun", + "뿑": "ppun", + "뿒": "ppun", + "뿓": "pput", + "뿔": "ppul", + "뿕": "ppuk", + "뿖": "ppum", + "뿗": "ppup", + "뿘": "pput", + "뿙": "pput", + "뿚": "ppup", + "뿛": "ppul", + "뿜": "ppum", + "뿝": "ppup", + "뿞": "ppup", + "뿟": "pput", + "뿠": "pput", + "뿡": "ppung", + "뿢": "pput", + "뿣": "pput", + "뿤": "ppuk", + "뿥": "pput", + "뿦": "ppup", + "뿧": "pput", + "뿨": "ppwo", + "뿩": "ppwok", + "뿪": "ppwokk", + "뿫": "ppwok", + "뿬": "ppwon", + "뿭": "ppwon", + "뿮": "ppwon", + "뿯": "ppwot", + "뿰": "ppwol", + "뿱": "ppwok", + "뿲": "ppwom", + "뿳": "ppwop", + "뿴": "ppwot", + "뿵": "ppwot", + "뿶": "ppwop", + "뿷": "ppwol", + "뿸": "ppwom", + "뿹": "ppwop", + "뿺": "ppwop", + "뿻": "ppwot", + "뿼": "ppwot", + "뿽": "ppwong", + "뿾": "ppwot", + "뿿": "ppwot", + "쀀": "ppwok", + "쀁": "ppwot", + "쀂": "ppwop", + "쀃": "ppwot", + "쀄": "ppwe", + "쀅": "ppwek", + "쀆": "ppwekk", + "쀇": "ppwek", + "쀈": "ppwen", + "쀉": "ppwen", + "쀊": "ppwen", + "쀋": "ppwet", + "쀌": "ppwel", + "쀍": "ppwek", + "쀎": "ppwem", + "쀏": "ppwep", + "쀐": "ppwet", + "쀑": "ppwet", + "쀒": "ppwep", + "쀓": "ppwel", + "쀔": "ppwem", + "쀕": "ppwep", + "쀖": "ppwep", + "쀗": "ppwet", + "쀘": "ppwet", + "쀙": "ppweng", + "쀚": "ppwet", + "쀛": "ppwet", + "쀜": "ppwek", + "쀝": "ppwet", + "쀞": "ppwep", + "쀟": "ppwet", + "쀠": "ppwi", + "쀡": "ppwik", + "쀢": "ppwikk", + "쀣": "ppwik", + "쀤": "ppwin", + "쀥": "ppwin", + "쀦": "ppwin", + "쀧": "ppwit", + "쀨": "ppwil", + "쀩": "ppwik", + "쀪": "ppwim", + "쀫": "ppwip", + "쀬": "ppwit", + "쀭": "ppwit", + "쀮": "ppwip", + "쀯": "ppwil", + "쀰": "ppwim", + "쀱": "ppwip", + "쀲": "ppwip", + "쀳": "ppwit", + "쀴": "ppwit", + "쀵": "ppwing", + "쀶": "ppwit", + "쀷": "ppwit", + "쀸": "ppwik", + "쀹": "ppwit", + "쀺": "ppwip", + "쀻": "ppwit", + "쀼": "ppyu", + "쀽": "ppyuk", + "쀾": "ppyukk", + "쀿": "ppyuk", + "쁀": "ppyun", + "쁁": "ppyun", + "쁂": "ppyun", + "쁃": "ppyut", + "쁄": "ppyul", + "쁅": "ppyuk", + "쁆": "ppyum", + "쁇": "ppyup", + "쁈": "ppyut", + "쁉": "ppyut", + "쁊": "ppyup", + "쁋": "ppyul", + "쁌": "ppyum", + "쁍": "ppyup", + "쁎": "ppyup", + "쁏": "ppyut", + "쁐": "ppyut", + "쁑": "ppyung", + "쁒": "ppyut", + "쁓": "ppyut", + "쁔": "ppyuk", + "쁕": "ppyut", + "쁖": "ppyup", + "쁗": "ppyut", + "쁘": "ppeu", + "쁙": "ppeuk", + "쁚": "ppeukk", + "쁛": "ppeuk", + "쁜": "ppeun", + "쁝": "ppeun", + "쁞": "ppeun", + "쁟": "ppeut", + "쁠": "ppeul", + "쁡": "ppeuk", + "쁢": "ppeum", + "쁣": "ppeup", + "쁤": "ppeut", + "쁥": "ppeut", + "쁦": "ppeup", + "쁧": "ppeul", + "쁨": "ppeum", + "쁩": "ppeup", + "쁪": "ppeup", + "쁫": "ppeut", + "쁬": "ppeut", + "쁭": "ppeung", + "쁮": "ppeut", + "쁯": "ppeut", + "쁰": "ppeuk", + "쁱": "ppeut", + "쁲": "ppeup", + "쁳": "ppeut", + "쁴": "ppeui", + "쁵": "ppeuik", + "쁶": "ppeuikk", + "쁷": "ppeuik", + "쁸": "ppeuin", + "쁹": "ppeuin", + "쁺": "ppeuin", + "쁻": "ppeuit", + "쁼": "ppeuil", + "쁽": "ppeuik", + "쁾": "ppeuim", + "쁿": "ppeuip", + "삀": "ppeuit", + "삁": "ppeuit", + "삂": "ppeuip", + "삃": "ppeuil", + "삄": "ppeuim", + "삅": "ppeuip", + "삆": "ppeuip", + "삇": "ppeuit", + "삈": "ppeuit", + "삉": "ppeuing", + "삊": "ppeuit", + "삋": "ppeuit", + "삌": "ppeuik", + "삍": "ppeuit", + "삎": "ppeuip", + "삏": "ppeuit", + "삐": "ppi", + "삑": "ppik", + "삒": "ppikk", + "삓": "ppik", + "삔": "ppin", + "삕": "ppin", + "삖": "ppin", + "삗": "ppit", + "삘": "ppil", + "삙": "ppik", + "삚": "ppim", + "삛": "ppip", + "삜": "ppit", + "삝": "ppit", + "삞": "ppip", + "삟": "ppil", + "삠": "ppim", + "삡": "ppip", + "삢": "ppip", + "삣": "ppit", + "삤": "ppit", + "삥": "pping", + "삦": "ppit", + "삧": "ppit", + "삨": "ppik", + "삩": "ppit", + "삪": "ppip", + "삫": "ppit", + "사": "sa", + "삭": "sak", + "삮": "sakk", + "삯": "sak", + "산": "san", + "삱": "san", + "삲": "san", + "삳": "sat", + "살": "sal", + "삵": "sak", + "삶": "sam", + "삷": "sap", + "삸": "sat", + "삹": "sat", + "삺": "sap", + "삻": "sal", + "삼": "sam", + "삽": "sap", + "삾": "sap", + "삿": "sat", + "샀": "sat", + "상": "sang", + "샂": "sat", + "샃": "sat", + "샄": "sak", + "샅": "sat", + "샆": "sap", + "샇": "sat", + "새": "sae", + "색": "saek", + "샊": "saekk", + "샋": "saek", + "샌": "saen", + "샍": "saen", + "샎": "saen", + "샏": "saet", + "샐": "sael", + "샑": "saek", + "샒": "saem", + "샓": "saep", + "샔": "saet", + "샕": "saet", + "샖": "saep", + "샗": "sael", + "샘": "saem", + "샙": "saep", + "샚": "saep", + "샛": "saet", + "샜": "saet", + "생": "saeng", + "샞": "saet", + "샟": "saet", + "샠": "saek", + "샡": "saet", + "샢": "saep", + "샣": "saet", + "샤": "sya", + "샥": "syak", + "샦": "syakk", + "샧": "syak", + "샨": "syan", + "샩": "syan", + "샪": "syan", + "샫": "syat", + "샬": "syal", + "샭": "syak", + "샮": "syam", + "샯": "syap", + "샰": "syat", + "샱": "syat", + "샲": "syap", + "샳": "syal", + "샴": "syam", + "샵": "syap", + "샶": "syap", + "샷": "syat", + "샸": "syat", + "샹": "syang", + "샺": "syat", + "샻": "syat", + "샼": "syak", + "샽": "syat", + "샾": "syap", + "샿": "syat", + "섀": "syae", + "섁": "syaek", + "섂": "syaekk", + "섃": "syaek", + "섄": "syaen", + "섅": "syaen", + "섆": "syaen", + "섇": "syaet", + "섈": "syael", + "섉": "syaek", + "섊": "syaem", + "섋": "syaep", + "섌": "syaet", + "섍": "syaet", + "섎": "syaep", + "섏": "syael", + "섐": "syaem", + "섑": "syaep", + "섒": "syaep", + "섓": "syaet", + "섔": "syaet", + "섕": "syaeng", + "섖": "syaet", + "섗": "syaet", + "섘": "syaek", + "섙": "syaet", + "섚": "syaep", + "섛": "syaet", + "서": "seo", + "석": "seok", + "섞": "seokk", + "섟": "seok", + "선": "seon", + "섡": "seon", + "섢": "seon", + "섣": "seot", + "설": "seol", + "섥": "seok", + "섦": "seom", + "섧": "seop", + "섨": "seot", + "섩": "seot", + "섪": "seop", + "섫": "seol", + "섬": "seom", + "섭": "seop", + "섮": "seop", + "섯": "seot", + "섰": "seot", + "성": "seong", + "섲": "seot", + "섳": "seot", + "섴": "seok", + "섵": "seot", + "섶": "seop", + "섷": "seot", + "세": "se", + "섹": "sek", + "섺": "sekk", + "섻": "sek", + "센": "sen", + "섽": "sen", + "섾": "sen", + "섿": "set", + "셀": "sel", + "셁": "sek", + "셂": "sem", + "셃": "sep", + "셄": "set", + "셅": "set", + "셆": "sep", + "셇": "sel", + "셈": "sem", + "셉": "sep", + "셊": "sep", + "셋": "set", + "셌": "set", + "셍": "seng", + "셎": "set", + "셏": "set", + "셐": "sek", + "셑": "set", + "셒": "sep", + "셓": "set", + "셔": "syeo", + "셕": "syeok", + "셖": "syeokk", + "셗": "syeok", + "션": "syeon", + "셙": "syeon", + "셚": "syeon", + "셛": "syeot", + "셜": "syeol", + "셝": "syeok", + "셞": "syeom", + "셟": "syeop", + "셠": "syeot", + "셡": "syeot", + "셢": "syeop", + "셣": "syeol", + "셤": "syeom", + "셥": "syeop", + "셦": "syeop", + "셧": "syeot", + "셨": "syeot", + "셩": "syeong", + "셪": "syeot", + "셫": "syeot", + "셬": "syeok", + "셭": "syeot", + "셮": "syeop", + "셯": "syeot", + "셰": "sye", + "셱": "syek", + "셲": "syekk", + "셳": "syek", + "셴": "syen", + "셵": "syen", + "셶": "syen", + "셷": "syet", + "셸": "syel", + "셹": "syek", + "셺": "syem", + "셻": "syep", + "셼": "syet", + "셽": "syet", + "셾": "syep", + "셿": "syel", + "솀": "syem", + "솁": "syep", + "솂": "syep", + "솃": "syet", + "솄": "syet", + "솅": "syeng", + "솆": "syet", + "솇": "syet", + "솈": "syek", + "솉": "syet", + "솊": "syep", + "솋": "syet", + "소": "so", + "속": "sok", + "솎": "sokk", + "솏": "sok", + "손": "son", + "솑": "son", + "솒": "son", + "솓": "sot", + "솔": "sol", + "솕": "sok", + "솖": "som", + "솗": "sop", + "솘": "sot", + "솙": "sot", + "솚": "sop", + "솛": "sol", + "솜": "som", + "솝": "sop", + "솞": "sop", + "솟": "sot", + "솠": "sot", + "송": "song", + "솢": "sot", + "솣": "sot", + "솤": "sok", + "솥": "sot", + "솦": "sop", + "솧": "sot", + "솨": "swa", + "솩": "swak", + "솪": "swakk", + "솫": "swak", + "솬": "swan", + "솭": "swan", + "솮": "swan", + "솯": "swat", + "솰": "swal", + "솱": "swak", + "솲": "swam", + "솳": "swap", + "솴": "swat", + "솵": "swat", + "솶": "swap", + "솷": "swal", + "솸": "swam", + "솹": "swap", + "솺": "swap", + "솻": "swat", + "솼": "swat", + "솽": "swang", + "솾": "swat", + "솿": "swat", + "쇀": "swak", + "쇁": "swat", + "쇂": "swap", + "쇃": "swat", + "쇄": "swae", + "쇅": "swaek", + "쇆": "swaekk", + "쇇": "swaek", + "쇈": "swaen", + "쇉": "swaen", + "쇊": "swaen", + "쇋": "swaet", + "쇌": "swael", + "쇍": "swaek", + "쇎": "swaem", + "쇏": "swaep", + "쇐": "swaet", + "쇑": "swaet", + "쇒": "swaep", + "쇓": "swael", + "쇔": "swaem", + "쇕": "swaep", + "쇖": "swaep", + "쇗": "swaet", + "쇘": "swaet", + "쇙": "swaeng", + "쇚": "swaet", + "쇛": "swaet", + "쇜": "swaek", + "쇝": "swaet", + "쇞": "swaep", + "쇟": "swaet", + "쇠": "soe", + "쇡": "soek", + "쇢": "soekk", + "쇣": "soek", + "쇤": "soen", + "쇥": "soen", + "쇦": "soen", + "쇧": "soet", + "쇨": "soel", + "쇩": "soek", + "쇪": "soem", + "쇫": "soep", + "쇬": "soet", + "쇭": "soet", + "쇮": "soep", + "쇯": "soel", + "쇰": "soem", + "쇱": "soep", + "쇲": "soep", + "쇳": "soet", + "쇴": "soet", + "쇵": "soeng", + "쇶": "soet", + "쇷": "soet", + "쇸": "soek", + "쇹": "soet", + "쇺": "soep", + "쇻": "soet", + "쇼": "syo", + "쇽": "syok", + "쇾": "syokk", + "쇿": "syok", + "숀": "syon", + "숁": "syon", + "숂": "syon", + "숃": "syot", + "숄": "syol", + "숅": "syok", + "숆": "syom", + "숇": "syop", + "숈": "syot", + "숉": "syot", + "숊": "syop", + "숋": "syol", + "숌": "syom", + "숍": "syop", + "숎": "syop", + "숏": "syot", + "숐": "syot", + "숑": "syong", + "숒": "syot", + "숓": "syot", + "숔": "syok", + "숕": "syot", + "숖": "syop", + "숗": "syot", + "수": "su", + "숙": "suk", + "숚": "sukk", + "숛": "suk", + "순": "sun", + "숝": "sun", + "숞": "sun", + "숟": "sut", + "술": "sul", + "숡": "suk", + "숢": "sum", + "숣": "sup", + "숤": "sut", + "숥": "sut", + "숦": "sup", + "숧": "sul", + "숨": "sum", + "숩": "sup", + "숪": "sup", + "숫": "sut", + "숬": "sut", + "숭": "sung", + "숮": "sut", + "숯": "sut", + "숰": "suk", + "숱": "sut", + "숲": "sup", + "숳": "sut", + "숴": "swo", + "숵": "swok", + "숶": "swokk", + "숷": "swok", + "숸": "swon", + "숹": "swon", + "숺": "swon", + "숻": "swot", + "숼": "swol", + "숽": "swok", + "숾": "swom", + "숿": "swop", + "쉀": "swot", + "쉁": "swot", + "쉂": "swop", + "쉃": "swol", + "쉄": "swom", + "쉅": "swop", + "쉆": "swop", + "쉇": "swot", + "쉈": "swot", + "쉉": "swong", + "쉊": "swot", + "쉋": "swot", + "쉌": "swok", + "쉍": "swot", + "쉎": "swop", + "쉏": "swot", + "쉐": "swe", + "쉑": "swek", + "쉒": "swekk", + "쉓": "swek", + "쉔": "swen", + "쉕": "swen", + "쉖": "swen", + "쉗": "swet", + "쉘": "swel", + "쉙": "swek", + "쉚": "swem", + "쉛": "swep", + "쉜": "swet", + "쉝": "swet", + "쉞": "swep", + "쉟": "swel", + "쉠": "swem", + "쉡": "swep", + "쉢": "swep", + "쉣": "swet", + "쉤": "swet", + "쉥": "sweng", + "쉦": "swet", + "쉧": "swet", + "쉨": "swek", + "쉩": "swet", + "쉪": "swep", + "쉫": "swet", + "쉬": "swi", + "쉭": "swik", + "쉮": "swikk", + "쉯": "swik", + "쉰": "swin", + "쉱": "swin", + "쉲": "swin", + "쉳": "swit", + "쉴": "swil", + "쉵": "swik", + "쉶": "swim", + "쉷": "swip", + "쉸": "swit", + "쉹": "swit", + "쉺": "swip", + "쉻": "swil", + "쉼": "swim", + "쉽": "swip", + "쉾": "swip", + "쉿": "swit", + "슀": "swit", + "슁": "swing", + "슂": "swit", + "슃": "swit", + "슄": "swik", + "슅": "swit", + "슆": "swip", + "슇": "swit", + "슈": "syu", + "슉": "syuk", + "슊": "syukk", + "슋": "syuk", + "슌": "syun", + "슍": "syun", + "슎": "syun", + "슏": "syut", + "슐": "syul", + "슑": "syuk", + "슒": "syum", + "슓": "syup", + "슔": "syut", + "슕": "syut", + "슖": "syup", + "슗": "syul", + "슘": "syum", + "슙": "syup", + "슚": "syup", + "슛": "syut", + "슜": "syut", + "슝": "syung", + "슞": "syut", + "슟": "syut", + "슠": "syuk", + "슡": "syut", + "슢": "syup", + "슣": "syut", + "스": "seu", + "슥": "seuk", + "슦": "seukk", + "슧": "seuk", + "슨": "seun", + "슩": "seun", + "슪": "seun", + "슫": "seut", + "슬": "seul", + "슭": "seuk", + "슮": "seum", + "슯": "seup", + "슰": "seut", + "슱": "seut", + "슲": "seup", + "슳": "seul", + "슴": "seum", + "습": "seup", + "슶": "seup", + "슷": "seut", + "슸": "seut", + "승": "seung", + "슺": "seut", + "슻": "seut", + "슼": "seuk", + "슽": "seut", + "슾": "seup", + "슿": "seut", + "싀": "seui", + "싁": "seuik", + "싂": "seuikk", + "싃": "seuik", + "싄": "seuin", + "싅": "seuin", + "싆": "seuin", + "싇": "seuit", + "싈": "seuil", + "싉": "seuik", + "싊": "seuim", + "싋": "seuip", + "싌": "seuit", + "싍": "seuit", + "싎": "seuip", + "싏": "seuil", + "싐": "seuim", + "싑": "seuip", + "싒": "seuip", + "싓": "seuit", + "싔": "seuit", + "싕": "seuing", + "싖": "seuit", + "싗": "seuit", + "싘": "seuik", + "싙": "seuit", + "싚": "seuip", + "싛": "seuit", + "시": "si", + "식": "sik", + "싞": "sikk", + "싟": "sik", + "신": "sin", + "싡": "sin", + "싢": "sin", + "싣": "sit", + "실": "sil", + "싥": "sik", + "싦": "sim", + "싧": "sip", + "싨": "sit", + "싩": "sit", + "싪": "sip", + "싫": "sil", + "심": "sim", + "십": "sip", + "싮": "sip", + "싯": "sit", + "싰": "sit", + "싱": "sing", + "싲": "sit", + "싳": "sit", + "싴": "sik", + "싵": "sit", + "싶": "sip", + "싷": "sit", + "싸": "ssa", + "싹": "ssak", + "싺": "ssakk", + "싻": "ssak", + "싼": "ssan", + "싽": "ssan", + "싾": "ssan", + "싿": "ssat", + "쌀": "ssal", + "쌁": "ssak", + "쌂": "ssam", + "쌃": "ssap", + "쌄": "ssat", + "쌅": "ssat", + "쌆": "ssap", + "쌇": "ssal", + "쌈": "ssam", + "쌉": "ssap", + "쌊": "ssap", + "쌋": "ssat", + "쌌": "ssat", + "쌍": "ssang", + "쌎": "ssat", + "쌏": "ssat", + "쌐": "ssak", + "쌑": "ssat", + "쌒": "ssap", + "쌓": "ssat", + "쌔": "ssae", + "쌕": "ssaek", + "쌖": "ssaekk", + "쌗": "ssaek", + "쌘": "ssaen", + "쌙": "ssaen", + "쌚": "ssaen", + "쌛": "ssaet", + "쌜": "ssael", + "쌝": "ssaek", + "쌞": "ssaem", + "쌟": "ssaep", + "쌠": "ssaet", + "쌡": "ssaet", + "쌢": "ssaep", + "쌣": "ssael", + "쌤": "ssaem", + "쌥": "ssaep", + "쌦": "ssaep", + "쌧": "ssaet", + "쌨": "ssaet", + "쌩": "ssaeng", + "쌪": "ssaet", + "쌫": "ssaet", + "쌬": "ssaek", + "쌭": "ssaet", + "쌮": "ssaep", + "쌯": "ssaet", + "쌰": "ssya", + "쌱": "ssyak", + "쌲": "ssyakk", + "쌳": "ssyak", + "쌴": "ssyan", + "쌵": "ssyan", + "쌶": "ssyan", + "쌷": "ssyat", + "쌸": "ssyal", + "쌹": "ssyak", + "쌺": "ssyam", + "쌻": "ssyap", + "쌼": "ssyat", + "쌽": "ssyat", + "쌾": "ssyap", + "쌿": "ssyal", + "썀": "ssyam", + "썁": "ssyap", + "썂": "ssyap", + "썃": "ssyat", + "썄": "ssyat", + "썅": "ssyang", + "썆": "ssyat", + "썇": "ssyat", + "썈": "ssyak", + "썉": "ssyat", + "썊": "ssyap", + "썋": "ssyat", + "썌": "ssyae", + "썍": "ssyaek", + "썎": "ssyaekk", + "썏": "ssyaek", + "썐": "ssyaen", + "썑": "ssyaen", + "썒": "ssyaen", + "썓": "ssyaet", + "썔": "ssyael", + "썕": "ssyaek", + "썖": "ssyaem", + "썗": "ssyaep", + "썘": "ssyaet", + "썙": "ssyaet", + "썚": "ssyaep", + "썛": "ssyael", + "썜": "ssyaem", + "썝": "ssyaep", + "썞": "ssyaep", + "썟": "ssyaet", + "썠": "ssyaet", + "썡": "ssyaeng", + "썢": "ssyaet", + "썣": "ssyaet", + "썤": "ssyaek", + "썥": "ssyaet", + "썦": "ssyaep", + "썧": "ssyaet", + "써": "sseo", + "썩": "sseok", + "썪": "sseokk", + "썫": "sseok", + "썬": "sseon", + "썭": "sseon", + "썮": "sseon", + "썯": "sseot", + "썰": "sseol", + "썱": "sseok", + "썲": "sseom", + "썳": "sseop", + "썴": "sseot", + "썵": "sseot", + "썶": "sseop", + "썷": "sseol", + "썸": "sseom", + "썹": "sseop", + "썺": "sseop", + "썻": "sseot", + "썼": "sseot", + "썽": "sseong", + "썾": "sseot", + "썿": "sseot", + "쎀": "sseok", + "쎁": "sseot", + "쎂": "sseop", + "쎃": "sseot", + "쎄": "sse", + "쎅": "ssek", + "쎆": "ssekk", + "쎇": "ssek", + "쎈": "ssen", + "쎉": "ssen", + "쎊": "ssen", + "쎋": "sset", + "쎌": "ssel", + "쎍": "ssek", + "쎎": "ssem", + "쎏": "ssep", + "쎐": "sset", + "쎑": "sset", + "쎒": "ssep", + "쎓": "ssel", + "쎔": "ssem", + "쎕": "ssep", + "쎖": "ssep", + "쎗": "sset", + "쎘": "sset", + "쎙": "sseng", + "쎚": "sset", + "쎛": "sset", + "쎜": "ssek", + "쎝": "sset", + "쎞": "ssep", + "쎟": "sset", + "쎠": "ssyeo", + "쎡": "ssyeok", + "쎢": "ssyeokk", + "쎣": "ssyeok", + "쎤": "ssyeon", + "쎥": "ssyeon", + "쎦": "ssyeon", + "쎧": "ssyeot", + "쎨": "ssyeol", + "쎩": "ssyeok", + "쎪": "ssyeom", + "쎫": "ssyeop", + "쎬": "ssyeot", + "쎭": "ssyeot", + "쎮": "ssyeop", + "쎯": "ssyeol", + "쎰": "ssyeom", + "쎱": "ssyeop", + "쎲": "ssyeop", + "쎳": "ssyeot", + "쎴": "ssyeot", + "쎵": "ssyeong", + "쎶": "ssyeot", + "쎷": "ssyeot", + "쎸": "ssyeok", + "쎹": "ssyeot", + "쎺": "ssyeop", + "쎻": "ssyeot", + "쎼": "ssye", + "쎽": "ssyek", + "쎾": "ssyekk", + "쎿": "ssyek", + "쏀": "ssyen", + "쏁": "ssyen", + "쏂": "ssyen", + "쏃": "ssyet", + "쏄": "ssyel", + "쏅": "ssyek", + "쏆": "ssyem", + "쏇": "ssyep", + "쏈": "ssyet", + "쏉": "ssyet", + "쏊": "ssyep", + "쏋": "ssyel", + "쏌": "ssyem", + "쏍": "ssyep", + "쏎": "ssyep", + "쏏": "ssyet", + "쏐": "ssyet", + "쏑": "ssyeng", + "쏒": "ssyet", + "쏓": "ssyet", + "쏔": "ssyek", + "쏕": "ssyet", + "쏖": "ssyep", + "쏗": "ssyet", + "쏘": "sso", + "쏙": "ssok", + "쏚": "ssokk", + "쏛": "ssok", + "쏜": "sson", + "쏝": "sson", + "쏞": "sson", + "쏟": "ssot", + "쏠": "ssol", + "쏡": "ssok", + "쏢": "ssom", + "쏣": "ssop", + "쏤": "ssot", + "쏥": "ssot", + "쏦": "ssop", + "쏧": "ssol", + "쏨": "ssom", + "쏩": "ssop", + "쏪": "ssop", + "쏫": "ssot", + "쏬": "ssot", + "쏭": "ssong", + "쏮": "ssot", + "쏯": "ssot", + "쏰": "ssok", + "쏱": "ssot", + "쏲": "ssop", + "쏳": "ssot", + "쏴": "sswa", + "쏵": "sswak", + "쏶": "sswakk", + "쏷": "sswak", + "쏸": "sswan", + "쏹": "sswan", + "쏺": "sswan", + "쏻": "sswat", + "쏼": "sswal", + "쏽": "sswak", + "쏾": "sswam", + "쏿": "sswap", + "쐀": "sswat", + "쐁": "sswat", + "쐂": "sswap", + "쐃": "sswal", + "쐄": "sswam", + "쐅": "sswap", + "쐆": "sswap", + "쐇": "sswat", + "쐈": "sswat", + "쐉": "sswang", + "쐊": "sswat", + "쐋": "sswat", + "쐌": "sswak", + "쐍": "sswat", + "쐎": "sswap", + "쐏": "sswat", + "쐐": "sswae", + "쐑": "sswaek", + "쐒": "sswaekk", + "쐓": "sswaek", + "쐔": "sswaen", + "쐕": "sswaen", + "쐖": "sswaen", + "쐗": "sswaet", + "쐘": "sswael", + "쐙": "sswaek", + "쐚": "sswaem", + "쐛": "sswaep", + "쐜": "sswaet", + "쐝": "sswaet", + "쐞": "sswaep", + "쐟": "sswael", + "쐠": "sswaem", + "쐡": "sswaep", + "쐢": "sswaep", + "쐣": "sswaet", + "쐤": "sswaet", + "쐥": "sswaeng", + "쐦": "sswaet", + "쐧": "sswaet", + "쐨": "sswaek", + "쐩": "sswaet", + "쐪": "sswaep", + "쐫": "sswaet", + "쐬": "ssoe", + "쐭": "ssoek", + "쐮": "ssoekk", + "쐯": "ssoek", + "쐰": "ssoen", + "쐱": "ssoen", + "쐲": "ssoen", + "쐳": "ssoet", + "쐴": "ssoel", + "쐵": "ssoek", + "쐶": "ssoem", + "쐷": "ssoep", + "쐸": "ssoet", + "쐹": "ssoet", + "쐺": "ssoep", + "쐻": "ssoel", + "쐼": "ssoem", + "쐽": "ssoep", + "쐾": "ssoep", + "쐿": "ssoet", + "쑀": "ssoet", + "쑁": "ssoeng", + "쑂": "ssoet", + "쑃": "ssoet", + "쑄": "ssoek", + "쑅": "ssoet", + "쑆": "ssoep", + "쑇": "ssoet", + "쑈": "ssyo", + "쑉": "ssyok", + "쑊": "ssyokk", + "쑋": "ssyok", + "쑌": "ssyon", + "쑍": "ssyon", + "쑎": "ssyon", + "쑏": "ssyot", + "쑐": "ssyol", + "쑑": "ssyok", + "쑒": "ssyom", + "쑓": "ssyop", + "쑔": "ssyot", + "쑕": "ssyot", + "쑖": "ssyop", + "쑗": "ssyol", + "쑘": "ssyom", + "쑙": "ssyop", + "쑚": "ssyop", + "쑛": "ssyot", + "쑜": "ssyot", + "쑝": "ssyong", + "쑞": "ssyot", + "쑟": "ssyot", + "쑠": "ssyok", + "쑡": "ssyot", + "쑢": "ssyop", + "쑣": "ssyot", + "쑤": "ssu", + "쑥": "ssuk", + "쑦": "ssukk", + "쑧": "ssuk", + "쑨": "ssun", + "쑩": "ssun", + "쑪": "ssun", + "쑫": "ssut", + "쑬": "ssul", + "쑭": "ssuk", + "쑮": "ssum", + "쑯": "ssup", + "쑰": "ssut", + "쑱": "ssut", + "쑲": "ssup", + "쑳": "ssul", + "쑴": "ssum", + "쑵": "ssup", + "쑶": "ssup", + "쑷": "ssut", + "쑸": "ssut", + "쑹": "ssung", + "쑺": "ssut", + "쑻": "ssut", + "쑼": "ssuk", + "쑽": "ssut", + "쑾": "ssup", + "쑿": "ssut", + "쒀": "sswo", + "쒁": "sswok", + "쒂": "sswokk", + "쒃": "sswok", + "쒄": "sswon", + "쒅": "sswon", + "쒆": "sswon", + "쒇": "sswot", + "쒈": "sswol", + "쒉": "sswok", + "쒊": "sswom", + "쒋": "sswop", + "쒌": "sswot", + "쒍": "sswot", + "쒎": "sswop", + "쒏": "sswol", + "쒐": "sswom", + "쒑": "sswop", + "쒒": "sswop", + "쒓": "sswot", + "쒔": "sswot", + "쒕": "sswong", + "쒖": "sswot", + "쒗": "sswot", + "쒘": "sswok", + "쒙": "sswot", + "쒚": "sswop", + "쒛": "sswot", + "쒜": "sswe", + "쒝": "sswek", + "쒞": "sswekk", + "쒟": "sswek", + "쒠": "sswen", + "쒡": "sswen", + "쒢": "sswen", + "쒣": "sswet", + "쒤": "sswel", + "쒥": "sswek", + "쒦": "sswem", + "쒧": "sswep", + "쒨": "sswet", + "쒩": "sswet", + "쒪": "sswep", + "쒫": "sswel", + "쒬": "sswem", + "쒭": "sswep", + "쒮": "sswep", + "쒯": "sswet", + "쒰": "sswet", + "쒱": "ssweng", + "쒲": "sswet", + "쒳": "sswet", + "쒴": "sswek", + "쒵": "sswet", + "쒶": "sswep", + "쒷": "sswet", + "쒸": "sswi", + "쒹": "sswik", + "쒺": "sswikk", + "쒻": "sswik", + "쒼": "sswin", + "쒽": "sswin", + "쒾": "sswin", + "쒿": "sswit", + "쓀": "sswil", + "쓁": "sswik", + "쓂": "sswim", + "쓃": "sswip", + "쓄": "sswit", + "쓅": "sswit", + "쓆": "sswip", + "쓇": "sswil", + "쓈": "sswim", + "쓉": "sswip", + "쓊": "sswip", + "쓋": "sswit", + "쓌": "sswit", + "쓍": "sswing", + "쓎": "sswit", + "쓏": "sswit", + "쓐": "sswik", + "쓑": "sswit", + "쓒": "sswip", + "쓓": "sswit", + "쓔": "ssyu", + "쓕": "ssyuk", + "쓖": "ssyukk", + "쓗": "ssyuk", + "쓘": "ssyun", + "쓙": "ssyun", + "쓚": "ssyun", + "쓛": "ssyut", + "쓜": "ssyul", + "쓝": "ssyuk", + "쓞": "ssyum", + "쓟": "ssyup", + "쓠": "ssyut", + "쓡": "ssyut", + "쓢": "ssyup", + "쓣": "ssyul", + "쓤": "ssyum", + "쓥": "ssyup", + "쓦": "ssyup", + "쓧": "ssyut", + "쓨": "ssyut", + "쓩": "ssyung", + "쓪": "ssyut", + "쓫": "ssyut", + "쓬": "ssyuk", + "쓭": "ssyut", + "쓮": "ssyup", + "쓯": "ssyut", + "쓰": "sseu", + "쓱": "sseuk", + "쓲": "sseukk", + "쓳": "sseuk", + "쓴": "sseun", + "쓵": "sseun", + "쓶": "sseun", + "쓷": "sseut", + "쓸": "sseul", + "쓹": "sseuk", + "쓺": "sseum", + "쓻": "sseup", + "쓼": "sseut", + "쓽": "sseut", + "쓾": "sseup", + "쓿": "sseul", + "씀": "sseum", + "씁": "sseup", + "씂": "sseup", + "씃": "sseut", + "씄": "sseut", + "씅": "sseung", + "씆": "sseut", + "씇": "sseut", + "씈": "sseuk", + "씉": "sseut", + "씊": "sseup", + "씋": "sseut", + "씌": "sseui", + "씍": "sseuik", + "씎": "sseuikk", + "씏": "sseuik", + "씐": "sseuin", + "씑": "sseuin", + "씒": "sseuin", + "씓": "sseuit", + "씔": "sseuil", + "씕": "sseuik", + "씖": "sseuim", + "씗": "sseuip", + "씘": "sseuit", + "씙": "sseuit", + "씚": "sseuip", + "씛": "sseuil", + "씜": "sseuim", + "씝": "sseuip", + "씞": "sseuip", + "씟": "sseuit", + "씠": "sseuit", + "씡": "sseuing", + "씢": "sseuit", + "씣": "sseuit", + "씤": "sseuik", + "씥": "sseuit", + "씦": "sseuip", + "씧": "sseuit", + "씨": "ssi", + "씩": "ssik", + "씪": "ssikk", + "씫": "ssik", + "씬": "ssin", + "씭": "ssin", + "씮": "ssin", + "씯": "ssit", + "씰": "ssil", + "씱": "ssik", + "씲": "ssim", + "씳": "ssip", + "씴": "ssit", + "씵": "ssit", + "씶": "ssip", + "씷": "ssil", + "씸": "ssim", + "씹": "ssip", + "씺": "ssip", + "씻": "ssit", + "씼": "ssit", + "씽": "ssing", + "씾": "ssit", + "씿": "ssit", + "앀": "ssik", + "앁": "ssit", + "앂": "ssip", + "앃": "ssit", + "아": "a", + "악": "ak", + "앆": "akk", + "앇": "ak", + "안": "an", + "앉": "an", + "않": "an", + "앋": "at", + "알": "al", + "앍": "ak", + "앎": "am", + "앏": "ap", + "앐": "at", + "앑": "at", + "앒": "ap", + "앓": "al", + "암": "am", + "압": "ap", + "앖": "ap", + "앗": "at", + "았": "at", + "앙": "ang", + "앚": "at", + "앛": "at", + "앜": "ak", + "앝": "at", + "앞": "ap", + "앟": "at", + "애": "ae", + "액": "aek", + "앢": "aekk", + "앣": "aek", + "앤": "aen", + "앥": "aen", + "앦": "aen", + "앧": "aet", + "앨": "ael", + "앩": "aek", + "앪": "aem", + "앫": "aep", + "앬": "aet", + "앭": "aet", + "앮": "aep", + "앯": "ael", + "앰": "aem", + "앱": "aep", + "앲": "aep", + "앳": "aet", + "앴": "aet", + "앵": "aeng", + "앶": "aet", + "앷": "aet", + "앸": "aek", + "앹": "aet", + "앺": "aep", + "앻": "aet", + "야": "ya", + "약": "yak", + "앾": "yakk", + "앿": "yak", + "얀": "yan", + "얁": "yan", + "얂": "yan", + "얃": "yat", + "얄": "yal", + "얅": "yak", + "얆": "yam", + "얇": "yap", + "얈": "yat", + "얉": "yat", + "얊": "yap", + "얋": "yal", + "얌": "yam", + "얍": "yap", + "얎": "yap", + "얏": "yat", + "얐": "yat", + "양": "yang", + "얒": "yat", + "얓": "yat", + "얔": "yak", + "얕": "yat", + "얖": "yap", + "얗": "yat", + "얘": "yae", + "얙": "yaek", + "얚": "yaekk", + "얛": "yaek", + "얜": "yaen", + "얝": "yaen", + "얞": "yaen", + "얟": "yaet", + "얠": "yael", + "얡": "yaek", + "얢": "yaem", + "얣": "yaep", + "얤": "yaet", + "얥": "yaet", + "얦": "yaep", + "얧": "yael", + "얨": "yaem", + "얩": "yaep", + "얪": "yaep", + "얫": "yaet", + "얬": "yaet", + "얭": "yaeng", + "얮": "yaet", + "얯": "yaet", + "얰": "yaek", + "얱": "yaet", + "얲": "yaep", + "얳": "yaet", + "어": "eo", + "억": "eok", + "얶": "eokk", + "얷": "eok", + "언": "eon", + "얹": "eon", + "얺": "eon", + "얻": "eot", + "얼": "eol", + "얽": "eok", + "얾": "eom", + "얿": "eop", + "엀": "eot", + "엁": "eot", + "엂": "eop", + "엃": "eol", + "엄": "eom", + "업": "eop", + "없": "eop", + "엇": "eot", + "었": "eot", + "엉": "eong", + "엊": "eot", + "엋": "eot", + "엌": "eok", + "엍": "eot", + "엎": "eop", + "엏": "eot", + "에": "e", + "엑": "ek", + "엒": "ekk", + "엓": "ek", + "엔": "en", + "엕": "en", + "엖": "en", + "엗": "et", + "엘": "el", + "엙": "ek", + "엚": "em", + "엛": "ep", + "엜": "et", + "엝": "et", + "엞": "ep", + "엟": "el", + "엠": "em", + "엡": "ep", + "엢": "ep", + "엣": "et", + "엤": "et", + "엥": "eng", + "엦": "et", + "엧": "et", + "엨": "ek", + "엩": "et", + "엪": "ep", + "엫": "et", + "여": "yeo", + "역": "yeok", + "엮": "yeokk", + "엯": "yeok", + "연": "yeon", + "엱": "yeon", + "엲": "yeon", + "엳": "yeot", + "열": "yeol", + "엵": "yeok", + "엶": "yeom", + "엷": "yeop", + "엸": "yeot", + "엹": "yeot", + "엺": "yeop", + "엻": "yeol", + "염": "yeom", + "엽": "yeop", + "엾": "yeop", + "엿": "yeot", + "였": "yeot", + "영": "yeong", + "옂": "yeot", + "옃": "yeot", + "옄": "yeok", + "옅": "yeot", + "옆": "yeop", + "옇": "yeot", + "예": "ye", + "옉": "yek", + "옊": "yekk", + "옋": "yek", + "옌": "yen", + "옍": "yen", + "옎": "yen", + "옏": "yet", + "옐": "yel", + "옑": "yek", + "옒": "yem", + "옓": "yep", + "옔": "yet", + "옕": "yet", + "옖": "yep", + "옗": "yel", + "옘": "yem", + "옙": "yep", + "옚": "yep", + "옛": "yet", + "옜": "yet", + "옝": "yeng", + "옞": "yet", + "옟": "yet", + "옠": "yek", + "옡": "yet", + "옢": "yep", + "옣": "yet", + "오": "o", + "옥": "ok", + "옦": "okk", + "옧": "ok", + "온": "on", + "옩": "on", + "옪": "on", + "옫": "ot", + "올": "ol", + "옭": "ok", + "옮": "om", + "옯": "op", + "옰": "ot", + "옱": "ot", + "옲": "op", + "옳": "ol", + "옴": "om", + "옵": "op", + "옶": "op", + "옷": "ot", + "옸": "ot", + "옹": "ong", + "옺": "ot", + "옻": "ot", + "옼": "ok", + "옽": "ot", + "옾": "op", + "옿": "ot", + "와": "wa", + "왁": "wak", + "왂": "wakk", + "왃": "wak", + "완": "wan", + "왅": "wan", + "왆": "wan", + "왇": "wat", + "왈": "wal", + "왉": "wak", + "왊": "wam", + "왋": "wap", + "왌": "wat", + "왍": "wat", + "왎": "wap", + "왏": "wal", + "왐": "wam", + "왑": "wap", + "왒": "wap", + "왓": "wat", + "왔": "wat", + "왕": "wang", + "왖": "wat", + "왗": "wat", + "왘": "wak", + "왙": "wat", + "왚": "wap", + "왛": "wat", + "왜": "wae", + "왝": "waek", + "왞": "waekk", + "왟": "waek", + "왠": "waen", + "왡": "waen", + "왢": "waen", + "왣": "waet", + "왤": "wael", + "왥": "waek", + "왦": "waem", + "왧": "waep", + "왨": "waet", + "왩": "waet", + "왪": "waep", + "왫": "wael", + "왬": "waem", + "왭": "waep", + "왮": "waep", + "왯": "waet", + "왰": "waet", + "왱": "waeng", + "왲": "waet", + "왳": "waet", + "왴": "waek", + "왵": "waet", + "왶": "waep", + "왷": "waet", + "외": "oe", + "왹": "oek", + "왺": "oekk", + "왻": "oek", + "왼": "oen", + "왽": "oen", + "왾": "oen", + "왿": "oet", + "욀": "oel", + "욁": "oek", + "욂": "oem", + "욃": "oep", + "욄": "oet", + "욅": "oet", + "욆": "oep", + "욇": "oel", + "욈": "oem", + "욉": "oep", + "욊": "oep", + "욋": "oet", + "욌": "oet", + "욍": "oeng", + "욎": "oet", + "욏": "oet", + "욐": "oek", + "욑": "oet", + "욒": "oep", + "욓": "oet", + "요": "yo", + "욕": "yok", + "욖": "yokk", + "욗": "yok", + "욘": "yon", + "욙": "yon", + "욚": "yon", + "욛": "yot", + "욜": "yol", + "욝": "yok", + "욞": "yom", + "욟": "yop", + "욠": "yot", + "욡": "yot", + "욢": "yop", + "욣": "yol", + "욤": "yom", + "욥": "yop", + "욦": "yop", + "욧": "yot", + "욨": "yot", + "용": "yong", + "욪": "yot", + "욫": "yot", + "욬": "yok", + "욭": "yot", + "욮": "yop", + "욯": "yot", + "우": "u", + "욱": "uk", + "욲": "ukk", + "욳": "uk", + "운": "un", + "욵": "un", + "욶": "un", + "욷": "ut", + "울": "ul", + "욹": "uk", + "욺": "um", + "욻": "up", + "욼": "ut", + "욽": "ut", + "욾": "up", + "욿": "ul", + "움": "um", + "웁": "up", + "웂": "up", + "웃": "ut", + "웄": "ut", + "웅": "ung", + "웆": "ut", + "웇": "ut", + "웈": "uk", + "웉": "ut", + "웊": "up", + "웋": "ut", + "워": "wo", + "웍": "wok", + "웎": "wokk", + "웏": "wok", + "원": "won", + "웑": "won", + "웒": "won", + "웓": "wot", + "월": "wol", + "웕": "wok", + "웖": "wom", + "웗": "wop", + "웘": "wot", + "웙": "wot", + "웚": "wop", + "웛": "wol", + "웜": "wom", + "웝": "wop", + "웞": "wop", + "웟": "wot", + "웠": "wot", + "웡": "wong", + "웢": "wot", + "웣": "wot", + "웤": "wok", + "웥": "wot", + "웦": "wop", + "웧": "wot", + "웨": "we", + "웩": "wek", + "웪": "wekk", + "웫": "wek", + "웬": "wen", + "웭": "wen", + "웮": "wen", + "웯": "wet", + "웰": "wel", + "웱": "wek", + "웲": "wem", + "웳": "wep", + "웴": "wet", + "웵": "wet", + "웶": "wep", + "웷": "wel", + "웸": "wem", + "웹": "wep", + "웺": "wep", + "웻": "wet", + "웼": "wet", + "웽": "weng", + "웾": "wet", + "웿": "wet", + "윀": "wek", + "윁": "wet", + "윂": "wep", + "윃": "wet", + "위": "wi", + "윅": "wik", + "윆": "wikk", + "윇": "wik", + "윈": "win", + "윉": "win", + "윊": "win", + "윋": "wit", + "윌": "wil", + "윍": "wik", + "윎": "wim", + "윏": "wip", + "윐": "wit", + "윑": "wit", + "윒": "wip", + "윓": "wil", + "윔": "wim", + "윕": "wip", + "윖": "wip", + "윗": "wit", + "윘": "wit", + "윙": "wing", + "윚": "wit", + "윛": "wit", + "윜": "wik", + "윝": "wit", + "윞": "wip", + "윟": "wit", + "유": "yu", + "육": "yuk", + "윢": "yukk", + "윣": "yuk", + "윤": "yun", + "윥": "yun", + "윦": "yun", + "윧": "yut", + "율": "yul", + "윩": "yuk", + "윪": "yum", + "윫": "yup", + "윬": "yut", + "윭": "yut", + "윮": "yup", + "윯": "yul", + "윰": "yum", + "윱": "yup", + "윲": "yup", + "윳": "yut", + "윴": "yut", + "융": "yung", + "윶": "yut", + "윷": "yut", + "윸": "yuk", + "윹": "yut", + "윺": "yup", + "윻": "yut", + "으": "eu", + "윽": "euk", + "윾": "eukk", + "윿": "euk", + "은": "eun", + "읁": "eun", + "읂": "eun", + "읃": "eut", + "을": "eul", + "읅": "euk", + "읆": "eum", + "읇": "eup", + "읈": "eut", + "읉": "eut", + "읊": "eup", + "읋": "eul", + "음": "eum", + "읍": "eup", + "읎": "eup", + "읏": "eut", + "읐": "eut", + "응": "eung", + "읒": "eut", + "읓": "eut", + "읔": "euk", + "읕": "eut", + "읖": "eup", + "읗": "eut", + "의": "eui", + "읙": "euik", + "읚": "euikk", + "읛": "euik", + "읜": "euin", + "읝": "euin", + "읞": "euin", + "읟": "euit", + "읠": "euil", + "읡": "euik", + "읢": "euim", + "읣": "euip", + "읤": "euit", + "읥": "euit", + "읦": "euip", + "읧": "euil", + "읨": "euim", + "읩": "euip", + "읪": "euip", + "읫": "euit", + "읬": "euit", + "읭": "euing", + "읮": "euit", + "읯": "euit", + "읰": "euik", + "읱": "euit", + "읲": "euip", + "읳": "euit", + "이": "i", + "익": "ik", + "읶": "ikk", + "읷": "ik", + "인": "in", + "읹": "in", + "읺": "in", + "읻": "it", + "일": "il", + "읽": "ik", + "읾": "im", + "읿": "ip", + "잀": "it", + "잁": "it", + "잂": "ip", + "잃": "il", + "임": "im", + "입": "ip", + "잆": "ip", + "잇": "it", + "있": "it", + "잉": "ing", + "잊": "it", + "잋": "it", + "잌": "ik", + "잍": "it", + "잎": "ip", + "잏": "it", + "자": "ja", + "작": "jak", + "잒": "jakk", + "잓": "jak", + "잔": "jan", + "잕": "jan", + "잖": "jan", + "잗": "jat", + "잘": "jal", + "잙": "jak", + "잚": "jam", + "잛": "jap", + "잜": "jat", + "잝": "jat", + "잞": "jap", + "잟": "jal", + "잠": "jam", + "잡": "jap", + "잢": "jap", + "잣": "jat", + "잤": "jat", + "장": "jang", + "잦": "jat", + "잧": "jat", + "잨": "jak", + "잩": "jat", + "잪": "jap", + "잫": "jat", + "재": "jae", + "잭": "jaek", + "잮": "jaekk", + "잯": "jaek", + "잰": "jaen", + "잱": "jaen", + "잲": "jaen", + "잳": "jaet", + "잴": "jael", + "잵": "jaek", + "잶": "jaem", + "잷": "jaep", + "잸": "jaet", + "잹": "jaet", + "잺": "jaep", + "잻": "jael", + "잼": "jaem", + "잽": "jaep", + "잾": "jaep", + "잿": "jaet", + "쟀": "jaet", + "쟁": "jaeng", + "쟂": "jaet", + "쟃": "jaet", + "쟄": "jaek", + "쟅": "jaet", + "쟆": "jaep", + "쟇": "jaet", + "쟈": "jya", + "쟉": "jyak", + "쟊": "jyakk", + "쟋": "jyak", + "쟌": "jyan", + "쟍": "jyan", + "쟎": "jyan", + "쟏": "jyat", + "쟐": "jyal", + "쟑": "jyak", + "쟒": "jyam", + "쟓": "jyap", + "쟔": "jyat", + "쟕": "jyat", + "쟖": "jyap", + "쟗": "jyal", + "쟘": "jyam", + "쟙": "jyap", + "쟚": "jyap", + "쟛": "jyat", + "쟜": "jyat", + "쟝": "jyang", + "쟞": "jyat", + "쟟": "jyat", + "쟠": "jyak", + "쟡": "jyat", + "쟢": "jyap", + "쟣": "jyat", + "쟤": "jyae", + "쟥": "jyaek", + "쟦": "jyaekk", + "쟧": "jyaek", + "쟨": "jyaen", + "쟩": "jyaen", + "쟪": "jyaen", + "쟫": "jyaet", + "쟬": "jyael", + "쟭": "jyaek", + "쟮": "jyaem", + "쟯": "jyaep", + "쟰": "jyaet", + "쟱": "jyaet", + "쟲": "jyaep", + "쟳": "jyael", + "쟴": "jyaem", + "쟵": "jyaep", + "쟶": "jyaep", + "쟷": "jyaet", + "쟸": "jyaet", + "쟹": "jyaeng", + "쟺": "jyaet", + "쟻": "jyaet", + "쟼": "jyaek", + "쟽": "jyaet", + "쟾": "jyaep", + "쟿": "jyaet", + "저": "jeo", + "적": "jeok", + "젂": "jeokk", + "젃": "jeok", + "전": "jeon", + "젅": "jeon", + "젆": "jeon", + "젇": "jeot", + "절": "jeol", + "젉": "jeok", + "젊": "jeom", + "젋": "jeop", + "젌": "jeot", + "젍": "jeot", + "젎": "jeop", + "젏": "jeol", + "점": "jeom", + "접": "jeop", + "젒": "jeop", + "젓": "jeot", + "젔": "jeot", + "정": "jeong", + "젖": "jeot", + "젗": "jeot", + "젘": "jeok", + "젙": "jeot", + "젚": "jeop", + "젛": "jeot", + "제": "je", + "젝": "jek", + "젞": "jekk", + "젟": "jek", + "젠": "jen", + "젡": "jen", + "젢": "jen", + "젣": "jet", + "젤": "jel", + "젥": "jek", + "젦": "jem", + "젧": "jep", + "젨": "jet", + "젩": "jet", + "젪": "jep", + "젫": "jel", + "젬": "jem", + "젭": "jep", + "젮": "jep", + "젯": "jet", + "젰": "jet", + "젱": "jeng", + "젲": "jet", + "젳": "jet", + "젴": "jek", + "젵": "jet", + "젶": "jep", + "젷": "jet", + "져": "jyeo", + "젹": "jyeok", + "젺": "jyeokk", + "젻": "jyeok", + "젼": "jyeon", + "젽": "jyeon", + "젾": "jyeon", + "젿": "jyeot", + "졀": "jyeol", + "졁": "jyeok", + "졂": "jyeom", + "졃": "jyeop", + "졄": "jyeot", + "졅": "jyeot", + "졆": "jyeop", + "졇": "jyeol", + "졈": "jyeom", + "졉": "jyeop", + "졊": "jyeop", + "졋": "jyeot", + "졌": "jyeot", + "졍": "jyeong", + "졎": "jyeot", + "졏": "jyeot", + "졐": "jyeok", + "졑": "jyeot", + "졒": "jyeop", + "졓": "jyeot", + "졔": "jye", + "졕": "jyek", + "졖": "jyekk", + "졗": "jyek", + "졘": "jyen", + "졙": "jyen", + "졚": "jyen", + "졛": "jyet", + "졜": "jyel", + "졝": "jyek", + "졞": "jyem", + "졟": "jyep", + "졠": "jyet", + "졡": "jyet", + "졢": "jyep", + "졣": "jyel", + "졤": "jyem", + "졥": "jyep", + "졦": "jyep", + "졧": "jyet", + "졨": "jyet", + "졩": "jyeng", + "졪": "jyet", + "졫": "jyet", + "졬": "jyek", + "졭": "jyet", + "졮": "jyep", + "졯": "jyet", + "조": "jo", + "족": "jok", + "졲": "jokk", + "졳": "jok", + "존": "jon", + "졵": "jon", + "졶": "jon", + "졷": "jot", + "졸": "jol", + "졹": "jok", + "졺": "jom", + "졻": "jop", + "졼": "jot", + "졽": "jot", + "졾": "jop", + "졿": "jol", + "좀": "jom", + "좁": "jop", + "좂": "jop", + "좃": "jot", + "좄": "jot", + "종": "jong", + "좆": "jot", + "좇": "jot", + "좈": "jok", + "좉": "jot", + "좊": "jop", + "좋": "jot", + "좌": "jwa", + "좍": "jwak", + "좎": "jwakk", + "좏": "jwak", + "좐": "jwan", + "좑": "jwan", + "좒": "jwan", + "좓": "jwat", + "좔": "jwal", + "좕": "jwak", + "좖": "jwam", + "좗": "jwap", + "좘": "jwat", + "좙": "jwat", + "좚": "jwap", + "좛": "jwal", + "좜": "jwam", + "좝": "jwap", + "좞": "jwap", + "좟": "jwat", + "좠": "jwat", + "좡": "jwang", + "좢": "jwat", + "좣": "jwat", + "좤": "jwak", + "좥": "jwat", + "좦": "jwap", + "좧": "jwat", + "좨": "jwae", + "좩": "jwaek", + "좪": "jwaekk", + "좫": "jwaek", + "좬": "jwaen", + "좭": "jwaen", + "좮": "jwaen", + "좯": "jwaet", + "좰": "jwael", + "좱": "jwaek", + "좲": "jwaem", + "좳": "jwaep", + "좴": "jwaet", + "좵": "jwaet", + "좶": "jwaep", + "좷": "jwael", + "좸": "jwaem", + "좹": "jwaep", + "좺": "jwaep", + "좻": "jwaet", + "좼": "jwaet", + "좽": "jwaeng", + "좾": "jwaet", + "좿": "jwaet", + "죀": "jwaek", + "죁": "jwaet", + "죂": "jwaep", + "죃": "jwaet", + "죄": "joe", + "죅": "joek", + "죆": "joekk", + "죇": "joek", + "죈": "joen", + "죉": "joen", + "죊": "joen", + "죋": "joet", + "죌": "joel", + "죍": "joek", + "죎": "joem", + "죏": "joep", + "죐": "joet", + "죑": "joet", + "죒": "joep", + "죓": "joel", + "죔": "joem", + "죕": "joep", + "죖": "joep", + "죗": "joet", + "죘": "joet", + "죙": "joeng", + "죚": "joet", + "죛": "joet", + "죜": "joek", + "죝": "joet", + "죞": "joep", + "죟": "joet", + "죠": "jyo", + "죡": "jyok", + "죢": "jyokk", + "죣": "jyok", + "죤": "jyon", + "죥": "jyon", + "죦": "jyon", + "죧": "jyot", + "죨": "jyol", + "죩": "jyok", + "죪": "jyom", + "죫": "jyop", + "죬": "jyot", + "죭": "jyot", + "죮": "jyop", + "죯": "jyol", + "죰": "jyom", + "죱": "jyop", + "죲": "jyop", + "죳": "jyot", + "죴": "jyot", + "죵": "jyong", + "죶": "jyot", + "죷": "jyot", + "죸": "jyok", + "죹": "jyot", + "죺": "jyop", + "죻": "jyot", + "주": "ju", + "죽": "juk", + "죾": "jukk", + "죿": "juk", + "준": "jun", + "줁": "jun", + "줂": "jun", + "줃": "jut", + "줄": "jul", + "줅": "juk", + "줆": "jum", + "줇": "jup", + "줈": "jut", + "줉": "jut", + "줊": "jup", + "줋": "jul", + "줌": "jum", + "줍": "jup", + "줎": "jup", + "줏": "jut", + "줐": "jut", + "중": "jung", + "줒": "jut", + "줓": "jut", + "줔": "juk", + "줕": "jut", + "줖": "jup", + "줗": "jut", + "줘": "jwo", + "줙": "jwok", + "줚": "jwokk", + "줛": "jwok", + "줜": "jwon", + "줝": "jwon", + "줞": "jwon", + "줟": "jwot", + "줠": "jwol", + "줡": "jwok", + "줢": "jwom", + "줣": "jwop", + "줤": "jwot", + "줥": "jwot", + "줦": "jwop", + "줧": "jwol", + "줨": "jwom", + "줩": "jwop", + "줪": "jwop", + "줫": "jwot", + "줬": "jwot", + "줭": "jwong", + "줮": "jwot", + "줯": "jwot", + "줰": "jwok", + "줱": "jwot", + "줲": "jwop", + "줳": "jwot", + "줴": "jwe", + "줵": "jwek", + "줶": "jwekk", + "줷": "jwek", + "줸": "jwen", + "줹": "jwen", + "줺": "jwen", + "줻": "jwet", + "줼": "jwel", + "줽": "jwek", + "줾": "jwem", + "줿": "jwep", + "쥀": "jwet", + "쥁": "jwet", + "쥂": "jwep", + "쥃": "jwel", + "쥄": "jwem", + "쥅": "jwep", + "쥆": "jwep", + "쥇": "jwet", + "쥈": "jwet", + "쥉": "jweng", + "쥊": "jwet", + "쥋": "jwet", + "쥌": "jwek", + "쥍": "jwet", + "쥎": "jwep", + "쥏": "jwet", + "쥐": "jwi", + "쥑": "jwik", + "쥒": "jwikk", + "쥓": "jwik", + "쥔": "jwin", + "쥕": "jwin", + "쥖": "jwin", + "쥗": "jwit", + "쥘": "jwil", + "쥙": "jwik", + "쥚": "jwim", + "쥛": "jwip", + "쥜": "jwit", + "쥝": "jwit", + "쥞": "jwip", + "쥟": "jwil", + "쥠": "jwim", + "쥡": "jwip", + "쥢": "jwip", + "쥣": "jwit", + "쥤": "jwit", + "쥥": "jwing", + "쥦": "jwit", + "쥧": "jwit", + "쥨": "jwik", + "쥩": "jwit", + "쥪": "jwip", + "쥫": "jwit", + "쥬": "jyu", + "쥭": "jyuk", + "쥮": "jyukk", + "쥯": "jyuk", + "쥰": "jyun", + "쥱": "jyun", + "쥲": "jyun", + "쥳": "jyut", + "쥴": "jyul", + "쥵": "jyuk", + "쥶": "jyum", + "쥷": "jyup", + "쥸": "jyut", + "쥹": "jyut", + "쥺": "jyup", + "쥻": "jyul", + "쥼": "jyum", + "쥽": "jyup", + "쥾": "jyup", + "쥿": "jyut", + "즀": "jyut", + "즁": "jyung", + "즂": "jyut", + "즃": "jyut", + "즄": "jyuk", + "즅": "jyut", + "즆": "jyup", + "즇": "jyut", + "즈": "jeu", + "즉": "jeuk", + "즊": "jeukk", + "즋": "jeuk", + "즌": "jeun", + "즍": "jeun", + "즎": "jeun", + "즏": "jeut", + "즐": "jeul", + "즑": "jeuk", + "즒": "jeum", + "즓": "jeup", + "즔": "jeut", + "즕": "jeut", + "즖": "jeup", + "즗": "jeul", + "즘": "jeum", + "즙": "jeup", + "즚": "jeup", + "즛": "jeut", + "즜": "jeut", + "증": "jeung", + "즞": "jeut", + "즟": "jeut", + "즠": "jeuk", + "즡": "jeut", + "즢": "jeup", + "즣": "jeut", + "즤": "jeui", + "즥": "jeuik", + "즦": "jeuikk", + "즧": "jeuik", + "즨": "jeuin", + "즩": "jeuin", + "즪": "jeuin", + "즫": "jeuit", + "즬": "jeuil", + "즭": "jeuik", + "즮": "jeuim", + "즯": "jeuip", + "즰": "jeuit", + "즱": "jeuit", + "즲": "jeuip", + "즳": "jeuil", + "즴": "jeuim", + "즵": "jeuip", + "즶": "jeuip", + "즷": "jeuit", + "즸": "jeuit", + "즹": "jeuing", + "즺": "jeuit", + "즻": "jeuit", + "즼": "jeuik", + "즽": "jeuit", + "즾": "jeuip", + "즿": "jeuit", + "지": "ji", + "직": "jik", + "짂": "jikk", + "짃": "jik", + "진": "jin", + "짅": "jin", + "짆": "jin", + "짇": "jit", + "질": "jil", + "짉": "jik", + "짊": "jim", + "짋": "jip", + "짌": "jit", + "짍": "jit", + "짎": "jip", + "짏": "jil", + "짐": "jim", + "집": "jip", + "짒": "jip", + "짓": "jit", + "짔": "jit", + "징": "jing", + "짖": "jit", + "짗": "jit", + "짘": "jik", + "짙": "jit", + "짚": "jip", + "짛": "jit", + "짜": "jja", + "짝": "jjak", + "짞": "jjakk", + "짟": "jjak", + "짠": "jjan", + "짡": "jjan", + "짢": "jjan", + "짣": "jjat", + "짤": "jjal", + "짥": "jjak", + "짦": "jjam", + "짧": "jjap", + "짨": "jjat", + "짩": "jjat", + "짪": "jjap", + "짫": "jjal", + "짬": "jjam", + "짭": "jjap", + "짮": "jjap", + "짯": "jjat", + "짰": "jjat", + "짱": "jjang", + "짲": "jjat", + "짳": "jjat", + "짴": "jjak", + "짵": "jjat", + "짶": "jjap", + "짷": "jjat", + "째": "jjae", + "짹": "jjaek", + "짺": "jjaekk", + "짻": "jjaek", + "짼": "jjaen", + "짽": "jjaen", + "짾": "jjaen", + "짿": "jjaet", + "쨀": "jjael", + "쨁": "jjaek", + "쨂": "jjaem", + "쨃": "jjaep", + "쨄": "jjaet", + "쨅": "jjaet", + "쨆": "jjaep", + "쨇": "jjael", + "쨈": "jjaem", + "쨉": "jjaep", + "쨊": "jjaep", + "쨋": "jjaet", + "쨌": "jjaet", + "쨍": "jjaeng", + "쨎": "jjaet", + "쨏": "jjaet", + "쨐": "jjaek", + "쨑": "jjaet", + "쨒": "jjaep", + "쨓": "jjaet", + "쨔": "jjya", + "쨕": "jjyak", + "쨖": "jjyakk", + "쨗": "jjyak", + "쨘": "jjyan", + "쨙": "jjyan", + "쨚": "jjyan", + "쨛": "jjyat", + "쨜": "jjyal", + "쨝": "jjyak", + "쨞": "jjyam", + "쨟": "jjyap", + "쨠": "jjyat", + "쨡": "jjyat", + "쨢": "jjyap", + "쨣": "jjyal", + "쨤": "jjyam", + "쨥": "jjyap", + "쨦": "jjyap", + "쨧": "jjyat", + "쨨": "jjyat", + "쨩": "jjyang", + "쨪": "jjyat", + "쨫": "jjyat", + "쨬": "jjyak", + "쨭": "jjyat", + "쨮": "jjyap", + "쨯": "jjyat", + "쨰": "jjyae", + "쨱": "jjyaek", + "쨲": "jjyaekk", + "쨳": "jjyaek", + "쨴": "jjyaen", + "쨵": "jjyaen", + "쨶": "jjyaen", + "쨷": "jjyaet", + "쨸": "jjyael", + "쨹": "jjyaek", + "쨺": "jjyaem", + "쨻": "jjyaep", + "쨼": "jjyaet", + "쨽": "jjyaet", + "쨾": "jjyaep", + "쨿": "jjyael", + "쩀": "jjyaem", + "쩁": "jjyaep", + "쩂": "jjyaep", + "쩃": "jjyaet", + "쩄": "jjyaet", + "쩅": "jjyaeng", + "쩆": "jjyaet", + "쩇": "jjyaet", + "쩈": "jjyaek", + "쩉": "jjyaet", + "쩊": "jjyaep", + "쩋": "jjyaet", + "쩌": "jjeo", + "쩍": "jjeok", + "쩎": "jjeokk", + "쩏": "jjeok", + "쩐": "jjeon", + "쩑": "jjeon", + "쩒": "jjeon", + "쩓": "jjeot", + "쩔": "jjeol", + "쩕": "jjeok", + "쩖": "jjeom", + "쩗": "jjeop", + "쩘": "jjeot", + "쩙": "jjeot", + "쩚": "jjeop", + "쩛": "jjeol", + "쩜": "jjeom", + "쩝": "jjeop", + "쩞": "jjeop", + "쩟": "jjeot", + "쩠": "jjeot", + "쩡": "jjeong", + "쩢": "jjeot", + "쩣": "jjeot", + "쩤": "jjeok", + "쩥": "jjeot", + "쩦": "jjeop", + "쩧": "jjeot", + "쩨": "jje", + "쩩": "jjek", + "쩪": "jjekk", + "쩫": "jjek", + "쩬": "jjen", + "쩭": "jjen", + "쩮": "jjen", + "쩯": "jjet", + "쩰": "jjel", + "쩱": "jjek", + "쩲": "jjem", + "쩳": "jjep", + "쩴": "jjet", + "쩵": "jjet", + "쩶": "jjep", + "쩷": "jjel", + "쩸": "jjem", + "쩹": "jjep", + "쩺": "jjep", + "쩻": "jjet", + "쩼": "jjet", + "쩽": "jjeng", + "쩾": "jjet", + "쩿": "jjet", + "쪀": "jjek", + "쪁": "jjet", + "쪂": "jjep", + "쪃": "jjet", + "쪄": "jjyeo", + "쪅": "jjyeok", + "쪆": "jjyeokk", + "쪇": "jjyeok", + "쪈": "jjyeon", + "쪉": "jjyeon", + "쪊": "jjyeon", + "쪋": "jjyeot", + "쪌": "jjyeol", + "쪍": "jjyeok", + "쪎": "jjyeom", + "쪏": "jjyeop", + "쪐": "jjyeot", + "쪑": "jjyeot", + "쪒": "jjyeop", + "쪓": "jjyeol", + "쪔": "jjyeom", + "쪕": "jjyeop", + "쪖": "jjyeop", + "쪗": "jjyeot", + "쪘": "jjyeot", + "쪙": "jjyeong", + "쪚": "jjyeot", + "쪛": "jjyeot", + "쪜": "jjyeok", + "쪝": "jjyeot", + "쪞": "jjyeop", + "쪟": "jjyeot", + "쪠": "jjye", + "쪡": "jjyek", + "쪢": "jjyekk", + "쪣": "jjyek", + "쪤": "jjyen", + "쪥": "jjyen", + "쪦": "jjyen", + "쪧": "jjyet", + "쪨": "jjyel", + "쪩": "jjyek", + "쪪": "jjyem", + "쪫": "jjyep", + "쪬": "jjyet", + "쪭": "jjyet", + "쪮": "jjyep", + "쪯": "jjyel", + "쪰": "jjyem", + "쪱": "jjyep", + "쪲": "jjyep", + "쪳": "jjyet", + "쪴": "jjyet", + "쪵": "jjyeng", + "쪶": "jjyet", + "쪷": "jjyet", + "쪸": "jjyek", + "쪹": "jjyet", + "쪺": "jjyep", + "쪻": "jjyet", + "쪼": "jjo", + "쪽": "jjok", + "쪾": "jjokk", + "쪿": "jjok", + "쫀": "jjon", + "쫁": "jjon", + "쫂": "jjon", + "쫃": "jjot", + "쫄": "jjol", + "쫅": "jjok", + "쫆": "jjom", + "쫇": "jjop", + "쫈": "jjot", + "쫉": "jjot", + "쫊": "jjop", + "쫋": "jjol", + "쫌": "jjom", + "쫍": "jjop", + "쫎": "jjop", + "쫏": "jjot", + "쫐": "jjot", + "쫑": "jjong", + "쫒": "jjot", + "쫓": "jjot", + "쫔": "jjok", + "쫕": "jjot", + "쫖": "jjop", + "쫗": "jjot", + "쫘": "jjwa", + "쫙": "jjwak", + "쫚": "jjwakk", + "쫛": "jjwak", + "쫜": "jjwan", + "쫝": "jjwan", + "쫞": "jjwan", + "쫟": "jjwat", + "쫠": "jjwal", + "쫡": "jjwak", + "쫢": "jjwam", + "쫣": "jjwap", + "쫤": "jjwat", + "쫥": "jjwat", + "쫦": "jjwap", + "쫧": "jjwal", + "쫨": "jjwam", + "쫩": "jjwap", + "쫪": "jjwap", + "쫫": "jjwat", + "쫬": "jjwat", + "쫭": "jjwang", + "쫮": "jjwat", + "쫯": "jjwat", + "쫰": "jjwak", + "쫱": "jjwat", + "쫲": "jjwap", + "쫳": "jjwat", + "쫴": "jjwae", + "쫵": "jjwaek", + "쫶": "jjwaekk", + "쫷": "jjwaek", + "쫸": "jjwaen", + "쫹": "jjwaen", + "쫺": "jjwaen", + "쫻": "jjwaet", + "쫼": "jjwael", + "쫽": "jjwaek", + "쫾": "jjwaem", + "쫿": "jjwaep", + "쬀": "jjwaet", + "쬁": "jjwaet", + "쬂": "jjwaep", + "쬃": "jjwael", + "쬄": "jjwaem", + "쬅": "jjwaep", + "쬆": "jjwaep", + "쬇": "jjwaet", + "쬈": "jjwaet", + "쬉": "jjwaeng", + "쬊": "jjwaet", + "쬋": "jjwaet", + "쬌": "jjwaek", + "쬍": "jjwaet", + "쬎": "jjwaep", + "쬏": "jjwaet", + "쬐": "jjoe", + "쬑": "jjoek", + "쬒": "jjoekk", + "쬓": "jjoek", + "쬔": "jjoen", + "쬕": "jjoen", + "쬖": "jjoen", + "쬗": "jjoet", + "쬘": "jjoel", + "쬙": "jjoek", + "쬚": "jjoem", + "쬛": "jjoep", + "쬜": "jjoet", + "쬝": "jjoet", + "쬞": "jjoep", + "쬟": "jjoel", + "쬠": "jjoem", + "쬡": "jjoep", + "쬢": "jjoep", + "쬣": "jjoet", + "쬤": "jjoet", + "쬥": "jjoeng", + "쬦": "jjoet", + "쬧": "jjoet", + "쬨": "jjoek", + "쬩": "jjoet", + "쬪": "jjoep", + "쬫": "jjoet", + "쬬": "jjyo", + "쬭": "jjyok", + "쬮": "jjyokk", + "쬯": "jjyok", + "쬰": "jjyon", + "쬱": "jjyon", + "쬲": "jjyon", + "쬳": "jjyot", + "쬴": "jjyol", + "쬵": "jjyok", + "쬶": "jjyom", + "쬷": "jjyop", + "쬸": "jjyot", + "쬹": "jjyot", + "쬺": "jjyop", + "쬻": "jjyol", + "쬼": "jjyom", + "쬽": "jjyop", + "쬾": "jjyop", + "쬿": "jjyot", + "쭀": "jjyot", + "쭁": "jjyong", + "쭂": "jjyot", + "쭃": "jjyot", + "쭄": "jjyok", + "쭅": "jjyot", + "쭆": "jjyop", + "쭇": "jjyot", + "쭈": "jju", + "쭉": "jjuk", + "쭊": "jjukk", + "쭋": "jjuk", + "쭌": "jjun", + "쭍": "jjun", + "쭎": "jjun", + "쭏": "jjut", + "쭐": "jjul", + "쭑": "jjuk", + "쭒": "jjum", + "쭓": "jjup", + "쭔": "jjut", + "쭕": "jjut", + "쭖": "jjup", + "쭗": "jjul", + "쭘": "jjum", + "쭙": "jjup", + "쭚": "jjup", + "쭛": "jjut", + "쭜": "jjut", + "쭝": "jjung", + "쭞": "jjut", + "쭟": "jjut", + "쭠": "jjuk", + "쭡": "jjut", + "쭢": "jjup", + "쭣": "jjut", + "쭤": "jjwo", + "쭥": "jjwok", + "쭦": "jjwokk", + "쭧": "jjwok", + "쭨": "jjwon", + "쭩": "jjwon", + "쭪": "jjwon", + "쭫": "jjwot", + "쭬": "jjwol", + "쭭": "jjwok", + "쭮": "jjwom", + "쭯": "jjwop", + "쭰": "jjwot", + "쭱": "jjwot", + "쭲": "jjwop", + "쭳": "jjwol", + "쭴": "jjwom", + "쭵": "jjwop", + "쭶": "jjwop", + "쭷": "jjwot", + "쭸": "jjwot", + "쭹": "jjwong", + "쭺": "jjwot", + "쭻": "jjwot", + "쭼": "jjwok", + "쭽": "jjwot", + "쭾": "jjwop", + "쭿": "jjwot", + "쮀": "jjwe", + "쮁": "jjwek", + "쮂": "jjwekk", + "쮃": "jjwek", + "쮄": "jjwen", + "쮅": "jjwen", + "쮆": "jjwen", + "쮇": "jjwet", + "쮈": "jjwel", + "쮉": "jjwek", + "쮊": "jjwem", + "쮋": "jjwep", + "쮌": "jjwet", + "쮍": "jjwet", + "쮎": "jjwep", + "쮏": "jjwel", + "쮐": "jjwem", + "쮑": "jjwep", + "쮒": "jjwep", + "쮓": "jjwet", + "쮔": "jjwet", + "쮕": "jjweng", + "쮖": "jjwet", + "쮗": "jjwet", + "쮘": "jjwek", + "쮙": "jjwet", + "쮚": "jjwep", + "쮛": "jjwet", + "쮜": "jjwi", + "쮝": "jjwik", + "쮞": "jjwikk", + "쮟": "jjwik", + "쮠": "jjwin", + "쮡": "jjwin", + "쮢": "jjwin", + "쮣": "jjwit", + "쮤": "jjwil", + "쮥": "jjwik", + "쮦": "jjwim", + "쮧": "jjwip", + "쮨": "jjwit", + "쮩": "jjwit", + "쮪": "jjwip", + "쮫": "jjwil", + "쮬": "jjwim", + "쮭": "jjwip", + "쮮": "jjwip", + "쮯": "jjwit", + "쮰": "jjwit", + "쮱": "jjwing", + "쮲": "jjwit", + "쮳": "jjwit", + "쮴": "jjwik", + "쮵": "jjwit", + "쮶": "jjwip", + "쮷": "jjwit", + "쮸": "jjyu", + "쮹": "jjyuk", + "쮺": "jjyukk", + "쮻": "jjyuk", + "쮼": "jjyun", + "쮽": "jjyun", + "쮾": "jjyun", + "쮿": "jjyut", + "쯀": "jjyul", + "쯁": "jjyuk", + "쯂": "jjyum", + "쯃": "jjyup", + "쯄": "jjyut", + "쯅": "jjyut", + "쯆": "jjyup", + "쯇": "jjyul", + "쯈": "jjyum", + "쯉": "jjyup", + "쯊": "jjyup", + "쯋": "jjyut", + "쯌": "jjyut", + "쯍": "jjyung", + "쯎": "jjyut", + "쯏": "jjyut", + "쯐": "jjyuk", + "쯑": "jjyut", + "쯒": "jjyup", + "쯓": "jjyut", + "쯔": "jjeu", + "쯕": "jjeuk", + "쯖": "jjeukk", + "쯗": "jjeuk", + "쯘": "jjeun", + "쯙": "jjeun", + "쯚": "jjeun", + "쯛": "jjeut", + "쯜": "jjeul", + "쯝": "jjeuk", + "쯞": "jjeum", + "쯟": "jjeup", + "쯠": "jjeut", + "쯡": "jjeut", + "쯢": "jjeup", + "쯣": "jjeul", + "쯤": "jjeum", + "쯥": "jjeup", + "쯦": "jjeup", + "쯧": "jjeut", + "쯨": "jjeut", + "쯩": "jjeung", + "쯪": "jjeut", + "쯫": "jjeut", + "쯬": "jjeuk", + "쯭": "jjeut", + "쯮": "jjeup", + "쯯": "jjeut", + "쯰": "jjeui", + "쯱": "jjeuik", + "쯲": "jjeuikk", + "쯳": "jjeuik", + "쯴": "jjeuin", + "쯵": "jjeuin", + "쯶": "jjeuin", + "쯷": "jjeuit", + "쯸": "jjeuil", + "쯹": "jjeuik", + "쯺": "jjeuim", + "쯻": "jjeuip", + "쯼": "jjeuit", + "쯽": "jjeuit", + "쯾": "jjeuip", + "쯿": "jjeuil", + "찀": "jjeuim", + "찁": "jjeuip", + "찂": "jjeuip", + "찃": "jjeuit", + "찄": "jjeuit", + "찅": "jjeuing", + "찆": "jjeuit", + "찇": "jjeuit", + "찈": "jjeuik", + "찉": "jjeuit", + "찊": "jjeuip", + "찋": "jjeuit", + "찌": "jji", + "찍": "jjik", + "찎": "jjikk", + "찏": "jjik", + "찐": "jjin", + "찑": "jjin", + "찒": "jjin", + "찓": "jjit", + "찔": "jjil", + "찕": "jjik", + "찖": "jjim", + "찗": "jjip", + "찘": "jjit", + "찙": "jjit", + "찚": "jjip", + "찛": "jjil", + "찜": "jjim", + "찝": "jjip", + "찞": "jjip", + "찟": "jjit", + "찠": "jjit", + "찡": "jjing", + "찢": "jjit", + "찣": "jjit", + "찤": "jjik", + "찥": "jjit", + "찦": "jjip", + "찧": "jjit", + "차": "cha", + "착": "chak", + "찪": "chakk", + "찫": "chak", + "찬": "chan", + "찭": "chan", + "찮": "chan", + "찯": "chat", + "찰": "chal", + "찱": "chak", + "찲": "cham", + "찳": "chap", + "찴": "chat", + "찵": "chat", + "찶": "chap", + "찷": "chal", + "참": "cham", + "찹": "chap", + "찺": "chap", + "찻": "chat", + "찼": "chat", + "창": "chang", + "찾": "chat", + "찿": "chat", + "챀": "chak", + "챁": "chat", + "챂": "chap", + "챃": "chat", + "채": "chae", + "책": "chaek", + "챆": "chaekk", + "챇": "chaek", + "챈": "chaen", + "챉": "chaen", + "챊": "chaen", + "챋": "chaet", + "챌": "chael", + "챍": "chaek", + "챎": "chaem", + "챏": "chaep", + "챐": "chaet", + "챑": "chaet", + "챒": "chaep", + "챓": "chael", + "챔": "chaem", + "챕": "chaep", + "챖": "chaep", + "챗": "chaet", + "챘": "chaet", + "챙": "chaeng", + "챚": "chaet", + "챛": "chaet", + "챜": "chaek", + "챝": "chaet", + "챞": "chaep", + "챟": "chaet", + "챠": "chya", + "챡": "chyak", + "챢": "chyakk", + "챣": "chyak", + "챤": "chyan", + "챥": "chyan", + "챦": "chyan", + "챧": "chyat", + "챨": "chyal", + "챩": "chyak", + "챪": "chyam", + "챫": "chyap", + "챬": "chyat", + "챭": "chyat", + "챮": "chyap", + "챯": "chyal", + "챰": "chyam", + "챱": "chyap", + "챲": "chyap", + "챳": "chyat", + "챴": "chyat", + "챵": "chyang", + "챶": "chyat", + "챷": "chyat", + "챸": "chyak", + "챹": "chyat", + "챺": "chyap", + "챻": "chyat", + "챼": "chyae", + "챽": "chyaek", + "챾": "chyaekk", + "챿": "chyaek", + "첀": "chyaen", + "첁": "chyaen", + "첂": "chyaen", + "첃": "chyaet", + "첄": "chyael", + "첅": "chyaek", + "첆": "chyaem", + "첇": "chyaep", + "첈": "chyaet", + "첉": "chyaet", + "첊": "chyaep", + "첋": "chyael", + "첌": "chyaem", + "첍": "chyaep", + "첎": "chyaep", + "첏": "chyaet", + "첐": "chyaet", + "첑": "chyaeng", + "첒": "chyaet", + "첓": "chyaet", + "첔": "chyaek", + "첕": "chyaet", + "첖": "chyaep", + "첗": "chyaet", + "처": "cheo", + "척": "cheok", + "첚": "cheokk", + "첛": "cheok", + "천": "cheon", + "첝": "cheon", + "첞": "cheon", + "첟": "cheot", + "철": "cheol", + "첡": "cheok", + "첢": "cheom", + "첣": "cheop", + "첤": "cheot", + "첥": "cheot", + "첦": "cheop", + "첧": "cheol", + "첨": "cheom", + "첩": "cheop", + "첪": "cheop", + "첫": "cheot", + "첬": "cheot", + "청": "cheong", + "첮": "cheot", + "첯": "cheot", + "첰": "cheok", + "첱": "cheot", + "첲": "cheop", + "첳": "cheot", + "체": "che", + "첵": "chek", + "첶": "chekk", + "첷": "chek", + "첸": "chen", + "첹": "chen", + "첺": "chen", + "첻": "chet", + "첼": "chel", + "첽": "chek", + "첾": "chem", + "첿": "chep", + "쳀": "chet", + "쳁": "chet", + "쳂": "chep", + "쳃": "chel", + "쳄": "chem", + "쳅": "chep", + "쳆": "chep", + "쳇": "chet", + "쳈": "chet", + "쳉": "cheng", + "쳊": "chet", + "쳋": "chet", + "쳌": "chek", + "쳍": "chet", + "쳎": "chep", + "쳏": "chet", + "쳐": "chyeo", + "쳑": "chyeok", + "쳒": "chyeokk", + "쳓": "chyeok", + "쳔": "chyeon", + "쳕": "chyeon", + "쳖": "chyeon", + "쳗": "chyeot", + "쳘": "chyeol", + "쳙": "chyeok", + "쳚": "chyeom", + "쳛": "chyeop", + "쳜": "chyeot", + "쳝": "chyeot", + "쳞": "chyeop", + "쳟": "chyeol", + "쳠": "chyeom", + "쳡": "chyeop", + "쳢": "chyeop", + "쳣": "chyeot", + "쳤": "chyeot", + "쳥": "chyeong", + "쳦": "chyeot", + "쳧": "chyeot", + "쳨": "chyeok", + "쳩": "chyeot", + "쳪": "chyeop", + "쳫": "chyeot", + "쳬": "chye", + "쳭": "chyek", + "쳮": "chyekk", + "쳯": "chyek", + "쳰": "chyen", + "쳱": "chyen", + "쳲": "chyen", + "쳳": "chyet", + "쳴": "chyel", + "쳵": "chyek", + "쳶": "chyem", + "쳷": "chyep", + "쳸": "chyet", + "쳹": "chyet", + "쳺": "chyep", + "쳻": "chyel", + "쳼": "chyem", + "쳽": "chyep", + "쳾": "chyep", + "쳿": "chyet", + "촀": "chyet", + "촁": "chyeng", + "촂": "chyet", + "촃": "chyet", + "촄": "chyek", + "촅": "chyet", + "촆": "chyep", + "촇": "chyet", + "초": "cho", + "촉": "chok", + "촊": "chokk", + "촋": "chok", + "촌": "chon", + "촍": "chon", + "촎": "chon", + "촏": "chot", + "촐": "chol", + "촑": "chok", + "촒": "chom", + "촓": "chop", + "촔": "chot", + "촕": "chot", + "촖": "chop", + "촗": "chol", + "촘": "chom", + "촙": "chop", + "촚": "chop", + "촛": "chot", + "촜": "chot", + "총": "chong", + "촞": "chot", + "촟": "chot", + "촠": "chok", + "촡": "chot", + "촢": "chop", + "촣": "chot", + "촤": "chwa", + "촥": "chwak", + "촦": "chwakk", + "촧": "chwak", + "촨": "chwan", + "촩": "chwan", + "촪": "chwan", + "촫": "chwat", + "촬": "chwal", + "촭": "chwak", + "촮": "chwam", + "촯": "chwap", + "촰": "chwat", + "촱": "chwat", + "촲": "chwap", + "촳": "chwal", + "촴": "chwam", + "촵": "chwap", + "촶": "chwap", + "촷": "chwat", + "촸": "chwat", + "촹": "chwang", + "촺": "chwat", + "촻": "chwat", + "촼": "chwak", + "촽": "chwat", + "촾": "chwap", + "촿": "chwat", + "쵀": "chwae", + "쵁": "chwaek", + "쵂": "chwaekk", + "쵃": "chwaek", + "쵄": "chwaen", + "쵅": "chwaen", + "쵆": "chwaen", + "쵇": "chwaet", + "쵈": "chwael", + "쵉": "chwaek", + "쵊": "chwaem", + "쵋": "chwaep", + "쵌": "chwaet", + "쵍": "chwaet", + "쵎": "chwaep", + "쵏": "chwael", + "쵐": "chwaem", + "쵑": "chwaep", + "쵒": "chwaep", + "쵓": "chwaet", + "쵔": "chwaet", + "쵕": "chwaeng", + "쵖": "chwaet", + "쵗": "chwaet", + "쵘": "chwaek", + "쵙": "chwaet", + "쵚": "chwaep", + "쵛": "chwaet", + "최": "choe", + "쵝": "choek", + "쵞": "choekk", + "쵟": "choek", + "쵠": "choen", + "쵡": "choen", + "쵢": "choen", + "쵣": "choet", + "쵤": "choel", + "쵥": "choek", + "쵦": "choem", + "쵧": "choep", + "쵨": "choet", + "쵩": "choet", + "쵪": "choep", + "쵫": "choel", + "쵬": "choem", + "쵭": "choep", + "쵮": "choep", + "쵯": "choet", + "쵰": "choet", + "쵱": "choeng", + "쵲": "choet", + "쵳": "choet", + "쵴": "choek", + "쵵": "choet", + "쵶": "choep", + "쵷": "choet", + "쵸": "chyo", + "쵹": "chyok", + "쵺": "chyokk", + "쵻": "chyok", + "쵼": "chyon", + "쵽": "chyon", + "쵾": "chyon", + "쵿": "chyot", + "춀": "chyol", + "춁": "chyok", + "춂": "chyom", + "춃": "chyop", + "춄": "chyot", + "춅": "chyot", + "춆": "chyop", + "춇": "chyol", + "춈": "chyom", + "춉": "chyop", + "춊": "chyop", + "춋": "chyot", + "춌": "chyot", + "춍": "chyong", + "춎": "chyot", + "춏": "chyot", + "춐": "chyok", + "춑": "chyot", + "춒": "chyop", + "춓": "chyot", + "추": "chu", + "축": "chuk", + "춖": "chukk", + "춗": "chuk", + "춘": "chun", + "춙": "chun", + "춚": "chun", + "춛": "chut", + "출": "chul", + "춝": "chuk", + "춞": "chum", + "춟": "chup", + "춠": "chut", + "춡": "chut", + "춢": "chup", + "춣": "chul", + "춤": "chum", + "춥": "chup", + "춦": "chup", + "춧": "chut", + "춨": "chut", + "충": "chung", + "춪": "chut", + "춫": "chut", + "춬": "chuk", + "춭": "chut", + "춮": "chup", + "춯": "chut", + "춰": "chwo", + "춱": "chwok", + "춲": "chwokk", + "춳": "chwok", + "춴": "chwon", + "춵": "chwon", + "춶": "chwon", + "춷": "chwot", + "춸": "chwol", + "춹": "chwok", + "춺": "chwom", + "춻": "chwop", + "춼": "chwot", + "춽": "chwot", + "춾": "chwop", + "춿": "chwol", + "췀": "chwom", + "췁": "chwop", + "췂": "chwop", + "췃": "chwot", + "췄": "chwot", + "췅": "chwong", + "췆": "chwot", + "췇": "chwot", + "췈": "chwok", + "췉": "chwot", + "췊": "chwop", + "췋": "chwot", + "췌": "chwe", + "췍": "chwek", + "췎": "chwekk", + "췏": "chwek", + "췐": "chwen", + "췑": "chwen", + "췒": "chwen", + "췓": "chwet", + "췔": "chwel", + "췕": "chwek", + "췖": "chwem", + "췗": "chwep", + "췘": "chwet", + "췙": "chwet", + "췚": "chwep", + "췛": "chwel", + "췜": "chwem", + "췝": "chwep", + "췞": "chwep", + "췟": "chwet", + "췠": "chwet", + "췡": "chweng", + "췢": "chwet", + "췣": "chwet", + "췤": "chwek", + "췥": "chwet", + "췦": "chwep", + "췧": "chwet", + "취": "chwi", + "췩": "chwik", + "췪": "chwikk", + "췫": "chwik", + "췬": "chwin", + "췭": "chwin", + "췮": "chwin", + "췯": "chwit", + "췰": "chwil", + "췱": "chwik", + "췲": "chwim", + "췳": "chwip", + "췴": "chwit", + "췵": "chwit", + "췶": "chwip", + "췷": "chwil", + "췸": "chwim", + "췹": "chwip", + "췺": "chwip", + "췻": "chwit", + "췼": "chwit", + "췽": "chwing", + "췾": "chwit", + "췿": "chwit", + "츀": "chwik", + "츁": "chwit", + "츂": "chwip", + "츃": "chwit", + "츄": "chyu", + "츅": "chyuk", + "츆": "chyukk", + "츇": "chyuk", + "츈": "chyun", + "츉": "chyun", + "츊": "chyun", + "츋": "chyut", + "츌": "chyul", + "츍": "chyuk", + "츎": "chyum", + "츏": "chyup", + "츐": "chyut", + "츑": "chyut", + "츒": "chyup", + "츓": "chyul", + "츔": "chyum", + "츕": "chyup", + "츖": "chyup", + "츗": "chyut", + "츘": "chyut", + "츙": "chyung", + "츚": "chyut", + "츛": "chyut", + "츜": "chyuk", + "츝": "chyut", + "츞": "chyup", + "츟": "chyut", + "츠": "cheu", + "측": "cheuk", + "츢": "cheukk", + "츣": "cheuk", + "츤": "cheun", + "츥": "cheun", + "츦": "cheun", + "츧": "cheut", + "츨": "cheul", + "츩": "cheuk", + "츪": "cheum", + "츫": "cheup", + "츬": "cheut", + "츭": "cheut", + "츮": "cheup", + "츯": "cheul", + "츰": "cheum", + "츱": "cheup", + "츲": "cheup", + "츳": "cheut", + "츴": "cheut", + "층": "cheung", + "츶": "cheut", + "츷": "cheut", + "츸": "cheuk", + "츹": "cheut", + "츺": "cheup", + "츻": "cheut", + "츼": "cheui", + "츽": "cheuik", + "츾": "cheuikk", + "츿": "cheuik", + "칀": "cheuin", + "칁": "cheuin", + "칂": "cheuin", + "칃": "cheuit", + "칄": "cheuil", + "칅": "cheuik", + "칆": "cheuim", + "칇": "cheuip", + "칈": "cheuit", + "칉": "cheuit", + "칊": "cheuip", + "칋": "cheuil", + "칌": "cheuim", + "칍": "cheuip", + "칎": "cheuip", + "칏": "cheuit", + "칐": "cheuit", + "칑": "cheuing", + "칒": "cheuit", + "칓": "cheuit", + "칔": "cheuik", + "칕": "cheuit", + "칖": "cheuip", + "칗": "cheuit", + "치": "chi", + "칙": "chik", + "칚": "chikk", + "칛": "chik", + "친": "chin", + "칝": "chin", + "칞": "chin", + "칟": "chit", + "칠": "chil", + "칡": "chik", + "칢": "chim", + "칣": "chip", + "칤": "chit", + "칥": "chit", + "칦": "chip", + "칧": "chil", + "침": "chim", + "칩": "chip", + "칪": "chip", + "칫": "chit", + "칬": "chit", + "칭": "ching", + "칮": "chit", + "칯": "chit", + "칰": "chik", + "칱": "chit", + "칲": "chip", + "칳": "chit", + "카": "ka", + "칵": "kak", + "칶": "kakk", + "칷": "kak", + "칸": "kan", + "칹": "kan", + "칺": "kan", + "칻": "kat", + "칼": "kal", + "칽": "kak", + "칾": "kam", + "칿": "kap", + "캀": "kat", + "캁": "kat", + "캂": "kap", + "캃": "kal", + "캄": "kam", + "캅": "kap", + "캆": "kap", + "캇": "kat", + "캈": "kat", + "캉": "kang", + "캊": "kat", + "캋": "kat", + "캌": "kak", + "캍": "kat", + "캎": "kap", + "캏": "kat", + "캐": "kae", + "캑": "kaek", + "캒": "kaekk", + "캓": "kaek", + "캔": "kaen", + "캕": "kaen", + "캖": "kaen", + "캗": "kaet", + "캘": "kael", + "캙": "kaek", + "캚": "kaem", + "캛": "kaep", + "캜": "kaet", + "캝": "kaet", + "캞": "kaep", + "캟": "kael", + "캠": "kaem", + "캡": "kaep", + "캢": "kaep", + "캣": "kaet", + "캤": "kaet", + "캥": "kaeng", + "캦": "kaet", + "캧": "kaet", + "캨": "kaek", + "캩": "kaet", + "캪": "kaep", + "캫": "kaet", + "캬": "kya", + "캭": "kyak", + "캮": "kyakk", + "캯": "kyak", + "캰": "kyan", + "캱": "kyan", + "캲": "kyan", + "캳": "kyat", + "캴": "kyal", + "캵": "kyak", + "캶": "kyam", + "캷": "kyap", + "캸": "kyat", + "캹": "kyat", + "캺": "kyap", + "캻": "kyal", + "캼": "kyam", + "캽": "kyap", + "캾": "kyap", + "캿": "kyat", + "컀": "kyat", + "컁": "kyang", + "컂": "kyat", + "컃": "kyat", + "컄": "kyak", + "컅": "kyat", + "컆": "kyap", + "컇": "kyat", + "컈": "kyae", + "컉": "kyaek", + "컊": "kyaekk", + "컋": "kyaek", + "컌": "kyaen", + "컍": "kyaen", + "컎": "kyaen", + "컏": "kyaet", + "컐": "kyael", + "컑": "kyaek", + "컒": "kyaem", + "컓": "kyaep", + "컔": "kyaet", + "컕": "kyaet", + "컖": "kyaep", + "컗": "kyael", + "컘": "kyaem", + "컙": "kyaep", + "컚": "kyaep", + "컛": "kyaet", + "컜": "kyaet", + "컝": "kyaeng", + "컞": "kyaet", + "컟": "kyaet", + "컠": "kyaek", + "컡": "kyaet", + "컢": "kyaep", + "컣": "kyaet", + "커": "keo", + "컥": "keok", + "컦": "keokk", + "컧": "keok", + "컨": "keon", + "컩": "keon", + "컪": "keon", + "컫": "keot", + "컬": "keol", + "컭": "keok", + "컮": "keom", + "컯": "keop", + "컰": "keot", + "컱": "keot", + "컲": "keop", + "컳": "keol", + "컴": "keom", + "컵": "keop", + "컶": "keop", + "컷": "keot", + "컸": "keot", + "컹": "keong", + "컺": "keot", + "컻": "keot", + "컼": "keok", + "컽": "keot", + "컾": "keop", + "컿": "keot", + "케": "ke", + "켁": "kek", + "켂": "kekk", + "켃": "kek", + "켄": "ken", + "켅": "ken", + "켆": "ken", + "켇": "ket", + "켈": "kel", + "켉": "kek", + "켊": "kem", + "켋": "kep", + "켌": "ket", + "켍": "ket", + "켎": "kep", + "켏": "kel", + "켐": "kem", + "켑": "kep", + "켒": "kep", + "켓": "ket", + "켔": "ket", + "켕": "keng", + "켖": "ket", + "켗": "ket", + "켘": "kek", + "켙": "ket", + "켚": "kep", + "켛": "ket", + "켜": "kyeo", + "켝": "kyeok", + "켞": "kyeokk", + "켟": "kyeok", + "켠": "kyeon", + "켡": "kyeon", + "켢": "kyeon", + "켣": "kyeot", + "켤": "kyeol", + "켥": "kyeok", + "켦": "kyeom", + "켧": "kyeop", + "켨": "kyeot", + "켩": "kyeot", + "켪": "kyeop", + "켫": "kyeol", + "켬": "kyeom", + "켭": "kyeop", + "켮": "kyeop", + "켯": "kyeot", + "켰": "kyeot", + "켱": "kyeong", + "켲": "kyeot", + "켳": "kyeot", + "켴": "kyeok", + "켵": "kyeot", + "켶": "kyeop", + "켷": "kyeot", + "켸": "kye", + "켹": "kyek", + "켺": "kyekk", + "켻": "kyek", + "켼": "kyen", + "켽": "kyen", + "켾": "kyen", + "켿": "kyet", + "콀": "kyel", + "콁": "kyek", + "콂": "kyem", + "콃": "kyep", + "콄": "kyet", + "콅": "kyet", + "콆": "kyep", + "콇": "kyel", + "콈": "kyem", + "콉": "kyep", + "콊": "kyep", + "콋": "kyet", + "콌": "kyet", + "콍": "kyeng", + "콎": "kyet", + "콏": "kyet", + "콐": "kyek", + "콑": "kyet", + "콒": "kyep", + "콓": "kyet", + "코": "ko", + "콕": "kok", + "콖": "kokk", + "콗": "kok", + "콘": "kon", + "콙": "kon", + "콚": "kon", + "콛": "kot", + "콜": "kol", + "콝": "kok", + "콞": "kom", + "콟": "kop", + "콠": "kot", + "콡": "kot", + "콢": "kop", + "콣": "kol", + "콤": "kom", + "콥": "kop", + "콦": "kop", + "콧": "kot", + "콨": "kot", + "콩": "kong", + "콪": "kot", + "콫": "kot", + "콬": "kok", + "콭": "kot", + "콮": "kop", + "콯": "kot", + "콰": "kwa", + "콱": "kwak", + "콲": "kwakk", + "콳": "kwak", + "콴": "kwan", + "콵": "kwan", + "콶": "kwan", + "콷": "kwat", + "콸": "kwal", + "콹": "kwak", + "콺": "kwam", + "콻": "kwap", + "콼": "kwat", + "콽": "kwat", + "콾": "kwap", + "콿": "kwal", + "쾀": "kwam", + "쾁": "kwap", + "쾂": "kwap", + "쾃": "kwat", + "쾄": "kwat", + "쾅": "kwang", + "쾆": "kwat", + "쾇": "kwat", + "쾈": "kwak", + "쾉": "kwat", + "쾊": "kwap", + "쾋": "kwat", + "쾌": "kwae", + "쾍": "kwaek", + "쾎": "kwaekk", + "쾏": "kwaek", + "쾐": "kwaen", + "쾑": "kwaen", + "쾒": "kwaen", + "쾓": "kwaet", + "쾔": "kwael", + "쾕": "kwaek", + "쾖": "kwaem", + "쾗": "kwaep", + "쾘": "kwaet", + "쾙": "kwaet", + "쾚": "kwaep", + "쾛": "kwael", + "쾜": "kwaem", + "쾝": "kwaep", + "쾞": "kwaep", + "쾟": "kwaet", + "쾠": "kwaet", + "쾡": "kwaeng", + "쾢": "kwaet", + "쾣": "kwaet", + "쾤": "kwaek", + "쾥": "kwaet", + "쾦": "kwaep", + "쾧": "kwaet", + "쾨": "koe", + "쾩": "koek", + "쾪": "koekk", + "쾫": "koek", + "쾬": "koen", + "쾭": "koen", + "쾮": "koen", + "쾯": "koet", + "쾰": "koel", + "쾱": "koek", + "쾲": "koem", + "쾳": "koep", + "쾴": "koet", + "쾵": "koet", + "쾶": "koep", + "쾷": "koel", + "쾸": "koem", + "쾹": "koep", + "쾺": "koep", + "쾻": "koet", + "쾼": "koet", + "쾽": "koeng", + "쾾": "koet", + "쾿": "koet", + "쿀": "koek", + "쿁": "koet", + "쿂": "koep", + "쿃": "koet", + "쿄": "kyo", + "쿅": "kyok", + "쿆": "kyokk", + "쿇": "kyok", + "쿈": "kyon", + "쿉": "kyon", + "쿊": "kyon", + "쿋": "kyot", + "쿌": "kyol", + "쿍": "kyok", + "쿎": "kyom", + "쿏": "kyop", + "쿐": "kyot", + "쿑": "kyot", + "쿒": "kyop", + "쿓": "kyol", + "쿔": "kyom", + "쿕": "kyop", + "쿖": "kyop", + "쿗": "kyot", + "쿘": "kyot", + "쿙": "kyong", + "쿚": "kyot", + "쿛": "kyot", + "쿜": "kyok", + "쿝": "kyot", + "쿞": "kyop", + "쿟": "kyot", + "쿠": "ku", + "쿡": "kuk", + "쿢": "kukk", + "쿣": "kuk", + "쿤": "kun", + "쿥": "kun", + "쿦": "kun", + "쿧": "kut", + "쿨": "kul", + "쿩": "kuk", + "쿪": "kum", + "쿫": "kup", + "쿬": "kut", + "쿭": "kut", + "쿮": "kup", + "쿯": "kul", + "쿰": "kum", + "쿱": "kup", + "쿲": "kup", + "쿳": "kut", + "쿴": "kut", + "쿵": "kung", + "쿶": "kut", + "쿷": "kut", + "쿸": "kuk", + "쿹": "kut", + "쿺": "kup", + "쿻": "kut", + "쿼": "kwo", + "쿽": "kwok", + "쿾": "kwokk", + "쿿": "kwok", + "퀀": "kwon", + "퀁": "kwon", + "퀂": "kwon", + "퀃": "kwot", + "퀄": "kwol", + "퀅": "kwok", + "퀆": "kwom", + "퀇": "kwop", + "퀈": "kwot", + "퀉": "kwot", + "퀊": "kwop", + "퀋": "kwol", + "퀌": "kwom", + "퀍": "kwop", + "퀎": "kwop", + "퀏": "kwot", + "퀐": "kwot", + "퀑": "kwong", + "퀒": "kwot", + "퀓": "kwot", + "퀔": "kwok", + "퀕": "kwot", + "퀖": "kwop", + "퀗": "kwot", + "퀘": "kwe", + "퀙": "kwek", + "퀚": "kwekk", + "퀛": "kwek", + "퀜": "kwen", + "퀝": "kwen", + "퀞": "kwen", + "퀟": "kwet", + "퀠": "kwel", + "퀡": "kwek", + "퀢": "kwem", + "퀣": "kwep", + "퀤": "kwet", + "퀥": "kwet", + "퀦": "kwep", + "퀧": "kwel", + "퀨": "kwem", + "퀩": "kwep", + "퀪": "kwep", + "퀫": "kwet", + "퀬": "kwet", + "퀭": "kweng", + "퀮": "kwet", + "퀯": "kwet", + "퀰": "kwek", + "퀱": "kwet", + "퀲": "kwep", + "퀳": "kwet", + "퀴": "kwi", + "퀵": "kwik", + "퀶": "kwikk", + "퀷": "kwik", + "퀸": "kwin", + "퀹": "kwin", + "퀺": "kwin", + "퀻": "kwit", + "퀼": "kwil", + "퀽": "kwik", + "퀾": "kwim", + "퀿": "kwip", + "큀": "kwit", + "큁": "kwit", + "큂": "kwip", + "큃": "kwil", + "큄": "kwim", + "큅": "kwip", + "큆": "kwip", + "큇": "kwit", + "큈": "kwit", + "큉": "kwing", + "큊": "kwit", + "큋": "kwit", + "큌": "kwik", + "큍": "kwit", + "큎": "kwip", + "큏": "kwit", + "큐": "kyu", + "큑": "kyuk", + "큒": "kyukk", + "큓": "kyuk", + "큔": "kyun", + "큕": "kyun", + "큖": "kyun", + "큗": "kyut", + "큘": "kyul", + "큙": "kyuk", + "큚": "kyum", + "큛": "kyup", + "큜": "kyut", + "큝": "kyut", + "큞": "kyup", + "큟": "kyul", + "큠": "kyum", + "큡": "kyup", + "큢": "kyup", + "큣": "kyut", + "큤": "kyut", + "큥": "kyung", + "큦": "kyut", + "큧": "kyut", + "큨": "kyuk", + "큩": "kyut", + "큪": "kyup", + "큫": "kyut", + "크": "keu", + "큭": "keuk", + "큮": "keukk", + "큯": "keuk", + "큰": "keun", + "큱": "keun", + "큲": "keun", + "큳": "keut", + "클": "keul", + "큵": "keuk", + "큶": "keum", + "큷": "keup", + "큸": "keut", + "큹": "keut", + "큺": "keup", + "큻": "keul", + "큼": "keum", + "큽": "keup", + "큾": "keup", + "큿": "keut", + "킀": "keut", + "킁": "keung", + "킂": "keut", + "킃": "keut", + "킄": "keuk", + "킅": "keut", + "킆": "keup", + "킇": "keut", + "킈": "keui", + "킉": "keuik", + "킊": "keuikk", + "킋": "keuik", + "킌": "keuin", + "킍": "keuin", + "킎": "keuin", + "킏": "keuit", + "킐": "keuil", + "킑": "keuik", + "킒": "keuim", + "킓": "keuip", + "킔": "keuit", + "킕": "keuit", + "킖": "keuip", + "킗": "keuil", + "킘": "keuim", + "킙": "keuip", + "킚": "keuip", + "킛": "keuit", + "킜": "keuit", + "킝": "keuing", + "킞": "keuit", + "킟": "keuit", + "킠": "keuik", + "킡": "keuit", + "킢": "keuip", + "킣": "keuit", + "키": "ki", + "킥": "kik", + "킦": "kikk", + "킧": "kik", + "킨": "kin", + "킩": "kin", + "킪": "kin", + "킫": "kit", + "킬": "kil", + "킭": "kik", + "킮": "kim", + "킯": "kip", + "킰": "kit", + "킱": "kit", + "킲": "kip", + "킳": "kil", + "킴": "kim", + "킵": "kip", + "킶": "kip", + "킷": "kit", + "킸": "kit", + "킹": "king", + "킺": "kit", + "킻": "kit", + "킼": "kik", + "킽": "kit", + "킾": "kip", + "킿": "kit", + "타": "ta", + "탁": "tak", + "탂": "takk", + "탃": "tak", + "탄": "tan", + "탅": "tan", + "탆": "tan", + "탇": "tat", + "탈": "tal", + "탉": "tak", + "탊": "tam", + "탋": "tap", + "탌": "tat", + "탍": "tat", + "탎": "tap", + "탏": "tal", + "탐": "tam", + "탑": "tap", + "탒": "tap", + "탓": "tat", + "탔": "tat", + "탕": "tang", + "탖": "tat", + "탗": "tat", + "탘": "tak", + "탙": "tat", + "탚": "tap", + "탛": "tat", + "태": "tae", + "택": "taek", + "탞": "taekk", + "탟": "taek", + "탠": "taen", + "탡": "taen", + "탢": "taen", + "탣": "taet", + "탤": "tael", + "탥": "taek", + "탦": "taem", + "탧": "taep", + "탨": "taet", + "탩": "taet", + "탪": "taep", + "탫": "tael", + "탬": "taem", + "탭": "taep", + "탮": "taep", + "탯": "taet", + "탰": "taet", + "탱": "taeng", + "탲": "taet", + "탳": "taet", + "탴": "taek", + "탵": "taet", + "탶": "taep", + "탷": "taet", + "탸": "tya", + "탹": "tyak", + "탺": "tyakk", + "탻": "tyak", + "탼": "tyan", + "탽": "tyan", + "탾": "tyan", + "탿": "tyat", + "턀": "tyal", + "턁": "tyak", + "턂": "tyam", + "턃": "tyap", + "턄": "tyat", + "턅": "tyat", + "턆": "tyap", + "턇": "tyal", + "턈": "tyam", + "턉": "tyap", + "턊": "tyap", + "턋": "tyat", + "턌": "tyat", + "턍": "tyang", + "턎": "tyat", + "턏": "tyat", + "턐": "tyak", + "턑": "tyat", + "턒": "tyap", + "턓": "tyat", + "턔": "tyae", + "턕": "tyaek", + "턖": "tyaekk", + "턗": "tyaek", + "턘": "tyaen", + "턙": "tyaen", + "턚": "tyaen", + "턛": "tyaet", + "턜": "tyael", + "턝": "tyaek", + "턞": "tyaem", + "턟": "tyaep", + "턠": "tyaet", + "턡": "tyaet", + "턢": "tyaep", + "턣": "tyael", + "턤": "tyaem", + "턥": "tyaep", + "턦": "tyaep", + "턧": "tyaet", + "턨": "tyaet", + "턩": "tyaeng", + "턪": "tyaet", + "턫": "tyaet", + "턬": "tyaek", + "턭": "tyaet", + "턮": "tyaep", + "턯": "tyaet", + "터": "teo", + "턱": "teok", + "턲": "teokk", + "턳": "teok", + "턴": "teon", + "턵": "teon", + "턶": "teon", + "턷": "teot", + "털": "teol", + "턹": "teok", + "턺": "teom", + "턻": "teop", + "턼": "teot", + "턽": "teot", + "턾": "teop", + "턿": "teol", + "텀": "teom", + "텁": "teop", + "텂": "teop", + "텃": "teot", + "텄": "teot", + "텅": "teong", + "텆": "teot", + "텇": "teot", + "텈": "teok", + "텉": "teot", + "텊": "teop", + "텋": "teot", + "테": "te", + "텍": "tek", + "텎": "tekk", + "텏": "tek", + "텐": "ten", + "텑": "ten", + "텒": "ten", + "텓": "tet", + "텔": "tel", + "텕": "tek", + "텖": "tem", + "텗": "tep", + "텘": "tet", + "텙": "tet", + "텚": "tep", + "텛": "tel", + "템": "tem", + "텝": "tep", + "텞": "tep", + "텟": "tet", + "텠": "tet", + "텡": "teng", + "텢": "tet", + "텣": "tet", + "텤": "tek", + "텥": "tet", + "텦": "tep", + "텧": "tet", + "텨": "tyeo", + "텩": "tyeok", + "텪": "tyeokk", + "텫": "tyeok", + "텬": "tyeon", + "텭": "tyeon", + "텮": "tyeon", + "텯": "tyeot", + "텰": "tyeol", + "텱": "tyeok", + "텲": "tyeom", + "텳": "tyeop", + "텴": "tyeot", + "텵": "tyeot", + "텶": "tyeop", + "텷": "tyeol", + "텸": "tyeom", + "텹": "tyeop", + "텺": "tyeop", + "텻": "tyeot", + "텼": "tyeot", + "텽": "tyeong", + "텾": "tyeot", + "텿": "tyeot", + "톀": "tyeok", + "톁": "tyeot", + "톂": "tyeop", + "톃": "tyeot", + "톄": "tye", + "톅": "tyek", + "톆": "tyekk", + "톇": "tyek", + "톈": "tyen", + "톉": "tyen", + "톊": "tyen", + "톋": "tyet", + "톌": "tyel", + "톍": "tyek", + "톎": "tyem", + "톏": "tyep", + "톐": "tyet", + "톑": "tyet", + "톒": "tyep", + "톓": "tyel", + "톔": "tyem", + "톕": "tyep", + "톖": "tyep", + "톗": "tyet", + "톘": "tyet", + "톙": "tyeng", + "톚": "tyet", + "톛": "tyet", + "톜": "tyek", + "톝": "tyet", + "톞": "tyep", + "톟": "tyet", + "토": "to", + "톡": "tok", + "톢": "tokk", + "톣": "tok", + "톤": "ton", + "톥": "ton", + "톦": "ton", + "톧": "tot", + "톨": "tol", + "톩": "tok", + "톪": "tom", + "톫": "top", + "톬": "tot", + "톭": "tot", + "톮": "top", + "톯": "tol", + "톰": "tom", + "톱": "top", + "톲": "top", + "톳": "tot", + "톴": "tot", + "통": "tong", + "톶": "tot", + "톷": "tot", + "톸": "tok", + "톹": "tot", + "톺": "top", + "톻": "tot", + "톼": "twa", + "톽": "twak", + "톾": "twakk", + "톿": "twak", + "퇀": "twan", + "퇁": "twan", + "퇂": "twan", + "퇃": "twat", + "퇄": "twal", + "퇅": "twak", + "퇆": "twam", + "퇇": "twap", + "퇈": "twat", + "퇉": "twat", + "퇊": "twap", + "퇋": "twal", + "퇌": "twam", + "퇍": "twap", + "퇎": "twap", + "퇏": "twat", + "퇐": "twat", + "퇑": "twang", + "퇒": "twat", + "퇓": "twat", + "퇔": "twak", + "퇕": "twat", + "퇖": "twap", + "퇗": "twat", + "퇘": "twae", + "퇙": "twaek", + "퇚": "twaekk", + "퇛": "twaek", + "퇜": "twaen", + "퇝": "twaen", + "퇞": "twaen", + "퇟": "twaet", + "퇠": "twael", + "퇡": "twaek", + "퇢": "twaem", + "퇣": "twaep", + "퇤": "twaet", + "퇥": "twaet", + "퇦": "twaep", + "퇧": "twael", + "퇨": "twaem", + "퇩": "twaep", + "퇪": "twaep", + "퇫": "twaet", + "퇬": "twaet", + "퇭": "twaeng", + "퇮": "twaet", + "퇯": "twaet", + "퇰": "twaek", + "퇱": "twaet", + "퇲": "twaep", + "퇳": "twaet", + "퇴": "toe", + "퇵": "toek", + "퇶": "toekk", + "퇷": "toek", + "퇸": "toen", + "퇹": "toen", + "퇺": "toen", + "퇻": "toet", + "퇼": "toel", + "퇽": "toek", + "퇾": "toem", + "퇿": "toep", + "툀": "toet", + "툁": "toet", + "툂": "toep", + "툃": "toel", + "툄": "toem", + "툅": "toep", + "툆": "toep", + "툇": "toet", + "툈": "toet", + "툉": "toeng", + "툊": "toet", + "툋": "toet", + "툌": "toek", + "툍": "toet", + "툎": "toep", + "툏": "toet", + "툐": "tyo", + "툑": "tyok", + "툒": "tyokk", + "툓": "tyok", + "툔": "tyon", + "툕": "tyon", + "툖": "tyon", + "툗": "tyot", + "툘": "tyol", + "툙": "tyok", + "툚": "tyom", + "툛": "tyop", + "툜": "tyot", + "툝": "tyot", + "툞": "tyop", + "툟": "tyol", + "툠": "tyom", + "툡": "tyop", + "툢": "tyop", + "툣": "tyot", + "툤": "tyot", + "툥": "tyong", + "툦": "tyot", + "툧": "tyot", + "툨": "tyok", + "툩": "tyot", + "툪": "tyop", + "툫": "tyot", + "투": "tu", + "툭": "tuk", + "툮": "tukk", + "툯": "tuk", + "툰": "tun", + "툱": "tun", + "툲": "tun", + "툳": "tut", + "툴": "tul", + "툵": "tuk", + "툶": "tum", + "툷": "tup", + "툸": "tut", + "툹": "tut", + "툺": "tup", + "툻": "tul", + "툼": "tum", + "툽": "tup", + "툾": "tup", + "툿": "tut", + "퉀": "tut", + "퉁": "tung", + "퉂": "tut", + "퉃": "tut", + "퉄": "tuk", + "퉅": "tut", + "퉆": "tup", + "퉇": "tut", + "퉈": "two", + "퉉": "twok", + "퉊": "twokk", + "퉋": "twok", + "퉌": "twon", + "퉍": "twon", + "퉎": "twon", + "퉏": "twot", + "퉐": "twol", + "퉑": "twok", + "퉒": "twom", + "퉓": "twop", + "퉔": "twot", + "퉕": "twot", + "퉖": "twop", + "퉗": "twol", + "퉘": "twom", + "퉙": "twop", + "퉚": "twop", + "퉛": "twot", + "퉜": "twot", + "퉝": "twong", + "퉞": "twot", + "퉟": "twot", + "퉠": "twok", + "퉡": "twot", + "퉢": "twop", + "퉣": "twot", + "퉤": "twe", + "퉥": "twek", + "퉦": "twekk", + "퉧": "twek", + "퉨": "twen", + "퉩": "twen", + "퉪": "twen", + "퉫": "twet", + "퉬": "twel", + "퉭": "twek", + "퉮": "twem", + "퉯": "twep", + "퉰": "twet", + "퉱": "twet", + "퉲": "twep", + "퉳": "twel", + "퉴": "twem", + "퉵": "twep", + "퉶": "twep", + "퉷": "twet", + "퉸": "twet", + "퉹": "tweng", + "퉺": "twet", + "퉻": "twet", + "퉼": "twek", + "퉽": "twet", + "퉾": "twep", + "퉿": "twet", + "튀": "twi", + "튁": "twik", + "튂": "twikk", + "튃": "twik", + "튄": "twin", + "튅": "twin", + "튆": "twin", + "튇": "twit", + "튈": "twil", + "튉": "twik", + "튊": "twim", + "튋": "twip", + "튌": "twit", + "튍": "twit", + "튎": "twip", + "튏": "twil", + "튐": "twim", + "튑": "twip", + "튒": "twip", + "튓": "twit", + "튔": "twit", + "튕": "twing", + "튖": "twit", + "튗": "twit", + "튘": "twik", + "튙": "twit", + "튚": "twip", + "튛": "twit", + "튜": "tyu", + "튝": "tyuk", + "튞": "tyukk", + "튟": "tyuk", + "튠": "tyun", + "튡": "tyun", + "튢": "tyun", + "튣": "tyut", + "튤": "tyul", + "튥": "tyuk", + "튦": "tyum", + "튧": "tyup", + "튨": "tyut", + "튩": "tyut", + "튪": "tyup", + "튫": "tyul", + "튬": "tyum", + "튭": "tyup", + "튮": "tyup", + "튯": "tyut", + "튰": "tyut", + "튱": "tyung", + "튲": "tyut", + "튳": "tyut", + "튴": "tyuk", + "튵": "tyut", + "튶": "tyup", + "튷": "tyut", + "트": "teu", + "특": "teuk", + "튺": "teukk", + "튻": "teuk", + "튼": "teun", + "튽": "teun", + "튾": "teun", + "튿": "teut", + "틀": "teul", + "틁": "teuk", + "틂": "teum", + "틃": "teup", + "틄": "teut", + "틅": "teut", + "틆": "teup", + "틇": "teul", + "틈": "teum", + "틉": "teup", + "틊": "teup", + "틋": "teut", + "틌": "teut", + "틍": "teung", + "틎": "teut", + "틏": "teut", + "틐": "teuk", + "틑": "teut", + "틒": "teup", + "틓": "teut", + "틔": "teui", + "틕": "teuik", + "틖": "teuikk", + "틗": "teuik", + "틘": "teuin", + "틙": "teuin", + "틚": "teuin", + "틛": "teuit", + "틜": "teuil", + "틝": "teuik", + "틞": "teuim", + "틟": "teuip", + "틠": "teuit", + "틡": "teuit", + "틢": "teuip", + "틣": "teuil", + "틤": "teuim", + "틥": "teuip", + "틦": "teuip", + "틧": "teuit", + "틨": "teuit", + "틩": "teuing", + "틪": "teuit", + "틫": "teuit", + "틬": "teuik", + "틭": "teuit", + "틮": "teuip", + "틯": "teuit", + "티": "ti", + "틱": "tik", + "틲": "tikk", + "틳": "tik", + "틴": "tin", + "틵": "tin", + "틶": "tin", + "틷": "tit", + "틸": "til", + "틹": "tik", + "틺": "tim", + "틻": "tip", + "틼": "tit", + "틽": "tit", + "틾": "tip", + "틿": "til", + "팀": "tim", + "팁": "tip", + "팂": "tip", + "팃": "tit", + "팄": "tit", + "팅": "ting", + "팆": "tit", + "팇": "tit", + "팈": "tik", + "팉": "tit", + "팊": "tip", + "팋": "tit", + "파": "pa", + "팍": "pak", + "팎": "pakk", + "팏": "pak", + "판": "pan", + "팑": "pan", + "팒": "pan", + "팓": "pat", + "팔": "pal", + "팕": "pak", + "팖": "pam", + "팗": "pap", + "팘": "pat", + "팙": "pat", + "팚": "pap", + "팛": "pal", + "팜": "pam", + "팝": "pap", + "팞": "pap", + "팟": "pat", + "팠": "pat", + "팡": "pang", + "팢": "pat", + "팣": "pat", + "팤": "pak", + "팥": "pat", + "팦": "pap", + "팧": "pat", + "패": "pae", + "팩": "paek", + "팪": "paekk", + "팫": "paek", + "팬": "paen", + "팭": "paen", + "팮": "paen", + "팯": "paet", + "팰": "pael", + "팱": "paek", + "팲": "paem", + "팳": "paep", + "팴": "paet", + "팵": "paet", + "팶": "paep", + "팷": "pael", + "팸": "paem", + "팹": "paep", + "팺": "paep", + "팻": "paet", + "팼": "paet", + "팽": "paeng", + "팾": "paet", + "팿": "paet", + "퍀": "paek", + "퍁": "paet", + "퍂": "paep", + "퍃": "paet", + "퍄": "pya", + "퍅": "pyak", + "퍆": "pyakk", + "퍇": "pyak", + "퍈": "pyan", + "퍉": "pyan", + "퍊": "pyan", + "퍋": "pyat", + "퍌": "pyal", + "퍍": "pyak", + "퍎": "pyam", + "퍏": "pyap", + "퍐": "pyat", + "퍑": "pyat", + "퍒": "pyap", + "퍓": "pyal", + "퍔": "pyam", + "퍕": "pyap", + "퍖": "pyap", + "퍗": "pyat", + "퍘": "pyat", + "퍙": "pyang", + "퍚": "pyat", + "퍛": "pyat", + "퍜": "pyak", + "퍝": "pyat", + "퍞": "pyap", + "퍟": "pyat", + "퍠": "pyae", + "퍡": "pyaek", + "퍢": "pyaekk", + "퍣": "pyaek", + "퍤": "pyaen", + "퍥": "pyaen", + "퍦": "pyaen", + "퍧": "pyaet", + "퍨": "pyael", + "퍩": "pyaek", + "퍪": "pyaem", + "퍫": "pyaep", + "퍬": "pyaet", + "퍭": "pyaet", + "퍮": "pyaep", + "퍯": "pyael", + "퍰": "pyaem", + "퍱": "pyaep", + "퍲": "pyaep", + "퍳": "pyaet", + "퍴": "pyaet", + "퍵": "pyaeng", + "퍶": "pyaet", + "퍷": "pyaet", + "퍸": "pyaek", + "퍹": "pyaet", + "퍺": "pyaep", + "퍻": "pyaet", + "퍼": "peo", + "퍽": "peok", + "퍾": "peokk", + "퍿": "peok", + "펀": "peon", + "펁": "peon", + "펂": "peon", + "펃": "peot", + "펄": "peol", + "펅": "peok", + "펆": "peom", + "펇": "peop", + "펈": "peot", + "펉": "peot", + "펊": "peop", + "펋": "peol", + "펌": "peom", + "펍": "peop", + "펎": "peop", + "펏": "peot", + "펐": "peot", + "펑": "peong", + "펒": "peot", + "펓": "peot", + "펔": "peok", + "펕": "peot", + "펖": "peop", + "펗": "peot", + "페": "pe", + "펙": "pek", + "펚": "pekk", + "펛": "pek", + "펜": "pen", + "펝": "pen", + "펞": "pen", + "펟": "pet", + "펠": "pel", + "펡": "pek", + "펢": "pem", + "펣": "pep", + "펤": "pet", + "펥": "pet", + "펦": "pep", + "펧": "pel", + "펨": "pem", + "펩": "pep", + "펪": "pep", + "펫": "pet", + "펬": "pet", + "펭": "peng", + "펮": "pet", + "펯": "pet", + "펰": "pek", + "펱": "pet", + "펲": "pep", + "펳": "pet", + "펴": "pyeo", + "펵": "pyeok", + "펶": "pyeokk", + "펷": "pyeok", + "편": "pyeon", + "펹": "pyeon", + "펺": "pyeon", + "펻": "pyeot", + "펼": "pyeol", + "펽": "pyeok", + "펾": "pyeom", + "펿": "pyeop", + "폀": "pyeot", + "폁": "pyeot", + "폂": "pyeop", + "폃": "pyeol", + "폄": "pyeom", + "폅": "pyeop", + "폆": "pyeop", + "폇": "pyeot", + "폈": "pyeot", + "평": "pyeong", + "폊": "pyeot", + "폋": "pyeot", + "폌": "pyeok", + "폍": "pyeot", + "폎": "pyeop", + "폏": "pyeot", + "폐": "pye", + "폑": "pyek", + "폒": "pyekk", + "폓": "pyek", + "폔": "pyen", + "폕": "pyen", + "폖": "pyen", + "폗": "pyet", + "폘": "pyel", + "폙": "pyek", + "폚": "pyem", + "폛": "pyep", + "폜": "pyet", + "폝": "pyet", + "폞": "pyep", + "폟": "pyel", + "폠": "pyem", + "폡": "pyep", + "폢": "pyep", + "폣": "pyet", + "폤": "pyet", + "폥": "pyeng", + "폦": "pyet", + "폧": "pyet", + "폨": "pyek", + "폩": "pyet", + "폪": "pyep", + "폫": "pyet", + "포": "po", + "폭": "pok", + "폮": "pokk", + "폯": "pok", + "폰": "pon", + "폱": "pon", + "폲": "pon", + "폳": "pot", + "폴": "pol", + "폵": "pok", + "폶": "pom", + "폷": "pop", + "폸": "pot", + "폹": "pot", + "폺": "pop", + "폻": "pol", + "폼": "pom", + "폽": "pop", + "폾": "pop", + "폿": "pot", + "퐀": "pot", + "퐁": "pong", + "퐂": "pot", + "퐃": "pot", + "퐄": "pok", + "퐅": "pot", + "퐆": "pop", + "퐇": "pot", + "퐈": "pwa", + "퐉": "pwak", + "퐊": "pwakk", + "퐋": "pwak", + "퐌": "pwan", + "퐍": "pwan", + "퐎": "pwan", + "퐏": "pwat", + "퐐": "pwal", + "퐑": "pwak", + "퐒": "pwam", + "퐓": "pwap", + "퐔": "pwat", + "퐕": "pwat", + "퐖": "pwap", + "퐗": "pwal", + "퐘": "pwam", + "퐙": "pwap", + "퐚": "pwap", + "퐛": "pwat", + "퐜": "pwat", + "퐝": "pwang", + "퐞": "pwat", + "퐟": "pwat", + "퐠": "pwak", + "퐡": "pwat", + "퐢": "pwap", + "퐣": "pwat", + "퐤": "pwae", + "퐥": "pwaek", + "퐦": "pwaekk", + "퐧": "pwaek", + "퐨": "pwaen", + "퐩": "pwaen", + "퐪": "pwaen", + "퐫": "pwaet", + "퐬": "pwael", + "퐭": "pwaek", + "퐮": "pwaem", + "퐯": "pwaep", + "퐰": "pwaet", + "퐱": "pwaet", + "퐲": "pwaep", + "퐳": "pwael", + "퐴": "pwaem", + "퐵": "pwaep", + "퐶": "pwaep", + "퐷": "pwaet", + "퐸": "pwaet", + "퐹": "pwaeng", + "퐺": "pwaet", + "퐻": "pwaet", + "퐼": "pwaek", + "퐽": "pwaet", + "퐾": "pwaep", + "퐿": "pwaet", + "푀": "poe", + "푁": "poek", + "푂": "poekk", + "푃": "poek", + "푄": "poen", + "푅": "poen", + "푆": "poen", + "푇": "poet", + "푈": "poel", + "푉": "poek", + "푊": "poem", + "푋": "poep", + "푌": "poet", + "푍": "poet", + "푎": "poep", + "푏": "poel", + "푐": "poem", + "푑": "poep", + "푒": "poep", + "푓": "poet", + "푔": "poet", + "푕": "poeng", + "푖": "poet", + "푗": "poet", + "푘": "poek", + "푙": "poet", + "푚": "poep", + "푛": "poet", + "표": "pyo", + "푝": "pyok", + "푞": "pyokk", + "푟": "pyok", + "푠": "pyon", + "푡": "pyon", + "푢": "pyon", + "푣": "pyot", + "푤": "pyol", + "푥": "pyok", + "푦": "pyom", + "푧": "pyop", + "푨": "pyot", + "푩": "pyot", + "푪": "pyop", + "푫": "pyol", + "푬": "pyom", + "푭": "pyop", + "푮": "pyop", + "푯": "pyot", + "푰": "pyot", + "푱": "pyong", + "푲": "pyot", + "푳": "pyot", + "푴": "pyok", + "푵": "pyot", + "푶": "pyop", + "푷": "pyot", + "푸": "pu", + "푹": "puk", + "푺": "pukk", + "푻": "puk", + "푼": "pun", + "푽": "pun", + "푾": "pun", + "푿": "put", + "풀": "pul", + "풁": "puk", + "풂": "pum", + "풃": "pup", + "풄": "put", + "풅": "put", + "풆": "pup", + "풇": "pul", + "품": "pum", + "풉": "pup", + "풊": "pup", + "풋": "put", + "풌": "put", + "풍": "pung", + "풎": "put", + "풏": "put", + "풐": "puk", + "풑": "put", + "풒": "pup", + "풓": "put", + "풔": "pwo", + "풕": "pwok", + "풖": "pwokk", + "풗": "pwok", + "풘": "pwon", + "풙": "pwon", + "풚": "pwon", + "풛": "pwot", + "풜": "pwol", + "풝": "pwok", + "풞": "pwom", + "풟": "pwop", + "풠": "pwot", + "풡": "pwot", + "풢": "pwop", + "풣": "pwol", + "풤": "pwom", + "풥": "pwop", + "풦": "pwop", + "풧": "pwot", + "풨": "pwot", + "풩": "pwong", + "풪": "pwot", + "풫": "pwot", + "풬": "pwok", + "풭": "pwot", + "풮": "pwop", + "풯": "pwot", + "풰": "pwe", + "풱": "pwek", + "풲": "pwekk", + "풳": "pwek", + "풴": "pwen", + "풵": "pwen", + "풶": "pwen", + "풷": "pwet", + "풸": "pwel", + "풹": "pwek", + "풺": "pwem", + "풻": "pwep", + "풼": "pwet", + "풽": "pwet", + "풾": "pwep", + "풿": "pwel", + "퓀": "pwem", + "퓁": "pwep", + "퓂": "pwep", + "퓃": "pwet", + "퓄": "pwet", + "퓅": "pweng", + "퓆": "pwet", + "퓇": "pwet", + "퓈": "pwek", + "퓉": "pwet", + "퓊": "pwep", + "퓋": "pwet", + "퓌": "pwi", + "퓍": "pwik", + "퓎": "pwikk", + "퓏": "pwik", + "퓐": "pwin", + "퓑": "pwin", + "퓒": "pwin", + "퓓": "pwit", + "퓔": "pwil", + "퓕": "pwik", + "퓖": "pwim", + "퓗": "pwip", + "퓘": "pwit", + "퓙": "pwit", + "퓚": "pwip", + "퓛": "pwil", + "퓜": "pwim", + "퓝": "pwip", + "퓞": "pwip", + "퓟": "pwit", + "퓠": "pwit", + "퓡": "pwing", + "퓢": "pwit", + "퓣": "pwit", + "퓤": "pwik", + "퓥": "pwit", + "퓦": "pwip", + "퓧": "pwit", + "퓨": "pyu", + "퓩": "pyuk", + "퓪": "pyukk", + "퓫": "pyuk", + "퓬": "pyun", + "퓭": "pyun", + "퓮": "pyun", + "퓯": "pyut", + "퓰": "pyul", + "퓱": "pyuk", + "퓲": "pyum", + "퓳": "pyup", + "퓴": "pyut", + "퓵": "pyut", + "퓶": "pyup", + "퓷": "pyul", + "퓸": "pyum", + "퓹": "pyup", + "퓺": "pyup", + "퓻": "pyut", + "퓼": "pyut", + "퓽": "pyung", + "퓾": "pyut", + "퓿": "pyut", + "픀": "pyuk", + "픁": "pyut", + "픂": "pyup", + "픃": "pyut", + "프": "peu", + "픅": "peuk", + "픆": "peukk", + "픇": "peuk", + "픈": "peun", + "픉": "peun", + "픊": "peun", + "픋": "peut", + "플": "peul", + "픍": "peuk", + "픎": "peum", + "픏": "peup", + "픐": "peut", + "픑": "peut", + "픒": "peup", + "픓": "peul", + "픔": "peum", + "픕": "peup", + "픖": "peup", + "픗": "peut", + "픘": "peut", + "픙": "peung", + "픚": "peut", + "픛": "peut", + "픜": "peuk", + "픝": "peut", + "픞": "peup", + "픟": "peut", + "픠": "peui", + "픡": "peuik", + "픢": "peuikk", + "픣": "peuik", + "픤": "peuin", + "픥": "peuin", + "픦": "peuin", + "픧": "peuit", + "픨": "peuil", + "픩": "peuik", + "픪": "peuim", + "픫": "peuip", + "픬": "peuit", + "픭": "peuit", + "픮": "peuip", + "픯": "peuil", + "픰": "peuim", + "픱": "peuip", + "픲": "peuip", + "픳": "peuit", + "픴": "peuit", + "픵": "peuing", + "픶": "peuit", + "픷": "peuit", + "픸": "peuik", + "픹": "peuit", + "픺": "peuip", + "픻": "peuit", + "피": "pi", + "픽": "pik", + "픾": "pikk", + "픿": "pik", + "핀": "pin", + "핁": "pin", + "핂": "pin", + "핃": "pit", + "필": "pil", + "핅": "pik", + "핆": "pim", + "핇": "pip", + "핈": "pit", + "핉": "pit", + "핊": "pip", + "핋": "pil", + "핌": "pim", + "핍": "pip", + "핎": "pip", + "핏": "pit", + "핐": "pit", + "핑": "ping", + "핒": "pit", + "핓": "pit", + "핔": "pik", + "핕": "pit", + "핖": "pip", + "핗": "pit", + "하": "ha", + "학": "hak", + "핚": "hakk", + "핛": "hak", + "한": "han", + "핝": "han", + "핞": "han", + "핟": "hat", + "할": "hal", + "핡": "hak", + "핢": "ham", + "핣": "hap", + "핤": "hat", + "핥": "hat", + "핦": "hap", + "핧": "hal", + "함": "ham", + "합": "hap", + "핪": "hap", + "핫": "hat", + "핬": "hat", + "항": "hang", + "핮": "hat", + "핯": "hat", + "핰": "hak", + "핱": "hat", + "핲": "hap", + "핳": "hat", + "해": "hae", + "핵": "haek", + "핶": "haekk", + "핷": "haek", + "핸": "haen", + "핹": "haen", + "핺": "haen", + "핻": "haet", + "핼": "hael", + "핽": "haek", + "핾": "haem", + "핿": "haep", + "햀": "haet", + "햁": "haet", + "햂": "haep", + "햃": "hael", + "햄": "haem", + "햅": "haep", + "햆": "haep", + "햇": "haet", + "했": "haet", + "행": "haeng", + "햊": "haet", + "햋": "haet", + "햌": "haek", + "햍": "haet", + "햎": "haep", + "햏": "haet", + "햐": "hya", + "햑": "hyak", + "햒": "hyakk", + "햓": "hyak", + "햔": "hyan", + "햕": "hyan", + "햖": "hyan", + "햗": "hyat", + "햘": "hyal", + "햙": "hyak", + "햚": "hyam", + "햛": "hyap", + "햜": "hyat", + "햝": "hyat", + "햞": "hyap", + "햟": "hyal", + "햠": "hyam", + "햡": "hyap", + "햢": "hyap", + "햣": "hyat", + "햤": "hyat", + "향": "hyang", + "햦": "hyat", + "햧": "hyat", + "햨": "hyak", + "햩": "hyat", + "햪": "hyap", + "햫": "hyat", + "햬": "hyae", + "햭": "hyaek", + "햮": "hyaekk", + "햯": "hyaek", + "햰": "hyaen", + "햱": "hyaen", + "햲": "hyaen", + "햳": "hyaet", + "햴": "hyael", + "햵": "hyaek", + "햶": "hyaem", + "햷": "hyaep", + "햸": "hyaet", + "햹": "hyaet", + "햺": "hyaep", + "햻": "hyael", + "햼": "hyaem", + "햽": "hyaep", + "햾": "hyaep", + "햿": "hyaet", + "헀": "hyaet", + "헁": "hyaeng", + "헂": "hyaet", + "헃": "hyaet", + "헄": "hyaek", + "헅": "hyaet", + "헆": "hyaep", + "헇": "hyaet", + "허": "heo", + "헉": "heok", + "헊": "heokk", + "헋": "heok", + "헌": "heon", + "헍": "heon", + "헎": "heon", + "헏": "heot", + "헐": "heol", + "헑": "heok", + "헒": "heom", + "헓": "heop", + "헔": "heot", + "헕": "heot", + "헖": "heop", + "헗": "heol", + "험": "heom", + "헙": "heop", + "헚": "heop", + "헛": "heot", + "헜": "heot", + "헝": "heong", + "헞": "heot", + "헟": "heot", + "헠": "heok", + "헡": "heot", + "헢": "heop", + "헣": "heot", + "헤": "he", + "헥": "hek", + "헦": "hekk", + "헧": "hek", + "헨": "hen", + "헩": "hen", + "헪": "hen", + "헫": "het", + "헬": "hel", + "헭": "hek", + "헮": "hem", + "헯": "hep", + "헰": "het", + "헱": "het", + "헲": "hep", + "헳": "hel", + "헴": "hem", + "헵": "hep", + "헶": "hep", + "헷": "het", + "헸": "het", + "헹": "heng", + "헺": "het", + "헻": "het", + "헼": "hek", + "헽": "het", + "헾": "hep", + "헿": "het", + "혀": "hyeo", + "혁": "hyeok", + "혂": "hyeokk", + "혃": "hyeok", + "현": "hyeon", + "혅": "hyeon", + "혆": "hyeon", + "혇": "hyeot", + "혈": "hyeol", + "혉": "hyeok", + "혊": "hyeom", + "혋": "hyeop", + "혌": "hyeot", + "혍": "hyeot", + "혎": "hyeop", + "혏": "hyeol", + "혐": "hyeom", + "협": "hyeop", + "혒": "hyeop", + "혓": "hyeot", + "혔": "hyeot", + "형": "hyeong", + "혖": "hyeot", + "혗": "hyeot", + "혘": "hyeok", + "혙": "hyeot", + "혚": "hyeop", + "혛": "hyeot", + "혜": "hye", + "혝": "hyek", + "혞": "hyekk", + "혟": "hyek", + "혠": "hyen", + "혡": "hyen", + "혢": "hyen", + "혣": "hyet", + "혤": "hyel", + "혥": "hyek", + "혦": "hyem", + "혧": "hyep", + "혨": "hyet", + "혩": "hyet", + "혪": "hyep", + "혫": "hyel", + "혬": "hyem", + "혭": "hyep", + "혮": "hyep", + "혯": "hyet", + "혰": "hyet", + "혱": "hyeng", + "혲": "hyet", + "혳": "hyet", + "혴": "hyek", + "혵": "hyet", + "혶": "hyep", + "혷": "hyet", + "호": "ho", + "혹": "hok", + "혺": "hokk", + "혻": "hok", + "혼": "hon", + "혽": "hon", + "혾": "hon", + "혿": "hot", + "홀": "hol", + "홁": "hok", + "홂": "hom", + "홃": "hop", + "홄": "hot", + "홅": "hot", + "홆": "hop", + "홇": "hol", + "홈": "hom", + "홉": "hop", + "홊": "hop", + "홋": "hot", + "홌": "hot", + "홍": "hong", + "홎": "hot", + "홏": "hot", + "홐": "hok", + "홑": "hot", + "홒": "hop", + "홓": "hot", + "화": "hwa", + "확": "hwak", + "홖": "hwakk", + "홗": "hwak", + "환": "hwan", + "홙": "hwan", + "홚": "hwan", + "홛": "hwat", + "활": "hwal", + "홝": "hwak", + "홞": "hwam", + "홟": "hwap", + "홠": "hwat", + "홡": "hwat", + "홢": "hwap", + "홣": "hwal", + "홤": "hwam", + "홥": "hwap", + "홦": "hwap", + "홧": "hwat", + "홨": "hwat", + "황": "hwang", + "홪": "hwat", + "홫": "hwat", + "홬": "hwak", + "홭": "hwat", + "홮": "hwap", + "홯": "hwat", + "홰": "hwae", + "홱": "hwaek", + "홲": "hwaekk", + "홳": "hwaek", + "홴": "hwaen", + "홵": "hwaen", + "홶": "hwaen", + "홷": "hwaet", + "홸": "hwael", + "홹": "hwaek", + "홺": "hwaem", + "홻": "hwaep", + "홼": "hwaet", + "홽": "hwaet", + "홾": "hwaep", + "홿": "hwael", + "횀": "hwaem", + "횁": "hwaep", + "횂": "hwaep", + "횃": "hwaet", + "횄": "hwaet", + "횅": "hwaeng", + "횆": "hwaet", + "횇": "hwaet", + "횈": "hwaek", + "횉": "hwaet", + "횊": "hwaep", + "횋": "hwaet", + "회": "hoe", + "획": "hoek", + "횎": "hoekk", + "횏": "hoek", + "횐": "hoen", + "횑": "hoen", + "횒": "hoen", + "횓": "hoet", + "횔": "hoel", + "횕": "hoek", + "횖": "hoem", + "횗": "hoep", + "횘": "hoet", + "횙": "hoet", + "횚": "hoep", + "횛": "hoel", + "횜": "hoem", + "횝": "hoep", + "횞": "hoep", + "횟": "hoet", + "횠": "hoet", + "횡": "hoeng", + "횢": "hoet", + "횣": "hoet", + "횤": "hoek", + "횥": "hoet", + "횦": "hoep", + "횧": "hoet", + "효": "hyo", + "횩": "hyok", + "횪": "hyokk", + "횫": "hyok", + "횬": "hyon", + "횭": "hyon", + "횮": "hyon", + "횯": "hyot", + "횰": "hyol", + "횱": "hyok", + "횲": "hyom", + "횳": "hyop", + "횴": "hyot", + "횵": "hyot", + "횶": "hyop", + "횷": "hyol", + "횸": "hyom", + "횹": "hyop", + "횺": "hyop", + "횻": "hyot", + "횼": "hyot", + "횽": "hyong", + "횾": "hyot", + "횿": "hyot", + "훀": "hyok", + "훁": "hyot", + "훂": "hyop", + "훃": "hyot", + "후": "hu", + "훅": "huk", + "훆": "hukk", + "훇": "huk", + "훈": "hun", + "훉": "hun", + "훊": "hun", + "훋": "hut", + "훌": "hul", + "훍": "huk", + "훎": "hum", + "훏": "hup", + "훐": "hut", + "훑": "hut", + "훒": "hup", + "훓": "hul", + "훔": "hum", + "훕": "hup", + "훖": "hup", + "훗": "hut", + "훘": "hut", + "훙": "hung", + "훚": "hut", + "훛": "hut", + "훜": "huk", + "훝": "hut", + "훞": "hup", + "훟": "hut", + "훠": "hwo", + "훡": "hwok", + "훢": "hwokk", + "훣": "hwok", + "훤": "hwon", + "훥": "hwon", + "훦": "hwon", + "훧": "hwot", + "훨": "hwol", + "훩": "hwok", + "훪": "hwom", + "훫": "hwop", + "훬": "hwot", + "훭": "hwot", + "훮": "hwop", + "훯": "hwol", + "훰": "hwom", + "훱": "hwop", + "훲": "hwop", + "훳": "hwot", + "훴": "hwot", + "훵": "hwong", + "훶": "hwot", + "훷": "hwot", + "훸": "hwok", + "훹": "hwot", + "훺": "hwop", + "훻": "hwot", + "훼": "hwe", + "훽": "hwek", + "훾": "hwekk", + "훿": "hwek", + "휀": "hwen", + "휁": "hwen", + "휂": "hwen", + "휃": "hwet", + "휄": "hwel", + "휅": "hwek", + "휆": "hwem", + "휇": "hwep", + "휈": "hwet", + "휉": "hwet", + "휊": "hwep", + "휋": "hwel", + "휌": "hwem", + "휍": "hwep", + "휎": "hwep", + "휏": "hwet", + "휐": "hwet", + "휑": "hweng", + "휒": "hwet", + "휓": "hwet", + "휔": "hwek", + "휕": "hwet", + "휖": "hwep", + "휗": "hwet", + "휘": "hwi", + "휙": "hwik", + "휚": "hwikk", + "휛": "hwik", + "휜": "hwin", + "휝": "hwin", + "휞": "hwin", + "휟": "hwit", + "휠": "hwil", + "휡": "hwik", + "휢": "hwim", + "휣": "hwip", + "휤": "hwit", + "휥": "hwit", + "휦": "hwip", + "휧": "hwil", + "휨": "hwim", + "휩": "hwip", + "휪": "hwip", + "휫": "hwit", + "휬": "hwit", + "휭": "hwing", + "휮": "hwit", + "휯": "hwit", + "휰": "hwik", + "휱": "hwit", + "휲": "hwip", + "휳": "hwit", + "휴": "hyu", + "휵": "hyuk", + "휶": "hyukk", + "휷": "hyuk", + "휸": "hyun", + "휹": "hyun", + "휺": "hyun", + "휻": "hyut", + "휼": "hyul", + "휽": "hyuk", + "휾": "hyum", + "휿": "hyup", + "흀": "hyut", + "흁": "hyut", + "흂": "hyup", + "흃": "hyul", + "흄": "hyum", + "흅": "hyup", + "흆": "hyup", + "흇": "hyut", + "흈": "hyut", + "흉": "hyung", + "흊": "hyut", + "흋": "hyut", + "흌": "hyuk", + "흍": "hyut", + "흎": "hyup", + "흏": "hyut", + "흐": "heu", + "흑": "heuk", + "흒": "heukk", + "흓": "heuk", + "흔": "heun", + "흕": "heun", + "흖": "heun", + "흗": "heut", + "흘": "heul", + "흙": "heuk", + "흚": "heum", + "흛": "heup", + "흜": "heut", + "흝": "heut", + "흞": "heup", + "흟": "heul", + "흠": "heum", + "흡": "heup", + "흢": "heup", + "흣": "heut", + "흤": "heut", + "흥": "heung", + "흦": "heut", + "흧": "heut", + "흨": "heuk", + "흩": "heut", + "흪": "heup", + "흫": "heut", + "희": "heui", + "흭": "heuik", + "흮": "heuikk", + "흯": "heuik", + "흰": "heuin", + "흱": "heuin", + "흲": "heuin", + "흳": "heuit", + "흴": "heuil", + "흵": "heuik", + "흶": "heuim", + "흷": "heuip", + "흸": "heuit", + "흹": "heuit", + "흺": "heuip", + "흻": "heuil", + "흼": "heuim", + "흽": "heuip", + "흾": "heuip", + "흿": "heuit", + "힀": "heuit", + "힁": "heuing", + "힂": "heuit", + "힃": "heuit", + "힄": "heuik", + "힅": "heuit", + "힆": "heuip", + "힇": "heuit", + "히": "hi", + "힉": "hik", + "힊": "hikk", + "힋": "hik", + "힌": "hin", + "힍": "hin", + "힎": "hin", + "힏": "hit", + "힐": "hil", + "힑": "hik", + "힒": "him", + "힓": "hip", + "힔": "hit", + "힕": "hit", + "힖": "hip", + "힗": "hil", + "힘": "him", + "힙": "hip", + "힚": "hip", + "힛": "hit", + "힜": "hit", + "힝": "hing", + "힞": "hit", + "힟": "hit", + "힠": "hik", + "힡": "hit", + "힢": "hip", + "힣": "hit" +} \ No newline at end of file diff --git a/kirby/i18n/rules/lt.json b/kirby/i18n/rules/lt.json new file mode 100755 index 0000000..23e0d70 --- /dev/null +++ b/kirby/i18n/rules/lt.json @@ -0,0 +1,20 @@ +{ + "Ą": "A", + "Č": "C", + "Ę": "E", + "Ė": "E", + "Į": "I", + "Š": "S", + "Ų": "U", + "Ū": "U", + "Ž": "Z", + "ą": "a", + "č": "c", + "ę": "e", + "ė": "e", + "į": "i", + "š": "s", + "ų": "u", + "ū": "u", + "ž": "z" +} diff --git a/kirby/i18n/rules/lv.json b/kirby/i18n/rules/lv.json new file mode 100755 index 0000000..d5b0010 --- /dev/null +++ b/kirby/i18n/rules/lv.json @@ -0,0 +1,18 @@ +{ + "Ā": "A", + "Ē": "E", + "Ģ": "G", + "Ī": "I", + "Ķ": "K", + "Ļ": "L", + "Ņ": "N", + "Ū": "U", + "ā": "a", + "ē": "e", + "ģ": "g", + "ī": "i", + "ķ": "k", + "ļ": "l", + "ņ": "n", + "ū": "u" +} diff --git a/kirby/i18n/rules/mk.json b/kirby/i18n/rules/mk.json new file mode 100755 index 0000000..7a87f46 --- /dev/null +++ b/kirby/i18n/rules/mk.json @@ -0,0 +1,64 @@ +{ + "А": "A", + "Б": "B", + "В": "V", + "Г": "G", + "Д": "D", + "Ѓ": "Gj", + "Е": "E", + "Ж": "Zh", + "З": "Z", + "Ѕ": "Dz", + "И": "I", + "Ј": "J", + "К": "K", + "Л": "L", + "Љ": "Lj", + "М": "M", + "Н": "N", + "Њ": "Nj", + "О": "O", + "П": "P", + "Р": "R", + "С": "S", + "Т": "T", + "Ќ": "Kj", + "У": "U", + "Ф": "F", + "Х": "H", + "Ц": "C", + "Ч": "Ch", + "Џ": "Dj", + "Ш": "Sh", + "а": "a", + "б": "b", + "в": "v", + "г": "g", + "д": "d", + "ѓ": "gj", + "е": "e", + "ж": "zh", + "з": "z", + "ѕ": "dz", + "и": "i", + "ј": "j", + "к": "k", + "л": "l", + "љ": "lj", + "м": "m", + "н": "n", + "њ": "nj", + "о": "o", + "п": "p", + "р": "r", + "с": "s", + "т": "t", + "ќ": "kj", + "у": "u", + "ф": "f", + "х": "h", + "ц": "c", + "ч": "ch", + "џ": "dj", + "ш": "sh" +} diff --git a/kirby/i18n/rules/my.json b/kirby/i18n/rules/my.json new file mode 100755 index 0000000..08f5a0a --- /dev/null +++ b/kirby/i18n/rules/my.json @@ -0,0 +1,121 @@ +{ + "က": "k", + "ခ": "kh", + "ဂ": "g", + "ဃ": "ga", + "င": "ng", + "စ": "s", + "ဆ": "sa", + "ဇ": "z", + "စျ" : "za", + "ည": "ny", + "ဋ": "t", + "ဌ": "ta", + "ဍ": "d", + "ဎ": "da", + "ဏ": "na", + "တ": "t", + "ထ": "ta", + "ဒ": "d", + "ဓ": "da", + "န": "n", + "ပ": "p", + "ဖ": "pa", + "ဗ": "b", + "ဘ": "ba", + "မ": "m", + "ယ": "y", + "ရ": "ya", + "လ": "l", + "ဝ": "w", + "သ": "th", + "ဟ": "h", + "ဠ": "la", + "အ": "a", + + "ြ": "y", + "ျ": "ya", + "ွ": "w", + "ြွ": "yw", + "ျွ": "ywa", + "ှ": "h", + + "ဧ": "e", + "၏": "-e", + "ဣ": "i", + "ဤ": "-i", + "ဉ": "u", + "ဦ": "-u", + "ဩ": "aw", + "သြော" : "aw", + "ဪ": "aw", + "၍": "ywae", + "၌": "hnaik", + + "၀": "0", + "၁": "1", + "၂": "2", + "၃": "3", + "၄": "4", + "၅": "5", + "၆": "6", + "၇": "7", + "၈": "8", + "၉": "9", + + "္": "", + "့": "", + "း": "", + + "ာ": "a", + "ါ": "a", + "ေ": "e", + "ဲ": "e", + "ိ": "i", + "ီ": "i", + "ို": "o", + "ု": "u", + "ူ": "u", + "ေါင်": "aung", + "ော": "aw", + "ော်": "aw", + "ေါ": "aw", + "ေါ်": "aw", + "်": "at", + "က်": "et", + "ိုက်" : "aik", + "ောက်" : "auk", + "င်" : "in", + "ိုင်" : "aing", + "ောင်" : "aung", + "စ်" : "it", + "ည်" : "i", + "တ်" : "at", + "ိတ်" : "eik", + "ုတ်" : "ok", + "ွတ်" : "ut", + "ေတ်" : "it", + "ဒ်" : "d", + "ိုဒ်" : "ok", + "ုဒ်" : "ait", + "န်" : "an", + "ာန်" : "an", + "ိန်" : "ein", + "ုန်" : "on", + "ွန်" : "un", + "ပ်" : "at", + "ိပ်" : "eik", + "ုပ်" : "ok", + "ွပ်" : "ut", + "န်ုပ်" : "nub", + "မ်" : "an", + "ိမ်" : "ein", + "ုမ်" : "on", + "ွမ်" : "un", + "ယ်" : "e", + "ိုလ်" : "ol", + "ဉ်" : "in", + "ံ": "an", + "ိံ" : "ein", + "ုံ" : "on" +} diff --git a/kirby/i18n/rules/nb.json b/kirby/i18n/rules/nb.json new file mode 100755 index 0000000..66000ba --- /dev/null +++ b/kirby/i18n/rules/nb.json @@ -0,0 +1,8 @@ +{ + "Æ": "AE", + "Ø": "OE", + "Å": "AA", + "æ": "ae", + "ø": "oe", + "å": "aa" +} diff --git a/kirby/i18n/rules/pl.json b/kirby/i18n/rules/pl.json new file mode 100755 index 0000000..5d0c123 --- /dev/null +++ b/kirby/i18n/rules/pl.json @@ -0,0 +1,20 @@ +{ + "Ą": "A", + "Ć": "C", + "Ę": "E", + "Ł": "L", + "Ń": "N", + "Ó": "O", + "Ś": "S", + "Ź": "Z", + "Ż": "Z", + "ą": "a", + "ć": "c", + "ę": "e", + "ł": "l", + "ń": "n", + "ó": "o", + "ś": "s", + "ź": "z", + "ż": "z" +} diff --git a/kirby/i18n/rules/pt_BR.json b/kirby/i18n/rules/pt_BR.json new file mode 100755 index 0000000..39bca6c --- /dev/null +++ b/kirby/i18n/rules/pt_BR.json @@ -0,0 +1,187 @@ + +{ + "°": "0", + "¹": "1", + "²": "2", + "³": "3", + "⁴": "4", + "⁵": "5", + "⁶": "6", + "⁷": "7", + "⁸": "8", + "⁹": "9", + + "₀": "0", + "₁": "1", + "₂": "2", + "₃": "3", + "₄": "4", + "₅": "5", + "₆": "6", + "₇": "7", + "₈": "8", + "₉": "9", + + + "æ": "ae", + "ǽ": "ae", + "À": "A", + "Á": "A", + "Â": "A", + "Ã": "A", + "Å": "AA", + "Ǻ": "A", + "Ă": "A", + "Ǎ": "A", + "Æ": "AE", + "Ǽ": "AE", + "à": "a", + "á": "a", + "â": "a", + "ã": "a", + "å": "aa", + "ǻ": "a", + "ă": "a", + "ǎ": "a", + "ª": "a", + "@": "at", + "Ĉ": "C", + "Ċ": "C", + "Ç": "Ç", + "ç": "ç", + "ĉ": "c", + "ċ": "c", + "©": "c", + "Ð": "Dj", + "Đ": "D", + "ð": "dj", + "đ": "d", + "È": "E", + "É": "E", + "Ê": "E", + "Ë": "E", + "Ĕ": "E", + "Ė": "E", + "è": "e", + "é": "é", + "ê": "e", + "ë": "e", + "ĕ": "e", + "ė": "e", + "ƒ": "f", + "Ĝ": "G", + "Ġ": "G", + "ĝ": "g", + "ġ": "g", + "Ĥ": "H", + "Ħ": "H", + "ĥ": "h", + "ħ": "h", + "Ì": "I", + "Í": "I", + "Î": "I", + "Ï": "I", + "Ĩ": "I", + "Ĭ": "I", + "Ǐ": "I", + "Į": "I", + "IJ": "IJ", + "ì": "i", + "í": "i", + "î": "i", + "ï": "i", + "ĩ": "i", + "ĭ": "i", + "ǐ": "i", + "į": "i", + "ij": "ij", + "Ĵ": "J", + "ĵ": "j", + "Ĺ": "L", + "Ľ": "L", + "Ŀ": "L", + "ĺ": "l", + "ľ": "l", + "ŀ": "l", + "Ñ": "N", + "ñ": "n", + "ʼn": "n", + "Ò": "O", + "Ó": "O", + "Ô": "O", + "Õ": "O", + "Ō": "O", + "Ŏ": "O", + "Ǒ": "O", + "Ő": "O", + "Ơ": "O", + "Ø": "OE", + "Ǿ": "O", + "Œ": "OE", + "ò": "o", + "ó": "o", + "ô": "o", + "õ": "o", + "ō": "o", + "ŏ": "o", + "ǒ": "o", + "ő": "o", + "ơ": "o", + "ø": "oe", + "ǿ": "o", + "º": "o", + "œ": "oe", + "Ŕ": "R", + "Ŗ": "R", + "ŕ": "r", + "ŗ": "r", + "Ŝ": "S", + "Ș": "S", + "ŝ": "s", + "ș": "s", + "ſ": "s", + "Ţ": "T", + "Ț": "T", + "Ŧ": "T", + "Þ": "TH", + "ţ": "t", + "ț": "t", + "ŧ": "t", + "þ": "th", + "Ù": "U", + "Ú": "U", + "Û": "U", + "Ü": "U", + "Ũ": "U", + "Ŭ": "U", + "Ű": "U", + "Ų": "U", + "Ư": "U", + "Ǔ": "U", + "Ǖ": "U", + "Ǘ": "U", + "Ǚ": "U", + "Ǜ": "U", + "ù": "u", + "ú": "u", + "û": "u", + "ü": "u", + "ũ": "u", + "ŭ": "u", + "ű": "u", + "ų": "u", + "ư": "u", + "ǔ": "u", + "ǖ": "u", + "ǘ": "u", + "ǚ": "u", + "ǜ": "u", + "Ŵ": "W", + "ŵ": "w", + "Ý": "Y", + "Ÿ": "Y", + "Ŷ": "Y", + "ý": "y", + "ÿ": "y", + "ŷ": "y" +} diff --git a/kirby/i18n/rules/rm.json b/kirby/i18n/rules/rm.json new file mode 100755 index 0000000..47b9d9b --- /dev/null +++ b/kirby/i18n/rules/rm.json @@ -0,0 +1,16 @@ +{ + "ă": "a", + "î": "i", + "â": "a", + "ş": "s", + "ș": "s", + "ţ": "t", + "ț": "t", + "Ă": "A", + "Î": "I", + "Â": "A", + "Ş": "S", + "Ș": "S", + "Ţ": "T", + "Ț": "T" +} diff --git a/kirby/i18n/rules/ru.json b/kirby/i18n/rules/ru.json new file mode 100755 index 0000000..b8b354c --- /dev/null +++ b/kirby/i18n/rules/ru.json @@ -0,0 +1,68 @@ +{ + "Ъ": "", + "Ь": "", + "А": "A", + "Б": "B", + "Ц": "C", + "Ч": "Ch", + "Д": "D", + "Е": "E", + "Ё": "E", + "Э": "E", + "Ф": "F", + "Г": "G", + "Х": "H", + "И": "I", + "Й": "Y", + "Я": "Ya", + "Ю": "Yu", + "К": "K", + "Л": "L", + "М": "M", + "Н": "N", + "О": "O", + "П": "P", + "Р": "R", + "С": "S", + "Ш": "Sh", + "Щ": "Shch", + "Т": "T", + "У": "U", + "В": "V", + "Ы": "Y", + "З": "Z", + "Ж": "Zh", + "ъ": "", + "ь": "", + "а": "a", + "б": "b", + "ц": "c", + "ч": "ch", + "д": "d", + "е": "e", + "ё": "e", + "э": "e", + "ф": "f", + "г": "g", + "х": "h", + "и": "i", + "й": "y", + "я": "ya", + "ю": "yu", + "к": "k", + "л": "l", + "м": "m", + "н": "n", + "о": "o", + "п": "p", + "р": "r", + "с": "s", + "ш": "sh", + "щ": "shch", + "т": "t", + "у": "u", + "в": "v", + "ы": "y", + "з": "z", + "ж": "zh" +} diff --git a/kirby/i18n/rules/sr.json b/kirby/i18n/rules/sr.json new file mode 100755 index 0000000..f4c11db --- /dev/null +++ b/kirby/i18n/rules/sr.json @@ -0,0 +1,72 @@ +{ + "а": "a", + "б": "b", + "в": "v", + "г": "g", + "д": "d", + "ђ": "dj", + "е": "e", + "ж": "z", + "з": "z", + "и": "i", + "ј": "j", + "к": "k", + "л": "l", + "љ": "lj", + "м": "m", + "н": "n", + "њ": "nj", + "о": "o", + "п": "p", + "р": "r", + "с": "s", + "т": "t", + "ћ": "c", + "у": "u", + "ф": "f", + "х": "h", + "ц": "c", + "ч": "c", + "џ": "dz", + "ш": "s", + "А": "A", + "Б": "B", + "В": "V", + "Г": "G", + "Д": "D", + "Ђ": "Dj", + "Е": "E", + "Ж": "Z", + "З": "Z", + "И": "I", + "Ј": "J", + "К": "K", + "Л": "L", + "Љ": "Lj", + "М": "M", + "Н": "N", + "Њ": "Nj", + "О": "O", + "П": "P", + "Р": "R", + "С": "S", + "Т": "T", + "Ћ": "C", + "У": "U", + "Ф": "F", + "Х": "H", + "Ц": "C", + "Ч": "C", + "Џ": "Dz", + "Ш": "S", + "š": "s", + "đ": "dj", + "ž": "z", + "ć": "c", + "č": "c", + "Š": "S", + "Đ": "DJ", + "Ž": "Z", + "Ć": "C", + "Č": "C" +} \ No newline at end of file diff --git a/kirby/i18n/rules/sv_SE.json b/kirby/i18n/rules/sv_SE.json new file mode 100755 index 0000000..a22f3eb --- /dev/null +++ b/kirby/i18n/rules/sv_SE.json @@ -0,0 +1,8 @@ +{ + "Ä": "A", + "Å": "a", + "Ö": "O", + "ä": "a", + "å": "a", + "ö": "o" +} diff --git a/kirby/i18n/rules/tr.json b/kirby/i18n/rules/tr.json new file mode 100755 index 0000000..07fbae5 --- /dev/null +++ b/kirby/i18n/rules/tr.json @@ -0,0 +1,14 @@ +{ + "Ç": "C", + "Ğ": "G", + "İ": "I", + "Ş": "S", + "Ö": "O", + "Ü": "U", + "ç": "c", + "ğ": "g", + "ı": "i", + "ş": "s", + "ö": "o", + "ü": "u" +} diff --git a/kirby/i18n/rules/uk.json b/kirby/i18n/rules/uk.json new file mode 100755 index 0000000..673b7ed --- /dev/null +++ b/kirby/i18n/rules/uk.json @@ -0,0 +1,10 @@ +{ + "Ґ": "G", + "І": "I", + "Ї": "Ji", + "Є": "Ye", + "ґ": "g", + "і": "i", + "ї": "ji", + "є": "ye" +} diff --git a/kirby/i18n/rules/vi.json b/kirby/i18n/rules/vi.json new file mode 100755 index 0000000..fdeff69 --- /dev/null +++ b/kirby/i18n/rules/vi.json @@ -0,0 +1,135 @@ +{ + "à": "a", + "ạ": "a", + "á": "a", + "ả": "a", + "ã": "a", + "â": "a", + "ầ": "a", + "ấ": "a", + "ậ": "a", + "ẩ": "a", + "ẫ": "a", + "ă": "a", + "ằ": "a", + "ắ": "a", + "ặ": "a", + "ẳ": "a", + "ẵ": "a", + "è": "e", + "é": "e", + "ẹ": "e", + "ẻ": "e", + "ẽ": "e", + "ê": "e", + "ề": "e", + "ế": "e", + "ệ": "e", + "ể": "e", + "ễ": "e", + "ì": "i", + "í": "i", + "ị": "i", + "ỉ": "i", + "ĩ": "i", + "ò": "o", + "ó": "o", + "ọ": "o", + "ỏ": "o", + "õ": "o", + "ô": "o", + "ồ": "o", + "ố": "o", + "ộ": "o", + "ổ": "o", + "ỗ": "o", + "ơ": "o", + "ờ": "o", + "ớ": "o", + "ợ": "o", + "ở": "o", + "ỡ": "o", + "ù": "u", + "ú": "u", + "ụ": "u", + "ủ": "u", + "ũ": "u", + "ư": "u", + "ừ": "u", + "ứ": "u", + "ự": "u", + "ử": "u", + "ữ": "u", + "y": "y", + "ỳ": "y", + "ý": "y", + "ỵ": "y", + "ỷ": "y", + "ỹ": "y", + "À": "A", + "Á": "A", + "Ạ": "A", + "Ả": "A", + "Ã": "A", + "Â": "A", + "Ầ": "A", + "Ấ": "A", + "Ậ": "A", + "Ẩ": "A", + "Ẫ": "A", + "Ă": "A", + "Ằ": "A", + "Ắ": "A", + "Ặ": "A", + "Ẳ": "A", + "Ẵ": "A", + "È": "E", + "É": "E", + "Ẹ": "E", + "Ẻ": "E", + "Ẽ": "E", + "Ê": "E", + "Ề": "E", + "Ế": "E", + "Ệ": "E", + "Ể": "E", + "Ễ": "E", + "Ì": "I", + "Í": "I", + "Ị": "I", + "Ỉ": "I", + "Ĩ": "I", + "Ò": "O", + "Ó": "O", + "Ọ": "O", + "Ỏ": "O", + "Õ": "O", + "Ô": "O", + "Ồ": "O", + "Ố": "O", + "Ộ": "O", + "Ổ": "O", + "Ỗ": "O", + "Ơ": "O", + "Ờ": "O", + "Ớ": "O", + "Ợ": "O", + "Ở": "O", + "Ỡ": "O", + "Ù": "U", + "Ụ": "U", + "Ủ": "U", + "Ũ": "U", + "Ư": "U", + "Ừ": "U", + "Ứ": "U", + "Ự": "U", + "Ử": "U", + "Ữ": "U", + "Y": "Y", + "Ỳ": "Y", + "Ý": "Y", + "Ỵ": "Y", + "Ỷ": "Y", + "Ỹ": "Y" +} diff --git a/kirby/i18n/rules/zh.json b/kirby/i18n/rules/zh.json new file mode 100755 index 0000000..21ec594 --- /dev/null +++ b/kirby/i18n/rules/zh.json @@ -0,0 +1,6937 @@ +{ + "腌" : "yan", + "嗄" : "a", + "迫" : "po", + "捱" : "ai", + "艾" : "ai", + "瑷" : "ai", + "嗌" : "ai", + "犴" : "an", + "鳌" : "ao", + "廒" : "ao", + "拗" : "niu", + "岙" : "ao", + "鏊" : "ao", + "扒" : "ba", + "岜" : "ba", + "耙" : "pa", + "鲅" : "ba", + "癍" : "ban", + "膀" : "pang", + "磅" : "bang", + "炮" : "pao", + "曝" : "pu", + "刨" : "pao", + "瀑" : "pu", + "陂" : "bei", + "埤" : "pi", + "鹎" : "bei", + "邶" : "bei", + "孛" : "bei", + "鐾" : "bei", + "鞴" : "bei", + "畚" : "ben", + "甏" : "beng", + "舭" : "bi", + "秘" : "mi", + "辟" : "pi", + "泌" : "mi", + "裨" : "bi", + "濞" : "bi", + "庳" : "bi", + "嬖" : "bi", + "畀" : "bi", + "筚" : "bi", + "箅" : "bi", + "襞" : "bi", + "跸" : "bi", + "笾" : "bian", + "扁" : "bian", + "碥" : "bian", + "窆" : "bian", + "便" : "bian", + "弁" : "bian", + "缏" : "bian", + "骠" : "biao", + "杓" : "shao", + "飚" : "biao", + "飑" : "biao", + "瘭" : "biao", + "髟" : "biao", + "玢" : "bin", + "豳" : "bin", + "镔" : "bin", + "膑" : "bin", + "屏" : "ping", + "泊" : "bo", + "逋" : "bu", + "晡" : "bu", + "钸" : "bu", + "醭" : "bu", + "埔" : "pu", + "瓿" : "bu", + "礤" : "ca", + "骖" : "can", + "藏" : "cang", + "艚" : "cao", + "侧" : "ce", + "喳" : "zha", + "刹" : "sha", + "瘥" : "chai", + "禅" : "chan", + "廛" : "chan", + "镡" : "tan", + "澶" : "chan", + "躔" : "chan", + "阊" : "chang", + "鲳" : "chang", + "长" : "chang", + "苌" : "chang", + "氅" : "chang", + "鬯" : "chang", + "焯" : "chao", + "朝" : "chao", + "车" : "che", + "琛" : "chen", + "谶" : "chen", + "榇" : "chen", + "蛏" : "cheng", + "埕" : "cheng", + "枨" : "cheng", + "塍" : "cheng", + "裎" : "cheng", + "螭" : "chi", + "眵" : "chi", + "墀" : "chi", + "篪" : "chi", + "坻" : "di", + "瘛" : "chi", + "种" : "zhong", + "重" : "zhong", + "仇" : "chou", + "帱" : "chou", + "俦" : "chou", + "雠" : "chou", + "臭" : "chou", + "楮" : "chu", + "畜" : "chu", + "嘬" : "zuo", + "膪" : "chuai", + "巛" : "chuan", + "椎" : "zhui", + "呲" : "ci", + "兹" : "zi", + "伺" : "si", + "璁" : "cong", + "楱" : "cou", + "攒" : "zan", + "爨" : "cuan", + "隹" : "zhui", + "榱" : "cui", + "撮" : "cuo", + "鹾" : "cuo", + "嗒" : "da", + "哒" : "da", + "沓" : "ta", + "骀" : "tai", + "绐" : "dai", + "埭" : "dai", + "甙" : "dai", + "弹" : "dan", + "澹" : "dan", + "叨" : "dao", + "纛" : "dao", + "簦" : "deng", + "提" : "ti", + "翟" : "zhai", + "绨" : "ti", + "丶" : "dian", + "佃" : "dian", + "簟" : "dian", + "癜" : "dian", + "调" : "tiao", + "铞" : "diao", + "佚" : "yi", + "堞" : "die", + "瓞" : "die", + "揲" : "die", + "垤" : "die", + "疔" : "ding", + "岽" : "dong", + "硐" : "dong", + "恫" : "dong", + "垌" : "dong", + "峒" : "dong", + "芏" : "du", + "煅" : "duan", + "碓" : "dui", + "镦" : "dui", + "囤" : "tun", + "铎" : "duo", + "缍" : "duo", + "驮" : "tuo", + "沲" : "tuo", + "柁" : "tuo", + "哦" : "o", + "恶" : "e", + "轭" : "e", + "锷" : "e", + "鹗" : "e", + "阏" : "e", + "诶" : "ea", + "鲕" : "er", + "珥" : "er", + "佴" : "er", + "番" : "fan", + "彷" : "pang", + "霏" : "fei", + "蜚" : "fei", + "鲱" : "fei", + "芾" : "fei", + "瀵" : "fen", + "鲼" : "fen", + "否" : "fou", + "趺" : "fu", + "桴" : "fu", + "莩" : "fu", + "菔" : "fu", + "幞" : "fu", + "郛" : "fu", + "绂" : "fu", + "绋" : "fu", + "祓" : "fu", + "砩" : "fu", + "黻" : "fu", + "罘" : "fu", + "蚨" : "fu", + "脯" : "pu", + "滏" : "fu", + "黼" : "fu", + "鲋" : "fu", + "鳆" : "fu", + "咖" : "ka", + "噶" : "ga", + "轧" : "zha", + "陔" : "gai", + "戤" : "gai", + "扛" : "kang", + "戆" : "gang", + "筻" : "gang", + "槔" : "gao", + "藁" : "gao", + "缟" : "gao", + "咯" : "ge", + "仡" : "yi", + "搿" : "ge", + "塥" : "ge", + "鬲" : "ge", + "哿" : "ge", + "句" : "ju", + "缑" : "gou", + "鞲" : "gou", + "笱" : "gou", + "遘" : "gou", + "瞽" : "gu", + "罟" : "gu", + "嘏" : "gu", + "牿" : "gu", + "鲴" : "gu", + "栝" : "kuo", + "莞" : "guan", + "纶" : "lun", + "涫" : "guan", + "涡" : "wo", + "呙" : "guo", + "馘" : "guo", + "猓" : "guo", + "咳" : "ke", + "氦" : "hai", + "颔" : "han", + "吭" : "keng", + "颃" : "hang", + "巷" : "xiang", + "蚵" : "ke", + "翮" : "he", + "吓" : "xia", + "桁" : "heng", + "泓" : "hong", + "蕻" : "hong", + "黉" : "hong", + "後" : "hou", + "唿" : "hu", + "煳" : "hu", + "浒" : "hu", + "祜" : "hu", + "岵" : "hu", + "鬟" : "huan", + "圜" : "huan", + "郇" : "xun", + "锾" : "huan", + "逭" : "huan", + "咴" : "hui", + "虺" : "hui", + "会" : "hui", + "溃" : "kui", + "哕" : "hui", + "缋" : "hui", + "锪" : "huo", + "蠖" : "huo", + "缉" : "ji", + "稽" : "ji", + "赍" : "ji", + "丌" : "ji", + "咭" : "ji", + "亟" : "ji", + "殛" : "ji", + "戢" : "ji", + "嵴" : "ji", + "蕺" : "ji", + "系" : "xi", + "蓟" : "ji", + "霁" : "ji", + "荠" : "qi", + "跽" : "ji", + "哜" : "ji", + "鲚" : "ji", + "洎" : "ji", + "芰" : "ji", + "茄" : "qie", + "珈" : "jia", + "迦" : "jia", + "笳" : "jia", + "葭" : "jia", + "跏" : "jia", + "郏" : "jia", + "恝" : "jia", + "铗" : "jia", + "袷" : "qia", + "蛱" : "jia", + "角" : "jiao", + "挢" : "jiao", + "岬" : "jia", + "徼" : "jiao", + "湫" : "qiu", + "敫" : "jiao", + "瘕" : "jia", + "浅" : "qian", + "蒹" : "jian", + "搛" : "jian", + "湔" : "jian", + "缣" : "jian", + "犍" : "jian", + "鹣" : "jian", + "鲣" : "jian", + "鞯" : "jian", + "蹇" : "jian", + "謇" : "jian", + "硷" : "jian", + "枧" : "jian", + "戬" : "jian", + "谫" : "jian", + "囝" : "jian", + "裥" : "jian", + "笕" : "jian", + "翦" : "jian", + "趼" : "jian", + "楗" : "jian", + "牮" : "jian", + "踺" : "jian", + "茳" : "jiang", + "礓" : "jiang", + "耩" : "jiang", + "降" : "jiang", + "绛" : "jiang", + "洚" : "jiang", + "鲛" : "jiao", + "僬" : "jiao", + "鹪" : "jiao", + "艽" : "jiao", + "茭" : "jiao", + "嚼" : "jiao", + "峤" : "qiao", + "觉" : "jiao", + "校" : "xiao", + "噍" : "jiao", + "醮" : "jiao", + "疖" : "jie", + "喈" : "jie", + "桔" : "ju", + "拮" : "jie", + "桀" : "jie", + "颉" : "jie", + "婕" : "jie", + "羯" : "jie", + "鲒" : "jie", + "蚧" : "jie", + "骱" : "jie", + "衿" : "jin", + "馑" : "jin", + "卺" : "jin", + "廑" : "jin", + "堇" : "jin", + "槿" : "jin", + "靳" : "jin", + "缙" : "jin", + "荩" : "jin", + "赆" : "jin", + "妗" : "jin", + "旌" : "jing", + "腈" : "jing", + "憬" : "jing", + "肼" : "jing", + "迳" : "jing", + "胫" : "jing", + "弪" : "jing", + "獍" : "jing", + "扃" : "jiong", + "鬏" : "jiu", + "疚" : "jiu", + "僦" : "jiu", + "桕" : "jiu", + "疽" : "ju", + "裾" : "ju", + "苴" : "ju", + "椐" : "ju", + "锔" : "ju", + "琚" : "ju", + "鞫" : "ju", + "踽" : "ju", + "榉" : "ju", + "莒" : "ju", + "遽" : "ju", + "倨" : "ju", + "钜" : "ju", + "犋" : "ju", + "屦" : "ju", + "榘" : "ju", + "窭" : "ju", + "讵" : "ju", + "醵" : "ju", + "苣" : "ju", + "圈" : "quan", + "镌" : "juan", + "蠲" : "juan", + "锩" : "juan", + "狷" : "juan", + "桊" : "juan", + "鄄" : "juan", + "獗" : "jue", + "攫" : "jue", + "孓" : "jue", + "橛" : "jue", + "珏" : "jue", + "桷" : "jue", + "劂" : "jue", + "爝" : "jue", + "镢" : "jue", + "觖" : "jue", + "筠" : "jun", + "麇" : "jun", + "捃" : "jun", + "浚" : "jun", + "喀" : "ka", + "卡" : "ka", + "佧" : "ka", + "胩" : "ka", + "锎" : "kai", + "蒈" : "kai", + "剀" : "kai", + "垲" : "kai", + "锴" : "kai", + "戡" : "kan", + "莰" : "kan", + "闶" : "kang", + "钪" : "kang", + "尻" : "kao", + "栲" : "kao", + "柯" : "ke", + "疴" : "ke", + "钶" : "ke", + "颏" : "ke", + "珂" : "ke", + "髁" : "ke", + "壳" : "ke", + "岢" : "ke", + "溘" : "ke", + "骒" : "ke", + "缂" : "ke", + "氪" : "ke", + "锞" : "ke", + "裉" : "ken", + "倥" : "kong", + "崆" : "kong", + "箜" : "kong", + "芤" : "kou", + "眍" : "kou", + "筘" : "kou", + "刳" : "ku", + "堀" : "ku", + "喾" : "ku", + "侉" : "kua", + "蒯" : "kuai", + "哙" : "kuai", + "狯" : "kuai", + "郐" : "kuai", + "匡" : "kuang", + "夼" : "kuang", + "邝" : "kuang", + "圹" : "kuang", + "纩" : "kuang", + "贶" : "kuang", + "岿" : "kui", + "悝" : "kui", + "睽" : "kui", + "逵" : "kui", + "馗" : "kui", + "夔" : "kui", + "喹" : "kui", + "隗" : "wei", + "暌" : "kui", + "揆" : "kui", + "蝰" : "kui", + "跬" : "kui", + "喟" : "kui", + "聩" : "kui", + "篑" : "kui", + "蒉" : "kui", + "愦" : "kui", + "锟" : "kun", + "醌" : "kun", + "琨" : "kun", + "髡" : "kun", + "悃" : "kun", + "阃" : "kun", + "蛞" : "kuo", + "砬" : "la", + "落" : "luo", + "剌" : "la", + "瘌" : "la", + "涞" : "lai", + "崃" : "lai", + "铼" : "lai", + "赉" : "lai", + "濑" : "lai", + "斓" : "lan", + "镧" : "lan", + "谰" : "lan", + "漤" : "lan", + "罱" : "lan", + "稂" : "lang", + "阆" : "lang", + "莨" : "liang", + "蒗" : "lang", + "铹" : "lao", + "痨" : "lao", + "醪" : "lao", + "栳" : "lao", + "铑" : "lao", + "耢" : "lao", + "勒" : "le", + "仂" : "le", + "叻" : "le", + "泐" : "le", + "鳓" : "le", + "了" : "le", + "镭" : "lei", + "嫘" : "lei", + "缧" : "lei", + "檑" : "lei", + "诔" : "lei", + "耒" : "lei", + "酹" : "lei", + "塄" : "leng", + "愣" : "leng", + "藜" : "li", + "骊" : "li", + "黧" : "li", + "缡" : "li", + "嫠" : "li", + "鲡" : "li", + "蓠" : "li", + "澧" : "li", + "锂" : "li", + "醴" : "li", + "鳢" : "li", + "俪" : "li", + "砺" : "li", + "郦" : "li", + "詈" : "li", + "猁" : "li", + "溧" : "li", + "栎" : "li", + "轹" : "li", + "傈" : "li", + "坜" : "li", + "苈" : "li", + "疠" : "li", + "疬" : "li", + "篥" : "li", + "粝" : "li", + "跞" : "li", + "俩" : "liang", + "裢" : "lian", + "濂" : "lian", + "臁" : "lian", + "奁" : "lian", + "蠊" : "lian", + "琏" : "lian", + "蔹" : "lian", + "裣" : "lian", + "楝" : "lian", + "潋" : "lian", + "椋" : "liang", + "墚" : "liang", + "寮" : "liao", + "鹩" : "liao", + "蓼" : "liao", + "钌" : "liao", + "廖" : "liao", + "尥" : "liao", + "洌" : "lie", + "捩" : "lie", + "埒" : "lie", + "躐" : "lie", + "鬣" : "lie", + "辚" : "lin", + "遴" : "lin", + "啉" : "lin", + "瞵" : "lin", + "懔" : "lin", + "廪" : "lin", + "蔺" : "lin", + "膦" : "lin", + "酃" : "ling", + "柃" : "ling", + "鲮" : "ling", + "呤" : "ling", + "镏" : "liu", + "旒" : "liu", + "骝" : "liu", + "鎏" : "liu", + "锍" : "liu", + "碌" : "lu", + "鹨" : "liu", + "茏" : "long", + "栊" : "long", + "泷" : "long", + "砻" : "long", + "癃" : "long", + "垅" : "long", + "偻" : "lou", + "蝼" : "lou", + "蒌" : "lou", + "耧" : "lou", + "嵝" : "lou", + "露" : "lu", + "瘘" : "lou", + "噜" : "lu", + "轳" : "lu", + "垆" : "lu", + "胪" : "lu", + "舻" : "lu", + "栌" : "lu", + "镥" : "lu", + "绿" : "lv", + "辘" : "lu", + "簏" : "lu", + "潞" : "lu", + "辂" : "lu", + "渌" : "lu", + "氇" : "lu", + "捋" : "lv", + "稆" : "lv", + "率" : "lv", + "闾" : "lv", + "栾" : "luan", + "銮" : "luan", + "滦" : "luan", + "娈" : "luan", + "脔" : "luan", + "锊" : "lve", + "猡" : "luo", + "椤" : "luo", + "脶" : "luo", + "镙" : "luo", + "倮" : "luo", + "蠃" : "luo", + "瘰" : "luo", + "珞" : "luo", + "泺" : "luo", + "荦" : "luo", + "雒" : "luo", + "呒" : "mu", + "抹" : "mo", + "唛" : "mai", + "杩" : "ma", + "么" : "me", + "埋" : "mai", + "荬" : "mai", + "脉" : "mai", + "劢" : "mai", + "颟" : "man", + "蔓" : "man", + "鳗" : "man", + "鞔" : "man", + "螨" : "man", + "墁" : "man", + "缦" : "man", + "熳" : "man", + "镘" : "man", + "邙" : "mang", + "硭" : "mang", + "旄" : "mao", + "茆" : "mao", + "峁" : "mao", + "泖" : "mao", + "昴" : "mao", + "耄" : "mao", + "瑁" : "mao", + "懋" : "mao", + "瞀" : "mao", + "麽" : "me", + "没" : "mei", + "嵋" : "mei", + "湄" : "mei", + "猸" : "mei", + "镅" : "mei", + "鹛" : "mei", + "浼" : "mei", + "钔" : "men", + "瞢" : "meng", + "甍" : "meng", + "礞" : "meng", + "艨" : "meng", + "黾" : "mian", + "鳘" : "min", + "溟" : "ming", + "暝" : "ming", + "模" : "mo", + "谟" : "mo", + "嫫" : "mo", + "镆" : "mo", + "瘼" : "mo", + "耱" : "mo", + "貊" : "mo", + "貘" : "mo", + "牟" : "mou", + "鍪" : "mou", + "蛑" : "mou", + "侔" : "mou", + "毪" : "mu", + "坶" : "mu", + "仫" : "mu", + "唔" : "wu", + "那" : "na", + "镎" : "na", + "哪" : "na", + "呢" : "ne", + "肭" : "na", + "艿" : "nai", + "鼐" : "nai", + "萘" : "nai", + "柰" : "nai", + "蝻" : "nan", + "馕" : "nang", + "攮" : "nang", + "曩" : "nang", + "猱" : "nao", + "铙" : "nao", + "硇" : "nao", + "蛲" : "nao", + "垴" : "nao", + "坭" : "ni", + "猊" : "ni", + "铌" : "ni", + "鲵" : "ni", + "祢" : "mi", + "睨" : "ni", + "慝" : "te", + "伲" : "ni", + "鲇" : "nian", + "鲶" : "nian", + "埝" : "nian", + "嬲" : "niao", + "茑" : "niao", + "脲" : "niao", + "啮" : "nie", + "陧" : "nie", + "颞" : "nie", + "臬" : "nie", + "蘖" : "nie", + "甯" : "ning", + "聍" : "ning", + "狃" : "niu", + "侬" : "nong", + "耨" : "nou", + "孥" : "nu", + "胬" : "nu", + "钕" : "nv", + "恧" : "nv", + "褰" : "qian", + "掮" : "qian", + "荨" : "xun", + "钤" : "qian", + "箝" : "qian", + "鬈" : "quan", + "缱" : "qian", + "肷" : "qian", + "纤" : "xian", + "茜" : "qian", + "慊" : "qian", + "椠" : "qian", + "戗" : "qiang", + "镪" : "qiang", + "锖" : "qiang", + "樯" : "qiang", + "嫱" : "qiang", + "雀" : "que", + "缲" : "qiao", + "硗" : "qiao", + "劁" : "qiao", + "樵" : "qiao", + "谯" : "qiao", + "鞒" : "qiao", + "愀" : "qiao", + "鞘" : "qiao", + "郄" : "xi", + "箧" : "qie", + "亲" : "qin", + "覃" : "tan", + "溱" : "qin", + "檎" : "qin", + "锓" : "qin", + "嗪" : "qin", + "螓" : "qin", + "揿" : "qin", + "吣" : "qin", + "圊" : "qing", + "鲭" : "qing", + "檠" : "qing", + "黥" : "qing", + "謦" : "qing", + "苘" : "qing", + "磬" : "qing", + "箐" : "qing", + "綮" : "qi", + "茕" : "qiong", + "邛" : "dao", + "蛩" : "tun", + "筇" : "qiong", + "跫" : "qiong", + "銎" : "qiong", + "楸" : "qiu", + "俅" : "qiu", + "赇" : "qiu", + "逑" : "qiu", + "犰" : "qiu", + "蝤" : "qiu", + "巯" : "qiu", + "鼽" : "qiu", + "糗" : "qiu", + "区" : "qu", + "祛" : "qu", + "麴" : "qu", + "诎" : "qu", + "衢" : "qu", + "癯" : "qu", + "劬" : "qu", + "璩" : "qu", + "氍" : "qu", + "朐" : "qu", + "磲" : "qu", + "鸲" : "qu", + "蕖" : "qu", + "蠼" : "qu", + "蘧" : "qu", + "阒" : "qu", + "颧" : "quan", + "荃" : "quan", + "铨" : "quan", + "辁" : "quan", + "筌" : "quan", + "绻" : "quan", + "畎" : "quan", + "阕" : "que", + "悫" : "que", + "髯" : "ran", + "禳" : "rang", + "穰" : "rang", + "仞" : "ren", + "妊" : "ren", + "轫" : "ren", + "衽" : "ren", + "狨" : "rong", + "肜" : "rong", + "蝾" : "rong", + "嚅" : "ru", + "濡" : "ru", + "薷" : "ru", + "襦" : "ru", + "颥" : "ru", + "洳" : "ru", + "溽" : "ru", + "蓐" : "ru", + "朊" : "ruan", + "蕤" : "rui", + "枘" : "rui", + "箬" : "ruo", + "挲" : "suo", + "脎" : "sa", + "塞" : "sai", + "鳃" : "sai", + "噻" : "sai", + "毵" : "san", + "馓" : "san", + "糁" : "san", + "霰" : "xian", + "磉" : "sang", + "颡" : "sang", + "缫" : "sao", + "鳋" : "sao", + "埽" : "sao", + "瘙" : "sao", + "色" : "se", + "杉" : "shan", + "鲨" : "sha", + "痧" : "sha", + "裟" : "sha", + "铩" : "sha", + "唼" : "sha", + "酾" : "shai", + "栅" : "zha", + "跚" : "shan", + "芟" : "shan", + "埏" : "shan", + "钐" : "shan", + "舢" : "shan", + "剡" : "yan", + "鄯" : "shan", + "疝" : "shan", + "蟮" : "shan", + "墒" : "shang", + "垧" : "shang", + "绱" : "shang", + "蛸" : "shao", + "筲" : "shao", + "苕" : "tiao", + "召" : "zhao", + "劭" : "shao", + "猞" : "she", + "畲" : "she", + "折" : "zhe", + "滠" : "she", + "歙" : "xi", + "厍" : "she", + "莘" : "shen", + "娠" : "shen", + "诜" : "shen", + "什" : "shen", + "谂" : "shen", + "渖" : "shen", + "矧" : "shen", + "胂" : "shen", + "椹" : "shen", + "省" : "sheng", + "眚" : "sheng", + "嵊" : "sheng", + "嘘" : "xu", + "蓍" : "shi", + "鲺" : "shi", + "识" : "shi", + "拾" : "shi", + "埘" : "shi", + "莳" : "shi", + "炻" : "shi", + "鲥" : "shi", + "豕" : "shi", + "似" : "si", + "噬" : "shi", + "贳" : "shi", + "铈" : "shi", + "螫" : "shi", + "筮" : "shi", + "殖" : "zhi", + "熟" : "shu", + "艏" : "shou", + "菽" : "shu", + "摅" : "shu", + "纾" : "shu", + "毹" : "shu", + "疋" : "shu", + "数" : "shu", + "属" : "shu", + "术" : "shu", + "澍" : "shu", + "沭" : "shu", + "丨" : "shu", + "腧" : "shu", + "说" : "shuo", + "妁" : "shuo", + "蒴" : "shuo", + "槊" : "shuo", + "搠" : "shuo", + "鸶" : "si", + "澌" : "si", + "缌" : "si", + "锶" : "si", + "厶" : "si", + "蛳" : "si", + "驷" : "si", + "泗" : "si", + "汜" : "si", + "兕" : "si", + "姒" : "si", + "耜" : "si", + "笥" : "si", + "忪" : "song", + "淞" : "song", + "崧" : "song", + "凇" : "song", + "菘" : "song", + "竦" : "song", + "溲" : "sou", + "飕" : "sou", + "蜩" : "tiao", + "萜" : "tie", + "汀" : "ting", + "葶" : "ting", + "莛" : "ting", + "梃" : "ting", + "佟" : "tong", + "酮" : "tong", + "仝" : "tong", + "茼" : "tong", + "砼" : "tong", + "钭" : "dou", + "酴" : "tu", + "钍" : "tu", + "堍" : "tu", + "抟" : "tuan", + "忒" : "te", + "煺" : "tui", + "暾" : "tun", + "氽" : "tun", + "乇" : "tuo", + "砣" : "tuo", + "沱" : "tuo", + "跎" : "tuo", + "坨" : "tuo", + "橐" : "tuo", + "酡" : "tuo", + "鼍" : "tuo", + "庹" : "tuo", + "拓" : "tuo", + "柝" : "tuo", + "箨" : "tuo", + "腽" : "wa", + "崴" : "wai", + "芄" : "wan", + "畹" : "wan", + "琬" : "wan", + "脘" : "wan", + "菀" : "wan", + "尢" : "you", + "辋" : "wang", + "魍" : "wang", + "逶" : "wei", + "葳" : "wei", + "隈" : "wei", + "惟" : "wei", + "帏" : "wei", + "圩" : "wei", + "囗" : "wei", + "潍" : "wei", + "嵬" : "wei", + "沩" : "wei", + "涠" : "wei", + "尾" : "wei", + "玮" : "wei", + "炜" : "wei", + "韪" : "wei", + "洧" : "wei", + "艉" : "wei", + "鲔" : "wei", + "遗" : "yi", + "尉" : "wei", + "軎" : "wei", + "璺" : "wen", + "阌" : "wen", + "蓊" : "weng", + "蕹" : "weng", + "渥" : "wo", + "硪" : "wo", + "龌" : "wo", + "圬" : "wu", + "吾" : "wu", + "浯" : "wu", + "鼯" : "wu", + "牾" : "wu", + "迕" : "wu", + "庑" : "wu", + "痦" : "wu", + "芴" : "wu", + "杌" : "wu", + "焐" : "wu", + "阢" : "wu", + "婺" : "wu", + "鋈" : "wu", + "樨" : "xi", + "栖" : "qi", + "郗" : "xi", + "蹊" : "qi", + "淅" : "xi", + "熹" : "xi", + "浠" : "xi", + "僖" : "xi", + "穸" : "xi", + "螅" : "xi", + "菥" : "xi", + "舾" : "xi", + "矽" : "xi", + "粞" : "xi", + "硒" : "xi", + "醯" : "xi", + "欷" : "xi", + "鼷" : "xi", + "檄" : "xi", + "隰" : "xi", + "觋" : "xi", + "屣" : "xi", + "葸" : "xi", + "蓰" : "xi", + "铣" : "xi", + "饩" : "xi", + "阋" : "xi", + "禊" : "xi", + "舄" : "xi", + "狎" : "xia", + "硖" : "xia", + "柙" : "xia", + "暹" : "xian", + "莶" : "xian", + "祆" : "xian", + "籼" : "xian", + "跹" : "xian", + "鹇" : "xian", + "痫" : "xian", + "猃" : "xian", + "燹" : "xian", + "蚬" : "xian", + "筅" : "xian", + "冼" : "xian", + "岘" : "xian", + "骧" : "xiang", + "葙" : "xiang", + "芗" : "xiang", + "缃" : "xiang", + "庠" : "xiang", + "鲞" : "xiang", + "蟓" : "xiang", + "削" : "xue", + "枵" : "xiao", + "绡" : "xiao", + "筱" : "xiao", + "邪" : "xie", + "勰" : "xie", + "缬" : "xie", + "血" : "xue", + "榭" : "xie", + "瀣" : "xie", + "薤" : "xie", + "燮" : "xie", + "躞" : "xie", + "廨" : "xie", + "绁" : "xie", + "渫" : "xie", + "榍" : "xie", + "獬" : "xie", + "昕" : "xin", + "忻" : "xin", + "囟" : "xin", + "陉" : "jing", + "荥" : "ying", + "饧" : "tang", + "硎" : "xing", + "荇" : "xing", + "芎" : "xiong", + "馐" : "xiu", + "庥" : "xiu", + "鸺" : "xiu", + "貅" : "xiu", + "髹" : "xiu", + "宿" : "xiu", + "岫" : "xiu", + "溴" : "xiu", + "吁" : "xu", + "盱" : "xu", + "顼" : "xu", + "糈" : "xu", + "醑" : "xu", + "洫" : "xu", + "溆" : "xu", + "蓿" : "xu", + "萱" : "xuan", + "谖" : "xuan", + "儇" : "xuan", + "煊" : "xuan", + "痃" : "xuan", + "铉" : "xuan", + "泫" : "xuan", + "碹" : "xuan", + "楦" : "xuan", + "镟" : "xuan", + "踅" : "xue", + "泶" : "xue", + "鳕" : "xue", + "埙" : "xun", + "曛" : "xun", + "窨" : "xun", + "獯" : "xun", + "峋" : "xun", + "洵" : "xun", + "恂" : "xun", + "浔" : "xun", + "鲟" : "xun", + "蕈" : "xun", + "垭" : "ya", + "岈" : "ya", + "琊" : "ya", + "痖" : "ya", + "迓" : "ya", + "砑" : "ya", + "咽" : "yan", + "鄢" : "yan", + "菸" : "yan", + "崦" : "yan", + "铅" : "qian", + "芫" : "yuan", + "兖" : "yan", + "琰" : "yan", + "罨" : "yan", + "厣" : "yan", + "焱" : "yan", + "酽" : "yan", + "谳" : "yan", + "鞅" : "yang", + "炀" : "yang", + "蛘" : "yang", + "约" : "yue", + "珧" : "yao", + "轺" : "yao", + "繇" : "yao", + "鳐" : "yao", + "崾" : "yao", + "钥" : "yao", + "曜" : "yao", + "铘" : "ye", + "烨" : "ye", + "邺" : "ye", + "靥" : "ye", + "晔" : "ye", + "猗" : "yi", + "铱" : "yi", + "欹" : "qi", + "黟" : "yi", + "怡" : "yi", + "沂" : "yi", + "圯" : "yi", + "荑" : "yi", + "诒" : "yi", + "眙" : "yi", + "嶷" : "yi", + "钇" : "yi", + "舣" : "yi", + "酏" : "yi", + "熠" : "yi", + "弋" : "yi", + "懿" : "yi", + "镒" : "yi", + "峄" : "yi", + "怿" : "yi", + "悒" : "yi", + "佾" : "yi", + "殪" : "yi", + "挹" : "yi", + "埸" : "yi", + "劓" : "yi", + "镱" : "yi", + "瘗" : "yi", + "癔" : "yi", + "翊" : "yi", + "蜴" : "yi", + "氤" : "yin", + "堙" : "yin", + "洇" : "yin", + "鄞" : "yin", + "狺" : "yin", + "夤" : "yin", + "圻" : "qi", + "饮" : "yin", + "吲" : "yin", + "胤" : "yin", + "茚" : "yin", + "璎" : "ying", + "撄" : "ying", + "嬴" : "ying", + "滢" : "ying", + "潆" : "ying", + "蓥" : "ying", + "瘿" : "ying", + "郢" : "ying", + "媵" : "ying", + "邕" : "yong", + "镛" : "yong", + "墉" : "yong", + "慵" : "yong", + "痈" : "yong", + "鳙" : "yong", + "饔" : "yong", + "喁" : "yong", + "俑" : "yong", + "莸" : "you", + "猷" : "you", + "疣" : "you", + "蚰" : "you", + "蝣" : "you", + "莜" : "you", + "牖" : "you", + "铕" : "you", + "卣" : "you", + "宥" : "you", + "侑" : "you", + "蚴" : "you", + "釉" : "you", + "馀" : "yu", + "萸" : "yu", + "禺" : "yu", + "妤" : "yu", + "欤" : "yu", + "觎" : "yu", + "窬" : "yu", + "蝓" : "yu", + "嵛" : "yu", + "舁" : "yu", + "雩" : "yu", + "龉" : "yu", + "伛" : "yu", + "圉" : "yu", + "庾" : "yu", + "瘐" : "yu", + "窳" : "yu", + "俣" : "yu", + "毓" : "yu", + "峪" : "yu", + "煜" : "yu", + "燠" : "yu", + "蓣" : "yu", + "饫" : "yu", + "阈" : "yu", + "鬻" : "yu", + "聿" : "yu", + "钰" : "yu", + "鹆" : "yu", + "蜮" : "yu", + "眢" : "yuan", + "箢" : "yuan", + "员" : "yuan", + "沅" : "yuan", + "橼" : "yuan", + "塬" : "yuan", + "爰" : "yuan", + "螈" : "yuan", + "鼋" : "yuan", + "掾" : "yuan", + "垸" : "yuan", + "瑗" : "yuan", + "刖" : "yue", + "瀹" : "yue", + "樾" : "yue", + "龠" : "yue", + "氲" : "yun", + "昀" : "yun", + "郧" : "yun", + "狁" : "yun", + "郓" : "yun", + "韫" : "yun", + "恽" : "yun", + "扎" : "zha", + "拶" : "za", + "咋" : "za", + "仔" : "zai", + "昝" : "zan", + "瓒" : "zan", + "藏" : "zang", + "奘" : "zang", + "唣" : "zao", + "择" : "ze", + "迮" : "ze", + "赜" : "ze", + "笮" : "ze", + "箦" : "ze", + "舴" : "ze", + "昃" : "ze", + "缯" : "zeng", + "罾" : "zeng", + "齄" : "zha", + "柞" : "zha", + "痄" : "zha", + "瘵" : "zhai", + "旃" : "zhan", + "璋" : "zhang", + "漳" : "zhang", + "嫜" : "zhang", + "鄣" : "zhang", + "仉" : "zhang", + "幛" : "zhang", + "着" : "zhe", + "啁" : "zhou", + "爪" : "zhao", + "棹" : "zhao", + "笊" : "zhao", + "摺" : "zhe", + "磔" : "zhe", + "这" : "zhe", + "柘" : "zhe", + "桢" : "zhen", + "蓁" : "zhen", + "祯" : "zhen", + "浈" : "zhen", + "畛" : "zhen", + "轸" : "zhen", + "稹" : "zhen", + "圳" : "zhen", + "徵" : "zhi", + "钲" : "zheng", + "卮" : "zhi", + "胝" : "zhi", + "祗" : "zhi", + "摭" : "zhi", + "絷" : "zhi", + "埴" : "zhi", + "轵" : "zhi", + "黹" : "zhi", + "帙" : "zhi", + "轾" : "zhi", + "贽" : "zhi", + "陟" : "zhi", + "忮" : "zhi", + "彘" : "zhi", + "膣" : "zhi", + "鸷" : "zhi", + "骘" : "zhi", + "踬" : "zhi", + "郅" : "zhi", + "觯" : "zhi", + "锺" : "zhong", + "螽" : "zhong", + "舯" : "zhong", + "碡" : "zhou", + "绉" : "zhou", + "荮" : "zhou", + "籀" : "zhou", + "酎" : "zhou", + "洙" : "zhu", + "邾" : "zhu", + "潴" : "zhu", + "槠" : "zhu", + "橥" : "zhu", + "舳" : "zhu", + "瘃" : "zhu", + "渚" : "zhu", + "麈" : "zhu", + "箸" : "zhu", + "炷" : "zhu", + "杼" : "zhu", + "翥" : "zhu", + "疰" : "zhu", + "颛" : "zhuan", + "赚" : "zhuan", + "馔" : "zhuan", + "僮" : "tong", + "缒" : "zhui", + "肫" : "zhun", + "窀" : "zhun", + "涿" : "zhuo", + "倬" : "zhuo", + "濯" : "zhuo", + "诼" : "zhuo", + "禚" : "zhuo", + "浞" : "zhuo", + "谘" : "zi", + "淄" : "zi", + "髭" : "zi", + "孳" : "zi", + "粢" : "zi", + "趑" : "zi", + "觜" : "zui", + "缁" : "zi", + "鲻" : "zi", + "嵫" : "zi", + "笫" : "zi", + "耔" : "zi", + "腙" : "zong", + "偬" : "zong", + "诹" : "zou", + "陬" : "zou", + "鄹" : "zou", + "驺" : "zou", + "鲰" : "zou", + "菹" : "ju", + "镞" : "zu", + "躜" : "zuan", + "缵" : "zuan", + "蕞" : "zui", + "撙" : "zun", + "胙" : "zuo", + "阿" : "a", + "阿" : "e", + "柏" : "bai", + "蚌" : "beng", + "薄" : "bo", + "堡" : "bao", + "呗" : "bei", + "贲" : "ben", + "臂" : "bi", + "瘪" : "bie", + "槟" : "bin", + "剥" : "bo", + "伯" : "bo", + "卜" : "bu", + "参" : "can", + "嚓" : "ca", + "差" : "cha", + "孱" : "chan", + "绰" : "chuo", + "称" : "cheng", + "澄" : "cheng", + "大" : "da", + "单" : "dan", + "得" : "de", + "的" : "de", + "地" : "di", + "都" : "dou", + "读" : "du", + "度" : "du", + "蹲" : "dun", + "佛" : "fo", + "伽" : "jia", + "盖" : "gai", + "镐" : "hao", + "给" : "gei", + "呱" : "gua", + "氿" : "jiu", + "桧" : "hui", + "掴" : "guo", + "蛤" : "ha", + "还" : "hai", + "和" : "he", + "核" : "he", + "哼" : "heng", + "鹄" : "hu", + "划" : "hua", + "夹" : "jia", + "贾" : "jia", + "芥" : "jie", + "劲" : "jin", + "荆" : "jing", + "颈" : "jing", + "貉" : "he", + "吖" : "a", + "啊" : "a", + "锕" : "a", + "哎" : "ai", + "哀" : "ai", + "埃" : "ai", + "唉" : "ai", + "欸" : "ai", + "锿" : "ai", + "挨" : "ai", + "皑" : "ai", + "癌" : "ai", + "毐" : "ai", + "矮" : "ai", + "蔼" : "ai", + "霭" : "ai", + "砹" : "ai", + "爱" : "ai", + "隘" : "ai", + "碍" : "ai", + "嗳" : "ai", + "嫒" : "ai", + "叆" : "ai", + "暧" : "ai", + "安" : "an", + "桉" : "an", + "氨" : "an", + "庵" : "an", + "谙" : "an", + "鹌" : "an", + "鞍" : "an", + "俺" : "an", + "埯" : "an", + "唵" : "an", + "铵" : "an", + "揞" : "an", + "岸" : "an", + "按" : "an", + "胺" : "an", + "案" : "an", + "暗" : "an", + "黯" : "an", + "玵" : "an", + "肮" : "ang", + "昂" : "ang", + "盎" : "ang", + "凹" : "ao", + "敖" : "ao", + "遨" : "ao", + "嗷" : "ao", + "獒" : "ao", + "熬" : "ao", + "聱" : "ao", + "螯" : "ao", + "翱" : "ao", + "謷" : "ao", + "鏖" : "ao", + "袄" : "ao", + "媪" : "ao", + "坳" : "ao", + "傲" : "ao", + "奥" : "ao", + "骜" : "ao", + "澳" : "ao", + "懊" : "ao", + "八" : "ba", + "巴" : "ba", + "叭" : "ba", + "芭" : "ba", + "疤" : "ba", + "捌" : "ba", + "笆" : "ba", + "粑" : "ba", + "拔" : "ba", + "茇" : "ba", + "妭" : "ba", + "菝" : "ba", + "跋" : "ba", + "魃" : "ba", + "把" : "ba", + "靶" : "ba", + "坝" : "ba", + "爸" : "ba", + "罢" : "ba", + "霸" : "ba", + "灞" : "ba", + "吧" : "ba", + "钯" : "ba", + "掰" : "bai", + "白" : "bai", + "百" : "bai", + "佰" : "bai", + "捭" : "bai", + "摆" : "bai", + "败" : "bai", + "拜" : "bai", + "稗" : "bai", + "扳" : "ban", + "攽" : "ban", + "班" : "ban", + "般" : "ban", + "颁" : "ban", + "斑" : "ban", + "搬" : "ban", + "瘢" : "ban", + "阪" : "ban", + "坂" : "ban", + "板" : "ban", + "版" : "ban", + "钣" : "ban", + "舨" : "ban", + "办" : "ban", + "半" : "ban", + "伴" : "ban", + "拌" : "ban", + "绊" : "ban", + "瓣" : "ban", + "扮" : "ban", + "邦" : "bang", + "帮" : "bang", + "梆" : "bang", + "浜" : "bang", + "绑" : "bang", + "榜" : "bang", + "棒" : "bang", + "傍" : "bang", + "谤" : "bang", + "蒡" : "bang", + "镑" : "bang", + "包" : "bao", + "苞" : "bao", + "孢" : "bao", + "胞" : "bao", + "龅" : "bao", + "煲" : "bao", + "褒" : "bao", + "雹" : "bao", + "饱" : "bao", + "宝" : "bao", + "保" : "bao", + "鸨" : "bao", + "葆" : "bao", + "褓" : "bao", + "报" : "bao", + "抱" : "bao", + "趵" : "bao", + "豹" : "bao", + "鲍" : "bao", + "暴" : "bao", + "爆" : "bao", + "枹" : "bao", + "杯" : "bei", + "卑" : "bei", + "悲" : "bei", + "碑" : "bei", + "北" : "bei", + "贝" : "bei", + "狈" : "bei", + "备" : "bei", + "背" : "bei", + "钡" : "bei", + "倍" : "bei", + "悖" : "bei", + "被" : "bei", + "辈" : "bei", + "惫" : "bei", + "焙" : "bei", + "蓓" : "bei", + "碚" : "bei", + "褙" : "bei", + "别" : "bei", + "蹩" : "bei", + "椑" : "bei", + "奔" : "ben", + "倴" : "ben", + "犇" : "ben", + "锛" : "ben", + "本" : "ben", + "苯" : "ben", + "坌" : "ben", + "笨" : "ben", + "崩" : "beng", + "绷" : "beng", + "嘣" : "beng", + "甭" : "beng", + "泵" : "beng", + "迸" : "beng", + "镚" : "beng", + "蹦" : "beng", + "屄" : "bi", + "逼" : "bi", + "荸" : "bi", + "鼻" : "bi", + "匕" : "bi", + "比" : "bi", + "吡" : "bi", + "沘" : "bi", + "妣" : "bi", + "彼" : "bi", + "秕" : "bi", + "笔" : "bi", + "俾" : "bi", + "鄙" : "bi", + "币" : "bi", + "必" : "bi", + "毕" : "bi", + "闭" : "bi", + "庇" : "bi", + "诐" : "bi", + "苾" : "bi", + "荜" : "bi", + "毖" : "bi", + "哔" : "bi", + "陛" : "bi", + "毙" : "bi", + "铋" : "bi", + "狴" : "bi", + "萆" : "bi", + "梐" : "bi", + "敝" : "bi", + "婢" : "bi", + "赑" : "bi", + "愎" : "bi", + "弼" : "bi", + "蓖" : "bi", + "痹" : "bi", + "滗" : "bi", + "碧" : "bi", + "蔽" : "bi", + "馝" : "bi", + "弊" : "bi", + "薜" : "bi", + "篦" : "bi", + "壁" : "bi", + "避" : "bi", + "髀" : "bi", + "璧" : "bi", + "芘" : "bi", + "边" : "bian", + "砭" : "bian", + "萹" : "bian", + "编" : "bian", + "煸" : "bian", + "蝙" : "bian", + "鳊" : "bian", + "鞭" : "bian", + "贬" : "bian", + "匾" : "bian", + "褊" : "bian", + "藊" : "bian", + "卞" : "bian", + "抃" : "bian", + "苄" : "bian", + "汴" : "bian", + "忭" : "bian", + "变" : "bian", + "遍" : "bian", + "辨" : "bian", + "辩" : "bian", + "辫" : "bian", + "标" : "biao", + "骉" : "biao", + "彪" : "biao", + "摽" : "biao", + "膘" : "biao", + "飙" : "biao", + "镖" : "biao", + "瀌" : "biao", + "镳" : "biao", + "表" : "biao", + "婊" : "biao", + "裱" : "biao", + "鳔" : "biao", + "憋" : "bie", + "鳖" : "bie", + "宾" : "bin", + "彬" : "bin", + "傧" : "bin", + "滨" : "bin", + "缤" : "bin", + "濒" : "bin", + "摈" : "bin", + "殡" : "bin", + "髌" : "bin", + "鬓" : "bin", + "冰" : "bing", + "兵" : "bing", + "丙" : "bing", + "邴" : "bing", + "秉" : "bing", + "柄" : "bing", + "饼" : "bing", + "炳" : "bing", + "禀" : "bing", + "并" : "bing", + "病" : "bing", + "摒" : "bing", + "拨" : "bo", + "波" : "bo", + "玻" : "bo", + "钵" : "bo", + "饽" : "bo", + "袯" : "bo", + "菠" : "bo", + "播" : "bo", + "驳" : "bo", + "帛" : "bo", + "勃" : "bo", + "钹" : "bo", + "铂" : "bo", + "亳" : "bo", + "舶" : "bo", + "脖" : "bo", + "博" : "bo", + "鹁" : "bo", + "渤" : "bo", + "搏" : "bo", + "馎" : "bo", + "箔" : "bo", + "膊" : "bo", + "踣" : "bo", + "馞" : "bo", + "礴" : "bo", + "跛" : "bo", + "檗" : "bo", + "擘" : "bo", + "簸" : "bo", + "啵" : "bo", + "蕃" : "bo", + "哱" : "bo", + "卟" : "bu", + "补" : "bu", + "捕" : "bu", + "哺" : "bu", + "不" : "bu", + "布" : "bu", + "步" : "bu", + "怖" : "bu", + "钚" : "bu", + "部" : "bu", + "埠" : "bu", + "簿" : "bu", + "擦" : "ca", + "猜" : "cai", + "才" : "cai", + "材" : "cai", + "财" : "cai", + "裁" : "cai", + "采" : "cai", + "彩" : "cai", + "睬" : "cai", + "踩" : "cai", + "菜" : "cai", + "蔡" : "cai", + "餐" : "can", + "残" : "can", + "蚕" : "can", + "惭" : "can", + "惨" : "can", + "黪" : "can", + "灿" : "can", + "粲" : "can", + "璨" : "can", + "穇" : "can", + "仓" : "cang", + "伧" : "cang", + "苍" : "cang", + "沧" : "cang", + "舱" : "cang", + "操" : "cao", + "糙" : "cao", + "曹" : "cao", + "嘈" : "cao", + "漕" : "cao", + "槽" : "cao", + "螬" : "cao", + "草" : "cao", + "册" : "ce", + "厕" : "ce", + "测" : "ce", + "恻" : "ce", + "策" : "ce", + "岑" : "cen", + "涔" : "cen", + "噌" : "ceng", + "层" : "ceng", + "嶒" : "ceng", + "蹭" : "ceng", + "叉" : "cha", + "杈" : "cha", + "插" : "cha", + "馇" : "cha", + "锸" : "cha", + "茬" : "cha", + "茶" : "cha", + "搽" : "cha", + "嵖" : "cha", + "猹" : "cha", + "槎" : "cha", + "碴" : "cha", + "察" : "cha", + "檫" : "cha", + "衩" : "cha", + "镲" : "cha", + "汊" : "cha", + "岔" : "cha", + "侘" : "cha", + "诧" : "cha", + "姹" : "cha", + "蹅" : "cha", + "拆" : "chai", + "钗" : "chai", + "侪" : "chai", + "柴" : "chai", + "豺" : "chai", + "虿" : "chai", + "茝" : "chai", + "觇" : "chan", + "掺" : "chan", + "搀" : "chan", + "襜" : "chan", + "谗" : "chan", + "婵" : "chan", + "馋" : "chan", + "缠" : "chan", + "蝉" : "chan", + "潺" : "chan", + "蟾" : "chan", + "巉" : "chan", + "产" : "chan", + "浐" : "chan", + "谄" : "chan", + "铲" : "chan", + "阐" : "chan", + "蒇" : "chan", + "骣" : "chan", + "冁" : "chan", + "忏" : "chan", + "颤" : "chan", + "羼" : "chan", + "韂" : "chan", + "伥" : "chang", + "昌" : "chang", + "菖" : "chang", + "猖" : "chang", + "娼" : "chang", + "肠" : "chang", + "尝" : "chang", + "常" : "chang", + "偿" : "chang", + "徜" : "chang", + "嫦" : "chang", + "厂" : "chang", + "场" : "chang", + "昶" : "chang", + "惝" : "chang", + "敞" : "chang", + "怅" : "chang", + "畅" : "chang", + "倡" : "chang", + "唱" : "chang", + "裳" : "chang", + "抄" : "chao", + "怊" : "chao", + "钞" : "chao", + "超" : "chao", + "晁" : "chao", + "巢" : "chao", + "嘲" : "chao", + "潮" : "chao", + "吵" : "chao", + "炒" : "chao", + "耖" : "chao", + "砗" : "che", + "扯" : "che", + "彻" : "che", + "坼" : "che", + "掣" : "che", + "撤" : "che", + "澈" : "che", + "瞮" : "che", + "抻" : "chen", + "郴" : "chen", + "嗔" : "chen", + "瞋" : "chen", + "臣" : "chen", + "尘" : "chen", + "辰" : "chen", + "沉" : "chen", + "忱" : "chen", + "陈" : "chen", + "宸" : "chen", + "晨" : "chen", + "谌" : "chen", + "碜" : "chen", + "衬" : "chen", + "龀" : "chen", + "趁" : "chen", + "柽" : "cheng", + "琤" : "cheng", + "撑" : "cheng", + "瞠" : "cheng", + "成" : "cheng", + "丞" : "cheng", + "呈" : "cheng", + "诚" : "cheng", + "承" : "cheng", + "城" : "cheng", + "铖" : "cheng", + "程" : "cheng", + "惩" : "cheng", + "酲" : "cheng", + "橙" : "cheng", + "逞" : "cheng", + "骋" : "cheng", + "秤" : "cheng", + "铛" : "cheng", + "樘" : "cheng", + "吃" : "chi", + "哧" : "chi", + "鸱" : "chi", + "蚩" : "chi", + "笞" : "chi", + "嗤" : "chi", + "痴" : "chi", + "媸" : "chi", + "魑" : "chi", + "池" : "chi", + "弛" : "chi", + "驰" : "chi", + "迟" : "chi", + "茌" : "chi", + "持" : "chi", + "踟" : "chi", + "尺" : "chi", + "齿" : "chi", + "侈" : "chi", + "耻" : "chi", + "豉" : "chi", + "褫" : "chi", + "彳" : "chi", + "叱" : "chi", + "斥" : "chi", + "赤" : "chi", + "饬" : "chi", + "炽" : "chi", + "翅" : "chi", + "敕" : "chi", + "啻" : "chi", + "傺" : "chi", + "匙" : "chi", + "冲" : "chong", + "充" : "chong", + "忡" : "chong", + "茺" : "chong", + "舂" : "chong", + "憧" : "chong", + "艟" : "chong", + "虫" : "chong", + "崇" : "chong", + "宠" : "chong", + "铳" : "chong", + "抽" : "chou", + "瘳" : "chou", + "惆" : "chou", + "绸" : "chou", + "畴" : "chou", + "酬" : "chou", + "稠" : "chou", + "愁" : "chou", + "筹" : "chou", + "踌" : "chou", + "丑" : "chou", + "瞅" : "chou", + "出" : "chu", + "初" : "chu", + "樗" : "chu", + "刍" : "chu", + "除" : "chu", + "厨" : "chu", + "锄" : "chu", + "滁" : "chu", + "蜍" : "chu", + "雏" : "chu", + "橱" : "chu", + "躇" : "chu", + "蹰" : "chu", + "杵" : "chu", + "础" : "chu", + "储" : "chu", + "楚" : "chu", + "褚" : "chu", + "亍" : "chu", + "处" : "chu", + "怵" : "chu", + "绌" : "chu", + "搐" : "chu", + "触" : "chu", + "憷" : "chu", + "黜" : "chu", + "矗" : "chu", + "揣" : "chuai", + "搋" : "chuai", + "膗" : "chuai", + "踹" : "chuai", + "川" : "chuan", + "氚" : "chuan", + "穿" : "chuan", + "舡" : "chuan", + "船" : "chuan", + "遄" : "chuan", + "椽" : "chuan", + "舛" : "chuan", + "喘" : "chuan", + "串" : "chuan", + "钏" : "chuan", + "疮" : "chuang", + "窗" : "chuang", + "床" : "chuang", + "闯" : "chuang", + "创" : "chuang", + "怆" : "chuang", + "吹" : "chui", + "炊" : "chui", + "垂" : "chui", + "陲" : "chui", + "捶" : "chui", + "棰" : "chui", + "槌" : "chui", + "锤" : "chui", + "春" : "chun", + "瑃" : "chun", + "椿" : "chun", + "蝽" : "chun", + "纯" : "chun", + "莼" : "chun", + "唇" : "chun", + "淳" : "chun", + "鹑" : "chun", + "醇" : "chun", + "蠢" : "chun", + "踔" : "chuo", + "戳" : "chuo", + "啜" : "chuo", + "惙" : "chuo", + "辍" : "chuo", + "龊" : "chuo", + "歠" : "chuo", + "疵" : "ci", + "词" : "ci", + "茈" : "ci", + "茨" : "ci", + "祠" : "ci", + "瓷" : "ci", + "辞" : "ci", + "慈" : "ci", + "磁" : "ci", + "雌" : "ci", + "鹚" : "ci", + "糍" : "ci", + "此" : "ci", + "泚" : "ci", + "跐" : "ci", + "次" : "ci", + "刺" : "ci", + "佽" : "ci", + "赐" : "ci", + "匆" : "cong", + "苁" : "cong", + "囱" : "cong", + "枞" : "cong", + "葱" : "cong", + "骢" : "cong", + "聪" : "cong", + "从" : "cong", + "丛" : "cong", + "淙" : "cong", + "悰" : "cong", + "琮" : "cong", + "凑" : "cou", + "辏" : "cou", + "腠" : "cou", + "粗" : "cu", + "徂" : "cu", + "殂" : "cu", + "促" : "cu", + "猝" : "cu", + "蔟" : "cu", + "醋" : "cu", + "踧" : "cu", + "簇" : "cu", + "蹙" : "cu", + "蹴" : "cu", + "汆" : "cuan", + "撺" : "cuan", + "镩" : "cuan", + "蹿" : "cuan", + "窜" : "cuan", + "篡" : "cuan", + "崔" : "cui", + "催" : "cui", + "摧" : "cui", + "璀" : "cui", + "脆" : "cui", + "萃" : "cui", + "啐" : "cui", + "淬" : "cui", + "悴" : "cui", + "毳" : "cui", + "瘁" : "cui", + "粹" : "cui", + "翠" : "cui", + "村" : "cun", + "皴" : "cun", + "存" : "cun", + "忖" : "cun", + "寸" : "cun", + "吋" : "cun", + "搓" : "cuo", + "磋" : "cuo", + "蹉" : "cuo", + "嵯" : "cuo", + "矬" : "cuo", + "痤" : "cuo", + "脞" : "cuo", + "挫" : "cuo", + "莝" : "cuo", + "厝" : "cuo", + "措" : "cuo", + "锉" : "cuo", + "错" : "cuo", + "酇" : "cuo", + "咑" : "da", + "垯" : "da", + "耷" : "da", + "搭" : "da", + "褡" : "da", + "达" : "da", + "怛" : "da", + "妲" : "da", + "荙" : "da", + "笪" : "da", + "答" : "da", + "跶" : "da", + "靼" : "da", + "瘩" : "da", + "鞑" : "da", + "打" : "da", + "呆" : "dai", + "歹" : "dai", + "逮" : "dai", + "傣" : "dai", + "代" : "dai", + "岱" : "dai", + "迨" : "dai", + "玳" : "dai", + "带" : "dai", + "殆" : "dai", + "贷" : "dai", + "待" : "dai", + "怠" : "dai", + "袋" : "dai", + "叇" : "dai", + "戴" : "dai", + "黛" : "dai", + "襶" : "dai", + "呔" : "dai", + "丹" : "dan", + "担" : "dan", + "眈" : "dan", + "耽" : "dan", + "郸" : "dan", + "聃" : "dan", + "殚" : "dan", + "瘅" : "dan", + "箪" : "dan", + "儋" : "dan", + "胆" : "dan", + "疸" : "dan", + "掸" : "dan", + "亶" : "dan", + "旦" : "dan", + "但" : "dan", + "诞" : "dan", + "萏" : "dan", + "啖" : "dan", + "淡" : "dan", + "惮" : "dan", + "蛋" : "dan", + "氮" : "dan", + "赕" : "dan", + "当" : "dang", + "裆" : "dang", + "挡" : "dang", + "档" : "dang", + "党" : "dang", + "谠" : "dang", + "凼" : "dang", + "砀" : "dang", + "宕" : "dang", + "荡" : "dang", + "菪" : "dang", + "刀" : "dao", + "忉" : "dao", + "氘" : "dao", + "舠" : "dao", + "导" : "dao", + "岛" : "dao", + "捣" : "dao", + "倒" : "dao", + "捯" : "dao", + "祷" : "dao", + "蹈" : "dao", + "到" : "dao", + "盗" : "dao", + "悼" : "dao", + "道" : "dao", + "稻" : "dao", + "焘" : "dao", + "锝" : "de", + "嘚" : "de", + "德" : "de", + "扽" : "den", + "灯" : "deng", + "登" : "deng", + "噔" : "deng", + "蹬" : "deng", + "等" : "deng", + "戥" : "deng", + "邓" : "deng", + "僜" : "deng", + "凳" : "deng", + "嶝" : "deng", + "磴" : "deng", + "瞪" : "deng", + "镫" : "deng", + "低" : "di", + "羝" : "di", + "堤" : "di", + "嘀" : "di", + "滴" : "di", + "狄" : "di", + "迪" : "di", + "籴" : "di", + "荻" : "di", + "敌" : "di", + "涤" : "di", + "笛" : "di", + "觌" : "di", + "嫡" : "di", + "镝" : "di", + "氐" : "di", + "邸" : "di", + "诋" : "di", + "抵" : "di", + "底" : "di", + "柢" : "di", + "砥" : "di", + "骶" : "di", + "玓" : "di", + "弟" : "di", + "帝" : "di", + "递" : "di", + "娣" : "di", + "第" : "di", + "谛" : "di", + "蒂" : "di", + "棣" : "di", + "睇" : "di", + "缔" : "di", + "碲" : "di", + "嗲" : "dia", + "掂" : "dian", + "滇" : "dian", + "颠" : "dian", + "巅" : "dian", + "癫" : "dian", + "典" : "dian", + "点" : "dian", + "碘" : "dian", + "踮" : "dian", + "电" : "dian", + "甸" : "dian", + "阽" : "dian", + "坫" : "dian", + "店" : "dian", + "玷" : "dian", + "垫" : "dian", + "钿" : "dian", + "淀" : "dian", + "惦" : "dian", + "奠" : "dian", + "殿" : "dian", + "靛" : "dian", + "刁" : "diao", + "叼" : "diao", + "汈" : "diao", + "凋" : "diao", + "貂" : "diao", + "碉" : "diao", + "雕" : "diao", + "鲷" : "diao", + "屌" : "diao", + "吊" : "diao", + "钓" : "diao", + "窎" : "diao", + "掉" : "diao", + "铫" : "diao", + "爹" : "die", + "跌" : "die", + "迭" : "die", + "谍" : "die", + "耋" : "die", + "喋" : "die", + "牒" : "die", + "叠" : "die", + "碟" : "die", + "嵽" : "die", + "蝶" : "die", + "蹀" : "die", + "鲽" : "die", + "仃" : "ding", + "叮" : "ding", + "玎" : "ding", + "盯" : "ding", + "町" : "ding", + "耵" : "ding", + "顶" : "ding", + "酊" : "ding", + "鼎" : "ding", + "订" : "ding", + "钉" : "ding", + "定" : "ding", + "啶" : "ding", + "腚" : "ding", + "碇" : "ding", + "锭" : "ding", + "丢" : "diu", + "铥" : "diu", + "东" : "dong", + "冬" : "dong", + "咚" : "dong", + "氡" : "dong", + "鸫" : "dong", + "董" : "dong", + "懂" : "dong", + "动" : "dong", + "冻" : "dong", + "侗" : "dong", + "栋" : "dong", + "胨" : "dong", + "洞" : "dong", + "胴" : "dong", + "兜" : "dou", + "蔸" : "dou", + "篼" : "dou", + "抖" : "dou", + "陡" : "dou", + "蚪" : "dou", + "斗" : "dou", + "豆" : "dou", + "逗" : "dou", + "痘" : "dou", + "窦" : "dou", + "督" : "du", + "嘟" : "du", + "毒" : "du", + "独" : "du", + "渎" : "du", + "椟" : "du", + "犊" : "du", + "牍" : "du", + "黩" : "du", + "髑" : "du", + "厾" : "du", + "笃" : "du", + "堵" : "du", + "赌" : "du", + "睹" : "du", + "杜" : "du", + "肚" : "du", + "妒" : "du", + "渡" : "du", + "镀" : "du", + "蠹" : "du", + "端" : "duan", + "短" : "duan", + "段" : "duan", + "断" : "duan", + "缎" : "duan", + "椴" : "duan", + "锻" : "duan", + "簖" : "duan", + "堆" : "dui", + "队" : "dui", + "对" : "dui", + "兑" : "dui", + "怼" : "dui", + "憝" : "dui", + "吨" : "dun", + "惇" : "dun", + "敦" : "dun", + "墩" : "dun", + "礅" : "dun", + "盹" : "dun", + "趸" : "dun", + "沌" : "dun", + "炖" : "dun", + "砘" : "dun", + "钝" : "dun", + "盾" : "dun", + "顿" : "dun", + "遁" : "dun", + "多" : "duo", + "咄" : "duo", + "哆" : "duo", + "掇" : "duo", + "裰" : "duo", + "夺" : "duo", + "踱" : "duo", + "朵" : "duo", + "垛" : "duo", + "哚" : "duo", + "躲" : "duo", + "亸" : "duo", + "剁" : "duo", + "舵" : "duo", + "堕" : "duo", + "惰" : "duo", + "跺" : "duo", + "屙" : "e", + "婀" : "e", + "讹" : "e", + "囮" : "e", + "俄" : "e", + "莪" : "e", + "峨" : "e", + "娥" : "e", + "锇" : "e", + "鹅" : "e", + "蛾" : "e", + "额" : "e", + "厄" : "e", + "扼" : "e", + "苊" : "e", + "呃" : "e", + "垩" : "e", + "饿" : "e", + "鄂" : "e", + "谔" : "e", + "萼" : "e", + "遏" : "e", + "愕" : "e", + "腭" : "e", + "颚" : "e", + "噩" : "e", + "鳄" : "e", + "恩" : "en", + "蒽" : "en", + "摁" : "en", + "鞥" : "eng", + "儿" : "er", + "而" : "er", + "鸸" : "er", + "尔" : "er", + "耳" : "er", + "迩" : "er", + "饵" : "er", + "洱" : "er", + "铒" : "er", + "二" : "er", + "贰" : "er", + "发" : "fa", + "乏" : "fa", + "伐" : "fa", + "罚" : "fa", + "垡" : "fa", + "阀" : "fa", + "筏" : "fa", + "法" : "fa", + "砝" : "fa", + "珐" : "fa", + "帆" : "fan", + "幡" : "fan", + "藩" : "fan", + "翻" : "fan", + "凡" : "fan", + "矾" : "fan", + "钒" : "fan", + "烦" : "fan", + "樊" : "fan", + "燔" : "fan", + "繁" : "fan", + "蹯" : "fan", + "蘩" : "fan", + "反" : "fan", + "返" : "fan", + "犯" : "fan", + "饭" : "fan", + "泛" : "fan", + "范" : "fan", + "贩" : "fan", + "畈" : "fan", + "梵" : "fan", + "方" : "fang", + "邡" : "fang", + "坊" : "fang", + "芳" : "fang", + "枋" : "fang", + "钫" : "fang", + "防" : "fang", + "妨" : "fang", + "肪" : "fang", + "房" : "fang", + "鲂" : "fang", + "仿" : "fang", + "访" : "fang", + "纺" : "fang", + "舫" : "fang", + "放" : "fang", + "飞" : "fei", + "妃" : "fei", + "非" : "fei", + "菲" : "fei", + "啡" : "fei", + "绯" : "fei", + "扉" : "fei", + "肥" : "fei", + "淝" : "fei", + "腓" : "fei", + "匪" : "fei", + "诽" : "fei", + "悱" : "fei", + "棐" : "fei", + "斐" : "fei", + "榧" : "fei", + "翡" : "fei", + "篚" : "fei", + "吠" : "fei", + "肺" : "fei", + "狒" : "fei", + "废" : "fei", + "沸" : "fei", + "费" : "fei", + "痱" : "fei", + "镄" : "fei", + "分" : "fen", + "芬" : "fen", + "吩" : "fen", + "纷" : "fen", + "氛" : "fen", + "酚" : "fen", + "坟" : "fen", + "汾" : "fen", + "棼" : "fen", + "焚" : "fen", + "鼢" : "fen", + "粉" : "fen", + "份" : "fen", + "奋" : "fen", + "忿" : "fen", + "偾" : "fen", + "粪" : "fen", + "愤" : "fen", + "丰" : "feng", + "风" : "feng", + "沣" : "feng", + "枫" : "feng", + "封" : "feng", + "砜" : "feng", + "疯" : "feng", + "峰" : "feng", + "烽" : "feng", + "葑" : "feng", + "锋" : "feng", + "蜂" : "feng", + "酆" : "feng", + "冯" : "feng", + "逢" : "feng", + "缝" : "feng", + "讽" : "feng", + "唪" : "feng", + "凤" : "feng", + "奉" : "feng", + "俸" : "feng", + "缶" : "fou", + "夫" : "fu", + "呋" : "fu", + "肤" : "fu", + "麸" : "fu", + "跗" : "fu", + "稃" : "fu", + "孵" : "fu", + "敷" : "fu", + "弗" : "fu", + "伏" : "fu", + "凫" : "fu", + "扶" : "fu", + "芙" : "fu", + "孚" : "fu", + "拂" : "fu", + "苻" : "fu", + "服" : "fu", + "怫" : "fu", + "茯" : "fu", + "氟" : "fu", + "俘" : "fu", + "浮" : "fu", + "符" : "fu", + "匐" : "fu", + "涪" : "fu", + "艴" : "fu", + "幅" : "fu", + "辐" : "fu", + "蜉" : "fu", + "福" : "fu", + "蝠" : "fu", + "抚" : "fu", + "甫" : "fu", + "拊" : "fu", + "斧" : "fu", + "府" : "fu", + "俯" : "fu", + "釜" : "fu", + "辅" : "fu", + "腑" : "fu", + "腐" : "fu", + "父" : "fu", + "讣" : "fu", + "付" : "fu", + "负" : "fu", + "妇" : "fu", + "附" : "fu", + "咐" : "fu", + "阜" : "fu", + "驸" : "fu", + "赴" : "fu", + "复" : "fu", + "副" : "fu", + "赋" : "fu", + "傅" : "fu", + "富" : "fu", + "腹" : "fu", + "缚" : "fu", + "赙" : "fu", + "蝮" : "fu", + "覆" : "fu", + "馥" : "fu", + "袱" : "fu", + "旮" : "ga", + "嘎" : "ga", + "钆" : "ga", + "尜" : "ga", + "尕" : "ga", + "尬" : "ga", + "该" : "gai", + "垓" : "gai", + "荄" : "gai", + "赅" : "gai", + "改" : "gai", + "丐" : "gai", + "钙" : "gai", + "溉" : "gai", + "概" : "gai", + "甘" : "gan", + "玕" : "gan", + "肝" : "gan", + "坩" : "gan", + "苷" : "gan", + "矸" : "gan", + "泔" : "gan", + "柑" : "gan", + "竿" : "gan", + "酐" : "gan", + "疳" : "gan", + "尴" : "gan", + "杆" : "gan", + "秆" : "gan", + "赶" : "gan", + "敢" : "gan", + "感" : "gan", + "澉" : "gan", + "橄" : "gan", + "擀" : "gan", + "干" : "gan", + "旰" : "gan", + "绀" : "gan", + "淦" : "gan", + "骭" : "gan", + "赣" : "gan", + "冈" : "gang", + "冮" : "gang", + "刚" : "gang", + "肛" : "gang", + "纲" : "gang", + "钢" : "gang", + "缸" : "gang", + "罡" : "gang", + "岗" : "gang", + "港" : "gang", + "杠" : "gang", + "皋" : "gao", + "高" : "gao", + "羔" : "gao", + "睾" : "gao", + "膏" : "gao", + "篙" : "gao", + "糕" : "gao", + "杲" : "gao", + "搞" : "gao", + "槁" : "gao", + "稿" : "gao", + "告" : "gao", + "郜" : "gao", + "诰" : "gao", + "锆" : "gao", + "戈" : "ge", + "圪" : "ge", + "纥" : "ge", + "疙" : "ge", + "哥" : "ge", + "胳" : "ge", + "鸽" : "ge", + "袼" : "ge", + "搁" : "ge", + "割" : "ge", + "歌" : "ge", + "革" : "ge", + "阁" : "ge", + "格" : "ge", + "隔" : "ge", + "嗝" : "ge", + "膈" : "ge", + "骼" : "ge", + "镉" : "ge", + "舸" : "ge", + "葛" : "ge", + "个" : "ge", + "各" : "ge", + "虼" : "ge", + "硌" : "ge", + "铬" : "ge", + "根" : "gen", + "跟" : "gen", + "哏" : "gen", + "亘" : "gen", + "艮" : "gen", + "茛" : "gen", + "庚" : "geng", + "耕" : "geng", + "浭" : "geng", + "赓" : "geng", + "羹" : "geng", + "埂" : "geng", + "耿" : "geng", + "哽" : "geng", + "绠" : "geng", + "梗" : "geng", + "鲠" : "geng", + "更" : "geng", + "工" : "gong", + "弓" : "gong", + "公" : "gong", + "功" : "gong", + "攻" : "gong", + "肱" : "gong", + "宫" : "gong", + "恭" : "gong", + "蚣" : "gong", + "躬" : "gong", + "龚" : "gong", + "塨" : "gong", + "觥" : "gong", + "巩" : "gong", + "汞" : "gong", + "拱" : "gong", + "珙" : "gong", + "共" : "gong", + "贡" : "gong", + "供" : "gong", + "勾" : "gou", + "佝" : "gou", + "沟" : "gou", + "钩" : "gou", + "篝" : "gou", + "苟" : "gou", + "岣" : "gou", + "狗" : "gou", + "枸" : "gou", + "构" : "gou", + "购" : "gou", + "诟" : "gou", + "垢" : "gou", + "够" : "gou", + "彀" : "gou", + "媾" : "gou", + "觏" : "gou", + "估" : "gu", + "咕" : "gu", + "沽" : "gu", + "孤" : "gu", + "姑" : "gu", + "轱" : "gu", + "鸪" : "gu", + "菰" : "gu", + "菇" : "gu", + "蛄" : "gu", + "蓇" : "gu", + "辜" : "gu", + "酤" : "gu", + "觚" : "gu", + "毂" : "gu", + "箍" : "gu", + "古" : "gu", + "谷" : "gu", + "汩" : "gu", + "诂" : "gu", + "股" : "gu", + "骨" : "gu", + "牯" : "gu", + "钴" : "gu", + "羖" : "gu", + "蛊" : "gu", + "鼓" : "gu", + "榾" : "gu", + "鹘" : "gu", + "臌" : "gu", + "瀔" : "gu", + "固" : "gu", + "故" : "gu", + "顾" : "gu", + "梏" : "gu", + "崮" : "gu", + "雇" : "gu", + "锢" : "gu", + "痼" : "gu", + "瓜" : "gua", + "刮" : "gua", + "胍" : "gua", + "鸹" : "gua", + "剐" : "gua", + "寡" : "gua", + "卦" : "gua", + "诖" : "gua", + "挂" : "gua", + "褂" : "gua", + "乖" : "guai", + "拐" : "guai", + "怪" : "guai", + "关" : "guan", + "观" : "guan", + "官" : "guan", + "倌" : "guan", + "蒄" : "guan", + "棺" : "guan", + "瘝" : "guan", + "鳏" : "guan", + "馆" : "guan", + "管" : "guan", + "贯" : "guan", + "冠" : "guan", + "掼" : "guan", + "惯" : "guan", + "祼" : "guan", + "盥" : "guan", + "灌" : "guan", + "瓘" : "guan", + "鹳" : "guan", + "罐" : "guan", + "琯" : "guan", + "光" : "guang", + "咣" : "guang", + "胱" : "guang", + "广" : "guang", + "犷" : "guang", + "桄" : "guang", + "逛" : "guang", + "归" : "gui", + "圭" : "gui", + "龟" : "gui", + "妫" : "gui", + "规" : "gui", + "皈" : "gui", + "闺" : "gui", + "硅" : "gui", + "瑰" : "gui", + "鲑" : "gui", + "宄" : "gui", + "轨" : "gui", + "庋" : "gui", + "匦" : "gui", + "诡" : "gui", + "鬼" : "gui", + "姽" : "gui", + "癸" : "gui", + "晷" : "gui", + "簋" : "gui", + "柜" : "gui", + "炅" : "gui", + "刿" : "gui", + "刽" : "gui", + "贵" : "gui", + "桂" : "gui", + "跪" : "gui", + "鳜" : "gui", + "衮" : "gun", + "绲" : "gun", + "辊" : "gun", + "滚" : "gun", + "磙" : "gun", + "鲧" : "gun", + "棍" : "gun", + "埚" : "guo", + "郭" : "guo", + "啯" : "guo", + "崞" : "guo", + "聒" : "guo", + "锅" : "guo", + "蝈" : "guo", + "国" : "guo", + "帼" : "guo", + "虢" : "guo", + "果" : "guo", + "椁" : "guo", + "蜾" : "guo", + "裹" : "guo", + "过" : "guo", + "哈" : "ha", + "铪" : "ha", + "孩" : "hai", + "骸" : "hai", + "胲" : "hai", + "海" : "hai", + "醢" : "hai", + "亥" : "hai", + "骇" : "hai", + "害" : "hai", + "嗐" : "hai", + "嗨" : "hai", + "顸" : "han", + "蚶" : "han", + "酣" : "han", + "憨" : "han", + "鼾" : "han", + "邗" : "han", + "邯" : "han", + "含" : "han", + "函" : "han", + "晗" : "han", + "焓" : "han", + "涵" : "han", + "韩" : "han", + "寒" : "han", + "罕" : "han", + "喊" : "han", + "蔊" : "han", + "汉" : "han", + "汗" : "han", + "旱" : "han", + "捍" : "han", + "悍" : "han", + "菡" : "han", + "焊" : "han", + "撖" : "han", + "撼" : "han", + "翰" : "han", + "憾" : "han", + "瀚" : "han", + "夯" : "hang", + "杭" : "hang", + "绗" : "hang", + "航" : "hang", + "沆" : "hang", + "蒿" : "hao", + "薅" : "hao", + "嚆" : "hao", + "蚝" : "hao", + "毫" : "hao", + "嗥" : "hao", + "豪" : "hao", + "壕" : "hao", + "嚎" : "hao", + "濠" : "hao", + "好" : "hao", + "郝" : "hao", + "号" : "hao", + "昊" : "hao", + "耗" : "hao", + "浩" : "hao", + "皓" : "hao", + "滈" : "hao", + "颢" : "hao", + "灏" : "hao", + "诃" : "he", + "呵" : "he", + "喝" : "he", + "嗬" : "he", + "禾" : "he", + "合" : "he", + "何" : "he", + "劾" : "he", + "河" : "he", + "曷" : "he", + "阂" : "he", + "盍" : "he", + "荷" : "he", + "菏" : "he", + "盒" : "he", + "涸" : "he", + "颌" : "he", + "阖" : "he", + "贺" : "he", + "赫" : "he", + "褐" : "he", + "鹤" : "he", + "壑" : "he", + "黑" : "hei", + "嘿" : "hei", + "痕" : "hen", + "很" : "hen", + "狠" : "hen", + "恨" : "hen", + "亨" : "heng", + "恒" : "heng", + "珩" : "heng", + "横" : "heng", + "衡" : "heng", + "蘅" : "heng", + "啈" : "heng", + "轰" : "hong", + "訇" : "hong", + "烘" : "hong", + "薨" : "hong", + "弘" : "hong", + "红" : "hong", + "闳" : "hong", + "宏" : "hong", + "荭" : "hong", + "虹" : "hong", + "竑" : "hong", + "洪" : "hong", + "鸿" : "hong", + "哄" : "hong", + "讧" : "hong", + "吽" : "hong", + "齁" : "hou", + "侯" : "hou", + "喉" : "hou", + "猴" : "hou", + "瘊" : "hou", + "骺" : "hou", + "篌" : "hou", + "糇" : "hou", + "吼" : "hou", + "后" : "hou", + "郈" : "hou", + "厚" : "hou", + "垕" : "hou", + "逅" : "hou", + "候" : "hou", + "堠" : "hou", + "鲎" : "hou", + "乎" : "hu", + "呼" : "hu", + "忽" : "hu", + "轷" : "hu", + "烀" : "hu", + "惚" : "hu", + "滹" : "hu", + "囫" : "hu", + "狐" : "hu", + "弧" : "hu", + "胡" : "hu", + "壶" : "hu", + "斛" : "hu", + "葫" : "hu", + "猢" : "hu", + "湖" : "hu", + "瑚" : "hu", + "鹕" : "hu", + "槲" : "hu", + "蝴" : "hu", + "糊" : "hu", + "醐" : "hu", + "觳" : "hu", + "虎" : "hu", + "唬" : "hu", + "琥" : "hu", + "互" : "hu", + "户" : "hu", + "冱" : "hu", + "护" : "hu", + "沪" : "hu", + "枑" : "hu", + "怙" : "hu", + "戽" : "hu", + "笏" : "hu", + "瓠" : "hu", + "扈" : "hu", + "鹱" : "hu", + "花" : "hua", + "砉" : "hua", + "华" : "hua", + "哗" : "hua", + "骅" : "hua", + "铧" : "hua", + "猾" : "hua", + "滑" : "hua", + "化" : "hua", + "画" : "hua", + "话" : "hua", + "桦" : "hua", + "婳" : "hua", + "觟" : "hua", + "怀" : "huai", + "徊" : "huai", + "淮" : "huai", + "槐" : "huai", + "踝" : "huai", + "耲" : "huai", + "坏" : "huai", + "欢" : "huan", + "獾" : "huan", + "环" : "huan", + "洹" : "huan", + "桓" : "huan", + "萑" : "huan", + "寰" : "huan", + "缳" : "huan", + "缓" : "huan", + "幻" : "huan", + "奂" : "huan", + "宦" : "huan", + "换" : "huan", + "唤" : "huan", + "涣" : "huan", + "浣" : "huan", + "患" : "huan", + "焕" : "huan", + "痪" : "huan", + "豢" : "huan", + "漶" : "huan", + "鲩" : "huan", + "擐" : "huan", + "肓" : "huang", + "荒" : "huang", + "塃" : "huang", + "慌" : "huang", + "皇" : "huang", + "黄" : "huang", + "凰" : "huang", + "隍" : "huang", + "喤" : "huang", + "遑" : "huang", + "徨" : "huang", + "湟" : "huang", + "惶" : "huang", + "媓" : "huang", + "煌" : "huang", + "锽" : "huang", + "潢" : "huang", + "璜" : "huang", + "蝗" : "huang", + "篁" : "huang", + "艎" : "huang", + "磺" : "huang", + "癀" : "huang", + "蟥" : "huang", + "簧" : "huang", + "鳇" : "huang", + "恍" : "huang", + "晃" : "huang", + "谎" : "huang", + "幌" : "huang", + "滉" : "huang", + "皝" : "huang", + "灰" : "hui", + "诙" : "hui", + "挥" : "hui", + "恢" : "hui", + "晖" : "hui", + "辉" : "hui", + "麾" : "hui", + "徽" : "hui", + "隳" : "hui", + "回" : "hui", + "茴" : "hui", + "洄" : "hui", + "蛔" : "hui", + "悔" : "hui", + "毁" : "hui", + "卉" : "hui", + "汇" : "hui", + "讳" : "hui", + "荟" : "hui", + "浍" : "hui", + "诲" : "hui", + "绘" : "hui", + "恚" : "hui", + "贿" : "hui", + "烩" : "hui", + "彗" : "hui", + "晦" : "hui", + "秽" : "hui", + "惠" : "hui", + "喙" : "hui", + "慧" : "hui", + "蕙" : "hui", + "蟪" : "hui", + "珲" : "hun", + "昏" : "hun", + "荤" : "hun", + "阍" : "hun", + "惛" : "hun", + "婚" : "hun", + "浑" : "hun", + "馄" : "hun", + "混" : "hun", + "魂" : "hun", + "诨" : "hun", + "溷" : "hun", + "耠" : "huo", + "劐" : "huo", + "豁" : "huo", + "活" : "huo", + "火" : "huo", + "伙" : "huo", + "钬" : "huo", + "夥" : "huo", + "或" : "huo", + "货" : "huo", + "获" : "huo", + "祸" : "huo", + "惑" : "huo", + "霍" : "huo", + "镬" : "huo", + "攉" : "huo", + "藿" : "huo", + "嚯" : "huo", + "讥" : "ji", + "击" : "ji", + "叽" : "ji", + "饥" : "ji", + "玑" : "ji", + "圾" : "ji", + "芨" : "ji", + "机" : "ji", + "乩" : "ji", + "肌" : "ji", + "矶" : "ji", + "鸡" : "ji", + "剞" : "ji", + "唧" : "ji", + "积" : "ji", + "笄" : "ji", + "屐" : "ji", + "姬" : "ji", + "基" : "ji", + "犄" : "ji", + "嵇" : "ji", + "畸" : "ji", + "跻" : "ji", + "箕" : "ji", + "齑" : "ji", + "畿" : "ji", + "墼" : "ji", + "激" : "ji", + "羁" : "ji", + "及" : "ji", + "吉" : "ji", + "岌" : "ji", + "汲" : "ji", + "级" : "ji", + "极" : "ji", + "即" : "ji", + "佶" : "ji", + "笈" : "ji", + "急" : "ji", + "疾" : "ji", + "棘" : "ji", + "集" : "ji", + "蒺" : "ji", + "楫" : "ji", + "辑" : "ji", + "嫉" : "ji", + "瘠" : "ji", + "藉" : "ji", + "籍" : "ji", + "几" : "ji", + "己" : "ji", + "虮" : "ji", + "挤" : "ji", + "脊" : "ji", + "掎" : "ji", + "戟" : "ji", + "麂" : "ji", + "计" : "ji", + "记" : "ji", + "伎" : "ji", + "纪" : "ji", + "技" : "ji", + "忌" : "ji", + "际" : "ji", + "妓" : "ji", + "季" : "ji", + "剂" : "ji", + "迹" : "ji", + "济" : "ji", + "既" : "ji", + "觊" : "ji", + "继" : "ji", + "偈" : "ji", + "祭" : "ji", + "悸" : "ji", + "寄" : "ji", + "寂" : "ji", + "绩" : "ji", + "暨" : "ji", + "稷" : "ji", + "鲫" : "ji", + "髻" : "ji", + "冀" : "ji", + "骥" : "ji", + "加" : "jia", + "佳" : "jia", + "枷" : "jia", + "浃" : "jia", + "痂" : "jia", + "家" : "jia", + "袈" : "jia", + "嘉" : "jia", + "镓" : "jia", + "荚" : "jia", + "戛" : "jia", + "颊" : "jia", + "甲" : "jia", + "胛" : "jia", + "钾" : "jia", + "假" : "jia", + "价" : "jia", + "驾" : "jia", + "架" : "jia", + "嫁" : "jia", + "稼" : "jia", + "戋" : "jian", + "尖" : "jian", + "奸" : "jian", + "歼" : "jian", + "坚" : "jian", + "间" : "jian", + "肩" : "jian", + "艰" : "jian", + "监" : "jian", + "兼" : "jian", + "菅" : "jian", + "笺" : "jian", + "缄" : "jian", + "煎" : "jian", + "拣" : "jian", + "茧" : "jian", + "柬" : "jian", + "俭" : "jian", + "捡" : "jian", + "检" : "jian", + "减" : "jian", + "剪" : "jian", + "睑" : "jian", + "简" : "jian", + "碱" : "jian", + "见" : "jian", + "件" : "jian", + "饯" : "jian", + "建" : "jian", + "荐" : "jian", + "贱" : "jian", + "剑" : "jian", + "健" : "jian", + "舰" : "jian", + "涧" : "jian", + "渐" : "jian", + "谏" : "jian", + "践" : "jian", + "锏" : "jian", + "毽" : "jian", + "腱" : "jian", + "溅" : "jian", + "鉴" : "jian", + "键" : "jian", + "僭" : "jian", + "箭" : "jian", + "江" : "jiang", + "将" : "jiang", + "姜" : "jiang", + "豇" : "jiang", + "浆" : "jiang", + "僵" : "jiang", + "缰" : "jiang", + "疆" : "jiang", + "讲" : "jiang", + "奖" : "jiang", + "桨" : "jiang", + "蒋" : "jiang", + "匠" : "jiang", + "酱" : "jiang", + "犟" : "jiang", + "糨" : "jiang", + "交" : "jiao", + "郊" : "jiao", + "浇" : "jiao", + "娇" : "jiao", + "姣" : "jiao", + "骄" : "jiao", + "胶" : "jiao", + "椒" : "jiao", + "蛟" : "jiao", + "焦" : "jiao", + "跤" : "jiao", + "蕉" : "jiao", + "礁" : "jiao", + "佼" : "jiao", + "狡" : "jiao", + "饺" : "jiao", + "绞" : "jiao", + "铰" : "jiao", + "矫" : "jiao", + "皎" : "jiao", + "脚" : "jiao", + "搅" : "jiao", + "剿" : "jiao", + "缴" : "jiao", + "叫" : "jiao", + "轿" : "jiao", + "较" : "jiao", + "教" : "jiao", + "窖" : "jiao", + "酵" : "jiao", + "侥" : "jiao", + "阶" : "jie", + "皆" : "jie", + "接" : "jie", + "秸" : "jie", + "揭" : "jie", + "嗟" : "jie", + "街" : "jie", + "孑" : "jie", + "节" : "jie", + "讦" : "jie", + "劫" : "jie", + "杰" : "jie", + "诘" : "jie", + "洁" : "jie", + "结" : "jie", + "捷" : "jie", + "睫" : "jie", + "截" : "jie", + "碣" : "jie", + "竭" : "jie", + "姐" : "jie", + "解" : "jie", + "介" : "jie", + "戒" : "jie", + "届" : "jie", + "界" : "jie", + "疥" : "jie", + "诫" : "jie", + "借" : "jie", + "巾" : "jin", + "斤" : "jin", + "今" : "jin", + "金" : "jin", + "津" : "jin", + "矜" : "jin", + "筋" : "jin", + "襟" : "jin", + "仅" : "jin", + "紧" : "jin", + "锦" : "jin", + "谨" : "jin", + "尽" : "jin", + "进" : "jin", + "近" : "jin", + "晋" : "jin", + "烬" : "jin", + "浸" : "jin", + "禁" : "jin", + "觐" : "jin", + "噤" : "jin", + "茎" : "jing", + "京" : "jing", + "泾" : "jing", + "经" : "jing", + "菁" : "jing", + "惊" : "jing", + "晶" : "jing", + "睛" : "jing", + "粳" : "jing", + "兢" : "jing", + "精" : "jing", + "鲸" : "jing", + "井" : "jing", + "阱" : "jing", + "刭" : "jing", + "景" : "jing", + "儆" : "jing", + "警" : "jing", + "径" : "jing", + "净" : "jing", + "痉" : "jing", + "竞" : "jing", + "竟" : "jing", + "敬" : "jing", + "靖" : "jing", + "静" : "jing", + "境" : "jing", + "镜" : "jing", + "迥" : "jiong", + "炯" : "jiong", + "窘" : "jiong", + "纠" : "jiu", + "鸠" : "jiu", + "究" : "jiu", + "赳" : "jiu", + "阄" : "jiu", + "揪" : "jiu", + "啾" : "jiu", + "九" : "jiu", + "久" : "jiu", + "玖" : "jiu", + "灸" : "jiu", + "韭" : "jiu", + "酒" : "jiu", + "旧" : "jiu", + "臼" : "jiu", + "咎" : "jiu", + "柩" : "jiu", + "救" : "jiu", + "厩" : "jiu", + "就" : "jiu", + "舅" : "jiu", + "鹫" : "jiu", + "军" : "jun", + "均" : "jun", + "君" : "jun", + "钧" : "jun", + "菌" : "jun", + "皲" : "jun", + "俊" : "jun", + "郡" : "jun", + "峻" : "jun", + "骏" : "jun", + "竣" : "jun", + "拘" : "ju", + "狙" : "ju", + "居" : "ju", + "驹" : "ju", + "掬" : "ju", + "雎" : "ju", + "鞠" : "ju", + "局" : "ju", + "菊" : "ju", + "焗" : "ju", + "橘" : "ju", + "咀" : "ju", + "沮" : "ju", + "矩" : "ju", + "举" : "ju", + "龃" : "ju", + "巨" : "ju", + "拒" : "ju", + "具" : "ju", + "炬" : "ju", + "俱" : "ju", + "剧" : "ju", + "据" : "ju", + "距" : "ju", + "惧" : "ju", + "飓" : "ju", + "锯" : "ju", + "聚" : "ju", + "踞" : "ju", + "捐" : "juan", + "涓" : "juan", + "娟" : "juan", + "鹃" : "juan", + "卷" : "juan", + "倦" : "juan", + "绢" : "juan", + "眷" : "juan", + "隽" : "juan", + "撅" : "jue", + "噘" : "jue", + "决" : "jue", + "诀" : "jue", + "抉" : "jue", + "绝" : "jue", + "掘" : "jue", + "崛" : "jue", + "厥" : "jue", + "谲" : "jue", + "蕨" : "jue", + "爵" : "jue", + "蹶" : "jue", + "矍" : "jue", + "倔" : "jue", + "咔" : "ka", + "开" : "kai", + "揩" : "kai", + "凯" : "kai", + "铠" : "kai", + "慨" : "kai", + "楷" : "kai", + "忾" : "kai", + "刊" : "kan", + "勘" : "kan", + "龛" : "kan", + "堪" : "kan", + "坎" : "kan", + "侃" : "kan", + "砍" : "kan", + "槛" : "kan", + "看" : "kan", + "瞰" : "kan", + "康" : "kang", + "慷" : "kang", + "糠" : "kang", + "亢" : "kang", + "伉" : "kang", + "抗" : "kang", + "炕" : "kang", + "考" : "kao", + "拷" : "kao", + "烤" : "kao", + "铐" : "kao", + "犒" : "kao", + "靠" : "kao", + "苛" : "ke", + "轲" : "ke", + "科" : "ke", + "棵" : "ke", + "搕" : "ke", + "嗑" : "ke", + "稞" : "ke", + "窠" : "ke", + "颗" : "ke", + "磕" : "ke", + "瞌" : "ke", + "蝌" : "ke", + "可" : "ke", + "坷" : "ke", + "渴" : "ke", + "克" : "ke", + "刻" : "ke", + "恪" : "ke", + "客" : "ke", + "课" : "ke", + "肯" : "ken", + "垦" : "ken", + "恳" : "ken", + "啃" : "ken", + "坑" : "keng", + "铿" : "keng", + "空" : "kong", + "孔" : "kong", + "恐" : "kong", + "控" : "kong", + "抠" : "kou", + "口" : "kou", + "叩" : "kou", + "扣" : "kou", + "寇" : "kou", + "蔻" : "kou", + "枯" : "ku", + "哭" : "ku", + "窟" : "ku", + "骷" : "ku", + "苦" : "ku", + "库" : "ku", + "绔" : "ku", + "裤" : "ku", + "酷" : "ku", + "夸" : "kua", + "垮" : "kua", + "挎" : "kua", + "胯" : "kua", + "跨" : "kua", + "块" : "kuai", + "快" : "kuai", + "侩" : "kuai", + "脍" : "kuai", + "筷" : "kuai", + "宽" : "kuan", + "髋" : "kuan", + "款" : "kuan", + "诓" : "kuang", + "哐" : "kuang", + "筐" : "kuang", + "狂" : "kuang", + "诳" : "kuang", + "旷" : "kuang", + "况" : "kuang", + "矿" : "kuang", + "框" : "kuang", + "眶" : "kuang", + "亏" : "kui", + "盔" : "kui", + "窥" : "kui", + "葵" : "kui", + "魁" : "kui", + "傀" : "kui", + "匮" : "kui", + "馈" : "kui", + "愧" : "kui", + "坤" : "kun", + "昆" : "kun", + "鲲" : "kun", + "捆" : "kun", + "困" : "kun", + "扩" : "kuo", + "括" : "kuo", + "阔" : "kuo", + "廓" : "kuo", + "垃" : "la", + "拉" : "la", + "啦" : "la", + "邋" : "la", + "旯" : "la", + "喇" : "la", + "腊" : "la", + "蜡" : "la", + "辣" : "la", + "来" : "lai", + "莱" : "lai", + "徕" : "lai", + "睐" : "lai", + "赖" : "lai", + "癞" : "lai", + "籁" : "lai", + "兰" : "lan", + "岚" : "lan", + "拦" : "lan", + "栏" : "lan", + "婪" : "lan", + "阑" : "lan", + "蓝" : "lan", + "澜" : "lan", + "褴" : "lan", + "篮" : "lan", + "览" : "lan", + "揽" : "lan", + "缆" : "lan", + "榄" : "lan", + "懒" : "lan", + "烂" : "lan", + "滥" : "lan", + "啷" : "lang", + "郎" : "lang", + "狼" : "lang", + "琅" : "lang", + "廊" : "lang", + "榔" : "lang", + "锒" : "lang", + "螂" : "lang", + "朗" : "lang", + "浪" : "lang", + "捞" : "lao", + "劳" : "lao", + "牢" : "lao", + "崂" : "lao", + "老" : "lao", + "佬" : "lao", + "姥" : "lao", + "唠" : "lao", + "烙" : "lao", + "涝" : "lao", + "酪" : "lao", + "雷" : "lei", + "羸" : "lei", + "垒" : "lei", + "磊" : "lei", + "蕾" : "lei", + "儡" : "lei", + "肋" : "lei", + "泪" : "lei", + "类" : "lei", + "累" : "lei", + "擂" : "lei", + "嘞" : "lei", + "棱" : "leng", + "楞" : "leng", + "冷" : "leng", + "睖" : "leng", + "厘" : "li", + "狸" : "li", + "离" : "li", + "梨" : "li", + "犁" : "li", + "鹂" : "li", + "喱" : "li", + "蜊" : "li", + "漓" : "li", + "璃" : "li", + "黎" : "li", + "罹" : "li", + "篱" : "li", + "蠡" : "li", + "礼" : "li", + "李" : "li", + "里" : "li", + "俚" : "li", + "逦" : "li", + "哩" : "li", + "娌" : "li", + "理" : "li", + "鲤" : "li", + "力" : "li", + "历" : "li", + "厉" : "li", + "立" : "li", + "吏" : "li", + "丽" : "li", + "励" : "li", + "呖" : "li", + "利" : "li", + "沥" : "li", + "枥" : "li", + "例" : "li", + "戾" : "li", + "隶" : "li", + "荔" : "li", + "俐" : "li", + "莉" : "li", + "莅" : "li", + "栗" : "li", + "砾" : "li", + "蛎" : "li", + "唳" : "li", + "笠" : "li", + "粒" : "li", + "雳" : "li", + "痢" : "li", + "连" : "lian", + "怜" : "lian", + "帘" : "lian", + "莲" : "lian", + "涟" : "lian", + "联" : "lian", + "廉" : "lian", + "鲢" : "lian", + "镰" : "lian", + "敛" : "lian", + "脸" : "lian", + "练" : "lian", + "炼" : "lian", + "恋" : "lian", + "殓" : "lian", + "链" : "lian", + "良" : "liang", + "凉" : "liang", + "梁" : "liang", + "粮" : "liang", + "粱" : "liang", + "两" : "liang", + "魉" : "liang", + "亮" : "liang", + "谅" : "liang", + "辆" : "liang", + "靓" : "liang", + "量" : "liang", + "晾" : "liang", + "踉" : "liang", + "辽" : "liao", + "疗" : "liao", + "聊" : "liao", + "僚" : "liao", + "寥" : "liao", + "撩" : "liao", + "嘹" : "liao", + "獠" : "liao", + "潦" : "liao", + "缭" : "liao", + "燎" : "liao", + "料" : "liao", + "撂" : "liao", + "瞭" : "liao", + "镣" : "liao", + "咧" : "lie", + "列" : "lie", + "劣" : "lie", + "冽" : "lie", + "烈" : "lie", + "猎" : "lie", + "裂" : "lie", + "趔" : "lie", + "拎" : "lin", + "邻" : "lin", + "林" : "lin", + "临" : "lin", + "淋" : "lin", + "琳" : "lin", + "粼" : "lin", + "嶙" : "lin", + "潾" : "lin", + "霖" : "lin", + "磷" : "lin", + "鳞" : "lin", + "麟" : "lin", + "凛" : "lin", + "檩" : "lin", + "吝" : "lin", + "赁" : "lin", + "躏" : "lin", + "伶" : "ling", + "灵" : "ling", + "苓" : "ling", + "囹" : "ling", + "泠" : "ling", + "玲" : "ling", + "瓴" : "ling", + "铃" : "ling", + "凌" : "ling", + "陵" : "ling", + "聆" : "ling", + "菱" : "ling", + "棂" : "ling", + "蛉" : "ling", + "翎" : "ling", + "羚" : "ling", + "绫" : "ling", + "零" : "ling", + "龄" : "ling", + "岭" : "ling", + "领" : "ling", + "另" : "ling", + "令" : "ling", + "溜" : "liu", + "熘" : "liu", + "刘" : "liu", + "浏" : "liu", + "留" : "liu", + "流" : "liu", + "琉" : "liu", + "硫" : "liu", + "馏" : "liu", + "榴" : "liu", + "瘤" : "liu", + "柳" : "liu", + "绺" : "liu", + "六" : "liu", + "遛" : "liu", + "龙" : "long", + "咙" : "long", + "珑" : "long", + "胧" : "long", + "聋" : "long", + "笼" : "long", + "隆" : "long", + "窿" : "long", + "陇" : "long", + "拢" : "long", + "垄" : "long", + "娄" : "lou", + "楼" : "lou", + "髅" : "lou", + "搂" : "lou", + "篓" : "lou", + "陋" : "lou", + "镂" : "lou", + "漏" : "lou", + "喽" : "lou", + "撸" : "lu", + "卢" : "lu", + "芦" : "lu", + "庐" : "lu", + "炉" : "lu", + "泸" : "lu", + "鸬" : "lu", + "颅" : "lu", + "鲈" : "lu", + "卤" : "lu", + "虏" : "lu", + "掳" : "lu", + "鲁" : "lu", + "橹" : "lu", + "录" : "lu", + "赂" : "lu", + "鹿" : "lu", + "禄" : "lu", + "路" : "lu", + "箓" : "lu", + "漉" : "lu", + "戮" : "lu", + "鹭" : "lu", + "麓" : "lu", + "峦" : "luan", + "孪" : "luan", + "挛" : "luan", + "鸾" : "luan", + "卵" : "luan", + "乱" : "luan", + "抡" : "lun", + "仑" : "lun", + "伦" : "lun", + "囵" : "lun", + "沦" : "lun", + "轮" : "lun", + "论" : "lun", + "啰" : "luo", + "罗" : "luo", + "萝" : "luo", + "逻" : "luo", + "锣" : "luo", + "箩" : "luo", + "骡" : "luo", + "螺" : "luo", + "裸" : "luo", + "洛" : "luo", + "络" : "luo", + "骆" : "luo", + "摞" : "luo", + "漯" : "luo", + "驴" : "lv", + "榈" : "lv", + "吕" : "lv", + "侣" : "lv", + "旅" : "lv", + "铝" : "lv", + "屡" : "lv", + "缕" : "lv", + "膂" : "lv", + "褛" : "lv", + "履" : "lv", + "律" : "lv", + "虑" : "lv", + "氯" : "lv", + "滤" : "lv", + "掠" : "lve", + "略" : "lve", + "妈" : "ma", + "麻" : "ma", + "蟆" : "ma", + "马" : "ma", + "犸" : "ma", + "玛" : "ma", + "码" : "ma", + "蚂" : "ma", + "骂" : "ma", + "吗" : "ma", + "嘛" : "ma", + "霾" : "mai", + "买" : "mai", + "迈" : "mai", + "麦" : "mai", + "卖" : "mai", + "霡" : "mai", + "蛮" : "man", + "馒" : "man", + "瞒" : "man", + "满" : "man", + "曼" : "man", + "谩" : "man", + "幔" : "man", + "漫" : "man", + "慢" : "man", + "牤" : "mang", + "芒" : "mang", + "忙" : "mang", + "盲" : "mang", + "氓" : "mang", + "茫" : "mang", + "莽" : "mang", + "漭" : "mang", + "蟒" : "mang", + "猫" : "mao", + "毛" : "mao", + "矛" : "mao", + "茅" : "mao", + "牦" : "mao", + "锚" : "mao", + "髦" : "mao", + "蝥" : "mao", + "蟊" : "mao", + "冇" : "mao", + "卯" : "mao", + "铆" : "mao", + "茂" : "mao", + "冒" : "mao", + "贸" : "mao", + "袤" : "mao", + "帽" : "mao", + "貌" : "mao", + "玫" : "mei", + "枚" : "mei", + "眉" : "mei", + "莓" : "mei", + "梅" : "mei", + "媒" : "mei", + "楣" : "mei", + "煤" : "mei", + "酶" : "mei", + "霉" : "mei", + "每" : "mei", + "美" : "mei", + "镁" : "mei", + "妹" : "mei", + "昧" : "mei", + "袂" : "mei", + "寐" : "mei", + "媚" : "mei", + "魅" : "mei", + "门" : "men", + "扪" : "men", + "闷" : "men", + "焖" : "men", + "懑" : "men", + "们" : "men", + "虻" : "meng", + "萌" : "meng", + "蒙" : "meng", + "盟" : "meng", + "檬" : "meng", + "曚" : "meng", + "朦" : "meng", + "猛" : "meng", + "锰" : "meng", + "蜢" : "meng", + "懵" : "meng", + "孟" : "meng", + "梦" : "meng", + "咪" : "mi", + "眯" : "mi", + "弥" : "mi", + "迷" : "mi", + "猕" : "mi", + "谜" : "mi", + "醚" : "mi", + "糜" : "mi", + "麋" : "mi", + "靡" : "mi", + "米" : "mi", + "弭" : "mi", + "觅" : "mi", + "密" : "mi", + "幂" : "mi", + "谧" : "mi", + "蜜" : "mi", + "眠" : "mian", + "绵" : "mian", + "棉" : "mian", + "免" : "mian", + "勉" : "mian", + "娩" : "mian", + "冕" : "mian", + "渑" : "mian", + "湎" : "mian", + "缅" : "mian", + "腼" : "mian", + "面" : "mian", + "喵" : "miao", + "苗" : "miao", + "描" : "miao", + "瞄" : "miao", + "秒" : "miao", + "渺" : "miao", + "藐" : "miao", + "妙" : "miao", + "庙" : "miao", + "缥" : "miao", + "咩" : "mie", + "灭" : "mie", + "蔑" : "mie", + "篾" : "mie", + "乜" : "mie", + "民" : "min", + "皿" : "min", + "抿" : "min", + "泯" : "min", + "闽" : "min", + "悯" : "min", + "敏" : "min", + "名" : "ming", + "明" : "ming", + "鸣" : "ming", + "茗" : "ming", + "冥" : "ming", + "铭" : "ming", + "瞑" : "ming", + "螟" : "ming", + "酩" : "ming", + "命" : "ming", + "谬" : "miu", + "摸" : "mo", + "馍" : "mo", + "摹" : "mo", + "膜" : "mo", + "摩" : "mo", + "磨" : "mo", + "蘑" : "mo", + "魔" : "mo", + "末" : "mo", + "茉" : "mo", + "殁" : "mo", + "沫" : "mo", + "陌" : "mo", + "莫" : "mo", + "秣" : "mo", + "蓦" : "mo", + "漠" : "mo", + "寞" : "mo", + "墨" : "mo", + "默" : "mo", + "嬷" : "mo", + "缪" : "mou", + "哞" : "mou", + "眸" : "mou", + "谋" : "mou", + "某" : "mou", + "母" : "mu", + "牡" : "mu", + "亩" : "mu", + "拇" : "mu", + "姆" : "mu", + "木" : "mu", + "目" : "mu", + "沐" : "mu", + "苜" : "mu", + "牧" : "mu", + "钼" : "mu", + "募" : "mu", + "墓" : "mu", + "幕" : "mu", + "睦" : "mu", + "慕" : "mu", + "暮" : "mu", + "穆" : "mu", + "拿" : "na", + "呐" : "na", + "纳" : "na", + "钠" : "na", + "衲" : "na", + "捺" : "na", + "乃" : "nai", + "奶" : "nai", + "氖" : "nai", + "奈" : "nai", + "耐" : "nai", + "囡" : "nan", + "男" : "nan", + "南" : "nan", + "难" : "nan", + "喃" : "nan", + "楠" : "nan", + "赧" : "nan", + "腩" : "nan", + "囔" : "nang", + "囊" : "nang", + "孬" : "nao", + "呶" : "nao", + "挠" : "nao", + "恼" : "nao", + "脑" : "nao", + "瑙" : "nao", + "闹" : "nao", + "淖" : "nao", + "讷" : "ne", + "馁" : "nei", + "内" : "nei", + "嫩" : "nen", + "恁" : "nen", + "能" : "neng", + "嗯" : "ng", + "妮" : "ni", + "尼" : "ni", + "泥" : "ni", + "怩" : "ni", + "倪" : "ni", + "霓" : "ni", + "拟" : "ni", + "你" : "ni", + "旎" : "ni", + "昵" : "ni", + "逆" : "ni", + "匿" : "ni", + "腻" : "ni", + "溺" : "ni", + "拈" : "nian", + "蔫" : "nian", + "年" : "nian", + "黏" : "nian", + "捻" : "nian", + "辇" : "nian", + "撵" : "nian", + "碾" : "nian", + "廿" : "nian", + "念" : "nian", + "娘" : "niang", + "酿" : "niang", + "鸟" : "niao", + "袅" : "niao", + "尿" : "niao", + "捏" : "nie", + "聂" : "nie", + "涅" : "nie", + "嗫" : "nie", + "镊" : "nie", + "镍" : "nie", + "蹑" : "nie", + "孽" : "nie", + "您" : "nin", + "宁" : "ning", + "咛" : "ning", + "狞" : "ning", + "柠" : "ning", + "凝" : "ning", + "拧" : "ning", + "佞" : "ning", + "泞" : "ning", + "妞" : "niu", + "牛" : "niu", + "扭" : "niu", + "忸" : "niu", + "纽" : "niu", + "钮" : "niu", + "农" : "nong", + "哝" : "nong", + "浓" : "nong", + "脓" : "nong", + "弄" : "nong", + "奴" : "nu", + "驽" : "nu", + "努" : "nu", + "弩" : "nu", + "怒" : "nu", + "暖" : "nuan", + "疟" : "nue", + "虐" : "nue", + "挪" : "nuo", + "诺" : "nuo", + "喏" : "nuo", + "懦" : "nuo", + "糯" : "nuo", + "女" : "nv", + "噢" : "o", + "讴" : "ou", + "瓯" : "ou", + "欧" : "ou", + "殴" : "ou", + "鸥" : "ou", + "呕" : "ou", + "偶" : "ou", + "藕" : "ou", + "怄" : "ou", + "趴" : "pa", + "啪" : "pa", + "葩" : "pa", + "杷" : "pa", + "爬" : "pa", + "琶" : "pa", + "帕" : "pa", + "怕" : "pa", + "拍" : "pai", + "排" : "pai", + "徘" : "pai", + "牌" : "pai", + "哌" : "pai", + "派" : "pai", + "湃" : "pai", + "潘" : "pan", + "攀" : "pan", + "爿" : "pan", + "盘" : "pan", + "磐" : "pan", + "蹒" : "pan", + "蟠" : "pan", + "判" : "pan", + "盼" : "pan", + "叛" : "pan", + "畔" : "pan", + "乓" : "pang", + "滂" : "pang", + "庞" : "pang", + "旁" : "pang", + "螃" : "pang", + "耪" : "pang", + "抛" : "pao", + "咆" : "pao", + "庖" : "pao", + "袍" : "pao", + "跑" : "pao", + "泡" : "pao", + "呸" : "pei", + "胚" : "pei", + "陪" : "pei", + "培" : "pei", + "赔" : "pei", + "裴" : "pei", + "沛" : "pei", + "佩" : "pei", + "配" : "pei", + "喷" : "pen", + "盆" : "pen", + "抨" : "peng", + "怦" : "peng", + "砰" : "peng", + "烹" : "peng", + "嘭" : "peng", + "朋" : "peng", + "彭" : "peng", + "棚" : "peng", + "蓬" : "peng", + "硼" : "peng", + "鹏" : "peng", + "澎" : "peng", + "篷" : "peng", + "膨" : "peng", + "捧" : "peng", + "碰" : "peng", + "丕" : "pi", + "批" : "pi", + "纰" : "pi", + "坯" : "pi", + "披" : "pi", + "砒" : "pi", + "劈" : "pi", + "噼" : "pi", + "霹" : "pi", + "皮" : "pi", + "枇" : "pi", + "毗" : "pi", + "蚍" : "pi", + "疲" : "pi", + "啤" : "pi", + "琵" : "pi", + "脾" : "pi", + "貔" : "pi", + "匹" : "pi", + "痞" : "pi", + "癖" : "pi", + "屁" : "pi", + "睥" : "pi", + "媲" : "pi", + "僻" : "pi", + "譬" : "pi", + "偏" : "pian", + "篇" : "pian", + "翩" : "pian", + "骈" : "pian", + "蹁" : "pian", + "片" : "pian", + "骗" : "pian", + "剽" : "piao", + "漂" : "piao", + "飘" : "piao", + "瓢" : "piao", + "殍" : "piao", + "瞟" : "piao", + "票" : "piao", + "氕" : "pie", + "瞥" : "pie", + "撇" : "pie", + "拼" : "pin", + "姘" : "pin", + "贫" : "pin", + "频" : "pin", + "嫔" : "pin", + "颦" : "pin", + "品" : "pin", + "聘" : "pin", + "乒" : "ping", + "娉" : "ping", + "平" : "ping", + "评" : "ping", + "坪" : "ping", + "苹" : "ping", + "凭" : "ping", + "瓶" : "ping", + "萍" : "ping", + "钋" : "po", + "坡" : "po", + "泼" : "po", + "颇" : "po", + "婆" : "po", + "鄱" : "po", + "叵" : "po", + "珀" : "po", + "破" : "po", + "粕" : "po", + "魄" : "po", + "剖" : "pou", + "抔" : "pou", + "扑" : "pu", + "铺" : "pu", + "噗" : "pu", + "仆" : "pu", + "匍" : "pu", + "菩" : "pu", + "葡" : "pu", + "蒲" : "pu", + "璞" : "pu", + "圃" : "pu", + "浦" : "pu", + "普" : "pu", + "谱" : "pu", + "蹼" : "pu", + "七" : "qi", + "沏" : "qi", + "妻" : "qi", + "柒" : "qi", + "凄" : "qi", + "萋" : "qi", + "戚" : "qi", + "期" : "qi", + "欺" : "qi", + "嘁" : "qi", + "漆" : "qi", + "齐" : "qi", + "芪" : "qi", + "其" : "qi", + "歧" : "qi", + "祈" : "qi", + "祇" : "qi", + "脐" : "qi", + "畦" : "qi", + "跂" : "qi", + "崎" : "qi", + "骑" : "qi", + "琪" : "qi", + "棋" : "qi", + "旗" : "qi", + "鳍" : "qi", + "麒" : "qi", + "乞" : "qi", + "岂" : "qi", + "企" : "qi", + "杞" : "qi", + "启" : "qi", + "起" : "qi", + "绮" : "qi", + "气" : "qi", + "讫" : "qi", + "迄" : "qi", + "弃" : "qi", + "汽" : "qi", + "泣" : "qi", + "契" : "qi", + "砌" : "qi", + "葺" : "qi", + "器" : "qi", + "憩" : "qi", + "俟" : "qi", + "掐" : "qia", + "洽" : "qia", + "恰" : "qia", + "千" : "qian", + "仟" : "qian", + "阡" : "qian", + "芊" : "qian", + "迁" : "qian", + "钎" : "qian", + "牵" : "qian", + "悭" : "qian", + "谦" : "qian", + "签" : "qian", + "愆" : "qian", + "前" : "qian", + "虔" : "qian", + "钱" : "qian", + "钳" : "qian", + "乾" : "qian", + "潜" : "qian", + "黔" : "qian", + "遣" : "qian", + "谴" : "qian", + "欠" : "qian", + "芡" : "qian", + "倩" : "qian", + "堑" : "qian", + "嵌" : "qian", + "歉" : "qian", + "羌" : "qiang", + "枪" : "qiang", + "戕" : "qiang", + "腔" : "qiang", + "蜣" : "qiang", + "锵" : "qiang", + "墙" : "qiang", + "蔷" : "qiang", + "抢" : "qiang", + "羟" : "qiang", + "襁" : "qiang", + "呛" : "qiang", + "炝" : "qiang", + "跄" : "qiang", + "悄" : "qiao", + "跷" : "qiao", + "锹" : "qiao", + "敲" : "qiao", + "橇" : "qiao", + "乔" : "qiao", + "侨" : "qiao", + "荞" : "qiao", + "桥" : "qiao", + "憔" : "qiao", + "瞧" : "qiao", + "巧" : "qiao", + "俏" : "qiao", + "诮" : "qiao", + "峭" : "qiao", + "窍" : "qiao", + "翘" : "qiao", + "撬" : "qiao", + "切" : "qie", + "且" : "qie", + "妾" : "qie", + "怯" : "qie", + "窃" : "qie", + "挈" : "qie", + "惬" : "qie", + "趄" : "qie", + "锲" : "qie", + "钦" : "qin", + "侵" : "qin", + "衾" : "qin", + "芹" : "qin", + "芩" : "qin", + "秦" : "qin", + "琴" : "qin", + "禽" : "qin", + "勤" : "qin", + "擒" : "qin", + "噙" : "qin", + "寝" : "qin", + "沁" : "qin", + "青" : "qing", + "轻" : "qing", + "氢" : "qing", + "倾" : "qing", + "卿" : "qing", + "清" : "qing", + "蜻" : "qing", + "情" : "qing", + "晴" : "qing", + "氰" : "qing", + "擎" : "qing", + "顷" : "qing", + "请" : "qing", + "庆" : "qing", + "罄" : "qing", + "穷" : "qiong", + "穹" : "qiong", + "琼" : "qiong", + "丘" : "qiu", + "秋" : "qiu", + "蚯" : "qiu", + "鳅" : "qiu", + "囚" : "qiu", + "求" : "qiu", + "虬" : "qiu", + "泅" : "qiu", + "酋" : "qiu", + "球" : "qiu", + "遒" : "qiu", + "裘" : "qiu", + "岖" : "qu", + "驱" : "qu", + "屈" : "qu", + "蛆" : "qu", + "躯" : "qu", + "趋" : "qu", + "蛐" : "qu", + "黢" : "qu", + "渠" : "qu", + "瞿" : "qu", + "曲" : "qu", + "取" : "qu", + "娶" : "qu", + "龋" : "qu", + "去" : "qu", + "趣" : "qu", + "觑" : "qu", + "悛" : "quan", + "权" : "quan", + "全" : "quan", + "诠" : "quan", + "泉" : "quan", + "拳" : "quan", + "痊" : "quan", + "蜷" : "quan", + "醛" : "quan", + "犬" : "quan", + "劝" : "quan", + "券" : "quan", + "炔" : "que", + "缺" : "que", + "瘸" : "que", + "却" : "que", + "确" : "que", + "鹊" : "que", + "阙" : "que", + "榷" : "que", + "逡" : "qun", + "裙" : "qun", + "群" : "qun", + "蚺" : "ran", + "然" : "ran", + "燃" : "ran", + "冉" : "ran", + "苒" : "ran", + "染" : "ran", + "瓤" : "rang", + "壤" : "rang", + "攘" : "rang", + "嚷" : "rang", + "让" : "rang", + "荛" : "rao", + "饶" : "rao", + "娆" : "rao", + "桡" : "rao", + "扰" : "rao", + "绕" : "rao", + "惹" : "re", + "热" : "re", + "人" : "ren", + "壬" : "ren", + "仁" : "ren", + "忍" : "ren", + "荏" : "ren", + "稔" : "ren", + "刃" : "ren", + "认" : "ren", + "任" : "ren", + "纫" : "ren", + "韧" : "ren", + "饪" : "ren", + "扔" : "reng", + "仍" : "reng", + "日" : "ri", + "戎" : "rong", + "茸" : "rong", + "荣" : "rong", + "绒" : "rong", + "容" : "rong", + "嵘" : "rong", + "蓉" : "rong", + "溶" : "rong", + "榕" : "rong", + "熔" : "rong", + "融" : "rong", + "冗" : "rong", + "氄" : "rong", + "柔" : "rou", + "揉" : "rou", + "糅" : "rou", + "蹂" : "rou", + "鞣" : "rou", + "肉" : "rou", + "如" : "ru", + "茹" : "ru", + "铷" : "ru", + "儒" : "ru", + "孺" : "ru", + "蠕" : "ru", + "汝" : "ru", + "乳" : "ru", + "辱" : "ru", + "入" : "ru", + "缛" : "ru", + "褥" : "ru", + "阮" : "ruan", + "软" : "ruan", + "蕊" : "rui", + "蚋" : "rui", + "锐" : "rui", + "瑞" : "rui", + "睿" : "rui", + "闰" : "run", + "润" : "run", + "若" : "ruo", + "偌" : "ruo", + "弱" : "ruo", + "仨" : "sa", + "洒" : "sa", + "撒" : "sa", + "卅" : "sa", + "飒" : "sa", + "萨" : "sa", + "腮" : "sai", + "赛" : "sai", + "三" : "san", + "叁" : "san", + "伞" : "san", + "散" : "san", + "桑" : "sang", + "搡" : "sang", + "嗓" : "sang", + "丧" : "sang", + "搔" : "sao", + "骚" : "sao", + "扫" : "sao", + "嫂" : "sao", + "臊" : "sao", + "涩" : "se", + "啬" : "se", + "铯" : "se", + "瑟" : "se", + "穑" : "se", + "森" : "sen", + "僧" : "seng", + "杀" : "sha", + "沙" : "sha", + "纱" : "sha", + "砂" : "sha", + "啥" : "sha", + "傻" : "sha", + "厦" : "sha", + "歃" : "sha", + "煞" : "sha", + "霎" : "sha", + "筛" : "shai", + "晒" : "shai", + "山" : "shan", + "删" : "shan", + "苫" : "shan", + "衫" : "shan", + "姗" : "shan", + "珊" : "shan", + "煽" : "shan", + "潸" : "shan", + "膻" : "shan", + "闪" : "shan", + "陕" : "shan", + "讪" : "shan", + "汕" : "shan", + "扇" : "shan", + "善" : "shan", + "骟" : "shan", + "缮" : "shan", + "擅" : "shan", + "膳" : "shan", + "嬗" : "shan", + "赡" : "shan", + "鳝" : "shan", + "伤" : "shang", + "殇" : "shang", + "商" : "shang", + "觞" : "shang", + "熵" : "shang", + "晌" : "shang", + "赏" : "shang", + "上" : "shang", + "尚" : "shang", + "捎" : "shao", + "烧" : "shao", + "梢" : "shao", + "稍" : "shao", + "艄" : "shao", + "勺" : "shao", + "芍" : "shao", + "韶" : "shao", + "少" : "shao", + "邵" : "shao", + "绍" : "shao", + "哨" : "shao", + "潲" : "shao", + "奢" : "she", + "赊" : "she", + "舌" : "she", + "佘" : "she", + "蛇" : "she", + "舍" : "she", + "设" : "she", + "社" : "she", + "射" : "she", + "涉" : "she", + "赦" : "she", + "摄" : "she", + "慑" : "she", + "麝" : "she", + "申" : "shen", + "伸" : "shen", + "身" : "shen", + "呻" : "shen", + "绅" : "shen", + "砷" : "shen", + "深" : "shen", + "神" : "shen", + "沈" : "shen", + "审" : "shen", + "哂" : "shen", + "婶" : "shen", + "肾" : "shen", + "甚" : "shen", + "渗" : "shen", + "葚" : "shen", + "蜃" : "shen", + "慎" : "shen", + "升" : "sheng", + "生" : "sheng", + "声" : "sheng", + "昇" : "sheng", + "牲" : "sheng", + "笙" : "sheng", + "甥" : "sheng", + "绳" : "sheng", + "圣" : "sheng", + "胜" : "sheng", + "晟" : "sheng", + "剩" : "sheng", + "尸" : "shi", + "失" : "shi", + "师" : "shi", + "诗" : "shi", + "虱" : "shi", + "狮" : "shi", + "施" : "shi", + "湿" : "shi", + "十" : "shi", + "时" : "shi", + "实" : "shi", + "食" : "shi", + "蚀" : "shi", + "史" : "shi", + "矢" : "shi", + "使" : "shi", + "始" : "shi", + "驶" : "shi", + "屎" : "shi", + "士" : "shi", + "氏" : "shi", + "示" : "shi", + "世" : "shi", + "仕" : "shi", + "市" : "shi", + "式" : "shi", + "势" : "shi", + "事" : "shi", + "侍" : "shi", + "饰" : "shi", + "试" : "shi", + "视" : "shi", + "拭" : "shi", + "柿" : "shi", + "是" : "shi", + "适" : "shi", + "恃" : "shi", + "室" : "shi", + "逝" : "shi", + "轼" : "shi", + "舐" : "shi", + "弑" : "shi", + "释" : "shi", + "谥" : "shi", + "嗜" : "shi", + "誓" : "shi", + "收" : "shou", + "手" : "shou", + "守" : "shou", + "首" : "shou", + "寿" : "shou", + "受" : "shou", + "狩" : "shou", + "授" : "shou", + "售" : "shou", + "兽" : "shou", + "绶" : "shou", + "瘦" : "shou", + "殳" : "shu", + "书" : "shu", + "抒" : "shu", + "枢" : "shu", + "叔" : "shu", + "姝" : "shu", + "殊" : "shu", + "倏" : "shu", + "梳" : "shu", + "淑" : "shu", + "舒" : "shu", + "疏" : "shu", + "输" : "shu", + "蔬" : "shu", + "秫" : "shu", + "孰" : "shu", + "赎" : "shu", + "塾" : "shu", + "暑" : "shu", + "黍" : "shu", + "署" : "shu", + "蜀" : "shu", + "鼠" : "shu", + "薯" : "shu", + "曙" : "shu", + "戍" : "shu", + "束" : "shu", + "述" : "shu", + "树" : "shu", + "竖" : "shu", + "恕" : "shu", + "庶" : "shu", + "墅" : "shu", + "漱" : "shu", + "刷" : "shua", + "唰" : "shua", + "耍" : "shua", + "衰" : "shuai", + "摔" : "shuai", + "甩" : "shuai", + "帅" : "shuai", + "蟀" : "shuai", + "闩" : "shuan", + "拴" : "shuan", + "栓" : "shuan", + "涮" : "shuan", + "双" : "shuang", + "霜" : "shuang", + "孀" : "shuang", + "爽" : "shuang", + "谁" : "shui", + "水" : "shui", + "税" : "shui", + "睡" : "shui", + "吮" : "shun", + "顺" : "shun", + "舜" : "shun", + "瞬" : "shun", + "烁" : "shuo", + "铄" : "shuo", + "朔" : "shuo", + "硕" : "shuo", + "司" : "si", + "丝" : "si", + "私" : "si", + "咝" : "si", + "思" : "si", + "斯" : "si", + "厮" : "si", + "撕" : "si", + "嘶" : "si", + "死" : "si", + "巳" : "si", + "四" : "si", + "寺" : "si", + "祀" : "si", + "饲" : "si", + "肆" : "si", + "嗣" : "si", + "松" : "song", + "嵩" : "song", + "怂" : "song", + "耸" : "song", + "悚" : "song", + "讼" : "song", + "宋" : "song", + "送" : "song", + "诵" : "song", + "颂" : "song", + "搜" : "sou", + "嗖" : "sou", + "馊" : "sou", + "艘" : "sou", + "叟" : "sou", + "擞" : "sou", + "嗽" : "sou", + "苏" : "su", + "酥" : "su", + "俗" : "su", + "夙" : "su", + "诉" : "su", + "肃" : "su", + "素" : "su", + "速" : "su", + "粟" : "su", + "嗉" : "su", + "塑" : "su", + "溯" : "su", + "簌" : "su", + "酸" : "suan", + "蒜" : "suan", + "算" : "suan", + "虽" : "sui", + "睢" : "sui", + "绥" : "sui", + "隋" : "sui", + "随" : "sui", + "髓" : "sui", + "岁" : "sui", + "祟" : "sui", + "遂" : "sui", + "碎" : "sui", + "隧" : "sui", + "穗" : "sui", + "孙" : "sun", + "损" : "sun", + "笋" : "sun", + "隼" : "sun", + "唆" : "suo", + "梭" : "suo", + "蓑" : "suo", + "羧" : "suo", + "缩" : "suo", + "所" : "suo", + "索" : "suo", + "唢" : "suo", + "琐" : "suo", + "锁" : "suo", + "他" : "ta", + "它" : "ta", + "她" : "ta", + "铊" : "ta", + "塌" : "ta", + "塔" : "ta", + "獭" : "ta", + "挞" : "ta", + "榻" : "ta", + "踏" : "ta", + "蹋" : "ta", + "胎" : "tai", + "台" : "tai", + "邰" : "tai", + "抬" : "tai", + "苔" : "tai", + "跆" : "tai", + "太" : "tai", + "汰" : "tai", + "态" : "tai", + "钛" : "tai", + "泰" : "tai", + "酞" : "tai", + "贪" : "tan", + "摊" : "tan", + "滩" : "tan", + "瘫" : "tan", + "坛" : "tan", + "昙" : "tan", + "谈" : "tan", + "痰" : "tan", + "谭" : "tan", + "潭" : "tan", + "檀" : "tan", + "坦" : "tan", + "袒" : "tan", + "毯" : "tan", + "叹" : "tan", + "炭" : "tan", + "探" : "tan", + "碳" : "tan", + "汤" : "tang", + "嘡" : "tang", + "羰" : "tang", + "唐" : "tang", + "堂" : "tang", + "棠" : "tang", + "塘" : "tang", + "搪" : "tang", + "膛" : "tang", + "镗" : "tang", + "糖" : "tang", + "螳" : "tang", + "倘" : "tang", + "淌" : "tang", + "躺" : "tang", + "烫" : "tang", + "趟" : "tang", + "涛" : "tao", + "绦" : "tao", + "掏" : "tao", + "滔" : "tao", + "韬" : "tao", + "饕" : "tao", + "逃" : "tao", + "桃" : "tao", + "陶" : "tao", + "萄" : "tao", + "淘" : "tao", + "讨" : "tao", + "套" : "tao", + "特" : "te", + "疼" : "teng", + "腾" : "teng", + "誊" : "teng", + "滕" : "teng", + "藤" : "teng", + "剔" : "ti", + "梯" : "ti", + "踢" : "ti", + "啼" : "ti", + "题" : "ti", + "醍" : "ti", + "蹄" : "ti", + "体" : "ti", + "屉" : "ti", + "剃" : "ti", + "涕" : "ti", + "悌" : "ti", + "惕" : "ti", + "替" : "ti", + "天" : "tian", + "添" : "tian", + "田" : "tian", + "恬" : "tian", + "甜" : "tian", + "填" : "tian", + "忝" : "tian", + "殄" : "tian", + "舔" : "tian", + "掭" : "tian", + "佻" : "tiao", + "挑" : "tiao", + "条" : "tiao", + "迢" : "tiao", + "笤" : "tiao", + "髫" : "tiao", + "窕" : "tiao", + "眺" : "tiao", + "粜" : "tiao", + "跳" : "tiao", + "帖" : "tie", + "贴" : "tie", + "铁" : "tie", + "餮" : "tie", + "铤" : "ting", + "厅" : "ting", + "听" : "ting", + "烃" : "ting", + "廷" : "ting", + "亭" : "ting", + "庭" : "ting", + "停" : "ting", + "蜓" : "ting", + "婷" : "ting", + "霆" : "ting", + "挺" : "ting", + "艇" : "ting", + "通" : "tong", + "嗵" : "tong", + "同" : "tong", + "彤" : "tong", + "桐" : "tong", + "铜" : "tong", + "童" : "tong", + "潼" : "tong", + "瞳" : "tong", + "统" : "tong", + "捅" : "tong", + "桶" : "tong", + "筒" : "tong", + "恸" : "tong", + "痛" : "tong", + "偷" : "tou", + "头" : "tou", + "投" : "tou", + "骰" : "tou", + "透" : "tou", + "凸" : "tu", + "秃" : "tu", + "突" : "tu", + "图" : "tu", + "荼" : "tu", + "徒" : "tu", + "途" : "tu", + "涂" : "tu", + "屠" : "tu", + "土" : "tu", + "吐" : "tu", + "兔" : "tu", + "菟" : "tu", + "湍" : "tuan", + "团" : "tuan", + "疃" : "tuan", + "彖" : "tuan", + "推" : "tui", + "颓" : "tui", + "腿" : "tui", + "退" : "tui", + "蜕" : "tui", + "褪" : "tui", + "吞" : "tun", + "屯" : "tun", + "饨" : "tun", + "豚" : "tun", + "臀" : "tun", + "托" : "tuo", + "拖" : "tuo", + "脱" : "tuo", + "佗" : "tuo", + "陀" : "tuo", + "驼" : "tuo", + "鸵" : "tuo", + "妥" : "tuo", + "椭" : "tuo", + "唾" : "tuo", + "挖" : "wa", + "哇" : "wa", + "洼" : "wa", + "娲" : "wa", + "蛙" : "wa", + "娃" : "wa", + "瓦" : "wa", + "佤" : "wa", + "袜" : "wa", + "歪" : "wai", + "外" : "wai", + "弯" : "wan", + "剜" : "wan", + "湾" : "wan", + "蜿" : "wan", + "豌" : "wan", + "丸" : "wan", + "纨" : "wan", + "完" : "wan", + "玩" : "wan", + "顽" : "wan", + "烷" : "wan", + "宛" : "wan", + "挽" : "wan", + "晚" : "wan", + "惋" : "wan", + "婉" : "wan", + "绾" : "wan", + "皖" : "wan", + "碗" : "wan", + "万" : "wan", + "腕" : "wan", + "汪" : "wang", + "亡" : "wang", + "王" : "wang", + "网" : "wang", + "枉" : "wang", + "罔" : "wang", + "往" : "wang", + "惘" : "wang", + "妄" : "wang", + "忘" : "wang", + "旺" : "wang", + "望" : "wang", + "危" : "wei", + "威" : "wei", + "偎" : "wei", + "微" : "wei", + "煨" : "wei", + "薇" : "wei", + "巍" : "wei", + "韦" : "wei", + "为" : "wei", + "违" : "wei", + "围" : "wei", + "闱" : "wei", + "桅" : "wei", + "唯" : "wei", + "帷" : "wei", + "维" : "wei", + "伟" : "wei", + "伪" : "wei", + "苇" : "wei", + "纬" : "wei", + "委" : "wei", + "诿" : "wei", + "娓" : "wei", + "萎" : "wei", + "猥" : "wei", + "痿" : "wei", + "卫" : "wei", + "未" : "wei", + "位" : "wei", + "味" : "wei", + "畏" : "wei", + "胃" : "wei", + "谓" : "wei", + "喂" : "wei", + "猬" : "wei", + "渭" : "wei", + "蔚" : "wei", + "慰" : "wei", + "魏" : "wei", + "温" : "wen", + "瘟" : "wen", + "文" : "wen", + "纹" : "wen", + "闻" : "wen", + "蚊" : "wen", + "雯" : "wen", + "刎" : "wen", + "吻" : "wen", + "紊" : "wen", + "稳" : "wen", + "问" : "wen", + "汶" : "wen", + "翁" : "weng", + "嗡" : "weng", + "瓮" : "weng", + "挝" : "wo", + "莴" : "wo", + "倭" : "wo", + "喔" : "wo", + "窝" : "wo", + "蜗" : "wo", + "我" : "wo", + "肟" : "wo", + "沃" : "wo", + "卧" : "wo", + "握" : "wo", + "幄" : "wo", + "斡" : "wo", + "乌" : "wu", + "邬" : "wu", + "污" : "wu", + "巫" : "wu", + "呜" : "wu", + "钨" : "wu", + "诬" : "wu", + "屋" : "wu", + "无" : "wu", + "毋" : "wu", + "芜" : "wu", + "吴" : "wu", + "梧" : "wu", + "蜈" : "wu", + "五" : "wu", + "午" : "wu", + "伍" : "wu", + "仵" : "wu", + "怃" : "wu", + "忤" : "wu", + "妩" : "wu", + "武" : "wu", + "侮" : "wu", + "捂" : "wu", + "鹉" : "wu", + "舞" : "wu", + "兀" : "wu", + "勿" : "wu", + "戊" : "wu", + "务" : "wu", + "坞" : "wu", + "物" : "wu", + "误" : "wu", + "悟" : "wu", + "晤" : "wu", + "骛" : "wu", + "雾" : "wu", + "寤" : "wu", + "鹜" : "wu", + "夕" : "xi", + "兮" : "xi", + "西" : "xi", + "吸" : "xi", + "汐" : "xi", + "希" : "xi", + "昔" : "xi", + "析" : "xi", + "唏" : "xi", + "牺" : "xi", + "息" : "xi", + "奚" : "xi", + "悉" : "xi", + "烯" : "xi", + "惜" : "xi", + "晰" : "xi", + "稀" : "xi", + "翕" : "xi", + "犀" : "xi", + "皙" : "xi", + "锡" : "xi", + "溪" : "xi", + "熙" : "xi", + "蜥" : "xi", + "熄" : "xi", + "嘻" : "xi", + "膝" : "xi", + "嬉" : "xi", + "羲" : "xi", + "蟋" : "xi", + "曦" : "xi", + "习" : "xi", + "席" : "xi", + "袭" : "xi", + "媳" : "xi", + "洗" : "xi", + "玺" : "xi", + "徙" : "xi", + "喜" : "xi", + "禧" : "xi", + "戏" : "xi", + "细" : "xi", + "隙" : "xi", + "呷" : "xia", + "虾" : "xia", + "瞎" : "xia", + "匣" : "xia", + "侠" : "xia", + "峡" : "xia", + "狭" : "xia", + "遐" : "xia", + "瑕" : "xia", + "暇" : "xia", + "辖" : "xia", + "霞" : "xia", + "黠" : "xia", + "下" : "xia", + "夏" : "xia", + "罅" : "xia", + "仙" : "xian", + "先" : "xian", + "氙" : "xian", + "掀" : "xian", + "酰" : "xian", + "锨" : "xian", + "鲜" : "xian", + "闲" : "xian", + "贤" : "xian", + "弦" : "xian", + "咸" : "xian", + "涎" : "xian", + "娴" : "xian", + "衔" : "xian", + "舷" : "xian", + "嫌" : "xian", + "显" : "xian", + "险" : "xian", + "跣" : "xian", + "藓" : "xian", + "苋" : "xian", + "县" : "xian", + "现" : "xian", + "限" : "xian", + "线" : "xian", + "宪" : "xian", + "陷" : "xian", + "馅" : "xian", + "羡" : "xian", + "献" : "xian", + "腺" : "xian", + "乡" : "xiang", + "相" : "xiang", + "香" : "xiang", + "厢" : "xiang", + "湘" : "xiang", + "箱" : "xiang", + "襄" : "xiang", + "镶" : "xiang", + "详" : "xiang", + "祥" : "xiang", + "翔" : "xiang", + "享" : "xiang", + "响" : "xiang", + "饷" : "xiang", + "飨" : "xiang", + "想" : "xiang", + "向" : "xiang", + "项" : "xiang", + "象" : "xiang", + "像" : "xiang", + "橡" : "xiang", + "肖" : "xiao", + "枭" : "xiao", + "哓" : "xiao", + "骁" : "xiao", + "逍" : "xiao", + "消" : "xiao", + "宵" : "xiao", + "萧" : "xiao", + "硝" : "xiao", + "销" : "xiao", + "箫" : "xiao", + "潇" : "xiao", + "霄" : "xiao", + "魈" : "xiao", + "嚣" : "xiao", + "崤" : "xiao", + "淆" : "xiao", + "小" : "xiao", + "晓" : "xiao", + "孝" : "xiao", + "哮" : "xiao", + "笑" : "xiao", + "效" : "xiao", + "啸" : "xiao", + "挟" : "xie", + "些" : "xie", + "楔" : "xie", + "歇" : "xie", + "蝎" : "xie", + "协" : "xie", + "胁" : "xie", + "偕" : "xie", + "斜" : "xie", + "谐" : "xie", + "揳" : "xie", + "携" : "xie", + "撷" : "xie", + "鞋" : "xie", + "写" : "xie", + "泄" : "xie", + "泻" : "xie", + "卸" : "xie", + "屑" : "xie", + "械" : "xie", + "亵" : "xie", + "谢" : "xie", + "邂" : "xie", + "懈" : "xie", + "蟹" : "xie", + "心" : "xin", + "芯" : "xin", + "辛" : "xin", + "欣" : "xin", + "锌" : "xin", + "新" : "xin", + "歆" : "xin", + "薪" : "xin", + "馨" : "xin", + "鑫" : "xin", + "信" : "xin", + "衅" : "xin", + "星" : "xing", + "猩" : "xing", + "惺" : "xing", + "腥" : "xing", + "刑" : "xing", + "邢" : "xing", + "形" : "xing", + "型" : "xing", + "醒" : "xing", + "擤" : "xing", + "兴" : "xing", + "杏" : "xing", + "幸" : "xing", + "性" : "xing", + "姓" : "xing", + "悻" : "xing", + "凶" : "xiong", + "兄" : "xiong", + "匈" : "xiong", + "讻" : "xiong", + "汹" : "xiong", + "胸" : "xiong", + "雄" : "xiong", + "熊" : "xiong", + "休" : "xiu", + "咻" : "xiu", + "修" : "xiu", + "羞" : "xiu", + "朽" : "xiu", + "秀" : "xiu", + "袖" : "xiu", + "绣" : "xiu", + "锈" : "xiu", + "嗅" : "xiu", + "欻" : "xu", + "戌" : "xu", + "须" : "xu", + "胥" : "xu", + "虚" : "xu", + "墟" : "xu", + "需" : "xu", + "魆" : "xu", + "徐" : "xu", + "许" : "xu", + "诩" : "xu", + "栩" : "xu", + "旭" : "xu", + "序" : "xu", + "叙" : "xu", + "恤" : "xu", + "酗" : "xu", + "勖" : "xu", + "绪" : "xu", + "续" : "xu", + "絮" : "xu", + "婿" : "xu", + "蓄" : "xu", + "煦" : "xu", + "轩" : "xuan", + "宣" : "xuan", + "揎" : "xuan", + "喧" : "xuan", + "暄" : "xuan", + "玄" : "xuan", + "悬" : "xuan", + "旋" : "xuan", + "漩" : "xuan", + "璇" : "xuan", + "选" : "xuan", + "癣" : "xuan", + "炫" : "xuan", + "绚" : "xuan", + "眩" : "xuan", + "渲" : "xuan", + "靴" : "xue", + "薛" : "xue", + "穴" : "xue", + "学" : "xue", + "噱" : "xue", + "雪" : "xue", + "谑" : "xue", + "勋" : "xun", + "熏" : "xun", + "薰" : "xun", + "醺" : "xun", + "旬" : "xun", + "寻" : "xun", + "巡" : "xun", + "询" : "xun", + "荀" : "xun", + "循" : "xun", + "训" : "xun", + "讯" : "xun", + "汛" : "xun", + "迅" : "xun", + "驯" : "xun", + "徇" : "xun", + "逊" : "xun", + "殉" : "xun", + "巽" : "xun", + "丫" : "ya", + "压" : "ya", + "押" : "ya", + "鸦" : "ya", + "桠" : "ya", + "鸭" : "ya", + "牙" : "ya", + "伢" : "ya", + "芽" : "ya", + "蚜" : "ya", + "崖" : "ya", + "涯" : "ya", + "睚" : "ya", + "衙" : "ya", + "哑" : "ya", + "雅" : "ya", + "亚" : "ya", + "讶" : "ya", + "娅" : "ya", + "氩" : "ya", + "揠" : "ya", + "呀" : "ya", + "恹" : "yan", + "胭" : "yan", + "烟" : "yan", + "焉" : "yan", + "阉" : "yan", + "淹" : "yan", + "湮" : "yan", + "嫣" : "yan", + "延" : "yan", + "闫" : "yan", + "严" : "yan", + "言" : "yan", + "妍" : "yan", + "岩" : "yan", + "炎" : "yan", + "沿" : "yan", + "研" : "yan", + "盐" : "yan", + "阎" : "yan", + "蜒" : "yan", + "筵" : "yan", + "颜" : "yan", + "檐" : "yan", + "奄" : "yan", + "俨" : "yan", + "衍" : "yan", + "掩" : "yan", + "郾" : "yan", + "眼" : "yan", + "偃" : "yan", + "演" : "yan", + "魇" : "yan", + "鼹" : "yan", + "厌" : "yan", + "砚" : "yan", + "彦" : "yan", + "艳" : "yan", + "晏" : "yan", + "唁" : "yan", + "宴" : "yan", + "验" : "yan", + "谚" : "yan", + "堰" : "yan", + "雁" : "yan", + "焰" : "yan", + "滟" : "yan", + "餍" : "yan", + "燕" : "yan", + "赝" : "yan", + "央" : "yang", + "泱" : "yang", + "殃" : "yang", + "鸯" : "yang", + "秧" : "yang", + "扬" : "yang", + "羊" : "yang", + "阳" : "yang", + "杨" : "yang", + "佯" : "yang", + "疡" : "yang", + "徉" : "yang", + "洋" : "yang", + "仰" : "yang", + "养" : "yang", + "氧" : "yang", + "痒" : "yang", + "怏" : "yang", + "样" : "yang", + "恙" : "yang", + "烊" : "yang", + "漾" : "yang", + "幺" : "yao", + "夭" : "yao", + "吆" : "yao", + "妖" : "yao", + "腰" : "yao", + "邀" : "yao", + "爻" : "yao", + "尧" : "yao", + "肴" : "yao", + "姚" : "yao", + "窑" : "yao", + "谣" : "yao", + "摇" : "yao", + "徭" : "yao", + "遥" : "yao", + "瑶" : "yao", + "杳" : "yao", + "咬" : "yao", + "舀" : "yao", + "窈" : "yao", + "药" : "yao", + "要" : "yao", + "鹞" : "yao", + "耀" : "yao", + "耶" : "ye", + "掖" : "ye", + "椰" : "ye", + "噎" : "ye", + "爷" : "ye", + "揶" : "ye", + "也" : "ye", + "冶" : "ye", + "野" : "ye", + "业" : "ye", + "叶" : "ye", + "页" : "ye", + "曳" : "ye", + "夜" : "ye", + "液" : "ye", + "谒" : "ye", + "腋" : "ye", + "一" : "yi", + "伊" : "yi", + "衣" : "yi", + "医" : "yi", + "依" : "yi", + "咿" : "yi", + "揖" : "yi", + "壹" : "yi", + "漪" : "yi", + "噫" : "yi", + "仪" : "yi", + "夷" : "yi", + "饴" : "yi", + "宜" : "yi", + "咦" : "yi", + "贻" : "yi", + "姨" : "yi", + "胰" : "yi", + "移" : "yi", + "痍" : "yi", + "颐" : "yi", + "疑" : "yi", + "彝" : "yi", + "乙" : "yi", + "已" : "yi", + "以" : "yi", + "苡" : "yi", + "矣" : "yi", + "迤" : "yi", + "蚁" : "yi", + "倚" : "yi", + "椅" : "yi", + "旖" : "yi", + "乂" : "yi", + "亿" : "yi", + "义" : "yi", + "艺" : "yi", + "刈" : "yi", + "忆" : "yi", + "议" : "yi", + "屹" : "yi", + "亦" : "yi", + "异" : "yi", + "抑" : "yi", + "呓" : "yi", + "邑" : "yi", + "役" : "yi", + "译" : "yi", + "易" : "yi", + "诣" : "yi", + "绎" : "yi", + "驿" : "yi", + "轶" : "yi", + "弈" : "yi", + "奕" : "yi", + "疫" : "yi", + "羿" : "yi", + "益" : "yi", + "谊" : "yi", + "逸" : "yi", + "翌" : "yi", + "肄" : "yi", + "裔" : "yi", + "意" : "yi", + "溢" : "yi", + "缢" : "yi", + "毅" : "yi", + "薏" : "yi", + "翳" : "yi", + "臆" : "yi", + "翼" : "yi", + "因" : "yin", + "阴" : "yin", + "茵" : "yin", + "荫" : "yin", + "音" : "yin", + "姻" : "yin", + "铟" : "yin", + "喑" : "yin", + "愔" : "yin", + "吟" : "yin", + "垠" : "yin", + "银" : "yin", + "淫" : "yin", + "寅" : "yin", + "龈" : "yin", + "霪" : "yin", + "尹" : "yin", + "引" : "yin", + "蚓" : "yin", + "隐" : "yin", + "瘾" : "yin", + "印" : "yin", + "英" : "ying", + "莺" : "ying", + "婴" : "ying", + "嘤" : "ying", + "罂" : "ying", + "缨" : "ying", + "樱" : "ying", + "鹦" : "ying", + "膺" : "ying", + "鹰" : "ying", + "迎" : "ying", + "茔" : "ying", + "荧" : "ying", + "盈" : "ying", + "莹" : "ying", + "萤" : "ying", + "营" : "ying", + "萦" : "ying", + "楹" : "ying", + "蝇" : "ying", + "赢" : "ying", + "瀛" : "ying", + "颍" : "ying", + "颖" : "ying", + "影" : "ying", + "应" : "ying", + "映" : "ying", + "硬" : "ying", + "哟" : "yo", + "唷" : "yo", + "佣" : "yong", + "拥" : "yong", + "庸" : "yong", + "雍" : "yong", + "壅" : "yong", + "臃" : "yong", + "永" : "yong", + "甬" : "yong", + "咏" : "yong", + "泳" : "yong", + "勇" : "yong", + "涌" : "yong", + "恿" : "yong", + "蛹" : "yong", + "踊" : "yong", + "用" : "yong", + "优" : "you", + "攸" : "you", + "忧" : "you", + "呦" : "you", + "幽" : "you", + "悠" : "you", + "尤" : "you", + "由" : "you", + "邮" : "you", + "犹" : "you", + "油" : "you", + "铀" : "you", + "鱿" : "you", + "游" : "you", + "友" : "you", + "有" : "you", + "酉" : "you", + "莠" : "you", + "黝" : "you", + "又" : "you", + "右" : "you", + "幼" : "you", + "佑" : "you", + "柚" : "you", + "囿" : "you", + "诱" : "you", + "鼬" : "you", + "迂" : "yu", + "纡" : "yu", + "於" : "yu", + "淤" : "yu", + "瘀" : "yu", + "于" : "yu", + "余" : "yu", + "盂" : "yu", + "臾" : "yu", + "鱼" : "yu", + "竽" : "yu", + "俞" : "yu", + "狳" : "yu", + "谀" : "yu", + "娱" : "yu", + "渔" : "yu", + "隅" : "yu", + "揄" : "yu", + "逾" : "yu", + "腴" : "yu", + "渝" : "yu", + "愉" : "yu", + "瑜" : "yu", + "榆" : "yu", + "虞" : "yu", + "愚" : "yu", + "舆" : "yu", + "与" : "yu", + "予" : "yu", + "屿" : "yu", + "宇" : "yu", + "羽" : "yu", + "雨" : "yu", + "禹" : "yu", + "语" : "yu", + "圄" : "yu", + "玉" : "yu", + "驭" : "yu", + "芋" : "yu", + "妪" : "yu", + "郁" : "yu", + "育" : "yu", + "狱" : "yu", + "浴" : "yu", + "预" : "yu", + "域" : "yu", + "欲" : "yu", + "谕" : "yu", + "遇" : "yu", + "喻" : "yu", + "御" : "yu", + "寓" : "yu", + "裕" : "yu", + "愈" : "yu", + "誉" : "yu", + "豫" : "yu", + "鹬" : "yu", + "鸢" : "yuan", + "鸳" : "yuan", + "冤" : "yuan", + "渊" : "yuan", + "元" : "yuan", + "园" : "yuan", + "垣" : "yuan", + "袁" : "yuan", + "原" : "yuan", + "圆" : "yuan", + "援" : "yuan", + "媛" : "yuan", + "缘" : "yuan", + "猿" : "yuan", + "源" : "yuan", + "辕" : "yuan", + "远" : "yuan", + "苑" : "yuan", + "怨" : "yuan", + "院" : "yuan", + "愿" : "yuan", + "曰" : "yue", + "月" : "yue", + "岳" : "yue", + "钺" : "yue", + "阅" : "yue", + "悦" : "yue", + "跃" : "yue", + "越" : "yue", + "粤" : "yue", + "晕" : "yun", + "云" : "yun", + "匀" : "yun", + "芸" : "yun", + "纭" : "yun", + "耘" : "yun", + "允" : "yun", + "陨" : "yun", + "殒" : "yun", + "孕" : "yun", + "运" : "yun", + "酝" : "yun", + "愠" : "yun", + "韵" : "yun", + "蕴" : "yun", + "熨" : "yun", + "匝" : "za", + "咂" : "za", + "杂" : "za", + "砸" : "za", + "灾" : "zai", + "甾" : "zai", + "哉" : "zai", + "栽" : "zai", + "载" : "zai", + "宰" : "zai", + "崽" : "zai", + "再" : "zai", + "在" : "zai", + "糌" : "zan", + "簪" : "zan", + "咱" : "zan", + "趱" : "zan", + "暂" : "zan", + "錾" : "zan", + "赞" : "zan", + "赃" : "zang", + "脏" : "zang", + "臧" : "zang", + "驵" : "zang", + "葬" : "zang", + "遭" : "zao", + "糟" : "zao", + "凿" : "zao", + "早" : "zao", + "枣" : "zao", + "蚤" : "zao", + "澡" : "zao", + "藻" : "zao", + "皂" : "zao", + "灶" : "zao", + "造" : "zao", + "噪" : "zao", + "燥" : "zao", + "躁" : "zao", + "则" : "ze", + "责" : "ze", + "泽" : "ze", + "啧" : "ze", + "帻" : "ze", + "仄" : "ze", + "贼" : "zei", + "怎" : "zen", + "谮" : "zen", + "增" : "zeng", + "憎" : "zeng", + "锃" : "zeng", + "赠" : "zeng", + "甑" : "zeng", + "吒" : "zha", + "挓" : "zha", + "哳" : "zha", + "揸" : "zha", + "渣" : "zha", + "楂" : "zha", + "札" : "zha", + "闸" : "zha", + "铡" : "zha", + "眨" : "zha", + "砟" : "zha", + "乍" : "zha", + "诈" : "zha", + "咤" : "zha", + "炸" : "zha", + "蚱" : "zha", + "榨" : "zha", + "拃" : "zha", + "斋" : "zhai", + "摘" : "zhai", + "宅" : "zhai", + "窄" : "zhai", + "债" : "zhai", + "砦" : "zhai", + "寨" : "zhai", + "沾" : "zhan", + "毡" : "zhan", + "粘" : "zhan", + "詹" : "zhan", + "谵" : "zhan", + "瞻" : "zhan", + "斩" : "zhan", + "盏" : "zhan", + "展" : "zhan", + "崭" : "zhan", + "搌" : "zhan", + "辗" : "zhan", + "占" : "zhan", + "栈" : "zhan", + "战" : "zhan", + "站" : "zhan", + "绽" : "zhan", + "湛" : "zhan", + "蘸" : "zhan", + "张" : "zhang", + "章" : "zhang", + "獐" : "zhang", + "彰" : "zhang", + "樟" : "zhang", + "蟑" : "zhang", + "涨" : "zhang", + "掌" : "zhang", + "丈" : "zhang", + "仗" : "zhang", + "杖" : "zhang", + "帐" : "zhang", + "账" : "zhang", + "胀" : "zhang", + "障" : "zhang", + "嶂" : "zhang", + "瘴" : "zhang", + "钊" : "zhao", + "招" : "zhao", + "昭" : "zhao", + "找" : "zhao", + "沼" : "zhao", + "兆" : "zhao", + "诏" : "zhao", + "赵" : "zhao", + "照" : "zhao", + "罩" : "zhao", + "肇" : "zhao", + "蜇" : "zhe", + "遮" : "zhe", + "哲" : "zhe", + "辄" : "zhe", + "蛰" : "zhe", + "谪" : "zhe", + "辙" : "zhe", + "者" : "zhe", + "锗" : "zhe", + "赭" : "zhe", + "褶" : "zhe", + "浙" : "zhe", + "蔗" : "zhe", + "鹧" : "zhe", + "贞" : "zhen", + "针" : "zhen", + "侦" : "zhen", + "珍" : "zhen", + "帧" : "zhen", + "胗" : "zhen", + "真" : "zhen", + "砧" : "zhen", + "斟" : "zhen", + "甄" : "zhen", + "榛" : "zhen", + "箴" : "zhen", + "臻" : "zhen", + "诊" : "zhen", + "枕" : "zhen", + "疹" : "zhen", + "缜" : "zhen", + "阵" : "zhen", + "鸩" : "zhen", + "振" : "zhen", + "朕" : "zhen", + "赈" : "zhen", + "震" : "zhen", + "镇" : "zhen", + "争" : "zheng", + "征" : "zheng", + "怔" : "zheng", + "峥" : "zheng", + "狰" : "zheng", + "睁" : "zheng", + "铮" : "zheng", + "筝" : "zheng", + "蒸" : "zheng", + "拯" : "zheng", + "整" : "zheng", + "正" : "zheng", + "证" : "zheng", + "郑" : "zheng", + "诤" : "zheng", + "政" : "zheng", + "挣" : "zheng", + "症" : "zheng", + "之" : "zhi", + "支" : "zhi", + "只" : "zhi", + "汁" : "zhi", + "芝" : "zhi", + "吱" : "zhi", + "枝" : "zhi", + "知" : "zhi", + "肢" : "zhi", + "织" : "zhi", + "栀" : "zhi", + "脂" : "zhi", + "蜘" : "zhi", + "执" : "zhi", + "直" : "zhi", + "侄" : "zhi", + "值" : "zhi", + "职" : "zhi", + "植" : "zhi", + "跖" : "zhi", + "踯" : "zhi", + "止" : "zhi", + "旨" : "zhi", + "址" : "zhi", + "芷" : "zhi", + "纸" : "zhi", + "祉" : "zhi", + "指" : "zhi", + "枳" : "zhi", + "咫" : "zhi", + "趾" : "zhi", + "酯" : "zhi", + "至" : "zhi", + "志" : "zhi", + "豸" : "zhi", + "帜" : "zhi", + "制" : "zhi", + "质" : "zhi", + "炙" : "zhi", + "治" : "zhi", + "栉" : "zhi", + "峙" : "zhi", + "挚" : "zhi", + "桎" : "zhi", + "致" : "zhi", + "秩" : "zhi", + "掷" : "zhi", + "痔" : "zhi", + "窒" : "zhi", + "蛭" : "zhi", + "智" : "zhi", + "痣" : "zhi", + "滞" : "zhi", + "置" : "zhi", + "雉" : "zhi", + "稚" : "zhi", + "中" : "zhong", + "忠" : "zhong", + "终" : "zhong", + "盅" : "zhong", + "钟" : "zhong", + "衷" : "zhong", + "肿" : "zhong", + "冢" : "zhong", + "踵" : "zhong", + "仲" : "zhong", + "众" : "zhong", + "舟" : "zhou", + "州" : "zhou", + "诌" : "zhou", + "周" : "zhou", + "洲" : "zhou", + "粥" : "zhou", + "妯" : "zhou", + "轴" : "zhou", + "肘" : "zhou", + "纣" : "zhou", + "咒" : "zhou", + "宙" : "zhou", + "胄" : "zhou", + "昼" : "zhou", + "皱" : "zhou", + "骤" : "zhou", + "帚" : "zhou", + "朱" : "zhu", + "侏" : "zhu", + "诛" : "zhu", + "茱" : "zhu", + "珠" : "zhu", + "株" : "zhu", + "诸" : "zhu", + "铢" : "zhu", + "猪" : "zhu", + "蛛" : "zhu", + "竹" : "zhu", + "竺" : "zhu", + "逐" : "zhu", + "烛" : "zhu", + "躅" : "zhu", + "主" : "zhu", + "拄" : "zhu", + "煮" : "zhu", + "嘱" : "zhu", + "瞩" : "zhu", + "伫" : "zhu", + "苎" : "zhu", + "助" : "zhu", + "住" : "zhu", + "贮" : "zhu", + "注" : "zhu", + "驻" : "zhu", + "柱" : "zhu", + "祝" : "zhu", + "著" : "zhu", + "蛀" : "zhu", + "铸" : "zhu", + "筑" : "zhu", + "抓" : "zhua", + "跩" : "zhuai", + "拽" : "zhuai", + "专" : "zhuan", + "砖" : "zhuan", + "转" : "zhuan", + "啭" : "zhuan", + "撰" : "zhuan", + "篆" : "zhuan", + "妆" : "zhuang", + "庄" : "zhuang", + "桩" : "zhuang", + "装" : "zhuang", + "壮" : "zhuang", + "状" : "zhuang", + "撞" : "zhuang", + "幢" : "zhuang", + "追" : "zhui", + "骓" : "zhui", + "锥" : "zhui", + "坠" : "zhui", + "缀" : "zhui", + "惴" : "zhui", + "赘" : "zhui", + "谆" : "zhun", + "准" : "zhun", + "拙" : "zhuo", + "捉" : "zhuo", + "桌" : "zhuo", + "灼" : "zhuo", + "茁" : "zhuo", + "卓" : "zhuo", + "斫" : "zhuo", + "浊" : "zhuo", + "酌" : "zhuo", + "啄" : "zhuo", + "擢" : "zhuo", + "镯" : "zhuo", + "孜" : "zi", + "咨" : "zi", + "姿" : "zi", + "赀" : "zi", + "资" : "zi", + "辎" : "zi", + "嗞" : "zi", + "滋" : "zi", + "锱" : "zi", + "龇" : "zi", + "子" : "zi", + "姊" : "zi", + "秭" : "zi", + "籽" : "zi", + "梓" : "zi", + "紫" : "zi", + "訾" : "zi", + "滓" : "zi", + "自" : "zi", + "字" : "zi", + "恣" : "zi", + "眦" : "zi", + "渍" : "zi", + "宗" : "zong", + "综" : "zong", + "棕" : "zong", + "踪" : "zong", + "鬃" : "zong", + "总" : "zong", + "纵" : "zong", + "粽" : "zong", + "邹" : "zou", + "走" : "zou", + "奏" : "zou", + "揍" : "zou", + "租" : "zu", + "足" : "zu", + "卒" : "zu", + "族" : "zu", + "诅" : "zu", + "阻" : "zu", + "组" : "zu", + "俎" : "zu", + "祖" : "zu", + "纂" : "zuan", + "钻" : "zuan", + "攥" : "zuan", + "嘴" : "zui", + "最" : "zui", + "罪" : "zui", + "醉" : "zui", + "尊" : "zun", + "遵" : "zun", + "樽" : "zun", + "鳟" : "zun", + "昨" : "zuo", + "左" : "zuo", + "佐" : "zuo", + "作" : "zuo", + "坐" : "zuo", + "阼" : "zuo", + "怍" : "zuo", + "祚" : "zuo", + "唑" : "zuo", + "座" : "zuo", + "做" : "zuo", + "酢" : "zuo", + "斌" : "bin", + "曾" : "zeng", + "查" : "zha", + "査" : "zha", + "乘" : "cheng", + "传" : "chuan", + "丁" : "ding", + "行" : "xing", + "瑾" : "jin", + "婧" : "jing", + "恺" : "kai", + "阚" : "kan", + "奎" : "kui", + "乐" : "le", + "陆" : "lu", + "逯" : "lv", + "璐" : "lu", + "淼" : "miao", + "闵" : "min", + "娜" : "na", + "奇" : "qi", + "琦" : "qi", + "强" : "qiang", + "邱" : "qiu", + "芮" : "rui", + "莎" : "sha", + "盛" : "sheng", + "石" : "shi", + "祎" : "yi", + "殷" : "yin", + "瑛" : "ying", + "昱" : "yu", + "眃" : "yun", + "琢" : "zhuo", + "枰" : "ping", + "玟" : "min", + "珉" : "min", + "珣" : "xun", + "淇" : "qi", + "缈" : "miao", + "彧" : "yu", + "祺" : "qi", + "骞" : "qian", + "垚" : "yao", + "妸" : "e", + "烜" : "hui", + "祁" : "qi", + "傢" : "jia", + "珮" : "pei", + "濮" : "pu", + "屺" : "qi", + "珅" : "shen", + "缇" : "ti", + "霈" : "pei", + "晞" : "xi", + "璠" : "fan", + "骐" : "qi", + "姞" : "ji", + "偲" : "cai", + "齼" : "chu", + "宓" : "mi", + "朴" : "pu", + "萁" : "qi", + "颀" : "qi", + "阗" : "tian", + "湉" : "tian", + "翀" : "chong", + "岷" : "min", + "桤" : "qi", + "囯" : "guo", + "浛" : "han", + "勐" : "meng", + "苠" : "min", + "岍" : "qian", + "皞" : "hao", + "岐" : "qi", + "溥" : "pu", + "锘" : "muo", + "渼" : "mei", + "燊" : "shen", + "玚" : "chang", + "亓" : "qi", + "湋" : "wei", + "涴" : "wan", + "沤" : "ou", + "胖" : "pang", + "莆" : "pu", + "扦" : "qian", + "僳" : "su", + "坍" : "tan", + "锑" : "ti", + "嚏" : "ti", + "腆" : "tian", + "丿" : "pie", + "鼗" : "tao", + "芈" : "mi", + "匚" : "fang", + "刂" : "li", + "冂" : "tong", + "亻" : "dan", + "仳" : "pi", + "俜" : "ping", + "俳" : "pai", + "倜" : "ti", + "傥" : "tang", + "傩" : "nuo", + "佥" : "qian", + "勹" : "bao", + "亠" : "tou", + "廾" : "gong", + "匏" : "pao", + "扌" : "ti", + "拚" : "pin", + "掊" : "pou", + "搦" : "nuo", + "擗" : "pi", + "啕" : "tao", + "嗦" : "suo", + "嗍" : "suo", + "辔" : "pei", + "嘌" : "piao", + "嗾" : "sou", + "嘧" : "mi", + "帔" : "pei", + "帑" : "tang", + "彡" : "san", + "犭" : "fan", + "狍" : "pao", + "狲" : "sun", + "狻" : "jun", + "飧" : "sun", + "夂" : "zhi", + "饣" : "shi", + "庀" : "pi", + "忄" : "shu", + "愫" : "su", + "闼" : "ta", + "丬" : "jiang", + "氵" : "san", + "汔" : "qi", + "沔" : "mian", + "汨" : "mi", + "泮" : "pan", + "洮" : "tao", + "涑" : "su", + "淠" : "pi", + "湓" : "pen", + "溻" : "ta", + "溏" : "tang", + "濉" : "sui", + "宀" : "bao", + "搴" : "qian", + "辶" : "zou", + "逄" : "pang", + "逖" : "ti", + "遢" : "ta", + "邈" : "miao", + "邃" : "sui", + "彐" : "ji", + "屮" : "cao", + "娑" : "suo", + "嫖" : "piao", + "纟" : "jiao", + "缗" : "min", + "瑭" : "tang", + "杪" : "miao", + "桫" : "suo", + "榀" : "pin", + "榫" : "sun", + "槭" : "qi", + "甓" : "pi", + "攴" : "po", + "耆" : "qi", + "牝" : "pin", + "犏" : "pian", + "氆" : "pu", + "攵" : "fan", + "肽" : "tai", + "胼" : "pian", + "脒" : "mi", + "脬" : "pao", + "旆" : "pei", + "炱" : "tai", + "燧" : "sui", + "灬" : "biao", + "礻" : "shi", + "祧" : "tiao", + "忑" : "te", + "忐" : "tan", + "愍" : "min", + "肀" : "yu", + "碛" : "qi", + "眄" : "mian", + "眇" : "miao", + "眭" : "sui", + "睃" : "suo", + "瞍" : "sou", + "畋" : "tian", + "罴" : "pi", + "蠓" : "meng", + "蠛" : "mie", + "笸" : "po", + "筢" : "pa", + "衄" : "nv", + "艋" : "meng", + "敉" : "mi", + "糸" : "mi", + "綦" : "qi", + "醅" : "pei", + "醣" : "tang", + "趿" : "ta", + "觫" : "su", + "龆" : "tiao", + "鲆" : "ping", + "稣" : "su", + "鲐" : "tai", + "鲦" : "tiao", + "鳎" : "ta", + "髂" : "qia", + "縻" : "mi", + "裒" : "pou", + "冫" : "liang", + "冖" : "tu", + "讠" : "yan", + "谇" : "sui", + "谝" : "pian", + "谡" : "su", + "卩" : "dan", + "阝" : "zuo", + "陴" : "pi", + "邳" : "pi", + "郫" : "pi", + "郯" : "tan", + "廴" : "yin", + "凵" : "qian", + "圮" : "pi", + "堋" : "peng", + "鼙" : "pi", + "艹" : "cao", + "芑" : "qi", + "苤" : "pie", + "荪" : "sun", + "荽" : "sui", + "葜" : "qia", + "蒎" : "pai", + "蔌" : "su", + "蕲" : "qi", + "薮" : "sou", + "薹" : "tai", + "蘼" : "mi", + "钅" : "jin", + "钷" : "po", + "钽" : "tan", + "铍" : "pi", + "铴" : "tang", + "铽" : "te", + "锫" : "pei", + "锬" : "tan", + "锼" : "sou", + "镤" : "pu", + "镨" : "pu", + "皤" : "po", + "鹈" : "ti", + "鹋" : "miao", + "疒" : "bing", + "疱" : "pao", + "衤" : "yi", + "袢" : "pan", + "裼" : "ti", + "襻" : "pan", + "耥" : "tang", + "耦" : "ou", + "虍" : "hu", + "蛴" : "qi", + "蜞" : "qi", + "蜱" : "pi", + "螋" : "sou", + "螗" : "tang", + "螵" : "piao", + "蟛" : "peng" +} diff --git a/kirby/i18n/translations/bg.json b/kirby/i18n/translations/bg.json new file mode 100644 index 0000000..af7eea6 --- /dev/null +++ b/kirby/i18n/translations/bg.json @@ -0,0 +1,428 @@ +{ + "add": "\u0414\u043e\u0431\u0430\u0432\u0438", + "avatar": "Профилна снимка", + "back": "Назад", + "cancel": "\u041e\u0442\u043a\u0430\u0436\u0438", + "change": "\u041f\u0440\u043e\u043c\u0435\u043d\u0438", + "close": "\u0417\u0430\u0442\u0432\u043e\u0440\u0438", + "confirm": "Ок", + "copy": "Копирай", + "create": "Създай", + + "date": "Дата", + "date.select": "Select a date", + + "day": "Day", + "days.fri": "\u041f\u0442", + "days.mon": "\u041f\u043d", + "days.sat": "\u0421\u0431", + "days.sun": "\u041d\u0434", + "days.thu": "\u0427\u0442", + "days.tue": "\u0412\u0442", + "days.wed": "\u0421\u0440", + + "delete": "\u0418\u0437\u0442\u0440\u0438\u0439", + "dimensions": "Размери", + "disabled": "Disabled", + "discard": "\u041e\u0442\u043c\u0435\u043d\u0438", + "download": "Download", + "duplicate": "Duplicate", + "edit": "\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0430\u0439", + + "dialog.files.empty": "No files to select", + "dialog.pages.empty": "No pages to select", + "dialog.users.empty": "No users to select", + + "email": "Email", + "email.placeholder": "mail@example.com", + + "error.access.login": "Invalid login", + "error.access.panel": "Нямате права за достъп до панела", + "error.access.view": "You are not allowed to access this part of the panel", + + "error.avatar.create.fail": "Профилната снимка не може да се качи", + "error.avatar.delete.fail": "Профилната снимка не може да бъде изтрита", + "error.avatar.dimensions.invalid": "Моля запазете ширината и височината на профилната снимка под 3000 пиксела", + "error.avatar.mime.forbidden": "Профилната снимка трябва да бъде в JPEG или PNG формат", + + "error.blueprint.notFound": "Образецът \"{name}\" не може да бъде зареден", + + "error.email.preset.notFound": "Email шаблонът \"{name}\" не може да бъде открит", + + "error.field.converter.invalid": "Невалиден конвертор \"{converter}\"", + + "error.file.changeName.empty": "The name must not be empty", + "error.file.changeName.permission": "Не можете да смените името на \"{filename}\"", + "error.file.duplicate": "Файл с име \"{filename}\" вече съществува", + "error.file.extension.forbidden": "Файловото разширение \"{extension}\" не е позволено", + "error.file.extension.missing": "Липсва файлово разширение за файла \"{filename}\"", + "error.file.maxheight": "The height of the image must not exceed {height} pixels", + "error.file.maxsize": "The file is too large", + "error.file.maxwidth": "The width of the image must not exceed {width} pixels", + "error.file.mime.differs": "Каченият файл трябва да бъде от същия mime тип \"{mime}\"", + "error.file.mime.forbidden": "The media type \"{mime}\" is not allowed", + "error.file.mime.invalid": "Invalid mime type: {mime}", + "error.file.mime.missing": "The media type for \"{filename}\" cannot be detected", + "error.file.minheight": "The height of the image must be at least {height} pixels", + "error.file.minsize": "The file is too small", + "error.file.minwidth": "The width of the image must be at least {width} pixels", + "error.file.name.missing": "Името на файла е задължително", + "error.file.notFound": "Файлът \"{filename}\" не може да бъде намерен", + "error.file.orientation": "The orientation of the image must be \"{orientation}\"", + "error.file.type.forbidden": "Не е позволен ъплоуда на файлове от тип {type}", + "error.file.undefined": "\u0424\u0430\u0439\u043b\u044a\u0442 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d", + + "error.form.incomplete": "Моля коригирайте всички грешки във формата...", + "error.form.notSaved": "Формата не може да бъде запазена", + + "error.language.code": "Please enter a valid code for the language", + "error.language.duplicate": "The language already exists", + "error.language.name": "Please enter a valid name for the language", + + "error.license.format": "Please enter a valid license key", + "error.license.email": "Моля въведете валиден email адрес", + "error.license.verification": "The license could not be verified", + + "error.page.changeSlug.permission": "Не можете да смените URL на \"{slug}\"", + "error.page.changeStatus.incomplete": "Страницата съдържа грешки и не може да бъде публикувана", + "error.page.changeStatus.permission": "Статусът на страницата не може да бъде променен", + "error.page.changeStatus.toDraft.invalid": "Страницата \"{slug}\" не може да бъде променена в чернова", + "error.page.changeTemplate.invalid": "Темплейтът за страница \"{slug}\" не може да бъде променен", + "error.page.changeTemplate.permission": "Нямате права за да промените шаблона за \"{slug}\"", + "error.page.changeTitle.empty": "Заглавието е задължително", + "error.page.changeTitle.permission": "Не можете да промените заглавието на \"{slug}\"", + "error.page.create.permission": "Не можете да създадете \"{slug}\"", + "error.page.delete": "Страницата \"{slug}\" не може да бъде изтрита", + "error.page.delete.confirm": "Моля въведете името на страницата, за да потвърдите", + "error.page.delete.hasChildren": "Страницата има подстраници и не може да бъде изтрита", + "error.page.delete.permission": "Не можете да изтриете \"{slug}\"", + "error.page.draft.duplicate": "Вече съществува чернова с URL-добавка \"{slug}\"", + "error.page.duplicate": "Страница с URL-добавка \"{slug}\" вече съществува", + "error.page.duplicate.permission": "You are not allowed to duplicate \"{slug}\"", + "error.page.notFound": "Страницата \"{slug}\" не може да бъде намерена", + "error.page.num.invalid": "Моля въведете валидно число за сортиране. Числата не трябва да са негативни.", + "error.page.slug.invalid": "Моля въведете валиден URL префикс", + "error.page.slug.maxlength": "Slug length must be less than \"{length}\" characters", + "error.page.sort.permission": "Страницата \"{slug}\" не може да бъде сортирана", + "error.page.status.invalid": "Моля изберете валиден статус на страницата", + "error.page.undefined": "\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0430", + "error.page.update.permission": "Не можете да обновите \"{slug}\"", + + "error.section.files.max.plural": "Не можете да добавяте повече от {max} файлa в секция \"{section}\"", + "error.section.files.max.singular": "Не можете да добавяте повече от един файл в секция \"{section}\"", + "error.section.files.min.plural": "The \"{section}\" section requires at least {min} files", + "error.section.files.min.singular": "The \"{section}\" section requires at least one file", + + "error.section.pages.max.plural": "Не можете да добавяте повече от {max} страници в секция \"{section}\"", + "error.section.pages.max.singular": "Не можете да добавяте повече от една страница в секция \"{section}\"", + "error.section.pages.min.plural": "The \"{section}\" section requires at least {min} pages", + "error.section.pages.min.singular": "The \"{section}\" section requires at least one page", + + "error.section.notLoaded": "Секция \"{name}\" не може да бъде заредена", + "error.section.type.invalid": "Типът \"{type}\" на секция не е валиден", + + "error.site.changeTitle.empty": "Заглавието е задължително", + "error.site.changeTitle.permission": "Не може да променяте заглавието на сайта", + "error.site.update.permission": "Нямате права за да обновите сайта", + + "error.template.default.notFound": "Стандартният шаблон не съществува", + + "error.user.changeEmail.permission": "Нямате права да промените имейла на този потребител \"{name}\"", + "error.user.changeLanguage.permission": "Нямате права да промените езика за този потребител \"{name}\"", + "error.user.changeName.permission": "Нямате права да промените името на този потребител \"{name}\"", + "error.user.changePassword.permission": "Нямате права да промените паролата за този потребител \"{name}\"", + "error.user.changeRole.lastAdmin": "Ролята на последния администратор не може да бъде променена", + "error.user.changeRole.permission": "Нямате права да промените ролята на този потребител \"{name}\"", + "error.user.changeRole.toAdmin": "You are not allowed to promote someone to the admin role", + "error.user.create.permission": "Нямате права да създадете този потребител", + "error.user.delete": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0438\u0437\u0442\u0440\u0438\u0442", + "error.user.delete.lastAdmin": "\u041d\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0438\u0437\u0442\u0440\u0438\u0435\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u044f \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440", + "error.user.delete.lastUser": "Последният потребител не може да бъде изтрит", + "error.user.delete.permission": "\u041d\u0435 \u0435 \u043f\u043e\u0437\u0432\u043e\u043b\u0435\u043d\u043e \u0434\u0430 \u0438\u0437\u0442\u0440\u0438\u0432\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b", + "error.user.duplicate": "Потребител с имейл \"{email}\" вече съществува", + "error.user.email.invalid": "Моля въведете валиден email адрес", + "error.user.language.invalid": "Моля въведете валиден език", + "error.user.notFound": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d.", + "error.user.password.invalid": "Моля въведете валидна парола. Тя трабва да съдържа поне 8 символа.", + "error.user.password.notSame": "\u041c\u043e\u043b\u044f, \u043f\u043e\u0442\u0432\u044a\u0440\u0434\u0435\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430", + "error.user.password.undefined": "Потребителят няма парола", + "error.user.role.invalid": "Моля въведете валидна роля", + "error.user.update.permission": "Нямате права да обновите този потребител \"{name}\"", + + "error.validation.accepted": "Моля потвърдете", + "error.validation.alpha": "Моля въвдете символи измежду a-z", + "error.validation.alphanum": "Моля въвдете символи измежду a-z или цифри 0-9", + "error.validation.between": "Моля въведете стойност между \"{min}\" и \"{max}\"", + "error.validation.boolean": "Моля потвърдете или откажете", + "error.validation.contains": "Моля въведете стойност, която съдържа \"{needle}\"", + "error.validation.date": "Моля въведете валидна дата", + "error.validation.date.after": "Please enter a date after {date}", + "error.validation.date.before": "Please enter a date before {date}", + "error.validation.date.between": "Please enter a date between {min} and {max}", + "error.validation.denied": "Моля откажете", + "error.validation.different": "Стойността не трябва да е \"{other}\"", + "error.validation.email": "Моля въведете валиден email адрес", + "error.validation.endswith": "Стойността трябва да завършва с \"{end\"}", + "error.validation.filename": "Моля въведете валидно име на файла", + "error.validation.in": "Моля въведете едно от следните: ({in})", + "error.validation.integer": "Моля въведете валидно цяло число", + "error.validation.ip": "Моля въведете валиден IP адрес", + "error.validation.less": "Моля въведете стойност по-ниска от {max}", + "error.validation.match": "Стойността не съвпада с очаквания модел", + "error.validation.max": "Please enter a value equal to or lower than {max}", + "error.validation.maxlength": "Моля въведете по-къса стойност. (макс. {max} символа)", + "error.validation.maxwords": "Моля въведете не повече от {max} дума(и)", + "error.validation.min": "Please enter a value equal to or greater than {min}", + "error.validation.minlength": "Моля въведете по-дълга стойност. (мин. {min} символа)", + "error.validation.minwords": "Моля въведете поне {min} дума(и).", + "error.validation.more": "Моля въведете стойност по-висока от {min}", + "error.validation.notcontains": "Моля въведете стойност, която не съдържа \"{needle}\"", + "error.validation.notin": "Моля не въвеждайте нито едно от следните: ({notIn})", + "error.validation.option": "Моля изберете валидна опция", + "error.validation.num": "Моля въведете валидно число", + "error.validation.required": "Моля въведете нещо", + "error.validation.same": "Моля въведете \"{other}\"", + "error.validation.size": "Размерът на стойността трябва да бъде \"{size}\"", + "error.validation.startswith": "Стойността трябва да започва с \"{start}\"", + "error.validation.time": "Моля въведете валидно време", + "error.validation.url": "Моля въведете валиден URL", + + "field.required": "The field is required", + "field.files.empty": "Все още не са избрани файлове", + "field.pages.empty": "Все още не са избрани страници", + "field.structure.delete.confirm": "Сигурни ли сте, че искате да изтриете това вписване?", + "field.structure.empty": "Все още няма статии", + "field.users.empty": "Все още не са избрани потребители", + + "file.delete.confirm": "Сигурни ли сте, че искате да изтриете
{filename}?", + + "files": "Файлове", + "files.empty": "Няма файлове", + + "hour": "Hour", + "insert": "\u0412\u043c\u044a\u043a\u043d\u0438", + "install": "Инсталирай", + + "installation": "Инсталация", + "installation.completed": "The panel has been installed", + "installation.disabled": "The panel installer is disabled on public servers by default. Please run the installer on a local machine or enable it with the panel.install option.", + "installation.issues.accounts": "Папката /site/accounts не съществува или не позволява запис", + "installation.issues.content": "Папката /content и всички файлове в нея трябва да позволяват запис", + "installation.issues.curl": "Изисква се CURL разширението", + "installation.issues.headline": "Панелът не може да бъде инсталиран", + "installation.issues.mbstring": "Изисква се разширението MB String", + "installation.issues.media": "Папката /media не съществува или няма права за запис", + "installation.issues.php": "Бъдете сигурни, че използвате PHP 7+", + "installation.issues.server": "Kirby изисква Apache, Nginx или Caddy", + "installation.issues.sessions": "The /site/sessions folder does not exist or is not writable", + + "language": "\u0415\u0437\u0438\u043a", + "language.code": "Код", + "language.convert": "Направи по подразбиране", + "language.convert.confirm": "

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

", + "language.create": "Lisää uusi kieli", + "language.delete.confirm": "Haluatko varmasti poistaa kielen {name}, mukaanlukien kaikki käännökset? Tätä toimintoa ei voi peruuttaa!", + "language.deleted": "Kieli on poistettu", + "language.direction": "Lukusuunta", + "language.direction.ltr": "Vasemmalta oikealle", + "language.direction.rtl": "Oikealta vasemmalle", + "language.locale": "PHP-lokaalin tunniste", + "language.locale.warning": "You are using a custom locale set up. Please modify it in the language file in /site/languages", + "language.name": "Nimi", + "language.updated": "Kieli on päivitetty", + + "languages": "Kielet", + "languages.default": "Oletuskieli", + "languages.empty": "Kieliä ei ole vielä määritetty", + "languages.secondary": "Toissijaiset kielet", + "languages.secondary.empty": "Toissijaisia kieliä ei ole vielä määritetty", + + "license": "Lisenssi", + "license.buy": "Osta lisenssi", + "license.register": "Rekisteröi", + "license.register.help": "Lisenssiavain on lähetetty oston jälkeen sähköpostiisi. Kopioi ja liitä avain tähän.", + "license.register.label": "Anna lisenssiavain", + "license.register.success": "Kiitos kun tuet Kirbyä", + "license.unregistered": "Tämä on rekisteröimätön demo Kirbystä", + + "link": "Linkki", + "link.text": "Linkin teksti", + + "loading": "Ladataan", + + "lock.unsaved": "Tallentamattomia muutoksia", + "lock.unsaved.empty": "There are no more unsaved changes", + "lock.isLocked": "Käyttäjällä {email} on tallentamattomia muutoksia", + "lock.file.isLocked": "The file is currently being edited by {email} and cannot be changed.", + "lock.page.isLocked": "The page is currently being edited by {email} and cannot be changed.", + "lock.unlock": "Vapauta", + "lock.isUnlocked": "Your unsaved changes have been overwritten by another user. You can download your changes to merge them manually.", + + "login": "Kirjaudu", + "login.remember": "Pidä minut kirjautuneena", + + "logout": "Kirjaudu ulos", + + "menu": "Valikko", + "meridiem": "am/pm", + "mime": "Median tyyppi", + "minutes": "Minuutit", + + "month": "Kuukausi", + "months.april": "Huhtikuu", + "months.august": "Elokuu", + "months.december": "Joulukuu", + "months.february": "Helmikuu", + "months.january": "Tammikuu", + "months.july": "Hein\u00e4kuu", + "months.june": "Kes\u00e4kuu", + "months.march": "Maaliskuu", + "months.may": "Toukokuu", + "months.november": "Marraskuu", + "months.october": "Lokakuu", + "months.september": "Syyskuu", + + "more": "Lisää", + "name": "Nimi", + "next": "Seuraava", + "off": "off", + "on": "on", + "open": "Avaa", + "options": "Asetukset", + "options.none": "No options", + + "orientation": "Suunta", + "orientation.landscape": "Vaakasuuntainen", + "orientation.portrait": "Pystysuuntainen", + "orientation.square": "Neliskulmainen", + + "page.changeSlug": "Vaihda URL-osoite", + "page.changeSlug.fromTitle": "Luo nimen perusteella", + "page.changeStatus": "Muuta tilaa", + "page.changeStatus.position": "Valitse järjestyspaikka", + "page.changeStatus.select": "Valitse uusi tila", + "page.changeTemplate": "Vaihda sivupohja", + "page.delete.confirm": "Haluatko varmasti poistaa sivun {title}?", + "page.delete.confirm.subpages": "Tällä sivulla on alasivuja.
Myös kaikki alasivut poistetaan.", + "page.delete.confirm.title": "Anna vahvistuksena sivun nimi", + "page.draft.create": "Uusi luonnos", + "page.duplicate.appendix": "Kopioi", + "page.duplicate.files": "Copy files", + "page.duplicate.pages": "Copy pages", + "page.status": "Tila", + "page.status.draft": "Luonnos", + "page.status.draft.description": "The page is in draft mode and only visible for logged in editors or via secret link", + "page.status.listed": "Julkinen", + "page.status.listed.description": "Sivu on julkinen kaikille", + "page.status.unlisted": "Listaamaton", + "page.status.unlisted.description": "Sivulle pääsee vain URL:n kautta", + + "pages": "Sivut", + "pages.empty": "Sivuja ei ole vielä lisätty", + "pages.status.draft": "Luonnokset", + "pages.status.listed": "Julkaistut", + "pages.status.unlisted": "Listaamaton", + + "pagination.page": "Sivu", + + "password": "Salasana", + "pixel": "Pikseli", + "prev": "Edellinen", + "remove": "Poista", + "rename": "Nimeä uudelleen", + "replace": "Korvaa", + "retry": "Yrit\u00e4 uudelleen", + "revert": "Palauta", + "revert.confirm": "Do you really want to delete all unsaved changes?", + + "role": "K\u00e4ytt\u00e4j\u00e4taso", + "role.admin.description": "Pääkäyttäjällä on kaikki oikeudet", + "role.admin.title": "Pääkäyttäjä", + "role.all": "Kaikki", + "role.empty": "Tällä käyttäjätasolla ei ole yhtään käyttäjää", + "role.description.placeholder": "Ei kuvausta", + "role.nobody.description": "This is a fallback role without any permissions", + "role.nobody.title": "Nobody", + + "save": "Tallenna", + "search": "Haku", + "search.min": "Enter {min} characters to search", + "search.all": "Show all", + "search.results.none": "No results", + + "section.required": "Osio on pakollinen", + + "select": "Valitse", + "settings": "Asetukset", + "size": "Koko", + "slug": "URL-tunniste", + "sort": "Järjestele", + "title": "Nimi", + "template": "Sivupohja", + "today": "Tänään", + + "toolbar.button.code": "Koodi", + "toolbar.button.bold": "Lihavointi", + "toolbar.button.email": "S\u00e4hk\u00f6posti", + "toolbar.button.headings": "Otsikot", + "toolbar.button.heading.1": "Otsikko 1", + "toolbar.button.heading.2": "Otsikko 2", + "toolbar.button.heading.3": "Otsikko 3", + "toolbar.button.italic": "Kursivointi", + "toolbar.button.file": "Tiedosto", + "toolbar.button.file.select": "Valitse tiedosto", + "toolbar.button.file.upload": "Lähetä tiedosto", + "toolbar.button.link": "Linkki", + "toolbar.button.ol": "Järjestetty lista", + "toolbar.button.ul": "Järjestämätön lista", + + "translation.author": "Kirby-tiimi", + "translation.direction": "ltr", + "translation.name": "Suomi", + "translation.locale": "fi_FI", + + "upload": "Lähetä", + "upload.error.cantMove": "Lähetettyä tiedostoa ei voitu siirtää", + "upload.error.cantWrite": "Tiedoston kirjoitus levylle epäonnistui", + "upload.error.default": "Tiedostoa ei voitu lähettää", + "upload.error.extension": "File upload stopped by extension", + "upload.error.formSize": "Lähetetyn tiedoston koko ylittää lomakkeen sallitun ylärajan MAX_FILE_SIZE", + "upload.error.iniPostSize": "Lähetetyn tiedoston koko ylittää sallitun ylärajan post_max_size asetustiedostossa php.ini", + "upload.error.iniSize": "Lähetetyn tiedoston koko ylittää sallitun ylärajan upload_max_filesize asetustiedostossa php.ini", + "upload.error.noFile": "Tiedostoa ei lähetetty", + "upload.error.noFiles": "Tiedostoja ei lähetetty", + "upload.error.partial": "The uploaded file was only partially uploaded", + "upload.error.tmpDir": "Missing a temporary folder", + "upload.errors": "Virhe", + "upload.progress": "Lähetetään...", + + "url": "Url", + "url.placeholder": "https://esimerkki.fi", + + "user": "Käyttäjä", + "user.blueprint": "Voit määrittää lisää osioita ja lomakekenttiä tälle käyttäjälle kaavassa /site/blueprints/users/{role}.yml", + "user.changeEmail": "Muuta sähköpostiosoite", + "user.changeLanguage": "Vaihda kieli", + "user.changeName": "Nimeä uudelleen", + "user.changePassword": "Vaihda salasana", + "user.changePassword.new": "Uusi salasana", + "user.changePassword.new.confirm": "Vahvista uusi salasana...", + "user.changeRole": "Muuta käyttäjätasoa", + "user.changeRole.select": "Valitse uusi käyttäjätaso", + "user.create": "Lisää uusi käyttäjä", + "user.delete": "Poista tämä käyttäjä", + "user.delete.confirm": "Haluatko varmsti poistaa käyttäjän
{email}?", + + "users": "Käyttäjät", + + "version": "Versio", + + "view.account": "Oma käyttäjätili", + "view.installation": "Asennus", + "view.settings": "Asetukset", + "view.site": "Sivusto", + "view.users": "K\u00e4ytt\u00e4j\u00e4t", + + "welcome": "Tervetuloa", + "year": "Vuosi" +} diff --git a/kirby/i18n/translations/fr.json b/kirby/i18n/translations/fr.json new file mode 100644 index 0000000..d2e7647 --- /dev/null +++ b/kirby/i18n/translations/fr.json @@ -0,0 +1,428 @@ +{ + "add": "Ajouter", + "avatar": "Image du profil", + "back": "Retour", + "cancel": "Annuler", + "change": "Changer", + "close": "Fermer", + "confirm": "Ok", + "copy": "Copier", + "create": "Créer", + + "date": "Date", + "date.select": "Choisissez une date", + + "day": "Jour", + "days.fri": "Ven", + "days.mon": "Lun", + "days.sat": "Sam", + "days.sun": "Dim", + "days.thu": "Jeu", + "days.tue": "Mar", + "days.wed": "Mer", + + "delete": "Supprimer", + "dimensions": "Dimensions", + "disabled": "Désactivé", + "discard": "Supprimer", + "download": "Télécharger", + "duplicate": "Dupliquer", + "edit": "Éditer", + + "dialog.files.empty": "Aucun fichier à sélectionner", + "dialog.pages.empty": "Aucune page à sélectionner", + "dialog.users.empty": "Aucun utilisateur à sélectionner", + + "email": "Courriel", + "email.placeholder": "mail@example.com", + + "error.access.login": "Identifiant incorrect", + "error.access.panel": "Vous n’êtes pas autorisé à accéder au Panel", + "error.access.view": "Vous n’êtes pas autorisé à accéder à cette section du Panel", + + "error.avatar.create.fail": "L’image du profil n’a pu être transférée", + "error.avatar.delete.fail": "L’image du profil n’a pu être supprimée", + "error.avatar.dimensions.invalid": "Veuillez choisir une image de profil de largeur et hauteur inférieures à 3000 pixels", + "error.avatar.mime.forbidden": "L'image du profil utilisateur doit être un fichier JPEG ou PNG", + + "error.blueprint.notFound": "Le blueprint « {name} » n’a pu être chargé", + + "error.email.preset.notFound": "La configuration de courriel « {name} » n’a pu être trouvé", + + "error.field.converter.invalid": "Convertisseur « {converter} » incorrect", + + "error.file.changeName.empty": "Le nom ne peut être vide", + "error.file.changeName.permission": "Vous n’êtes pas autorisé à modifier le nom de « {filename} »", + "error.file.duplicate": "Un fichier nommé « {filename} » existe déjà", + "error.file.extension.forbidden": "L’extension « {extension} » n’est pas autorisée", + "error.file.extension.missing": "L’extension pour « {filename} » est manquante", + "error.file.maxheight": "La hauteur de l'image ne doit pas excéder {height} pixels", + "error.file.maxsize": "Le fichier est trop volumineux", + "error.file.maxwidth": "La largeur de l'image ne doit pas excéder {width} pixels", + "error.file.mime.differs": "Le fichier transféré doit être du même type de média « {mime} »", + "error.file.mime.forbidden": "Le type de média « {mime} » n’est pas autorisé", + "error.file.mime.invalid": "Type de média invalide : {mime}", + "error.file.mime.missing": "Le type de média de « {filename} » n’a pu être détecté", + "error.file.minheight": "La hauteur de l'image doit être au moins {height} pixels", + "error.file.minsize": "Le fichier n'est pas assez volumineux", + "error.file.minwidth": "La largeur de l'image doit être au moins {width} pixels", + "error.file.name.missing": "Veuillez entrer un titre", + "error.file.notFound": "Le fichier « {filename} » n’a pu être trouvé", + "error.file.orientation": "L'orientation de l'image doit être \"{orientation}\"", + "error.file.type.forbidden": "Vous n’êtes pas autorisé à transférer des fichiers {type}", + "error.file.undefined": "Le fichier n’a pu être trouvé", + + "error.form.incomplete": "Veuillez corriger toutes les erreurs du formulaire…", + "error.form.notSaved": "Le formulaire n’a pu être enregistré", + + "error.language.code": "Veuillez saisir un code valide pour cette langue", + "error.language.duplicate": "Cette langue existe déjà", + "error.language.name": "Veuillez saisir un nom valide pour cette langue", + + "error.license.format": "Veuillez saisir un numéro de licence valide", + "error.license.email": "Veuillez saisir un courriel valide", + "error.license.verification": "La licence n’a pu être vérifiée", + + "error.page.changeSlug.permission": "Vous n’êtes pas autorisé à modifier l’identifiant d’URL pour « {slug} »", + "error.page.changeStatus.incomplete": "La page comporte des erreurs et ne peut pas être publiée", + "error.page.changeStatus.permission": "Le statut de cette page ne peut être modifié", + "error.page.changeStatus.toDraft.invalid": "La page « {slug} » ne peut être convertie en brouillon", + "error.page.changeTemplate.invalid": "Le modèle de la page « {slug} » ne peut être changé", + "error.page.changeTemplate.permission": "Vous n’êtes pas autorisé à changer le modèle de « {slug} »", + "error.page.changeTitle.empty": "Le titre ne peut être vide", + "error.page.changeTitle.permission": "Vous n’êtes pas autorisé à modifier le titre de « {slug} »", + "error.page.create.permission": "Vous n’êtes pas autorisé à créer « {slug} »", + "error.page.delete": "La page « {slug} » ne peut être supprimée", + "error.page.delete.confirm": "Veuillez saisir le titre de la page pour confirmer", + "error.page.delete.hasChildren": "La page comporte des sous-pages et ne peut pas être supprimée", + "error.page.delete.permission": "Vous n’êtes pas autorisé à supprimer « {slug} »", + "error.page.draft.duplicate": "Un brouillon avec l’identifiant d’URL « {slug} » existe déjà", + "error.page.duplicate": "Une page avec l’identifiant d’URL « {slug} » existe déjà", + "error.page.duplicate.permission": "Vous n'êtes pas autorisé à dupliquer « {slug} »", + "error.page.notFound": "La page « {slug} » n’a pu être trouvée", + "error.page.num.invalid": "Veuillez saisir un numéro de position valide. Les numéros ne doivent pas être négatifs.", + "error.page.slug.invalid": "Veuillez saisir un préfixe d’URL valide", + "error.page.slug.maxlength": "L’identifiant d’URL doit faire moins de \"{length}\" caractères", + "error.page.sort.permission": "La page « {slug} » ne peut être réordonnée", + "error.page.status.invalid": "Veuillez choisir un statut de page valide", + "error.page.undefined": "La page n’a pu être trouvée", + "error.page.update.permission": "Vous n’êtes pas autorisé à modifier « {slug} »", + + "error.section.files.max.plural": "Vous ne pouvez ajouter plus de {max} fichier(s) à la section « {section} »", + "error.section.files.max.singular": "Vous ne pouvez ajouter plus d’un fichier à la section « {section} »", + "error.section.files.min.plural": "La section « {section}\" » requiert au moins {min} fichiers", + "error.section.files.min.singular": "La section « {section}\" » requiert au moins un fichier", + + "error.section.pages.max.plural": "Vous ne pouvez ajouter plus de {max} pages à la section « {section} »", + "error.section.pages.max.singular": "Vous ne pouvez ajouter plus d’une page à la section « {section} »", + "error.section.pages.min.plural": "La section « {section}\" » requiert au moins {min} pages", + "error.section.pages.min.singular": "La section « {section}\" » requiert au moins une page", + + "error.section.notLoaded": "La section « {name} » n’a pu être chargée", + "error.section.type.invalid": "Le type de section « {type} » est incorrect", + + "error.site.changeTitle.empty": "Le titre ne peut être vide", + "error.site.changeTitle.permission": "Vous n’êtes pas autorisé à modifier le titre du site", + "error.site.update.permission": "Vous n’êtes pas autorisé à modifier le contenu global du site", + + "error.template.default.notFound": "Le modèle par défaut n’existe pas", + + "error.user.changeEmail.permission": "Vous n’êtes pas autorisé à modifier le courriel de l’utilisateur « {name} »", + "error.user.changeLanguage.permission": "Vous n’êtes pas autorisé à changer la langue de l’utilisateur « {name} »", + "error.user.changeName.permission": "Vous n’êtes pas autorisé à modifier le nom de l’utilisateur « {name} »", + "error.user.changePassword.permission": "Vous n’êtes pas autorisé à changer le mot de passe de l’utilisateur « {name} »", + "error.user.changeRole.lastAdmin": "Le rôle du dernier administrateur ne peut être modifié", + "error.user.changeRole.permission": "Vous n’êtes pas autorisé à changer le rôle de l’utilisateur « {name} »", + "error.user.changeRole.toAdmin": "Vous n’êtes pas autorisé à attribuer le rôle d’administrateur aux utilisateurs", + "error.user.create.permission": "Vous n’êtes pas autorisé à créer cet utilisateur", + "error.user.delete": "L’utilisateur « {name} » ne peut être supprimé", + "error.user.delete.lastAdmin": "Le dernier administrateur ne peut être supprimé", + "error.user.delete.lastUser": "Le dernier utilisateur ne peut être supprimé", + "error.user.delete.permission": "Vous n’êtes pas autorisé à supprimer l’utilisateur « {name} »", + "error.user.duplicate": "Un utilisateur avec le courriel « {email} » existe déjà", + "error.user.email.invalid": "Veuillez saisir un courriel valide", + "error.user.language.invalid": "Veuillez saisir une langue valide", + "error.user.notFound": "L’utilisateur « {name} » n’a pu être trouvé", + "error.user.password.invalid": "Veuillez saisir un mot de passe valide. Les mots de passe doivent comporter au moins 8 caractères.", + "error.user.password.notSame": "Les mots de passe ne sont pas identiques", + "error.user.password.undefined": "Cet utilisateur n’a pas de mot de passe", + "error.user.role.invalid": "Veuillez saisir un rôle valide", + "error.user.update.permission": "Vous n’êtes pas autorisé à modifier l’utilisateur « {name} »", + + "error.validation.accepted": "Veuillez confirmer", + "error.validation.alpha": "Veuillez saisir uniquement des caractères alphabétiques minuscules", + "error.validation.alphanum": "Veuillez ne saisir que des minuscules de a à z et des chiffres de 0 à 9", + "error.validation.between": "Veuillez saisir une valeur entre « {min} » et « {max} »", + "error.validation.boolean": "Veuillez confirmer ou refuser", + "error.validation.contains": "Veuillez saisir une valeur contenant « {needle} »", + "error.validation.date": "Veuillez saisir une date valide", + "error.validation.date.after": "Veuillez saisir une date après {date}", + "error.validation.date.before": "Veuillez saisir une date avant {date}", + "error.validation.date.between": "Veuillez saisir une date entre {min} et {max}", + "error.validation.denied": "Veuillez refuser", + "error.validation.different": "La valeur ne doit pas être « {other} »", + "error.validation.email": "Veuillez saisir un courriel valide", + "error.validation.endswith": "La valeur doit se terminer par « {end} »", + "error.validation.filename": "Veuillez saisir un nom de fichier valide", + "error.validation.in": "Veuillez saisir l’un des éléments suivants: ({in})", + "error.validation.integer": "Veuillez saisir un entier valide", + "error.validation.ip": "Veuillez saisir une adresse IP valide", + "error.validation.less": "Veuillez saisir une valeur inférieure à {max}", + "error.validation.match": "La valeur ne correspond pas au modèle attendu", + "error.validation.max": "Veuillez saisir une valeur inférieure ou égale à {max}", + "error.validation.maxlength": "Veuillez saisir une valeur plus courte (max. {max} caractères)", + "error.validation.maxwords": "Veuillez ne pas saisir plus de {max} mot(s)", + "error.validation.min": "Veuillez saisir une valeur supérieure ou égale à {min}", + "error.validation.minlength": "Veuillez saisir une valeur plus longue (min. {min} caractères)", + "error.validation.minwords": "Veuillez saisir au moins {min} mot(s)", + "error.validation.more": "Veuillez saisir une valeur supérieure à {min}", + "error.validation.notcontains": "Veuillez saisir une valeur ne contenant pas « {needle} »", + "error.validation.notin": "Veuillez ne saisir aucun des éléments suivants: ({notIn})", + "error.validation.option": "Veuillez sélectionner une option valide", + "error.validation.num": "Veuillez saisir un nombre valide", + "error.validation.required": "Veuillez saisir quelque chose", + "error.validation.same": "Veuillez saisir « {other} »", + "error.validation.size": "La grandeur de la valeur doit être « {size} »", + "error.validation.startswith": "La valeur doit commencer par « {start} »", + "error.validation.time": "Veuillez saisir une heure valide", + "error.validation.url": "Veuillez saisir une URL valide", + + "field.required": "Le champ est obligatoire", + "field.files.empty": "Pas encore de fichier sélectionné", + "field.pages.empty": "Pas encore de page sélectionnée", + "field.structure.delete.confirm": "Voulez-vous vraiment supprimer cette ligne?", + "field.structure.empty": "Pas encore d’entrée", + "field.users.empty": "Pas encore d’utilisateur sélectionné", + + "file.delete.confirm": "Voulez-vous vraiment supprimer
{filename} ?", + + "files": "Fichiers", + "files.empty": "Pas encore de fichier", + + "hour": "Heure", + "insert": "Insérer", + "install": "Installer", + + "installation": "Installation", + "installation.completed": "Le Panel a été installé", + "installation.disabled": "L'installation du Panel est désactivée par défaut sur les serveurs publics. Veuillez lancer l'installation sur un serveur local, ou activez-la avec l'option panel.install.", + "installation.issues.accounts": "Le dossier /site/accounts n’existe pas ou n’est pas accessible en écriture", + "installation.issues.content": "Le dossier /content n’existe pas ou n’est pas accessible en écriture", + "installation.issues.curl": "L’extension CURL est requise", + "installation.issues.headline": "Le Panel ne peut être installé", + "installation.issues.mbstring": "L’extension MB String est requise", + "installation.issues.media": "Le dossier /media n’existe pas ou n’est pas accessible en écriture", + "installation.issues.php": "Veuillez utiliser PHP 7+", + "installation.issues.server": "Kirby requiert Apache, Nginx ou Caddy", + "installation.issues.sessions": "Le dossier /site/sessions n’existe pas ou n’est pas accessible en écriture", + + "language": "Langue", + "language.code": "Code", + "language.convert": "Choisir comme langue par défaut", + "language.convert.confirm": "

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

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

", + "language.create": "Ajouter une nouvelle langue", + "language.delete.confirm": "Voulez-vous vraiment supprimer la langue {name}, ainsi que toutes ses traductions ? Cette action ne peut être annulée !", + "language.deleted": "La langue a été supprimée", + "language.direction": "Sens de lecture", + "language.direction.ltr": "De gauche à droite", + "language.direction.rtl": "De droite à gauche", + "language.locale": "Locales PHP", + "language.locale.warning": "Vous utilisez une Locale PHP personnalisée. Veuillez la modifier dans le fichier de langue situé dans /site/languages", + "language.name": "Nom", + "language.updated": "La langue a été mise à jour", + + "languages": "Langages", + "languages.default": "Langue par défaut", + "languages.empty": "Il n’y a pas encore de langues", + "languages.secondary": "Langues secondaires", + "languages.secondary.empty": "Il n’y a pas encore de langues secondaires", + + "license": "Licence", + "license.buy": "Acheter une licence", + "license.register": "S’enregistrer", + "license.register.help": "Vous avez reçu votre numéro de licence par courriel après l'achat. Veuillez le copier et le coller ici pour l'enregistrer.", + "license.register.label": "Veuillez saisir votre numéro de licence", + "license.register.success": "Merci pour votre soutien à Kirby", + "license.unregistered": "Ceci est une démo non enregistrée de Kirby", + + "link": "Lien", + "link.text": "Texte du lien", + + "loading": "Chargement", + + "lock.unsaved": "Modifications non enregistrées", + "lock.unsaved.empty": "Il n’y a plus de modifications non enregistrées", + "lock.isLocked": "Modifications non enregistrées par {email}", + "lock.file.isLocked": "Le fichier est actuellement édité par {email} et ne peut être modifié.", + "lock.page.isLocked": "La page est actuellement éditée par {email} et ne peut être modifiée.", + "lock.unlock": "Déverrouiller", + "lock.isUnlocked": "Vos modifications non enregistrées ont été écrasées pas un autre utilisateur. Vous pouvez télécharger vos modifications pour les fusionner manuellement.", + + "login": "Se connecter", + "login.remember": "Rester connecté", + + "logout": "Se déconnecter", + + "menu": "Menu", + "meridiem": "AM/PM", + "mime": "Type de médias", + "minutes": "Minutes", + + "month": "Mois", + "months.april": "Avril", + "months.august": "Août", + "months.december": "Décembre", + "months.february": "Février", + "months.january": "Janvier", + "months.july": "Juillet", + "months.june": "Juin", + "months.march": "Mars", + "months.may": "Mai", + "months.november": "Novembre", + "months.october": "Octobre", + "months.september": "Septembre", + + "more": "Plus", + "name": "Nom", + "next": "Suivant", + "off": "off", + "on": "on", + "open": "Ouvrir", + "options": "Options", + "options.none": "Pas d’options", + + "orientation": "Orientation", + "orientation.landscape": "Paysage", + "orientation.portrait": "Portrait", + "orientation.square": "Carré", + + "page.changeSlug": "Modifier l’URL", + "page.changeSlug.fromTitle": "Créer à partir du titre", + "page.changeStatus": "Changer le statut", + "page.changeStatus.position": "Veuillez sélectionner une position", + "page.changeStatus.select": "Sélectionner un nouveau statut", + "page.changeTemplate": "Changer de modèle", + "page.delete.confirm": "Voulez-vous vraiment supprimer {title} ?", + "page.delete.confirm.subpages": "Cette page contient des sous-pages.
Toutes les sous-pages seront également supprimées.", + "page.delete.confirm.title": "Veuillez saisir le titre de la page pour confirmer", + "page.draft.create": "Créer un brouillon", + "page.duplicate.appendix": "Copier", + "page.duplicate.files": "Copier les fichiers", + "page.duplicate.pages": "Copier les pages", + "page.status": "Statut", + "page.status.draft": "Brouillon", + "page.status.draft.description": "Cette page est un brouillon et n’est visible que pour les éditeurs connectés ou par un lien secret", + "page.status.listed": "Public", + "page.status.listed.description": "La page est publique pour tout le monde", + "page.status.unlisted": "Non listé", + "page.status.unlisted.description": "La page est uniquement accessible par son URL", + + "pages": "Pages", + "pages.empty": "Pas encore de pages", + "pages.status.draft": "Brouillons", + "pages.status.listed": "Publié", + "pages.status.unlisted": "Non listé", + + "pagination.page": "Page", + + "password": "Mot de passe", + "pixel": "Pixel", + "prev": "Précédent", + "remove": "Supprimer", + "rename": "Renommer", + "replace": "Remplacer", + "retry": "Essayer à nouveau", + "revert": "Revenir", + "revert.confirm": "Voulez-vous vraiment supprimer toutes les modifications non-enregistrées ?", + + "role": "Rôle", + "role.admin.description": "L’administrateur dispose de tous les droits", + "role.admin.title": "Administrateur", + "role.all": "Tous", + "role.empty": "Il n’y a aucun utilisateur avec ce rôle", + "role.description.placeholder": "Pas de description", + "role.nobody.description": "Ceci est un rôle de secours sans aucune permission.", + "role.nobody.title": "Personne", + + "save": "Enregistrer", + "search": "Rechercher", + "search.min": "Entrez {min} caractères pour rechercher", + "search.all": "Tout afficher", + "search.results.none": "Pas de résultats", + + "section.required": "Cette section est obligatoire", + + "select": "Sélectionner", + "settings": "Paramètres", + "size": "Poids", + "slug": "Identifiant de l’URL", + "sort": "Trier", + "title": "Titre", + "template": "Modèle", + "today": "Aujourd’hui", + + "toolbar.button.code": "Code", + "toolbar.button.bold": "Gras", + "toolbar.button.email": "Courriel", + "toolbar.button.headings": "Titres", + "toolbar.button.heading.1": "Titre 1", + "toolbar.button.heading.2": "Titre 2", + "toolbar.button.heading.3": "Titre 3", + "toolbar.button.italic": "Italique", + "toolbar.button.file": "Fichier", + "toolbar.button.file.select": "Sélectionner un fichier", + "toolbar.button.file.upload": "Transférer un fichier", + "toolbar.button.link": "Lien", + "toolbar.button.ol": "Liste ordonnée", + "toolbar.button.ul": "Liste non-ordonnée", + + "translation.author": "Kirby Team", + "translation.direction": "ltr", + "translation.name": "Français", + "translation.locale": "fr_FR", + + "upload": "Transférer", + "upload.error.cantMove": "Le fichier transféré n’a pu être déplacé", + "upload.error.cantWrite": "Le fichier n’a pu être écrit sur le disque", + "upload.error.default": "Le fichier n’a pu être transféré", + "upload.error.extension": "Le transfert de fichier a été stoppé par une extension", + "upload.error.formSize": "Le fichier transféré excède la directive MAX_FILE_SIZE spécifiée dans le formulaire", + "upload.error.iniPostSize": "Le fichier transféré excède la directive post_max_size spécifiée dans php.ini", + "upload.error.iniSize": "Le fichier transféré excède la directive upload_max_filesize spécifiée dans php.ini", + "upload.error.noFile": "Aucun fichier n’a été transféré", + "upload.error.noFiles": "Aucun fichier n’a été transféré", + "upload.error.partial": "Le fichier n’a été que partiellement transféré", + "upload.error.tmpDir": "Un dossier temporaire est manquant", + "upload.errors": "Erreur", + "upload.progress": "Transfert en cours…", + + "url": "Url", + "url.placeholder": "https://example.com", + + "user": "Utilisateur", + "user.blueprint": "Vous pouvez définir des sections et des champs de formulaire supplémentaires pour ce rôle d’utilisateur dans /site/blueprints/users/{role}.yml", + "user.changeEmail": "Modifier le courriel", + "user.changeLanguage": "Modifier la langue", + "user.changeName": "Renommer cet utilisateur", + "user.changePassword": "Modifier le mot de passe", + "user.changePassword.new": "Nouveau mot de passe", + "user.changePassword.new.confirm": "Confirmer le nouveau mot de passe…", + "user.changeRole": "Modifier le rôle", + "user.changeRole.select": "Sélectionner un nouveau rôle", + "user.create": "Ajouter un nouvel utilisateur", + "user.delete": "Supprimer cet utilisateur", + "user.delete.confirm": "Voulez-vous vraiment supprimer
{email}?", + + "users": "Utilisateurs", + + "version": "Version", + + "view.account": "Votre compte", + "view.installation": "Installation", + "view.settings": "Paramètres", + "view.site": "Site", + "view.users": "Utilisateurs", + + "welcome": "Bienvenue", + "year": "Année" +} diff --git a/kirby/i18n/translations/hu.json b/kirby/i18n/translations/hu.json new file mode 100644 index 0000000..e2d5549 --- /dev/null +++ b/kirby/i18n/translations/hu.json @@ -0,0 +1,428 @@ +{ + "add": "Hozz\u00e1ad", + "avatar": "Profilkép", + "back": "Vissza", + "cancel": "M\u00e9gsem", + "change": "M\u00f3dos\u00edt\u00e1s", + "close": "Bez\u00e1r", + "confirm": "Mentés", + "copy": "Másol", + "create": "Létrehoz", + + "date": "Dátum", + "date.select": "Dátum kiválasztása", + + "day": "Nap", + "days.fri": "p\u00e9", + "days.mon": "h\u00e9", + "days.sat": "szo", + "days.sun": "va", + "days.thu": "cs\u00fc", + "days.tue": "ke", + "days.wed": "sze", + + "delete": "T\u00f6rl\u00e9s", + "dimensions": "Méretek", + "disabled": "Disabled", + "discard": "Visszavon\u00e1s", + "download": "Letöltés", + "duplicate": "Másolat", + "edit": "Aloldal szerkeszt\u00e9se", + + "dialog.files.empty": "Nincsenek fájlok kiválasztva", + "dialog.pages.empty": "Nincsenek oldalak kiválasztva", + "dialog.users.empty": "Nincsenek felhasználók kiválasztva", + + "email": "Email", + "email.placeholder": "mail@pelda.hu", + + "error.access.login": "Érvénytelen bejelentkezés", + "error.access.panel": "Nincs jogosultságod megnyitni a panelt", + "error.access.view": "You are not allowed to access this part of the panel", + + "error.avatar.create.fail": "A profilkép feltöltése nem sikerült", + "error.avatar.delete.fail": "A profilkép nem törölhető", + "error.avatar.dimensions.invalid": "A profilkép maximális szélessége és magassága 3000 pixel lehet", + "error.avatar.mime.forbidden": "A profilkép formátuma csak JPEG vagy PNG lehet", + + "error.blueprint.notFound": "A \"{name}\" oldalsablon nem tölthető be", + + "error.email.preset.notFound": "A \"{name}\" email-beállítás nem található", + + "error.field.converter.invalid": "Érvénytelen konverter: \"{converter}\"", + + "error.file.changeName.empty": "The name must not be empty", + "error.file.changeName.permission": "Nincs jogosultságod megváltoztatni a \"{filename}\" fájl nevét", + "error.file.duplicate": "Már létezik \"{filename}\" nevű fájl", + "error.file.extension.forbidden": "Tiltott kiterjeszt\u00e9s\u0171 f\u00e1jl", + "error.file.extension.missing": "Kiterjeszt\u00e9s n\u00e9lk\u00fcli f\u00e1jl nem t\u00f6lthet\u0151 fel", + "error.file.maxheight": "The height of the image must not exceed {height} pixels", + "error.file.maxsize": "The file is too large", + "error.file.maxwidth": "The width of the image must not exceed {width} pixels", + "error.file.mime.differs": "A feltöltött fájlnak azonos \"{mime}\" típusúnak kell lennie", + "error.file.mime.forbidden": "A \"{mime}\" típusú médiafájlok nem engedélyezettek", + "error.file.mime.invalid": "Invalid mime type: {mime}", + "error.file.mime.missing": "A \"{filename}\" fájl típusa nem állapítható meg", + "error.file.minheight": "The height of the image must be at least {height} pixels", + "error.file.minsize": "The file is too small", + "error.file.minwidth": "The width of the image must be at least {width} pixels", + "error.file.name.missing": "A fálj neve nem lehet üres", + "error.file.notFound": "A \"{filename}\" fájl nem található", + "error.file.orientation": "The orientation of the image must be \"{orientation}\"", + "error.file.type.forbidden": "Nem tölthetsz fel \"{type}\" típusú fájlokat", + "error.file.undefined": "A f\u00e1jl nem tal\u00e1lhat\u00f3", + + "error.form.incomplete": "Kérlek javítsd ki az összes hibát az űrlapon", + "error.form.notSaved": "Az űrlap nem menthető", + + "error.language.code": "Kérlek, add meg a nyelv érvényes kódját", + "error.language.duplicate": "A nyelv már létezik", + "error.language.name": "Kérlek, add meg a nyelv érvényes nevét", + + "error.license.format": "Kérlek, add meg az évényes lincensz kulcsot", + "error.license.email": "Kérlek adj meg egy valós email-címet", + "error.license.verification": "A licensz nem ellenőrizhető", + + "error.page.changeSlug.permission": "Nem változtathatod meg az URL-előtagot: \"{slug}\"", + "error.page.changeStatus.incomplete": "Az oldal hibákat tartalmaz és nem publikálható", + "error.page.changeStatus.permission": "Az oldal státusza nem változtatható meg", + "error.page.changeStatus.toDraft.invalid": "A(z) \"{slug}\" oldalt nem lehet piszkozattá alakítani", + "error.page.changeTemplate.invalid": "A \"{slug}\" oldal sablonját nem lehet megváltoztatni", + "error.page.changeTemplate.permission": "Nincs jogosultságod megváltoztatni a sablont ehhez: \"{slug}\"", + "error.page.changeTitle.empty": "A cím nem lehet üres", + "error.page.changeTitle.permission": "Nincs jogosultságod megváltoztatni a címet: \"{slug}\"", + "error.page.create.permission": "Nincs jogosultságod az oldal létrehozásához: \"{slug}\"", + "error.page.delete": "A(z) \"{slug}\" oldal nem törölhető", + "error.page.delete.confirm": "Megerősítéshez add meg az oldal címét", + "error.page.delete.hasChildren": "Az oldalnak vannak aloldalai és nem törölhető", + "error.page.delete.permission": "Nincs jogosultságod a(z) \"{slug}\" oldal törléséhez", + "error.page.draft.duplicate": "Van már egy másik oldal ezzel az URL-lel: \"{slug}\"", + "error.page.duplicate": "Van már egy másik oldal ezzel az URL-lel: \"{slug}\"", + "error.page.duplicate.permission": "Nincs engedélyed a(z) \"{slug}\" másolat keszítéséhez", + "error.page.notFound": "Az oldal nem tal\u00e1lhat\u00f3", + "error.page.num.invalid": "Kérlek megfelelő oldalszámozást adj meg. Negatív szám itt nem használható.", + "error.page.slug.invalid": "Kérlek megfelelő URL-előtagot adj meg", + "error.page.slug.maxlength": "Slug length must be less than \"{length}\" characters", + "error.page.sort.permission": "A(z) \"{slug}\" oldal nem illeszthető a sorrendbe", + "error.page.status.invalid": "Kérlek add meg a megfelelő oldalstátuszt", + "error.page.undefined": "Az oldal nem tal\u00e1lhat\u00f3", + "error.page.update.permission": "Nincs jogosultságod a(z) \"{slug}\" oldal frissítéséhez", + + "error.section.files.max.plural": "Maximum {max} fájlt adhatsz hozzá a(z) \"{section}\" szekcióhoz", + "error.section.files.max.singular": "Nem adhatsz hozzá egynél több fájlt a(z) \"{section}\" szekcióhoz", + "error.section.files.min.plural": "A \"{section}\" szakasz legalább {min} fájlt igényel", + "error.section.files.min.singular": "A \"{section}\" szakasz legalább egy fájlt igényel", + + "error.section.pages.max.plural": "Maximum {max} oldalt adhatsz hozzá a(z) \"{section}\" szekcióhoz", + "error.section.pages.max.singular": "Nem adhatsz hozzá egynél több oldalt a(z) \"{section}\" szekcióhoz", + "error.section.pages.min.plural": "A \"{section}\" szakasz legalább {min} oldalt igényel", + "error.section.pages.min.singular": "A \"{section}\" szakasz legalább egy oldalt igényel", + + "error.section.notLoaded": "A(z) \"{name}\" szekció nem tölthető be", + "error.section.type.invalid": "A szekció típusa (\"{type}\") nem megfelelő", + + "error.site.changeTitle.empty": "A cím nem lehet üres", + "error.site.changeTitle.permission": "Nincs jogosultságod megváltoztatni az honlap címét", + "error.site.update.permission": "Nincs jogosultságod frissíteni a honlapot", + + "error.template.default.notFound": "Az alapértelmezett sablon nem létezik", + + "error.user.changeEmail.permission": "Nincs jogosultságod megváltoztatni \"{name}\" felhasználó email-címét", + "error.user.changeLanguage.permission": "Nincs jogosultságod megváltoztatni \"{name}\" felhasználó nyelvi beállításait", + "error.user.changeName.permission": "Nincs jogosultságod megváltoztatni \"{name}\" felhasználó nevét", + "error.user.changePassword.permission": "Nincs jogosultságod megváltoztatni \"{name}\" felhasználó jelszavát", + "error.user.changeRole.lastAdmin": "Az egyedüli adminisztrátor szerepkörét nem lehet megváltoztatni", + "error.user.changeRole.permission": "Nincs jogosultságod megváltoztatni \"{name}\" felhasználó szerepkörét", + "error.user.changeRole.toAdmin": "You are not allowed to promote someone to the admin role", + "error.user.create.permission": "Nincs jogosultságod létrehozni ezt a felhasználót", + "error.user.delete": "A felhaszn\u00e1l\u00f3 nem t\u00f6r\u00f6lhet\u0151", + "error.user.delete.lastAdmin": "Nem t\u00f6r\u00f6lheted az egyetlen adminisztr\u00e1tort", + "error.user.delete.lastUser": "Nem törölheted az egyetlen felhasználót", + "error.user.delete.permission": "Nincs jogosults\u00e1god t\u00f6r\u00f6lni ezt a felhaszn\u00e1l\u00f3t", + "error.user.duplicate": "Már létezik felhasználó \"{email}\" email-címmel", + "error.user.email.invalid": "Kérlek adj meg egy valós email-címet", + "error.user.language.invalid": "Kérlek add meg a megfelelő nyelvi beállítást", + "error.user.notFound": "A felhaszn\u00e1l\u00f3 nem tal\u00e1lhat\u00f3", + "error.user.password.invalid": "Kérlek adj meg egy megfelelő jelszót. A jelszónak legalább 8 karakter hosszúságúnak kell lennie.", + "error.user.password.notSame": "K\u00e9rlek er\u0151s\u00edtsd meg a jelsz\u00f3t", + "error.user.password.undefined": "A felhasználónak nincs jelszó megadva", + "error.user.role.invalid": "Kérlek adj meg egy megfelelő szerepkört", + "error.user.update.permission": "Nincs jogosultságod frissíteni \"{name}\" felhasználó adatait", + + "error.validation.accepted": "Kérlek erősítsd meg", + "error.validation.alpha": "Kérlek csak kis betűket használj (a-z)", + "error.validation.alphanum": "Kérlek csak kis betűket és számjegyeket használj (a-z, 0-9)", + "error.validation.between": "Kérlek egy \"{min}\" és \"{max}\" közötti értéket adj meg", + "error.validation.boolean": "Kérlek erősítsd meg vagy vesd el", + "error.validation.contains": "Kérlek olyan értéket adj meg, amely tartalmazza ezt: \"{needle}\"", + "error.validation.date": "Kérlek megfelelő dátumot adj meg", + "error.validation.date.after": "Please enter a date after {date}", + "error.validation.date.before": "Please enter a date before {date}", + "error.validation.date.between": "Please enter a date between {min} and {max}", + "error.validation.denied": "Kérlek vesd el", + "error.validation.different": "Az érték nem lehet \"{other}\"", + "error.validation.email": "Kérlek adj meg egy valós email-címet", + "error.validation.endswith": "Az értéknek erre kell végződnie: \"{end}\"", + "error.validation.filename": "Kérlek megfelelő fájlnevet adj meg", + "error.validation.in": "Kérlek adj meg egyet az alábbiak közül: ({in})", + "error.validation.integer": "Kérlek valós számot adj meg", + "error.validation.ip": "Kérlek megfelelő IP-címet adj meg", + "error.validation.less": "A megadott érték kevesebb legyen, mint {max}", + "error.validation.match": "A megadott érték nem felel meg az elvárt struktúrának", + "error.validation.max": "A megadott érték egyenlő vagy kevesebb legyen, mint {max}", + "error.validation.maxlength": "Kérlek rövidebb értéket adj meg (legfeljebb {max} karakter)", + "error.validation.maxwords": "Kérlek ide legfeljebb {max} szót írj", + "error.validation.min": "A megadott érték egyenlő vagy nagyobb legyen, mint {min}", + "error.validation.minlength": "Kérlek hosszabb értéket adj meg (legalább {min} karakter)", + "error.validation.minwords": "Kérlek ide legalább {min} szót írj", + "error.validation.more": "A megadott érték legyen nagyobb, mint {min} ", + "error.validation.notcontains": "Kérlek olyan értéket adj meg, amely nem tartalmazza ezt: \"{needle}\" ", + "error.validation.notin": "Kérlek egyiket se használd az alábbiak közül: ({notIn})", + "error.validation.option": "Kérlek válassz egy megfelelő opciót", + "error.validation.num": "Kérlek adj meg egy megfelelő számot", + "error.validation.required": "Kérlek írj be valamit", + "error.validation.same": "Kérlek írd be: \"{other}\"", + "error.validation.size": "Az értéknek az alábbi méretűnek kell lennie: \"{size}\"", + "error.validation.startswith": "Az értéknek ezzel kell kezdődnie: \"{start}\"", + "error.validation.time": "Kérlek megfelelő időt adj meg", + "error.validation.url": "Kérlek megfelelő URL-t adj meg", + + "field.required": "The field is required", + "field.files.empty": "Nincs fálj kiválasztva", + "field.pages.empty": "Nincs oldal kiválasztva", + "field.structure.delete.confirm": "Biztos t\u00f6r\u00f6lni szeretn\u00e9d ezt a bejegyz\u00e9st?", + "field.structure.empty": "Nincs m\u00e9g bejegyz\u00e9s", + "field.users.empty": "Nincs felhasználó kiválasztva", + + "file.delete.confirm": "Biztos törölni akarod ezt a fájlt:
{filename}?", + + "files": "Fájlok", + "files.empty": "Még nincsenek fájlok", + + "hour": "Óra", + "insert": "Beilleszt", + "install": "Telepítés", + + "installation": "Telepítés", + "installation.completed": "A panel sikeresen telepítve", + "installation.disabled": "A panel telepítője alapértelmezés szerint le van tiltva a nyilvános szervereken. Kérlek, futtassd a telepítőt egy helyi gépen vagy engedélyezze a panel.install opcióval.", + "installation.issues.accounts": "A /site/accounts mappa nem létezik, vagy nem írható", + "installation.issues.content": "A /content mappa nem létezik vagy nem írható", + "installation.issues.curl": "A CURL bővítmény engedélyezése szükséges", + "installation.issues.headline": "A panel telepítése sikertelen", + "installation.issues.mbstring": "Az MB String bővítmény engedélyezése szükséges", + "installation.issues.media": "A /media mappa nem létezik vagy nem írható", + "installation.issues.php": "Bizonyosodj meg róla, hogy az általad használt PHP-verzió PHP 7+", + "installation.issues.server": "A Kirby az alábbi szervereken futtatható: Apache, Nginx vagy Caddy", + "installation.issues.sessions": "A /site/sessions könyvtár nem létezik vagy nem írható", + + "language": "Nyelv", + "language.code": "Kód", + "language.convert": "Alapértelmezettnek jelölés", + "language.convert.confirm": "

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

", + "language.create": "Adicionar novo idioma", + "language.delete.confirm": "Deseja realmente excluir o idioma {name} incluíndo todas as traduções? Esta ação não poderá ser revertida!", + "language.deleted": "Idioma excluído", + "language.direction": "Direção de leitura", + "language.direction.ltr": "Esquerda para direita", + "language.direction.rtl": "Direita para esquerda", + "language.locale": "String de localização do PHP", + "language.locale.warning": "Está a usar configurações de localização personalizadas. Corrija as mesmas no ficheiro /site/languages", + "language.name": "Nome", + "language.updated": "Idioma atualizado", + + "languages": "Idiomas", + "languages.default": "Idioma padrão", + "languages.empty": "Nenhum idioma", + "languages.secondary": "Idiomas secundários", + "languages.secondary.empty": "Nenhum idioma secundário", + + "license": "Licen\u00e7a do Kirby ", + "license.buy": "Comprar uma licença", + "license.register": "Registrar", + "license.register.help": "Recebeu o código da sua licença por email após a compra. Por favor, copie e cole-o para completar o registro.", + "license.register.label": "Por favor, digite o código da sua licença", + "license.register.success": "Obrigado por apoiar o Kirby", + "license.unregistered": "Esta é uma demonstração não registrada do Kirby", + + "link": "Link", + "link.text": "Texto do link", + + "loading": "A carregar", + + "lock.unsaved": "Alterações por guardar", + "lock.unsaved.empty": "Não existem alterações por guardar", + "lock.isLocked": "Alterações por guardar de {email}", + "lock.file.isLocked": "O arquivo está a ser editado por {email} e não pode ser alterado.", + "lock.page.isLocked": "A página está a ser editada por {email} e não pode ser alterada.", + "lock.unlock": "Desbloquear", + "lock.isUnlocked": "As suas alterações foram sobrepostas por outro utilizador. Pode descarregar as suas alterações e combiná-las manualmente.", + + "login": "Entrar", + "login.remember": "Manter-me conectado", + + "logout": "Sair", + + "menu": "Menu", + "meridiem": "AM/PM", + "mime": "Tipo de mídia", + "minutes": "Minutos", + + "month": "Mês", + "months.april": "Abril", + "months.august": "Agosto", + "months.december": "Dezembro", + "months.february": "Fevereiro", + "months.january": "Janeiro", + "months.july": "Julho", + "months.june": "Junho", + "months.march": "Mar\u00e7o", + "months.may": "Maio", + "months.november": "Novembro", + "months.october": "Outubro", + "months.september": "Setembro", + + "more": "Mais", + "name": "Nome", + "next": "Próximo", + "off": "off", + "on": "on", + "open": "Abrir", + "options": "Opções", + "options.none": "No options", + + "orientation": "Orientação", + "orientation.landscape": "Paisagem", + "orientation.portrait": "Retrato", + "orientation.square": "Quadrado", + + "page.changeSlug": "Alterar URL", + "page.changeSlug.fromTitle": "Criar a partir do t\u00edtulo", + "page.changeStatus": "Alterar estado", + "page.changeStatus.position": "Selecione uma posição", + "page.changeStatus.select": "Selecione um novo estado", + "page.changeTemplate": "Alterar tema", + "page.delete.confirm": "Deseja realmente excluir {title}?", + "page.delete.confirm.subpages": "Esta página possui subpáginas.
Todas as subpáginas serão excluídas também.", + "page.delete.confirm.title": "Digite o título da página para confirmar", + "page.draft.create": "Criar rascunho", + "page.duplicate.appendix": "Copiar", + "page.duplicate.files": "Copiar arquivos", + "page.duplicate.pages": "Copiar páginas", + "page.status": "Estado", + "page.status.draft": "Rascunho", + "page.status.draft.description": "A página está em modo de rascunho e é visível somente para editores", + "page.status.listed": "Pública", + "page.status.listed.description": "A página é pública para todos", + "page.status.unlisted": "Não listadas", + "page.status.unlisted.description": "Esta página é acessível somente através da URL", + + "pages": "Páginas", + "pages.empty": "Nenhuma página", + "pages.status.draft": "Rascunhos", + "pages.status.listed": "Publicadas", + "pages.status.unlisted": "Não listadas", + + "pagination.page": "Página", + + "password": "Palavra-passe", + "pixel": "Pixel", + "prev": "Anterior", + "remove": "Remover", + "rename": "Renomear", + "replace": "Substituir", + "retry": "Tentar novamente", + "revert": "Descartar", + "revert.confirm": "Tem a certeza que pretende eliminar todas as alterações por guardar?", + + "role": "Função", + "role.admin.description": "O administrador tem todas as permissões.", + "role.admin.title": "Administrador", + "role.all": "Todos", + "role.empty": "Não há utilizadores com esta função", + "role.description.placeholder": "Sem descrição", + "role.nobody.description": "Esta é uma função de salvaguarda sem permissões.", + "role.nobody.title": "Ninguém", + + "save": "Salvar", + "search": "Buscar", + "search.min": "Enter {min} characters to search", + "search.all": "Show all", + "search.results.none": "No results", + + "section.required": "Esta seção é necessária", + + "select": "Selecionar", + "settings": "Configurações", + "size": "Tamanho", + "slug": "URL", + "sort": "Ordenar", + "title": "Título", + "template": "Tema", + "today": "Hoje", + + "toolbar.button.code": "Código", + "toolbar.button.bold": "Negrito", + "toolbar.button.email": "Email", + "toolbar.button.headings": "Títulos", + "toolbar.button.heading.1": "Título 1", + "toolbar.button.heading.2": "Título 2", + "toolbar.button.heading.3": "Título 3", + "toolbar.button.italic": "Itálico", + "toolbar.button.file": "Ficheiro", + "toolbar.button.file.select": "Selecione o arquivo", + "toolbar.button.file.upload": "Carregue o arquivo", + "toolbar.button.link": "Link", + "toolbar.button.ol": "Lista ordenada", + "toolbar.button.ul": "Lista não-ordenada", + + "translation.author": "Kirby Team", + "translation.direction": "ltr", + "translation.name": "Português (Europeu)", + "translation.locale": "pt_PT", + + "upload": "Enviar", + "upload.error.cantMove": "Não foi possível mover o arquivo carregado", + "upload.error.cantWrite": "Não foi possível guardar o arquivo no sistema de ficheiros.", + "upload.error.default": "Não foi possível carregar o arquivo", + "upload.error.extension": "A extensão do arquivo não permite o carregamento", + "upload.error.formSize": "O arquivo excede o tamanho MAX_FILE_SIZE", + "upload.error.iniPostSize": "O arquivo excede o tamanho post_max_size", + "upload.error.iniSize": "O arquivo carregado excede a definição upload_max_filesize do php.ini", + "upload.error.noFile": "Nenhum arquivo carregado", + "upload.error.noFiles": "Nenhuns arquivos carregados", + "upload.error.partial": "O arquivo foi parcialmente carregado", + "upload.error.tmpDir": "Pasta temporária em falta", + "upload.errors": "Erro", + "upload.progress": "A enviar…", + + "url": "Url", + "url.placeholder": "https://exemplo.pt", + + "user": "Utilizador", + "user.blueprint": "Pode definir seções e campos de formulário adicionais para esta função de utilizador em /site/blueprints/users/{role}.yml", + "user.changeEmail": "Alterar email", + "user.changeLanguage": "Alterar idioma", + "user.changeName": "Renomear este utilizador", + "user.changePassword": "Alterar palavra-passe", + "user.changePassword.new": "Nova palavra-passe", + "user.changePassword.new.confirm": "Confirme a nova palavra-passe…", + "user.changeRole": "Alterar Função", + "user.changeRole.select": "Selecione uma nova função", + "user.create": "Adicionar novo utilizador", + "user.delete": "Excluir este utilizador", + "user.delete.confirm": "Deseja realmente excluir
{email}?", + + "users": "Utilizadores", + + "version": "Vers\u00e3o do Kirby", + + "view.account": "A sua conta", + "view.installation": "Instala\u00e7\u00e3o", + "view.settings": "Configurações", + "view.site": "Site", + "view.users": "Utilizadores", + + "welcome": "Bem-vindo", + "year": "Ano" +} diff --git a/kirby/i18n/translations/ru.json b/kirby/i18n/translations/ru.json new file mode 100644 index 0000000..187901d --- /dev/null +++ b/kirby/i18n/translations/ru.json @@ -0,0 +1,428 @@ +{ + "add": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c", + "avatar": "\u0410\u0432\u0430\u0442\u0430\u0440 (\u0444\u043e\u0442\u043e)", + "back": "Назад", + "cancel": "\u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c", + "change": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c", + "close": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c", + "confirm": "Ок", + "copy": "Скопировать", + "create": "Создать", + + "date": "Дата", + "date.select": "Выберите дату", + + "day": "День", + "days.fri": "\u041f\u0442", + "days.mon": "\u041f\u043d", + "days.sat": "\u0421\u0431", + "days.sun": "\u0412\u0441", + "days.thu": "\u0427\u0442", + "days.tue": "\u0412\u0442", + "days.wed": "\u0421\u0440", + + "delete": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c", + "dimensions": "Размеры", + "disabled": "Отключено", + "discard": "\u0421\u0431\u0440\u043e\u0441", + "download": "Скачать", + "duplicate": "Дублировать", + "edit": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c", + + "dialog.files.empty": "Нет файлов для выбора", + "dialog.pages.empty": "Нет страниц для выбора", + "dialog.users.empty": "Нет пользователей для выбора", + + "email": "Эл. почта", + "email.placeholder": "mail@example.com", + + "error.access.login": "Неправильный логин", + "error.access.panel": "У вас нет права доступа к панели", + "error.access.view": "У вас нет прав доступа к этой части панели", + + "error.avatar.create.fail": "Не удалось загрузить фотографию профиля", + "error.avatar.delete.fail": "\u0410\u0432\u0430\u0442\u0430\u0440 (\u0444\u043e\u0442\u043e) \u043a \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0443 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0443\u0434\u0430\u043b\u0435\u043d", + "error.avatar.dimensions.invalid": "Пожалуйста, сделайте чтобы ширина или высота фотографии была меньше 3000 пикселей", + "error.avatar.mime.forbidden": "Фотография профиля должна быть JPEG или PNG", + + "error.blueprint.notFound": "Не удалось загрузить blueprint \"{name}\"", + + "error.email.preset.notFound": "Шаблон эл. почты \"{name}\" не найден", + + "error.field.converter.invalid": "Неверный конвертер \"{converter}\"", + + "error.file.changeName.empty": "Название не может быть пустым", + "error.file.changeName.permission": "У вас нет права поменять название \"{filename}\"", + "error.file.duplicate": "Файл с названием \"{filename}\" уже есть", + "error.file.extension.forbidden": "Расширение файла \"{extension}\" неразрешено", + "error.file.extension.missing": "Файлу \"{filename}\" не хватает расширения", + "error.file.maxheight": "Высота картинки не должна превышать {height} px", + "error.file.maxsize": "Файл слишком большой", + "error.file.maxwidth": "Ширина картинки не должна превышать {width} px", + "error.file.mime.differs": "Загруженный файл должен быть того же mime типа: \"{mime}\"", + "error.file.mime.forbidden": "Тип медиа \"{mime}\" не допустим", + "error.file.mime.invalid": "Неверный тип mime: {mime}", + "error.file.mime.missing": "Не удалось определить тип медиа для файла \"{filename}\"", + "error.file.minheight": "Высота файла должна быть хотя бы {height} px", + "error.file.minsize": "Файл слишком маленький", + "error.file.minwidth": "Ширина файла должна быть хотя бы {width} px", + "error.file.name.missing": "Название файла не может быть пустым", + "error.file.notFound": "\u0424\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d", + "error.file.orientation": "Ориентация картинки должна быть \"{orientation}\"", + "error.file.type.forbidden": "У вас нет права загружать файлы {type}", + "error.file.undefined": "\u0424\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d", + + "error.form.incomplete": "Пожалуйста, исправьте все ошибки в форме", + "error.form.notSaved": "Форма не может быть сохранена", + + "error.language.code": "Пожалуйста, впишите правильный код языка", + "error.language.duplicate": "Язык уже есть", + "error.language.name": "Пожалуйста, впишите правильное название языка", + + "error.license.format": "Пожалуйста, введите правильный лицензионный код", + "error.license.email": "Пожалуйста, введите правильный адрес эл. почты", + "error.license.verification": "Лицензия не подтверждена", + + "error.page.changeSlug.permission": "\u0412\u044b \u043d\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c URL \u044d\u0442\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b", + "error.page.changeStatus.incomplete": "На странице есть ошибки и поэтому ее нельзя опубликовать", + "error.page.changeStatus.permission": "Невозможно поменять статус для этой страницы", + "error.page.changeStatus.toDraft.invalid": "Невозможно конвертировать в черновик страницу \"{slug}\"", + "error.page.changeTemplate.invalid": "Невозможно поменять шаблон страницы \"{slug}\"", + "error.page.changeTemplate.permission": "У вас нет права поменять шаблон для \"{slug}\"", + "error.page.changeTitle.empty": "Название не может быть пустым", + "error.page.changeTitle.permission": "у вас нет права поменять название \"{slug}\"", + "error.page.create.permission": "У вас нет права создать \"{slug}\"", + "error.page.delete": "Невозможно удалить страницу \"{slug}\"", + "error.page.delete.confirm": "Впишите название страницы чтобы подтвердить", + "error.page.delete.hasChildren": "У страницы есть внутренние страницы, поэтому ее невозможно удалить", + "error.page.delete.permission": "У вас нет права удалить \"{slug}\"", + "error.page.draft.duplicate": "Черновик страницы с аппендиксом URL \"{slug}\" уже есть", + "error.page.duplicate": "Страница с аппендиксом URL \"{slug}\" уже есть", + "error.page.duplicate.permission": "У вас нет права дублировать \"{slug}\"", + "error.page.notFound": "\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430", + "error.page.num.invalid": "Пожалуйста, впишите правильное число сортировки. Число не может быть отрицательным.", + "error.page.slug.invalid": "Пожалуйста, впишите правильный префикс URL", + "error.page.slug.maxlength": "Slug length must be less than \"{length}\" characters", + "error.page.sort.permission": "Невозможно сортировать страницу \"{slug}\"", + "error.page.status.invalid": "Пожалуйста, установите верный статус страницы", + "error.page.undefined": "\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430", + "error.page.update.permission": "У вас нет права обновить \"{slug}\"", + + "error.section.files.max.plural": "Нельзя добавить больше чем {max} файлов в секции \"{section}\"", + "error.section.files.max.singular": "Можно добавить не больше 1 файла в секции \"{section}\"", + "error.section.files.min.plural": "Секция \"{section}\" требует хотя бы {min} файлов", + "error.section.files.min.singular": "Секция \"{section}\" требует хотя бы 1 файл", + + "error.section.pages.max.plural": "Можно добавить не больше {max} страниц в секции \"{section}\"", + "error.section.pages.max.singular": "Нельзя добавить больше чем 1 страницу в секции \"{section}\"", + "error.section.pages.min.plural": "Секция \"{section}\" требует хотя бы {min} страниц", + "error.section.pages.min.singular": "Секция \"{section}\" требует хотя бы одну страницу", + + "error.section.notLoaded": "Секция \"{name}\" не может быть загружена", + "error.section.type.invalid": "Тип секции {type} неверный", + + "error.site.changeTitle.empty": "Название не может быть пустым", + "error.site.changeTitle.permission": "У вас нет права поменять название сайта", + "error.site.update.permission": "У вас нет права обновить сайт", + + "error.template.default.notFound": "Нет шаблона по умолчанию", + + "error.user.changeEmail.permission": "У вас нет права поменять эл. почту пользователя \"{name}\"", + "error.user.changeLanguage.permission": "У вас нет права поменять язык для пользователя \"{name}\"", + "error.user.changeName.permission": "У вас нет права поменять имя пользователя \"{name}\"", + "error.user.changePassword.permission": "У вас нет права поменять пароль для пользователя \"{name}\"", + "error.user.changeRole.lastAdmin": "Роль единственного администратора нельзя поменять", + "error.user.changeRole.permission": "У вас нет права поменять поль для пользователя \"{name}\"", + "error.user.changeRole.toAdmin": "У вас нет прав предоставить роль администратора", + "error.user.create.permission": "У вас нет права создать этого пользователя", + "error.user.delete": "\u0410\u043a\u043a\u0430\u0443\u043d\u0442 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0443\u0434\u0430\u043b\u0435\u043d", + "error.user.delete.lastAdmin": "\u0412\u044b \u043d\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0433\u043e \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430", + "error.user.delete.lastUser": "Нельзя удалить единственного пользователя", + "error.user.delete.permission": "У вас нет права удалить пользователя \"{name}\"", + "error.user.duplicate": "Пользователь с эл. почтой \"{email}\" уже есть", + "error.user.email.invalid": "Пожалуйста, введите правильный адрес эл. почты", + "error.user.language.invalid": "Введите правильный язык", + "error.user.notFound": "\u0410\u043a\u043a\u0430\u0443\u043d\u0442 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d", + "error.user.password.invalid": "Пожалуйста, введите правильный пароль. Он должен состоять минимум из 8 символов.", + "error.user.password.notSame": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c", + "error.user.password.undefined": "У пользователя нет пароля", + "error.user.role.invalid": "Введите правильную роль", + "error.user.update.permission": "У вас нет права обновить пользователя \"{name}\"", + + "error.validation.accepted": "Пожалуйста, подтвердите", + "error.validation.alpha": "Пожалуйста, введите только буквы a-z", + "error.validation.alphanum": "Пожалуйста, введите только буквы a-z или числа 0-9", + "error.validation.between": "Пожалуйста, введите значение от \"{min}\" до \"{max}\"", + "error.validation.boolean": "Пожалуйста, подтвердите или отмените", + "error.validation.contains": "Пожалуйста, впишите значение, которое содержит \"{needle}\"", + "error.validation.date": "Пожалуйста, укажите правильную дату", + "error.validation.date.after": "Пожалуйста, укажите дату после {date}", + "error.validation.date.before": "Пожалуйста, укажите дату до {date}", + "error.validation.date.between": "Пожалуйста, укажите дату между {min} и {max}", + "error.validation.denied": "Пожалуйста отмените", + "error.validation.different": "Значение не может быть \"{other}\"", + "error.validation.email": "Пожалуйста, введите правильный адрес эл. почты", + "error.validation.endswith": "Значение должно заканчиваться с \"{end}\"", + "error.validation.filename": "Пожалуйста, введите правильное название файла", + "error.validation.in": "Пожалуйста, введите одно из следующих: ({in})", + "error.validation.integer": "Пожалуйста, введите правильное целое число", + "error.validation.ip": "Пожалуйста, введите правильный IP адрес", + "error.validation.less": "Пожалуйста, введите значение меньше чем {max}", + "error.validation.match": "Значение не соответствует ожидаемому шаблону", + "error.validation.max": "Пожалуйста, введите значение равное или больше чем {max}", + "error.validation.maxlength": "Пожалуйста, введите значение короче (макс. {max} символов)", + "error.validation.maxwords": "Пожалуйста, введите не более {max} слов ", + "error.validation.min": "Пожалуйста, введите значение равное или больше чем {min}", + "error.validation.minlength": "Пожалуйста, введите значение длиннее (мин. {min} символов)", + "error.validation.minwords": "Пожалуйста, введите хотя бы {min} слов", + "error.validation.more": "Пожалуйста, введите значение больше, чем {min}", + "error.validation.notcontains": "Пожалуйста, введите значение, которое не содержит \"{needle}\"", + "error.validation.notin": "Пожалуйста, не вписывайте одно из: ({notIn})", + "error.validation.option": "Пожалуйста, выберите правильную опцию ", + "error.validation.num": "Пожалуйста, введите правильный номер", + "error.validation.required": "Пожалуйста, введите что-нибудь", + "error.validation.same": "Пожалуйста, введите \"{other}\"", + "error.validation.size": "Значение размера должно быть \"{size}\"", + "error.validation.startswith": "Значение должно начинаться с \"{start}\"", + "error.validation.time": "Пожалуйста, введите правильную дату", + "error.validation.url": "Пожалуйста, введите правильный URL", + + "field.required": "Поле обязательно", + "field.files.empty": "Еще не выбраны файлы", + "field.pages.empty": "Еще не выбраны страницы", + "field.structure.delete.confirm": "Вы точно хотите удалить эту запись?", + "field.structure.empty": "Еще нет записей", + "field.users.empty": "Еще нет пользователей", + + "file.delete.confirm": "Вы точно хотите удалить файл
{filename}?", + + "files": "Файлы", + "files.empty": "Еще нет файлов", + + "hour": "Час", + "insert": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c", + "install": "Установить", + + "installation": "Установка", + "installation.completed": "Панель установлена", + "installation.disabled": "Установка панели по умолчанию отключена на общедоступных серверах. Пожалуйста запустите установку на локальном сервере или включите такую возможность с помощью опции panel.install", + "installation.issues.accounts": "Каталог /site/accounts не существует или не имеет прав записи", + "installation.issues.content": "Каталог /content не существует или не имеет прав записи", + "installation.issues.curl": "Расширение CURL необходимо", + "installation.issues.headline": "Не удалось установить панель", + "installation.issues.mbstring": "Расширение MB String необходимо", + "installation.issues.media": "Каталог /media не существует или нет прав записи", + "installation.issues.php": "Убедитесь, что используется PHP 7+", + "installation.issues.server": "Kirby требует Apache, Nginx или Caddy ", + "installation.issues.sessions": "Каталог /site/sessions не существует или нет прав записи", + + "language": "\u042f\u0437\u044b\u043a", + "language.code": "Код", + "language.convert": "Установить по умолчанию", + "language.convert.confirm": "

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

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

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

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

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

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

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

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

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

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

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

", + "language.create": "Yeni bir dil ekle", + "language.delete.confirm": "Tüm çevirileri içeren {name} dilini gerçekten silmek istiyor musunuz? Bu geri alınamaz!", + "language.deleted": "Dil silindi", + "language.direction": "Okuma yönü", + "language.direction.ltr": "Soldan sağa", + "language.direction.rtl": "Sağdan sola", + "language.locale": "PHP yerel dizesi", + "language.locale.warning": "Özel bir yerel ayar kullanıyorsunuz. Lütfen /site/languages konumundaki dil dosyasından değiştirin.", + "language.name": "İsim", + "language.updated": "Dil güncellendi", + + "languages": "Diller", + "languages.default": "Varsayılan dil", + "languages.empty": "Henüz hiç dil yok", + "languages.secondary": "İkincil diller", + "languages.secondary.empty": "Henüz ikincil bir dil yok", + + "license": "Lisans", + "license.buy": "Bir lisans satın al", + "license.register": "Kayıt Ol", + "license.register.help": "Satın alma işleminden sonra e-posta yoluyla lisans kodunuzu aldınız. Lütfen kayıt olmak için kodu kopyalayıp yapıştırın.", + "license.register.label": "Lütfen lisans kodunu giriniz", + "license.register.success": "Kirby'yi desteklediğiniz için teşekkürler", + "license.unregistered": "Bu Kirby'nin kayıtsız bir demosu", + + "link": "Ba\u011flant\u0131", + "link.text": "Ba\u011flant\u0131 yaz\u0131s\u0131", + + "loading": "Yükleniyor", + + "lock.unsaved": "Kaydedilmemiş değişiklikler", + "lock.unsaved.empty": "Daha fazla kaydedilmemiş değişiklik yok", + "lock.isLocked": "{email} tarafından kaydedilmemiş değişiklikler", + "lock.file.isLocked": "Dosya şu anda {email} tarafından düzenlenmektedir ve değiştirilemez.", + "lock.page.isLocked": "Sayfa şu anda {email} tarafından düzenlenmektedir ve değiştirilemez.", + "lock.unlock": "Kilidi Aç", + "lock.isUnlocked": "Kaydedilmemiş değişikliklerin üzerine başka bir kullanıcı yazmış. Değişikliklerinizi el ile birleştirmek için değişikliklerinizi indirebilirsiniz.", + + "login": "Giri\u015f", + "login.remember": "Oturumumu açık tut", + + "logout": "Güvenli Çıkış", + + "menu": "Menü", + "meridiem": "AM/PM", + "mime": "Medya Türü", + "minutes": "Dakika", + + "month": "Ay", + "months.april": "Nisan", + "months.august": "A\u011fustos", + "months.december": "Aral\u0131k", + "months.february": "Şubat", + "months.january": "Ocak", + "months.july": "Temmuz", + "months.june": "Haziran", + "months.march": "Mart", + "months.may": "May\u0131s", + "months.november": "Kas\u0131m", + "months.october": "Ekim", + "months.september": "Eyl\u00fcl", + + "more": "Daha Fazla", + "name": "İsim", + "next": "Sonraki", + "off": "kapalı", + "on": "açık", + "open": "Önizleme", + "options": "Seçenekler", + "options.none": "Seçenek yok", + + "orientation": "Oryantasyon", + "orientation.landscape": "Yatay", + "orientation.portrait": "Dikey", + "orientation.square": "Kare", + + "page.changeSlug": "Web Adresini Değiştir", + "page.changeSlug.fromTitle": "Ba\u015fl\u0131ktan olu\u015ftur", + "page.changeStatus": "Durumu değiştir", + "page.changeStatus.position": "Lütfen bir pozisyon seçin", + "page.changeStatus.select": "Yeni bir durum seçin", + "page.changeTemplate": "Şablonu değiştir", + "page.delete.confirm": "{title} sayfasını silmek istediğinizden emin misiniz?", + "page.delete.confirm.subpages": "Bu sayfada alt sayfalar var.
Tüm alt sayfalar da silinecek.", + "page.delete.confirm.title": "Onaylamak için sayfa başlığını girin", + "page.draft.create": "Taslak oluştur", + "page.duplicate.appendix": "Kopya", + "page.duplicate.files": "Dosyaları kopyala", + "page.duplicate.pages": "Sayfaları kopyala", + "page.status": "Durum", + "page.status.draft": "Taslak", + "page.status.draft.description": "Sayfa taslak halinde ve yalnızca oturum açmış editörler için veya gizli bağlantı üzerinden görülebilir", + "page.status.listed": "Herkese Açık", + "page.status.listed.description": "Bu sayfa herkese açık", + "page.status.unlisted": "Liste Dışı", + "page.status.unlisted.description": "Bu sayfa sadece bağlantı adresi ile erişilebilir", + + "pages": "Sayfalar", + "pages.empty": "Henüz sayfa yok", + "pages.status.draft": "Taslaklar", + "pages.status.listed": "Yayınlandı", + "pages.status.unlisted": "Liste Dışı", + + "pagination.page": "Sayfa", + + "password": "\u015eifre", + "pixel": "Piksel", + "prev": "Önceki", + "remove": "Kaldır", + "rename": "Yeniden Adlandır", + "replace": "De\u011fi\u015ftir", + "retry": "Tekrar Dene", + "revert": "Vazge\u00e7", + "revert.confirm": "Gerçekten kaydedilmemiş tüm değişiklikleri silmek istiyor musunuz?", + + "role": "Rol", + "role.admin.description": "Yönetici tüm haklara sahiptir", + "role.admin.title": "Yönetici", + "role.all": "Tümü", + "role.empty": "Bu role ait kullanıcı bulunamadı", + "role.description.placeholder": "Açıklama yok", + "role.nobody.description": "Bu hiçbir izni olmayan bir geri dönüş rolüdür.", + "role.nobody.title": "Hiçkimse", + + "save": "Kaydet", + "search": "Arama", + "search.min": "Aramak için {min} karakter girin", + "search.all": "Tümünü göster", + "search.results.none": "Sonuç yok", + + "section.required": "Bölüm gereklidir", + + "select": "Seç", + "settings": "Ayarlar", + "size": "Boyut", + "slug": "Web Adres Uzantısı", + "sort": "Sırala", + "title": "Başlık", + "template": "\u015eablon", + "today": "Bugün", + + "toolbar.button.code": "Kod", + "toolbar.button.bold": "Kalın Yazı", + "toolbar.button.email": "E-Posta", + "toolbar.button.headings": "Başlıklar", + "toolbar.button.heading.1": "Başlık 1", + "toolbar.button.heading.2": "Başlık 2", + "toolbar.button.heading.3": "Başlık 3", + "toolbar.button.italic": "Eğik Yazı", + "toolbar.button.file": "Dosya", + "toolbar.button.file.select": "Bir dosya seçin", + "toolbar.button.file.upload": "Bir dosya yükleyin", + "toolbar.button.link": "Ba\u011flant\u0131", + "toolbar.button.ol": "Sıralı liste", + "toolbar.button.ul": "Madde listesi", + + "translation.author": "Kirby Takımı", + "translation.direction": "ltr", + "translation.name": "T\u00fcrk\u00e7e", + "translation.locale": "tr_TR", + + "upload": "Yükle", + "upload.error.cantMove": "Yüklenen dosya taşınamadı", + "upload.error.cantWrite": "Dosya diske yazılamadı", + "upload.error.default": "Dosya yüklenemedi", + "upload.error.extension": "Dosya yükleme uzantısı tarafından durduruldu", + "upload.error.formSize": "Yüklenen dosya, formda belirtilen MAX_FILE_SIZE yönergesini aşıyor", + "upload.error.iniPostSize": "Yüklenen dosya php.ini içindeki post_max_size yönergesini aşıyor", + "upload.error.iniSize": "Yüklenen dosya php.ini içindeki upload_max_filesize yönergesini aşıyor", + "upload.error.noFile": "Dosya yüklenmedi", + "upload.error.noFiles": "Dosyalar yüklenmedi", + "upload.error.partial": "Yüklenen dosya sadece kısmen yüklendi", + "upload.error.tmpDir": "Geçici klasör eksik", + "upload.errors": "Hata", + "upload.progress": "Yükleniyor...", + + "url": "Url", + "url.placeholder": "https://ornek.com", + + "user": "Kullanıcı", + "user.blueprint": "Bu kullanıcı rolü için /site/blueprints/users/{role}.yml içinde ek bölümler ve form alanları tanımlayabilirsiniz", + "user.changeEmail": "E-postayı değiştir", + "user.changeLanguage": "Dili değiştir", + "user.changeName": "Kullanıcıyı yeniden adlandır", + "user.changePassword": "Şifre değiştir", + "user.changePassword.new": "Yeni Şifre", + "user.changePassword.new.confirm": "Şifreyi onaylayın...", + "user.changeRole": "Rolü değiştir", + "user.changeRole.select": "Yeni bir rol seçin", + "user.create": "Yeni bir kullanıcı ekle", + "user.delete": "Bu kullanıcıyı sil", + "user.delete.confirm": "{email} kullanıcısını silmek istediğinizden emin misiniz?", + + "users": "Kullanıcılar", + + "version": "Versiyon", + + "view.account": "Hesap Bilgilerin", + "view.installation": "Kurulum", + "view.settings": "Ayarlar", + "view.site": "Site", + "view.users": "Kullan\u0131c\u0131lar", + + "welcome": "Hoşgeldiniz", + "year": "Yıl" +} diff --git a/kirby/kirby.pub b/kirby/kirby.pub new file mode 100644 index 0000000..ddf9130 --- /dev/null +++ b/kirby/kirby.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Ux4q7LmQ5hfTYTtz3/a +mohFJMWo/iCnxVcY84PZjLwWnT+G2DTKGaEWydB77TteJQnmsgtvO5734oj3Ga3r +QCfwr2gxo/0WDEBq7C5HP+YNJiuZ/iD/tYV+gloF+Aaa3Mo8AK5DYH3dnjuyfHc1 +veIlYX1D2MXji2IRqdweAzVi1dfI4I3Ys8awhzv653vFLj5LvAtlwlYlmYeRwci7 +GkAOWw709CuKQNdPBXGFQQ/pEB5mnp8mI31j8og845u6v/Sk4+85gFORSufIRfnQ +GFYrPOeavxfAWQGjh7JQjr/sbKSXaJ3nDlrYsOPIrC0Rwn/jsQPO7OLdVwkc9ofL +GQIDAQAB +-----END PUBLIC KEY----- diff --git a/kirby/panel/dist/apple-touch-icon.png b/kirby/panel/dist/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..832510ed868e140c0a680449c2d38a921d850877 GIT binary patch literal 4115 zcmds)cQ9Q4yT`Q!ZHZW-cTuuxgy>~g zZy^b>tmuoVxhFHfx%dA5yZ5ghN(fohur6m9)zrFgnylkjA_^adduQD`R3c8Xp4LNy`#kR`$q6N-jo>U z{Frbt!|*T%ZZVCd*Y)AycQB1nkhzG$wN8j`I_y!N8A{D$QVJz zO2Lj6f$D}r^c3In(?=FjnzQ8L%x_E3=wQ~hEvT;hT8#I1nz^YlbCA;sO!^|^*wKvt zXT$54*Wq4XRthdd`oT5B&x}GL;2)>c_93OZv7Du~PQ7J~jg2cb^UoPixf3pbdqs$w zWBE;Z7qSw;q?vS8=hZjZ-q#BUp@BT9f<`JLE~lU)Z9%ccZ^LqOVWC3l>729TVSlPb z`8A{T5?VHK>onJ^3gm)_OdcMivtzGnb_T!?PN=p7gZkXcUlfe40Mx1y7lA`RS?6?Z+`BjZa!6h!OMpQ3>{cPrPf)ZY- zK#hXE8R8jqw3U{n;CjD*7KanAtgJ+wkkJOu&X|7Ko)G$3Zb}W!3#kBMIQ(dpWRBWnsZt;qN&xM^ioT$KA=e-Pb zD!(34Ctg}Bgc7BBl|>dQ)8*EiAOP|sT1c*=(P-z{y2psE1uRH9x8urVa~i*=-7uNH z7;`jP_(}*iFaQ~&3(ASXi=d#A2Oxzs%$)CWv znnw|FLRyQ4Mc=!k23_BwR^hMkN+&fee5#@WJt0&GS<=P%>Epv+vzB}G+;ZBvAs2zF zfqURyQ#5HwdhKbsN z?8nMGNO7kcz-_rN!5laqKt)MFTCE&R+h+{Qv<#_WGIt($wcQIo zKbW3bRp4RkaY^Al?W7y}B$FNuS!?i^9$3Q{REm2~l$h7K45O`At5;OyU~!W$cnxJ2 zw}=}im;}{-9)1 zXa2dHU-?|clA1DGmg5^YKU@c8&ys@^LXnc`1=J>p_jtC~l)V<=;B9;c<&}8_EeHB3 zjIJEy53nV~a=h~Wm^h!R!3^2NiyRNdy6@^YXJ&Gzy71U_W)=W6vkG8%VF0Glh9ZWqi4C$0ZZPH&Q+oBpbY|YlHj8j}mykW$}pzYgJ z-!Bg5y^P-M9oC)9z7r!+e zhR%JSLpR%v50zIfCD1h=pBG?czV^@W0AJsdLZb|ES020P-!BzzgDi9<30AlG+^)FF z;FL}rEKEWnRwET=l&r+GGFIB9;M2`Av6ml9t|a3vr7u+IXjh*_K&BnWyfgEx0&X+1 zSytIT@yiU=p?&iObBWlj@}jg87w!=bnSM5~1KY|C+LCWiHd=DXuSitRROEMD&CbU$ z7M1+Am~VVKPp9F~qHIde$r|&#K=ViK!3l2XQD3v)M#{LC{wi>`jcWYs2i=swiom(8 zq-FQsuQkViXm^qd^dlv!K0+7-)jzIf!M&6e6>qEzt%+%4z&0fw)oqsN7pL2X)fttf zw@R^x&K@6XYt7q!|A;mCZk6c*~P9~j77#|L#3VZSGCVF|f+85kJa zGK^tCMYwC?)FR8NVB6WoXWt=y{ns+yTwlL<(za!Qz`C6*=~N$VwvTHE<>6k%bYb*7 zJUsTc1*V0MwwIR73jA9(HZ~@^n6xV9=I1AWe2I-vYp)iF9Jrqk_z?*$7Gr^|H7U$+ zHACK@8APXXjmM6a&wg-~L8z=P$|e(aMkUhQ)HsCkgZ-s8S`{*cGQ$M8(at@#&Ke-udb?~> zlGRy!jXclET?Kp-C-eE{W`)o8!P%$F6`zL0K!;9@a$GivLs#BXOc+rN;xIau-%Qar z4V1hVb1Z)@U!wX%9S4U}|E<^IEWFwWbmn_Q3Zv2Off#QPkw}Ct&FV7>s&fz6s*JKp zeWHRJgCgXuBLekV-r#Onx8V{N{O6zc7<9g0Z;2iNWFhW4l4DR#*kW;_p{$XMi92_= z(sLD-1S!s&uCNg;&`WGYzc(*{(GC!#np3@f6EDYV0&9TMx$YCz9~)cS;uomyCpaIl7O9cZX=B!aH<|>pXXs8 z`2X6e#7*(b1nUf0drwq?-@uz2w{o1MuL+GXIngVLEVnAmbR(-Y4Kd>}y~PfIB{iL} z8!x3;lHMNwC4wghLs}wMD{%|ALCdMyHPPqe6D5%*@e2(e8Ee;K;!_!KkiymLeAn-& z1|7ohYxpGgF{p}IrS{Dv)<)bC1X!b(4eLJn50AX#%QULxp7_**jp^ch3j5JHN!jsz z+>$*x#eHeG%$d8zCjEo59a6|)Ey){O%^NbaO$_ ze{{TTLDcU7x*Lt0TqdZ1Gaou8RuX^N+P1J0J_LQ zulpun(o5h^))Neb!rLwKtO7aO>MHm(ko6^zxli+@>th;kg8CsyC0iUfCEZJ^D;F z`_bD8Q?*G=%&Us;>${{XZ^)L1E1VSz`WHgyb5$xEMqMEsAR`dpK9TE`r@XmklO4PH z@0Rs1DCaHgk$kG76H(vT_`1DA7ZcnZ_7}I|c4@##xy%4)?w>nav{display:-webkit-box;display:-ms-flexbox;display:flex;direction:ltr}.k-calendar-input>nav .k-button{padding:.5rem}.k-calendar-selects{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}[dir=ltr] .k-calendar-selects{direction:ltr}[dir=rtl] .k-calendar-selects{direction:rtl}.k-calendar-selects .k-select-input{padding:0 .5rem;font-weight:400;font-size:.875rem}.k-calendar-selects .k-select-input:focus-within{color:#81a2be!important}.k-calendar-input th{padding:.5rem 0;color:#999;font-size:.75rem;font-weight:400;text-align:center}.k-calendar-day .k-button{width:2rem;height:2rem;margin:0 auto;color:#fff;line-height:1.75rem;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;border-radius:50%;border:2px solid transparent}.k-calendar-day .k-button .k-button-text{opacity:1}.k-calendar-table .k-button:hover{color:#fff}.k-calendar-day:hover .k-button{border-color:hsla(0,0%,100%,.25)}.k-calendar-day[aria-current=date] .k-button{color:#81a2be;font-weight:500}.k-calendar-day[aria-selected=date] .k-button{border-color:#a7bd68;color:#a7bd68}.k-calendar-today{text-align:center;padding-top:.5rem}.k-calendar-today .k-button{color:#81a2be;font-size:.75rem;padding:1rem}.k-calendar-today .k-button-text{opacity:1}.k-counter{font-size:.75rem;color:#16171a;font-weight:600}.k-counter[data-invalid]{-webkit-box-shadow:none;box-shadow:none;border:0;color:#c82829}.k-counter-rules{color:#777;font-weight:400}[dir=ltr] .k-counter-rules{padding-left:.5rem}[dir=rtl] .k-counter-rules{padding-right:.5rem}.k-form-submitter{display:none}.k-form-buttons[data-theme=changes]{background:#de935f}.k-form-buttons[data-theme=lock]{background:#d16464}.k-form-buttons[data-theme=unlock]{background:#81a2be}.k-form-buttons .k-view{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.k-form-button.k-button,.k-form-buttons .k-view{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.k-form-button.k-button{font-weight:500;white-space:nowrap;line-height:1;height:2.5rem;padding:0 1rem}.k-form-button:first-child{margin-left:-1rem}.k-form-button:last-child{margin-right:-1rem}.k-form-lock-info{display:-webkit-box;display:-ms-flexbox;display:flex;font-size:.875rem;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:1.5em;padding:.625rem 0;margin-right:3rem}.k-form-lock-info>.k-icon{margin-right:.5rem}.k-form-lock-buttons{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-negative:0;flex-shrink:0}.k-form-lock-loader{-webkit-animation:Spin 4s linear infinite;animation:Spin 4s linear infinite}.k-form-lock-loader .k-icon-loader{display:-webkit-box;display:-ms-flexbox;display:flex}.k-form-indicator-icon{color:#de935f}.k-form-indicator-info{font-size:.875rem;font-weight:600;padding:.75rem 1rem .25rem;line-height:1.25em;width:15rem}.k-field-label{font-weight:600;display:block;padding:0 0 .75rem;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;line-height:1.25rem}.k-field-label abbr{text-decoration:none;color:#999;padding-left:.25rem}.k-field-header{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}.k-field-options{position:absolute;top:calc(-.5rem - 1px)}[dir=ltr] .k-field-options{right:0}[dir=rtl] .k-field-options{left:0}.k-field-options.k-button-group .k-dropdown{height:auto}.k-field-options.k-button-group .k-field-options-button.k-button{padding:.75rem;display:-webkit-box;display:-ms-flexbox;display:flex}.k-field[data-disabled]{cursor:not-allowed;opacity:.4}.k-field[data-disabled] *{pointer-events:none}.k-field[data-disabled] .k-text[data-theme=help] *{pointer-events:auto}.k-field:focus-within>.k-field-header>.k-field-counter{display:block}.k-field-help{padding-top:.5rem}.k-fieldset{border:0}.k-fieldset .k-grid{grid-row-gap:2.25rem}@media screen and (min-width:30em){.k-fieldset .k-grid{grid-column-gap:1.5rem}}.k-sections>.k-column[data-width="1/3"] .k-fieldset .k-grid,.k-sections>.k-column[data-width="1/4"] .k-fieldset .k-grid{grid-template-columns:repeat(1,1fr)}.k-sections>.k-column[data-width="1/3"] .k-fieldset .k-grid .k-column,.k-sections>.k-column[data-width="1/4"] .k-fieldset .k-grid .k-column{grid-column-start:auto}.k-input{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:1;border:0;outline:0;background:none}.k-input-element{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.k-input-icon{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:0}.k-input[data-disabled]{pointer-events:none}.k-input[data-theme=field]{line-height:1;border:1px solid #ccc;background:#fff}.k-input[data-theme=field]:focus-within{border:1px solid #4271ae;-webkit-box-shadow:rgba(66,113,174,.25) 0 0 0 2px;box-shadow:0 0 0 2px rgba(66,113,174,.25)}.k-input[data-theme=field][data-disabled]{background:#efefef}.k-input[data-theme=field] .k-input-icon{width:2.25rem}.k-input[data-theme=field] .k-input-after,.k-input[data-theme=field] .k-input-before,.k-input[data-theme=field] .k-input-icon{-ms-flex-item-align:stretch;align-self:stretch;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-negative:0;flex-shrink:0}.k-input[data-theme=field] .k-input-after,.k-input[data-theme=field] .k-input-before{padding:0 .5rem}.k-input[data-theme=field] .k-input-before{color:#777;padding-right:0}.k-input[data-theme=field] .k-input-after{color:#777;padding-left:0}.k-input[data-theme=field] .k-input-icon>.k-dropdown{width:100%;height:100%}.k-input[data-theme=field] .k-input-icon-button{width:100%;height:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-ms-flex-negative:0;flex-shrink:0}.k-input[data-theme=field] .k-number-input,.k-input[data-theme=field] .k-select-input,.k-input[data-theme=field] .k-text-input{padding:.5rem;line-height:1.25rem}.k-input[data-theme=field] .k-date-input .k-select-input,.k-input[data-theme=field] .k-time-input .k-select-input{padding-left:0;padding-right:0}[dir=ltr] .k-input[data-theme=field] .k-date-input .k-select-input:first-child,[dir=ltr] .k-input[data-theme=field] .k-time-input .k-select-input:first-child{padding-left:.5rem}[dir=rtl] .k-input[data-theme=field] .k-date-input .k-select-input:first-child,[dir=rtl] .k-input[data-theme=field] .k-time-input .k-select-input:first-child{padding-right:.5rem}.k-input[data-theme=field] .k-date-input .k-select-input:focus-within,.k-input[data-theme=field] .k-time-input .k-select-input:focus-within{color:#4271ae;font-weight:600}.k-input[data-theme=field] .k-time-input .k-time-input-meridiem{padding-left:.5rem}.k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input li,.k-input[data-theme=field][data-type=checkboxes] .k-radio-input li,.k-input[data-theme=field][data-type=radio] .k-checkboxes-input li,.k-input[data-theme=field][data-type=radio] .k-radio-input li{min-width:0;overflow-wrap:break-word}.k-input[data-theme=field][data-type=checkboxes] .k-input-before{border-right:1px solid #efefef}.k-input[data-theme=field][data-type=checkboxes] .k-input-element+.k-input-after,.k-input[data-theme=field][data-type=checkboxes] .k-input-element+.k-input-icon{border-left:1px solid #efefef}.k-input[data-theme=field][data-type=checkboxes] .k-input-element{overflow:hidden}.k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input{display:grid;grid-template-columns:1fr;margin-bottom:-1px;margin-right:-1px}@media screen and (min-width:65em){.k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input{grid-template-columns:repeat(var(--columns),1fr)}}.k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input li{border-right:1px solid #efefef;border-bottom:1px solid #efefef}.k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input label{display:block;line-height:1.25rem;padding:.5rem .5rem}.k-input[data-theme=field][data-type=checkboxes] .k-checkbox-input-icon{top:.625rem;left:.5rem;margin-top:0}.k-input[data-theme=field][data-type=radio] .k-input-before{border-right:1px solid #efefef}.k-input[data-theme=field][data-type=radio] .k-input-element+.k-input-after,.k-input[data-theme=field][data-type=radio] .k-input-element+.k-input-icon{border-left:1px solid #efefef}.k-input[data-theme=field][data-type=radio] .k-input-element{overflow:hidden}.k-input[data-theme=field][data-type=radio] .k-radio-input{display:grid;grid-template-columns:1fr;margin-bottom:-1px;margin-right:-1px}@media screen and (min-width:65em){.k-input[data-theme=field][data-type=radio] .k-radio-input{grid-template-columns:repeat(var(--columns),1fr)}}.k-input[data-theme=field][data-type=radio] .k-radio-input li{border-right:1px solid #efefef;border-bottom:1px solid #efefef}.k-input[data-theme=field][data-type=radio] .k-radio-input label{display:block;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;min-height:2.25rem;line-height:1.25rem;padding:.5rem .5rem}.k-input[data-theme=field][data-type=radio] .k-radio-input label:before{top:.625rem;left:.5rem;margin-top:-1px}.k-input[data-theme=field][data-type=radio] .k-radio-input .k-radio-input-info{display:block;font-size:.875rem;color:#777;line-height:1.25rem;padding-top:.125rem}.k-input[data-theme=field][data-type=radio] .k-radio-input .k-icon{width:2.25rem;height:2.25rem;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.k-input[data-theme=field][data-type=range] .k-range-input{padding:.5rem}.k-input[data-theme=field][data-type=select]{position:relative}.k-input[data-theme=field][data-type=select] .k-input-icon{position:absolute;top:0;bottom:0}[dir=ltr] .k-input[data-theme=field][data-type=select] .k-input-icon{right:0}[dir=rtl] .k-input[data-theme=field][data-type=select] .k-input-icon{left:0}.k-input[data-theme=field][data-type=tags] .k-tags-input{padding:.25rem .25rem 0 .25rem}.k-input[data-theme=field][data-type=tags] .k-tag{margin-right:.25rem;margin-bottom:.25rem;height:1.75rem;font-size:.875rem}.k-input[data-theme=field][data-type=tags] .k-tags-input input{font-size:.875rem;padding:0 .25rem;height:1.75rem;line-height:1;margin-bottom:.25rem}.k-input[data-theme=field][data-type=tags] .k-tags-input .k-dropdown-content{top:calc(100% + .5rem + 2px)}.k-input[data-theme=field][data-type=multiselect]{position:relative}.k-input[data-theme=field][data-type=multiselect] .k-multiselect-input{padding:.25rem 2rem 0 .25rem;min-height:2.25rem}.k-input[data-theme=field][data-type=multiselect] .k-tag{margin-right:.25rem;margin-bottom:.25rem;height:1.75rem;font-size:.875rem}.k-input[data-theme=field][data-type=multiselect] .k-input-icon{position:absolute;top:0;right:0;bottom:0;pointer-events:none}.k-input[data-theme=field][data-type=textarea] .k-textarea-input-native{padding:.25rem .5rem;line-height:1.5rem}.k-input[data-theme=field][data-type=toggle] .k-input-before{padding-right:.25rem}.k-input[data-theme=field][data-type=toggle] .k-toggle-input{padding-left:.5rem}.k-input[data-theme=field][data-type=toggle] .k-toggle-input-label{padding:0 .5rem 0 .75rem;line-height:2.25rem}.k-upload input{position:absolute;top:0}[dir=ltr] .k-upload input{left:-3000px}[dir=rtl] .k-upload input{right:-3000px}.k-upload .k-headline{margin-bottom:.75rem}.k-upload-error-list,.k-upload-list{line-height:1.5em;font-size:.875rem}.k-upload-list-filename{color:#777}.k-upload-error-list li{padding:.75rem;background:#fff;border-radius:1px}.k-upload-error-list li:not(:last-child){margin-bottom:2px}.k-upload-error-filename{color:#c82829;font-weight:600}.k-upload-error-message{color:#777}.k-checkbox-input{position:relative;cursor:pointer}.k-checkbox-input-native{position:absolute;-webkit-appearance:none;-moz-appearance:none;appearance:none;width:0;height:0;opacity:0}.k-checkbox-input-label{display:block;padding-left:1.75rem}.k-checkbox-input-icon{position:absolute;left:0;width:1rem;height:1rem;border:2px solid #999}.k-checkbox-input-icon svg{position:absolute;width:12px;height:12px;display:none}.k-checkbox-input-icon path{stroke:#fff}.k-checkbox-input-native:checked+.k-checkbox-input-icon{border-color:#16171a;background:#16171a}.k-checkbox-input-native:checked+.k-checkbox-input-icon svg{display:block}.k-checkbox-input-native:focus+.k-checkbox-input-icon{border-color:#4271ae}.k-checkbox-input-native:focus:checked+.k-checkbox-input-icon{background:#4271ae}.k-date-input{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.k-date-input-separator{padding:0 .125rem}.k-datetime-input{display:-webkit-box;display:-ms-flexbox;display:flex}.k-datetime-input .k-time-input{padding-left:.5rem}.k-text-input{width:100%;border:0;background:none;font:inherit;color:inherit}.k-text-input::-webkit-input-placeholder{color:#999}.k-text-input::-moz-placeholder{color:#999}.k-text-input:-ms-input-placeholder{color:#999}.k-text-input::-ms-input-placeholder{color:#999}.k-text-input::placeholder{color:#999}.k-text-input:focus{outline:0}.k-text-input:invalid{-webkit-box-shadow:none;box-shadow:none;outline:0}.k-multiselect-input{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;position:relative;font-size:.875rem;min-height:2.25rem;line-height:1}.k-multiselect-input .k-sortable-ghost{background:#4271ae}.k-multiselect-input .k-dropdown-content{width:100%}.k-multiselect-search{margin-top:0!important;color:#fff;background:#16171a;border-bottom:1px dashed hsla(0,0%,100%,.2)}.k-multiselect-search>.k-button-text{-webkit-box-flex:1;-ms-flex:1;flex:1;opacity:1!important}.k-multiselect-search input{width:100%;color:#fff;background:none;border:none;outline:none;padding:.25rem 0;font:inherit}.k-multiselect-options{position:relative;max-height:275px;overflow-y:auto;padding:.5rem 0}.k-multiselect-option{position:relative}.k-multiselect-option.selected{color:#a7bd68}.k-multiselect-option.disabled:not(.selected) .k-icon{opacity:0}.k-multiselect-option b{color:#81a2be;font-weight:700}.k-multiselect-value{color:#999;margin-left:.25rem}.k-multiselect-value:before{content:" ("}.k-multiselect-value:after{content:")"}.k-multiselect-input[data-layout=list] .k-tag{width:100%;margin-right:0!important}.k-multiselect-more{width:100%;padding:.75rem;color:hsla(0,0%,100%,.8);text-align:center;border-top:1px dashed hsla(0,0%,100%,.2)}.k-multiselect-more:hover{color:#fff}.k-number-input{width:100%;border:0;background:none;font:inherit;color:inherit}.k-number-input::-webkit-input-placeholder{color:$color-light-grey}.k-number-input::-moz-placeholder{color:$color-light-grey}.k-number-input:-ms-input-placeholder{color:$color-light-grey}.k-number-input::-ms-input-placeholder{color:$color-light-grey}.k-number-input::placeholder{color:$color-light-grey}.k-number-input:focus{outline:0}.k-number-input:invalid{-webkit-box-shadow:none;box-shadow:none;outline:0}.k-radio-input li{position:relative;line-height:1.5rem;padding-left:1.75rem}.k-radio-input input{position:absolute;width:0;height:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;opacity:0}.k-radio-input label{cursor:pointer;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.k-radio-input label:before{position:absolute;top:.175em;left:0;content:"";width:1rem;height:1rem;border-radius:50%;border:2px solid #999;-webkit-box-shadow:#fff 0 0 0 2px inset;box-shadow:inset 0 0 0 2px #fff}.k-radio-input input:checked+label:before{border-color:#16171a;background:#16171a}.k-radio-input input:focus+label:before{border-color:#4271ae}.k-radio-input input:focus:checked+label:before{background:#4271ae}.k-radio-input-text{display:block}.k-range-input{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.k-range-input-native{--min:0;--max:100;--value:0;--range:calc(var(--max) - var(--min));--ratio:calc((var(--value) - var(--min))/var(--range));--position:calc(8px + var(--ratio)*(100% - 16px));-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:16px;background:transparent;font-size:.875rem;line-height:1}.k-range-input-native::-webkit-slider-thumb{-webkit-appearance:none;appearance:none}.k-range-input-native::-webkit-slider-runnable-track{border:none;border-radius:4px;width:100%;height:4px;background:#ccc;background:-webkit-gradient(linear,left top,left bottom,from(#16171a),to(#16171a)) 0/var(--position) 100% no-repeat #ccc;background:linear-gradient(#16171a,#16171a) 0/var(--position) 100% no-repeat #ccc}.k-range-input-native::-moz-range-track{border:none;border-radius:4px;width:100%;height:4px;background:#ccc}.k-range-input-native::-ms-track{border:none;border-radius:4px;width:100%;height:4px;background:#ccc}.k-range-input-native::-moz-range-progress{height:4px;background:#16171a}.k-range-input-native::-ms-fill-lower{height:4px;background:#16171a}.k-range-input-native::-webkit-slider-thumb{margin-top:-6px;-webkit-box-sizing:border-box;box-sizing:border-box;width:16px;height:16px;background:#efefef;border:4px solid #16171a;border-radius:50%;cursor:pointer}.k-range-input-native::-moz-range-thumb{box-sizing:border-box;width:16px;height:16px;background:#efefef;border:4px solid #16171a;border-radius:50%;cursor:pointer}.k-range-input-native::-ms-thumb{margin-top:0;box-sizing:border-box;width:16px;height:16px;background:#efefef;border:4px solid #16171a;border-radius:50%;cursor:pointer}.k-range-input-native::-ms-tooltip{display:none}.k-range-input-native:focus{outline:none}.k-range-input-native:focus::-webkit-slider-runnable-track{border:none;border-radius:4px;width:100%;height:4px;background:#ccc;background:-webkit-gradient(linear,left top,left bottom,from(#4271ae),to(#4271ae)) 0/var(--position) 100% no-repeat #ccc;background:linear-gradient(#4271ae,#4271ae) 0/var(--position) 100% no-repeat #ccc}.k-range-input-native:focus::-moz-range-progress{height:4px;background:#4271ae}.k-range-input-native:focus::-ms-fill-lower{height:4px;background:#4271ae}.k-range-input-native:focus::-webkit-slider-thumb{background:#efefef;border:4px solid #4271ae}.k-range-input-native:focus::-moz-range-thumb{background:#efefef;border:4px solid #4271ae}.k-range-input-native:focus::-ms-thumb{background:#efefef;border:4px solid #4271ae}.k-range-input-tooltip{position:relative;max-width:20%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:#fff;font-size:.75rem;line-height:1;text-align:center;border-radius:1px;background:#16171a;margin-left:1rem;padding:0 .25rem;white-space:nowrap}.k-range-input-tooltip:after{position:absolute;top:50%;left:-5px;width:0;height:0;-webkit-transform:translateY(-50%);transform:translateY(-50%);border-top:5px solid transparent;border-right:5px solid #16171a;border-bottom:5px solid transparent;content:""}.k-range-input-tooltip>*{padding:4px}.k-select-input{position:relative;display:block;cursor:pointer;overflow:hidden}.k-select-input-native{position:absolute;top:0;right:0;bottom:0;left:0;opacity:0;width:100%;font:inherit;z-index:1;cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none}.k-select-input-native[disabled]{cursor:default}.k-select-input-native{font-weight:400}.k-tags-input{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.k-tags-input .k-sortable-ghost{background:#4271ae}.k-tags-input-element{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-preferred-size:0;flex-basis:0;min-width:0}.k-tags-input:focus-within .k-tags-input-element{-ms-flex-preferred-size:4rem;flex-basis:4rem}.k-tags-input-element input{font:inherit;border:0;width:100%;background:none}.k-tags-input-element input:focus{outline:0}.k-tags-input[data-layout=list] .k-tag{width:100%;margin-right:0!important}.k-textarea-input-wrapper{position:relative}.k-textarea-input-native{resize:none;border:0;width:100%;background:none;font:inherit;line-height:1.5em;color:inherit}.k-textarea-input-native::-webkit-input-placeholder{color:#999}.k-textarea-input-native::-moz-placeholder{color:#999}.k-textarea-input-native:-ms-input-placeholder{color:#999}.k-textarea-input-native::-ms-input-placeholder{color:#999}.k-textarea-input-native::placeholder{color:#999}.k-textarea-input-native:focus{outline:0}.k-textarea-input-native:invalid{-webkit-box-shadow:none;box-shadow:none;outline:0}.k-textarea-input-native[data-size=small]{min-height:7.5rem}.k-textarea-input-native[data-size=medium]{min-height:15rem}.k-textarea-input-native[data-size=large]{min-height:30rem}.k-textarea-input-native[data-size=huge]{min-height:45rem}.k-textarea-input-native[data-font=monospace]{font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}.k-toolbar{margin-bottom:.25rem;color:#aaa}.k-textarea-input:focus-within .k-toolbar{position:-webkit-sticky;position:sticky;top:0;right:0;left:0;z-index:1;-webkit-box-shadow:rgba(0,0,0,.05) 0 2px 5px;box-shadow:0 2px 5px rgba(0,0,0,.05);border-bottom:1px solid rgba(0,0,0,.1);color:#000}.k-time-input{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:1}.k-time-input-separator{padding:0 .125rem}.k-time-input-meridiem{padding-left:.5rem}.k-toggle-input{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.k-toggle-input-native{position:relative;height:16px;width:32px;border-radius:16px;border:2px solid #999;-webkit-box-shadow:inset 0 0 0 2px #fff,inset -16px 0 0 2px #fff;box-shadow:inset 0 0 0 2px #fff,inset -16px 0 0 2px #fff;background-color:#999;outline:0;-webkit-transition:all .1s ease-in-out;transition:all .1s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer;-ms-flex-negative:0;flex-shrink:0}.k-toggle-input-native:checked{border-color:#16171a;-webkit-box-shadow:inset 0 0 0 2px #fff,inset 16px 0 0 2px #fff;box-shadow:inset 0 0 0 2px #fff,inset 16px 0 0 2px #fff;background-color:#16171a}.k-toggle-input-native[disabled]{border-color:#ccc;-webkit-box-shadow:inset 0 0 0 2px #efefef,inset -16px 0 0 2px #efefef;box-shadow:inset 0 0 0 2px #efefef,inset -16px 0 0 2px #efefef;background-color:#ccc}.k-toggle-input-native[disabled]:checked{-webkit-box-shadow:inset 0 0 0 2px #efefef,inset 16px 0 0 2px #efefef;box-shadow:inset 0 0 0 2px #efefef,inset 16px 0 0 2px #efefef}.k-toggle-input-native:focus:checked{border:2px solid #4271ae;background-color:#4271ae}.k-toggle-input-native::-ms-check{opacity:0}.k-toggle-input-label{cursor:pointer;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.k-files-field[data-disabled] *{pointer-events:all!important}body{counter-reset:headline-counter}.k-headline-field{position:relative;padding-top:1.5rem}.k-headline-field[data-numbered]:before{counter-increment:headline-counter;content:counter(headline-counter,decimal-leading-zero);color:#4271ae;font-weight:400;padding-right:.25rem}.k-fieldset>.k-grid .k-column:first-child .k-headline-field{padding-top:0}.k-info-field .k-headline{padding-bottom:.75rem;line-height:1.25rem}.k-line-field{position:relative;border:0;height:3rem;width:auto}.k-line-field:after{position:absolute;content:"";top:50%;margin-top:-1px;left:0;right:0;height:1px;background:#ccc}.k-pages-field[data-disabled] *{pointer-events:all!important}.k-structure-table{position:relative;table-layout:fixed;width:100%;background:#fff;font-size:.875rem;border-spacing:0;-webkit-box-shadow:rgba(22,23,26,.05) 0 2px 5px;box-shadow:0 2px 5px rgba(22,23,26,.05)}.k-structure-table td,.k-structure-table th{border-bottom:1px solid #efefef;line-height:1.25em;overflow:hidden;text-overflow:ellipsis}[dir=ltr] .k-structure-table td,[dir=ltr] .k-structure-table th{border-right:1px solid #efefef}[dir=rtl] .k-structure-table td,[dir=rtl] .k-structure-table th{border-left:1px solid #efefef}.k-structure-table td:last-child{overflow:visible}.k-structure-table th{position:-webkit-sticky;position:sticky;top:0;right:0;left:0;width:100%;background:#fff;font-weight:400;z-index:1;color:#777;padding:0 .75rem;height:38px}[dir=ltr] .k-structure-table th{text-align:left}[dir=rtl] .k-structure-table th{text-align:right}.k-structure-table td:last-child,.k-structure-table th:last-child{width:38px}[dir=ltr] .k-structure-table td:last-child,[dir=ltr] .k-structure-table th:last-child{border-right:0}[dir=rtl] .k-structure-table td:last-child,[dir=rtl] .k-structure-table th:last-child{border-left:0}.k-structure-table tr:last-child td{border-bottom:0}.k-structure-table tbody tr:hover td{background:hsla(0,0%,93.7%,.25)}@media screen and (max-width:65em){.k-structure-table td,.k-structure-table th{display:none}.k-structure-table td:first-child,.k-structure-table td:last-child,.k-structure-table td:nth-child(2),.k-structure-table th:first-child,.k-structure-table th:last-child,.k-structure-table th:nth-child(2){display:table-cell}}.k-structure-table .k-structure-table-column[data-align=center]{text-align:center}[dir=ltr] .k-structure-table .k-structure-table-column[data-align=right]{text-align:right}[dir=rtl] .k-structure-table .k-structure-table-column[data-align=right]{text-align:left}.k-structure-table .k-structure-table-column[data-align=right]>.k-input{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}.k-structure-table .k-structure-table-column[data-width="1/2"]{width:50%}.k-structure-table .k-structure-table-column[data-width="1/3"]{width:33.33%}.k-structure-table .k-structure-table-column[data-width="1/4"]{width:25%}.k-structure-table .k-structure-table-column[data-width="1/5"]{width:20%}.k-structure-table .k-structure-table-column[data-width="1/6"]{width:16.66%}.k-structure-table .k-structure-table-column[data-width="1/8"]{width:12.5%}.k-structure-table .k-structure-table-column[data-width="1/9"]{width:11.11%}.k-structure-table .k-structure-table-column[data-width="2/3"]{width:66.66%}.k-structure-table .k-structure-table-column[data-width="3/4"]{width:75%}.k-structure-table .k-structure-table-index{width:38px;text-align:center}.k-structure-table .k-structure-table-index-number{font-size:.75rem;color:#999;padding-top:.15rem}.k-structure-table .k-sort-handle{width:38px;height:38px;display:none}.k-structure-table[data-sortable] tr:hover .k-structure-table-index-number{display:none}.k-structure-table[data-sortable] tr:hover .k-sort-handle{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.k-structure-table .k-structure-table-options{position:relative;width:38px;text-align:center;height:38px}.k-structure-table .k-structure-table-options-button{width:38px;height:38px}.k-structure-table .k-structure-table-text{padding:0 .75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.k-structure-table .k-sortable-ghost{background:#fff;-webkit-box-shadow:rgba(22,23,26,.25) 0 5px 10px;box-shadow:0 5px 10px rgba(22,23,26,.25);outline:2px solid #4271ae;margin-bottom:2px;cursor:grabbing;cursor:-webkit-grabbing}.k-sortable-row-fallback{opacity:0!important}.k-structure-backdrop{position:absolute;top:0;right:0;bottom:0;left:0;z-index:2;height:100vh}.k-structure-form{position:relative;z-index:3;border-radius:1px;margin-bottom:1px;-webkit-box-shadow:rgba(22,23,26,.05) 0 0 0 3px;box-shadow:0 0 0 3px rgba(22,23,26,.05);border:1px solid #ccc;background:#efefef}.k-structure-form-fields{padding:1.5rem 1.5rem 2rem}.k-structure-form-buttons{border-top:1px solid #ccc;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.k-structure-form-buttons .k-pagination{display:none}@media screen and (min-width:65em){.k-structure-form-buttons .k-pagination{display:-webkit-box;display:-ms-flexbox;display:flex}}.k-structure-form-buttons .k-pagination>.k-button,.k-structure-form-buttons .k-pagination>span{padding:.875rem 1rem!important}.k-structure-form-cancel-button,.k-structure-form-submit-button{padding:.875rem 1.5rem;line-height:1rem;display:-webkit-box;display:-ms-flexbox;display:flex}.k-field-counter{display:none}.k-text-field:focus-within .k-field-counter{display:block}.k-users-field[data-disabled] *{pointer-events:all!important}.k-toolbar{background:#fff;border-bottom:1px solid #efefef;height:38px}.k-toolbar-wrapper{position:absolute;top:0;right:0;left:0;max-width:100%}.k-toolbar-buttons{display:-webkit-box;display:-ms-flexbox;display:flex}.k-toolbar-divider{width:1px;background:#efefef}.k-toolbar-button{width:36px;height:36px}.k-toolbar-button:hover{background:hsla(0,0%,93.7%,.5)}.k-files-field-preview{display:grid;grid-gap:.5rem;grid-template-columns:repeat(auto-fill,1.525rem);padding:0 .75rem}.k-files-field-preview li{line-height:0}.k-files-field-preview li .k-icon{height:100%}.k-url-field-preview{padding:0 .75rem}.k-url-field-preview a{color:#4271ae;text-decoration:underline;-webkit-transition:color .3s;transition:color .3s;overflow:hidden;white-space:nowrap;max-width:100%;text-overflow:ellipsis}.k-url-field-preview a:hover{color:#000}.k-pages-field-preview{padding:0 .25rem 0 .75rem;display:-webkit-box;display:-ms-flexbox;display:flex}.k-pages-field-preview li{line-height:0;margin-right:.5rem}.k-pages-field-preview .k-link{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;background:#efefef;-webkit-box-shadow:rgba(22,23,26,.05) 0 2px 5px;box-shadow:0 2px 5px rgba(22,23,26,.05)}.k-pages-field-preview-image{width:1.525rem;height:1.525rem;color:#999!important}.k-pages-field-preview figcaption{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;line-height:1.5em;padding:0 .5rem;border:1px solid #ccc;border-left:0;border-radius:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.k-toggle-field-preview label{padding:0 .25rem 0 .75rem;display:-webkit-box;display:-ms-flexbox;display:flex;height:38px;cursor:pointer;overflow:hidden;white-space:nowrap}[dir=ltr] .k-toggle-field-preview .k-toggle-input-label{padding-left:.5rem}[dir=ltr] [data-align=right] .k-toggle-field-preview .k-toggle-input-label,[dir=rtl] .k-toggle-field-preview .k-toggle-input-label{padding-right:.5rem}[dir=rtl] [data-align=right] .k-toggle-field-preview .k-toggle-input-label{padding-left:.5rem}[dir=ltr] .k-toggle-field-preview .k-toggle-input{padding:0 .25rem 0 .75rem}[dir=rtl] .k-toggle-field-preview .k-toggle-input{padding:0 .75rem 0 .25rem}[data-align=right] .k-toggle-field-preview .k-toggle-input{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}[dir=ltr] [data-align=right] .k-toggle-field-preview .k-toggle-input{padding:0 .75rem 0 .25rem}.k-users-field-preview,[dir=rtl] [data-align=right] .k-toggle-field-preview .k-toggle-input{padding:0 .25rem 0 .75rem}.k-users-field-preview{display:-webkit-box;display:-ms-flexbox;display:flex}.k-users-field-preview li{line-height:0;margin-right:.5rem}.k-users-field-preview .k-link{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;background:#efefef;-webkit-box-shadow:rgba(22,23,26,.05) 0 2px 5px;box-shadow:0 2px 5px rgba(22,23,26,.05)}.k-users-field-preview-avatar{width:1.525rem;height:1.525rem;color:#999!important}.k-users-field-preview-avatar.k-image{display:block}.k-users-field-preview figcaption{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;line-height:1.5em;padding:0 .5rem;border:1px solid #ccc;border-left:0;border-radius:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.k-bar{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;line-height:1}.k-bar-slot{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.k-bar-slot[data-position=center]{text-align:center}[dir=ltr] .k-bar-slot[data-position=right]{text-align:right}[dir=rtl] .k-bar-slot[data-position=right]{text-align:left}.k-box{word-wrap:break-word;font-size:.875rem}.k-box:not([data-theme=none]){background:#d9d9d9;border-radius:1px;padding:.375rem .75rem;line-height:1.25rem;border-left:2px solid #999;padding:.5rem 1.5rem}.k-box[data-theme=code]{background:#16171a;border:1px solid #000;color:#efefef;font-family:Input,Menlo,monospace;font-size:.875rem;line-height:1.5}.k-box[data-theme=button]{padding:0}.k-box[data-theme=button] .k-button{padding:0 .75rem;height:2.25rem;width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:2rem;text-align:left}.k-box[data-theme=positive]{background:#dbe4c1;border:0;border-left:2px solid #a7bd68;padding:.5rem 1.5rem}.k-box[data-theme=negative]{background:#eec6c6;border:0;border-left:2px solid #d16464;padding:.5rem 1.5rem}.k-box[data-theme=notice]{background:#f4dac9;border:0;border-left:2px solid #de935f;padding:.5rem 1.5rem}.k-box[data-theme=info]{background:#d5e0e9;border:0;border-left:2px solid #81a2be;padding:.5rem 1.5rem}.k-box[data-theme=empty]{text-align:center;border-left:0;padding:3rem 1.5rem;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;background:#efefef;border-radius:1px;color:#777;border:1px dashed #ccc}.k-box[data-theme=empty] .k-icon{margin-bottom:.5rem;color:#999}.k-box[data-theme=empty] p{color:#777}.k-card{position:relative;border-radius:1px;-webkit-box-shadow:rgba(22,23,26,.05) 0 2px 5px;box-shadow:0 2px 5px rgba(22,23,26,.05)}.k-card,.k-card a{min-width:0;background:#fff}.k-card:focus-within{-webkit-box-shadow:#4271ae 0 0 0 2px;box-shadow:0 0 0 2px #4271ae}.k-card a:focus{outline:0}.k-card .k-sort-handle{position:absolute;top:.75rem;width:2rem;height:2rem;border-radius:1px;background:#fff;opacity:0;color:#16171a;z-index:1;will-change:opacity;-webkit-transition:opacity .3s;transition:opacity .3s}[dir=ltr] .k-card .k-sort-handle{right:.75rem}[dir=rtl] .k-card .k-sort-handle{left:.75rem}.k-cards:hover .k-sort-handle{opacity:.25}.k-card:hover .k-sort-handle{opacity:1}.k-card.k-sortable-ghost{outline:2px solid #4271ae;border-radius:0}.k-card-icon,.k-card-image{border-top-left-radius:1px;border-top-right-radius:1px;overflow:hidden}.k-card-icon{position:relative;display:block}.k-card-icon .k-icon{position:absolute;top:0;right:0;bottom:0;left:0}.k-card-icon .k-icon-emoji{font-size:3rem}.k-card-icon .k-icon svg{width:3rem;height:3rem}.k-card-content{line-height:1.25rem;border-bottom-left-radius:1px;border-bottom-right-radius:1px;min-height:2.25rem;padding:.5rem .75rem;overflow-wrap:break-word;word-wrap:break-word}.k-card-text{display:block;font-weight:400;text-overflow:ellipsis;font-size:.875rem}.k-card-text[data-noinfo]:after{content:" ";height:1em;width:5rem;display:inline-block}.k-card-info{color:#777;display:block;font-size:.875rem;text-overflow:ellipsis;overflow:hidden}[dir=ltr] .k-card-info{margin-right:4rem}[dir=rtl] .k-card-info{margin-left:4rem}.k-card-options{position:absolute;bottom:0}[dir=ltr] .k-card-options{right:0}[dir=rtl] .k-card-options{left:0}.k-card-options>.k-button{position:relative;float:left;height:2.25rem;padding:0 .75rem;line-height:1}.k-card-options-dropdown{top:2.25rem}.k-cards{display:grid;grid-gap:1.5rem;grid-template-columns:repeat(auto-fit,minmax(12rem,1fr));grid-template-columns:repeat(auto-fill,minmax(min(12rem,100%),1fr))}@media screen and (min-width:30em){.k-cards[data-size=tiny]{grid-template-columns:repeat(auto-fill,minmax(10rem,1fr));grid-template-columns:repeat(auto-fill,minmax(min(10rem,100%),1fr))}.k-cards[data-size=small]{grid-template-columns:repeat(auto-fill,minmax(16rem,1fr));grid-template-columns:repeat(auto-fill,minmax(min(16rem,100%),1fr))}.k-cards[data-size=medium]{grid-template-columns:repeat(auto-fill,minmax(24rem,1fr));grid-template-columns:repeat(auto-fill,minmax(min(24rem,100%),1fr))}.k-cards[data-size=huge],.k-cards[data-size=large]{grid-template-columns:1fr}}@media screen and (min-width:65em){.k-cards[data-size=large]{grid-template-columns:repeat(auto-fill,minmax(32rem,1fr));grid-template-columns:repeat(auto-fill,minmax(min(32rem,100%),1fr))}}.k-collection-help{padding:.5rem .75rem}.k-collection-footer{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;margin-right:-.75rem;margin-left:-.75rem}.k-collection-pagination{line-height:1.25rem;min-height:2.75rem}.k-collection-pagination .k-pagination .k-button{padding:.5rem .75rem;line-height:1.125rem}.k-column{min-width:0;grid-column-start:span 12}.k-column[data-sticky]>div{position:-webkit-sticky;position:sticky;top:4vh;z-index:2}@media screen and (min-width:65em){.k-column[data-width="1/1"],.k-column[data-width="2/2"],.k-column[data-width="3/3"],.k-column[data-width="4/4"],.k-column[data-width="6/6"]{grid-column-start:span 12}.k-column[data-width="1/2"],.k-column[data-width="2/4"],.k-column[data-width="3/6"]{grid-column-start:span 6}.k-column[data-width="1/3"],.k-column[data-width="2/6"]{grid-column-start:span 4}.k-column[data-width="2/3"],.k-column[data-width="4/6"]{grid-column-start:span 8}.k-column[data-width="1/4"]{grid-column-start:span 3}.k-column[data-width="1/6"]{grid-column-start:span 2}.k-column[data-width="5/6"]{grid-column-start:span 10}.k-column[data-width="3/4"]{grid-column-start:span 9}}.k-dropzone{position:relative}.k-dropzone:after{content:"";position:absolute;top:0;right:0;bottom:0;left:0;display:none;pointer-events:none;z-index:1}.k-dropzone[data-over]:after{display:block;outline:1px solid #4271ae;-webkit-box-shadow:rgba(66,113,174,.25) 0 0 0 3px;box-shadow:0 0 0 3px rgba(66,113,174,.25)}.k-empty{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;border-radius:1px;color:#777;border:1px dashed #ccc}.k-empty p{font-size:.875rem;color:#777}.k-empty>.k-icon{color:#999}.k-empty[data-layout=cards]{text-align:center;padding:1.5rem;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.k-empty[data-layout=cards] .k-icon{margin-bottom:1rem}.k-empty[data-layout=cards] .k-icon svg{width:2rem;height:2rem}.k-empty[data-layout=list]{min-height:38px}.k-empty[data-layout=list]>.k-icon{width:36px;min-height:36px;border-right:1px solid rgba(0,0,0,.05)}.k-empty[data-layout=list]>p{line-height:1.25rem;padding:.5rem .75rem}.k-file-preview{background:#2d2f36}.k-file-preview-layout{display:grid}@media screen and (max-width:65em){.k-file-preview-layout{padding:0!important}}@media screen and (min-width:30em){.k-file-preview-layout{grid-template-columns:50% auto}}@media screen and (min-width:65em){.k-file-preview-layout{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}}.k-file-preview-layout>*{min-width:0}.k-file-preview-image{position:relative;background:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXR0ZXJuIGlkPSJhIiB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHBhdHRlcm5Vbml0cz0idXNlclNwYWNlT25Vc2UiPjxwYXRoIGZpbGw9InJnYmEoMCwgMCwgMCwgMC4yKSIgZD0iTTAgMGgxMHYxMEgwem0xMCAxMGgxMHYxMEgxMHoiLz48L3BhdHRlcm4+PHJlY3QgZmlsbD0idXJsKCNhKSIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIvPjwvc3ZnPg==")}@media screen and (min-width:65em){.k-file-preview-image{width:33.33%}}@media screen and (min-width:90em){.k-file-preview-image{width:25%}}.k-file-preview-image .k-image span{overflow:hidden;padding-bottom:66.66%}@media screen and (min-width:30em) and (max-width:65em){.k-file-preview-image .k-image span{position:absolute;top:0;left:0;bottom:0;right:0;padding-bottom:0!important}}@media screen and (min-width:65em){.k-file-preview-image .k-image span{padding-bottom:100%}}.k-file-preview-placeholder{display:block;padding-bottom:100%}.k-file-preview-image img{padding:3rem}.k-file-preview-image-link{display:block;outline:0}.k-file-preview-image-link.k-link[data-tabbed]{-webkit-box-shadow:none;box-shadow:none;outline:2px solid #4271ae;outline-offset:-2px}.k-file-preview-icon{position:relative;display:block;padding-bottom:100%;overflow:hidden;color:hsla(0,0%,100%,.5)}.k-file-preview-icon svg{position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%) scale(4);transform:translate(-50%,-50%) scale(4)}.k-file-preview-details{padding:1.5rem;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}@media screen and (min-width:65em){.k-file-preview-details{padding:3rem}}.k-file-preview-details ul{line-height:1.5em;max-width:50rem;display:grid;grid-gap:1.5rem 3rem;grid-template-columns:repeat(auto-fill,minmax(100px,1fr))}@media screen and (min-width:30em){.k-file-preview-details ul{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}.k-file-preview-details h3{font-size:.875rem;font-weight:500;color:#999}.k-file-preview-details p{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:hsla(0,0%,100%,.75);font-size:.875rem}.k-file-preview-details p a{display:block;width:100%;overflow:hidden;text-overflow:ellipsis}.k-grid{--columns:12;display:grid;grid-column-gap:0;grid-row-gap:0;grid-template-columns:1fr}@media screen and (min-width:30em){.k-grid[data-gutter=small]{grid-column-gap:1rem;grid-row-gap:1rem}.k-grid[data-gutter=huge],.k-grid[data-gutter=large],.k-grid[data-gutter=medium]{grid-column-gap:1.5rem;grid-row-gap:1.5rem}}@media screen and (min-width:65em){.k-grid{grid-template-columns:repeat(var(--columns),1fr)}.k-grid[data-gutter=large]{grid-column-gap:3rem}.k-grid[data-gutter=huge]{grid-column-gap:4.5rem}}@media screen and (min-width:90em){.k-grid[data-gutter=large]{grid-column-gap:4.5rem}.k-grid[data-gutter=huge]{grid-column-gap:6rem}}@media screen and (min-width:120em){.k-grid[data-gutter=large]{grid-column-gap:6rem}.k-grid[data-gutter=huge]{grid-column-gap:7.5rem}}.k-header{border-bottom:1px solid #ccc;margin-bottom:2rem;padding-top:4vh}.k-header .k-headline{min-height:1.25em;margin-bottom:.5rem;word-wrap:break-word}.k-header .k-header-buttons{margin-top:-.5rem;height:3.25rem}.k-header .k-headline-editable{cursor:pointer}.k-header .k-headline-editable .k-icon{color:#999;opacity:0;-webkit-transition:opacity .3s;transition:opacity .3s;display:inline-block}[dir=ltr] .k-header .k-headline-editable .k-icon{margin-left:.5rem}[dir=rtl] .k-header .k-headline-editable .k-icon{margin-right:.5rem}.k-header .k-headline-editable:hover .k-icon{opacity:1}.k-header-tabs{position:relative;background:#e9e9e9;border-top:1px solid #ccc;border-left:1px solid #ccc;border-right:1px solid #ccc}.k-header-tabs nav{display:-webkit-box;display:-ms-flexbox;display:flex;margin-left:-1px;margin-right:-1px}.k-header-tabs nav,.k-tab-button.k-button{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.k-tab-button.k-button{position:relative;z-index:1;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:.625rem .75rem;font-size:.75rem;text-transform:uppercase;text-align:center;font-weight:500;border-left:1px solid transparent;border-right:1px solid #ccc;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:1;flex-shrink:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;max-width:15rem}@media screen and (min-width:30em){.k-tab-button.k-button{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}}@media screen and (min-width:30em){.k-tab-button.k-button .k-icon{margin-right:.5rem}}.k-tab-button.k-button>.k-button-text{padding-top:.375rem;font-size:10px;overflow:hidden;max-width:10rem;text-overflow:ellipsis}[dir=ltr] .k-tab-button.k-button>.k-button-text{padding-left:0}[dir=rtl] .k-tab-button.k-button>.k-button-text{padding-right:0}@media screen and (min-width:30em){.k-tab-button.k-button>.k-button-text{font-size:.75rem;padding-top:0}}.k-tab-button:last-child{border-right:1px solid transparent}.k-tab-button[aria-current]{position:relative;background:#efefef;border-right:1px solid #ccc;pointer-events:none}.k-tab-button[aria-current]:first-child{border-left:1px solid #ccc}.k-tab-button[aria-current]:after,.k-tab-button[aria-current]:before{position:absolute;content:""}.k-tab-button[aria-current]:before{left:-1px;right:-1px;height:2px;top:-1px;background:#16171a}.k-tab-button[aria-current]:after{left:0;right:0;height:1px;bottom:-1px;background:#efefef}.k-tabs-dropdown{top:100%;right:0}.k-list .k-list-item:not(:last-child){margin-bottom:2px}.k-list-item{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;background:#fff;border-radius:1px;-webkit-box-shadow:rgba(22,23,26,.05) 0 2px 5px;box-shadow:0 2px 5px rgba(22,23,26,.05)}.k-list-item .k-sort-handle{position:absolute;left:-1.5rem;width:1.5rem;height:38px;opacity:0}.k-list:hover .k-sort-handle{opacity:.25}.k-list-item:hover .k-sort-handle{opacity:1}.k-list-item.k-sortable-ghost{position:relative;outline:2px solid #4271ae;z-index:1;-webkit-box-shadow:rgba(22,23,26,.25) 0 5px 10px;box-shadow:0 5px 10px rgba(22,23,26,.25)}.k-list-item.k-sortable-fallback{opacity:.25!important;overflow:hidden}.k-list-item-image{width:38px;height:38px;overflow:hidden;-ms-flex-negative:0;flex-shrink:0;line-height:0}.k-list-item-image .k-image{width:38px;height:38px;-o-object-fit:contain;object-fit:contain}.k-list-item-image .k-icon{width:38px;height:38px}.k-list-item-image .k-icon svg{opacity:.5}.k-list-item-content{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:1;flex-shrink:1;overflow:hidden;outline:none}.k-list-item-content[data-tabbed]{outline:none;-webkit-box-shadow:#4271ae 0 0 0 2px,rgba(66,113,174,.2) 0 0 0 2px;box-shadow:0 0 0 2px #4271ae,0 0 0 2px rgba(66,113,174,.2)}.k-list-item-text{display:-webkit-box;display:-ms-flexbox;display:flex;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline;width:100%;line-height:1.25rem;padding:.5rem .75rem}.k-list-item-text em{font-style:normal;margin-right:1rem;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;font-size:.875rem;color:#16171a}.k-list-item-text em,.k-list-item-text small{min-width:0;overflow:hidden;text-overflow:ellipsis}.k-list-item-text small{color:#999;font-size:.75rem;color:#777;display:none}@media screen and (min-width:30em){.k-list-item-text small{display:block}}.k-list-item-status{height:auto!important}.k-list-item-options{position:relative;-ms-flex-negative:0;flex-shrink:0}.k-list-item-options .k-dropdown-content{top:38px}.k-list-item-options>.k-button{height:38px;padding:0 12px}.k-list-item-options>.k-button>.k-button-icon{height:38px}.k-view{padding-left:1.5rem;padding-right:1.5rem;margin:0 auto;max-width:100rem}@media screen and (min-width:30em){.k-view{padding-left:3rem;padding-right:3rem}}@media screen and (min-width:90em){.k-view{padding-left:6rem;padding-right:6rem}}.k-view[data-align=center]{height:calc(100vh - 6rem);display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;padding:0 3rem;overflow:auto}.k-view[data-align=center]>*{-ms-flex-preferred-size:22.5rem;flex-basis:22.5rem}.k-headline{font-size:1rem;font-weight:600;line-height:1.5em}.k-headline[data-size=small]{font-size:.875rem}.k-headline[data-size=large]{font-size:1.25rem;font-weight:400}@media screen and (min-width:65em){.k-headline[data-size=large]{font-size:1.5rem}}.k-headline[data-size=huge]{font-size:1.5rem;line-height:1.15em}@media screen and (min-width:65em){.k-headline[data-size=huge]{font-size:1.75rem}}.k-headline[data-theme=negative]{color:#c82829}.k-headline[data-theme=positive]{color:#5d800d}.k-headline abbr{color:#999;padding-left:.25rem;text-decoration:none}.k-icon{position:relative;line-height:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-ms-flex-negative:0;flex-shrink:0}.k-icon svg{width:1rem;height:1rem;-moz-transform:scale(1)}.k-icon svg *{fill:currentColor}.k-icon[data-back=black]{background:#16171a;color:#fff}.k-icon[data-back=white]{background:#fff;color:#16171a}.k-icon[data-back=pattern]{background:#2d2f36 url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXR0ZXJuIGlkPSJhIiB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHBhdHRlcm5Vbml0cz0idXNlclNwYWNlT25Vc2UiPjxwYXRoIGZpbGw9InJnYmEoMCwgMCwgMCwgMC4yKSIgZD0iTTAgMGgxMHYxMEgwem0xMCAxMGgxMHYxMEgxMHoiLz48L3BhdHRlcm4+PHJlY3QgZmlsbD0idXJsKCNhKSIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIvPjwvc3ZnPg==");color:#fff}.k-icon[data-size=medium] svg{width:2rem;height:2rem}.k-icon[data-size=large] svg{width:3rem;height:3rem}.k-icon-emoji{display:block;line-height:1;font-style:normal;font-size:1rem;margin-left:-.3rem}@media not all,only screen and (-webkit-min-device-pixel-ratio:2),only screen and (min-resolution:2dppx),only screen and (min-resolution:192dpi){.k-icon-emoji{font-size:1.25rem;margin-left:-.15rem}}.k-image span{position:relative;display:block;line-height:0;padding-bottom:100%}.k-image img{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;-o-object-fit:contain;object-fit:contain}.k-image-error{position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);color:#fff;font-size:.9em}.k-image-error svg *{fill:hsla(0,0%,100%,.3)}.k-image[data-cover] img{-o-object-fit:cover;object-fit:cover}.k-image[data-back=black] span{background:#16171a}.k-image[data-back=white] span{background:#fff;color:#16171a}.k-image[data-back=white] .k-image-error{background:#16171a;color:#fff}.k-image[data-back=pattern] span{background:#2d2f36 url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXR0ZXJuIGlkPSJhIiB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHBhdHRlcm5Vbml0cz0idXNlclNwYWNlT25Vc2UiPjxwYXRoIGZpbGw9InJnYmEoMCwgMCwgMCwgMC4yKSIgZD0iTTAgMGgxMHYxMEgwem0xMCAxMGgxMHYxMEgxMHoiLz48L3BhdHRlcm4+PHJlY3QgZmlsbD0idXJsKCNhKSIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIvPjwvc3ZnPg==")}.k-progress{-webkit-appearance:none;width:100%;height:.5rem;border-radius:5rem;background:#ccc;overflow:hidden;border:none}.k-progress::-webkit-progress-bar{border:none;background:#ccc;height:.5rem;border-radius:20px}.k-progress::-webkit-progress-value{border-radius:inherit;background:#4271ae;-webkit-transition:width .3s;transition:width .3s}.k-progress::-moz-progress-bar{border-radius:inherit;background:#4271ae;-moz-transition:width .3s;transition:width .3s}.k-sort-handle{cursor:move;cursor:grab;cursor:-webkit-grab;color:#16171a;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:0;width:2rem;height:2rem;display:-webkit-box;display:-ms-flexbox;display:flex;will-change:opacity,color;-webkit-transition:opacity .3s;transition:opacity .3s;z-index:1}.k-sort-handle svg{width:1rem}.k-sort-handle:active{cursor:grabbing;cursor:-webkit-grabbing}.k-text{line-height:1.5em}.k-text p{margin-bottom:1.5em}.k-text a{text-decoration:underline}.k-text>:last-child{margin-bottom:0}.k-text[data-align=center]{text-align:center}.k-text[data-align=right]{text-align:right}.k-text[data-size=tiny]{font-size:.75rem}.k-text[data-size=small]{font-size:.875rem}.k-text[data-size=medium]{font-size:1rem}.k-text[data-size=large]{font-size:1.25rem}.k-text[data-theme=help]{font-size:.875rem;color:#777;line-height:1.25rem}.k-dialog-body .k-text{word-wrap:break-word}button{line-height:inherit;border:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;font-size:1rem;color:currentColor;background:none;cursor:pointer}button::-moz-focus-inner{padding:0;border:0}.k-button{display:inline-block;position:relative;font-size:.875rem;-webkit-transition:color .3s;transition:color .3s}.k-button,.k-button:focus,.k-button:hover{outline:none}.k-button[data-tabbed]{outline:none;-webkit-box-shadow:#4271ae 0 0 0 2px,rgba(66,113,174,.2) 0 0 0 2px;box-shadow:0 0 0 2px #4271ae,0 0 0 2px rgba(66,113,174,.2)}.k-button *{vertical-align:middle}.k-button[data-responsive] .k-button-text{display:none}@media screen and (min-width:30em){.k-button[data-responsive] .k-button-text{display:inline}}.k-button[data-theme=positive]{color:#5d800d}.k-button[data-theme=negative]{color:#c82829}.k-button-icon{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:0}[dir=ltr] .k-button-icon~.k-button-text{padding-left:.5rem}[dir=rtl] .k-button-icon~.k-button-text{padding-right:.5rem}.k-button-text{opacity:.75}.k-button:focus .k-button-text,.k-button:hover .k-button-text{opacity:1}.k-button-text b,.k-button-text span{vertical-align:baseline}.k-button[data-disabled]{opacity:.5;cursor:default}.k-card-options>.k-button[data-disabled]{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex}.k-button[data-disabled]:focus .k-button-text,.k-button[data-disabled]:hover .k-button-text{opacity:.75}.k-button-group{font-size:0;margin-left:-.75rem;margin-right:-.75rem}.k-button-group>.k-dropdown{height:3rem;display:inline-block}.k-button-group>.k-button,.k-button-group>.k-dropdown>.k-button{padding:1rem .75rem;line-height:1rem}.k-button-group .k-dropdown-content{top:calc(100% + 1px);margin:0 .75rem}.k-dropdown{position:relative}.k-dropdown-content{position:absolute;top:100%;background:#16171a;color:#fff;z-index:700;-webkit-box-shadow:rgba(22,23,26,.2) 0 2px 10px;box-shadow:0 2px 10px rgba(22,23,26,.2);border-radius:1px;text-align:left;margin-bottom:6rem}[dir=ltr] .k-dropdown-content{left:0}[dir=rtl] .k-dropdown-content{right:0}[dir=ltr] .k-dropdown-content[data-align=right]{left:auto;right:0}[dir=rtl] .k-dropdown-content[data-align=right]{left:0;right:auto}.k-dropdown-content>.k-dropdown-item:first-child{margin-top:.5rem}.k-dropdown-content>.k-dropdown-item:last-child{margin-bottom:.5rem}.k-dropdown-content hr{position:relative;padding:.5rem 0;border:0}.k-dropdown-content hr:after{position:absolute;top:.5rem;left:1rem;right:1rem;content:"";height:1px;background:currentColor;opacity:.2}.k-dropdown-item{white-space:nowrap;line-height:1;display:-webkit-box;display:-ms-flexbox;display:flex;width:100%;-webkit-box-align:center;-ms-flex-align:center;align-items:center;font-size:.875rem;padding:6px 16px}.k-dropdown-item:focus{outline:none;-webkit-box-shadow:#4271ae 0 0 0 2px,rgba(66,113,174,.2) 0 0 0 2px;box-shadow:0 0 0 2px #4271ae,0 0 0 2px rgba(66,113,174,.2)}.k-dropdown-item .k-button-figure{text-align:center;padding-right:.5rem}.k-link{outline:none}.k-link[data-tabbed]{outline:none;-webkit-box-shadow:#4271ae 0 0 0 2px,rgba(66,113,174,.2) 0 0 0 2px;box-shadow:0 0 0 2px #4271ae,0 0 0 2px rgba(66,113,174,.2)}.k-pagination{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;direction:ltr}.k-pagination .k-button{padding:1rem}.k-pagination-details{white-space:nowrap}.k-pagination>span{font-size:.875rem}.k-pagination[data-align=center]{text-align:center}.k-pagination[data-align=right]{text-align:right}.k-dropdown-content.k-pagination-selector{position:absolute;top:100%;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);background:#000}[dir=ltr] .k-dropdown-content.k-pagination-selector{direction:ltr}[dir=rtl] .k-dropdown-content.k-pagination-selector{direction:rtl}.k-pagination-settings{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.k-pagination-settings .k-button{line-height:1}.k-pagination-settings label{display:-webkit-box;display:-ms-flexbox;display:flex;border-right:1px solid hsla(0,0%,100%,.35);-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:.625rem 1rem;font-size:.75rem}.k-pagination-settings label span{margin-right:.5rem}.k-prev-next{direction:ltr}.k-search{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;overflow:auto;background:rgba(22,23,26,.6)}.k-search-box{max-width:30rem;margin:0 auto;-webkit-box-shadow:rgba(22,23,26,.2) 0 2px 10px;box-shadow:0 2px 10px rgba(22,23,26,.2)}@media screen and (min-width:65em){.k-search-box{margin:2.5rem auto}}.k-search-input{background:#efefef}.k-search-input,.k-search-types{display:-webkit-box;display:-ms-flexbox;display:flex}.k-search-types{-ms-flex-negative:0;flex-shrink:0}.k-search-types>.k-button{padding:0 0 0 .7rem;font-size:1rem;line-height:1;height:2.5rem}.k-search-types>.k-button .k-icon{height:2.5rem}.k-search-types>.k-button .k-button-text{opacity:1;font-weight:500}.k-search-input input{background:none;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;font:inherit;padding:.75rem;border:0;height:2.5rem}.k-search-close{width:2.5rem;line-height:1}.k-search input:focus{outline:0}.k-search ul{background:#fff}.k-search li{border-bottom:1px solid #efefef;line-height:1.125;display:-webkit-box;display:-ms-flexbox;display:flex}.k-search li .k-link{display:block;padding:.5rem .75rem;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.k-search li strong{display:block;font-size:.875rem;font-weight:400}.k-search li small{font-size:.75rem;color:#777}.k-search li[data-selected]{outline:2px solid #4271ae;background:rgba(66,113,174,.25);border-bottom:1px solid transparent}.k-search-empty{padding:.825rem .75rem;font-size:.75rem;background:#efefef;border-top:1px dashed #ccc;color:#777}.k-tag{position:relative;font-size:.875rem;line-height:1;cursor:pointer;background-color:#16171a;color:#efefef;border-radius:1px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.k-tag:focus{outline:0;background-color:#4271ae;border-color:#4271ae;color:#fff}.k-tag-text{padding:0 .75rem}.k-tag-toggle{color:hsla(0,0%,100%,.7);width:2rem;height:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;border-left:1px solid hsla(0,0%,100%,.15)}.k-tag-toggle:hover{background:hsla(0,0%,100%,.2);color:#fff}.k-topbar{position:relative;color:#fff;-ms-flex-negative:0;flex-shrink:0;height:2.5rem;line-height:1;background:#16171a}.k-topbar-wrapper{position:relative;margin-left:-.75rem;margin-right:-.75rem}.k-topbar-loader,.k-topbar-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.k-topbar-loader{position:absolute;top:0;right:0;bottom:0;height:2.5rem;width:2.5rem;padding:.75rem;background:#16171a;z-index:1;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.k-topbar-loader svg{height:18px;width:18px;-webkit-animation:Spin .9s linear infinite;animation:Spin .9s linear infinite}.k-topbar-menu{-ms-flex-negative:0;flex-shrink:0}.k-topbar-menu ul{padding:.5rem 0}.k-topbar-menu-button{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.k-topbar-menu-button .k-button-text{opacity:1}.k-topbar-button,.k-topbar-signals-button{padding:.75rem;line-height:1;font-size:.875rem}.k-topbar-signals .k-button .k-button-text{opacity:1}.k-topbar-button .k-button-text{display:-webkit-box;display:-ms-flexbox;display:flex;opacity:1}.k-topbar-view-button{-ms-flex-negative:0;flex-shrink:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}[dir=ltr] .k-topbar-view-button{padding-right:0}[dir=rtl] .k-topbar-view-button{padding-left:0}[dir=ltr] .k-topbar-view-button .k-icon{margin-right:.5rem}[dir=rtl] .k-topbar-view-button .k-icon{margin-left:.5rem}.k-topbar-crumbs{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;display:-webkit-box;display:-ms-flexbox;display:flex;overflow-y:hidden}.k-topbar-crumbs a{position:relative;font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:none;padding-top:.75rem;padding-bottom:.75rem;line-height:1;-webkit-transition:opacity .3s;transition:opacity .3s;outline:none}.k-topbar-crumbs a:before{content:"/";padding:0 .5rem;opacity:.25}.k-topbar-crumbs a:focus,.k-topbar-crumbs a:hover{opacity:1}.k-topbar-crumbs a[data-tabbed]{outline:none;-webkit-box-shadow:#4271ae 0 0 0 2px,rgba(66,113,174,.2) 0 0 0 2px;box-shadow:0 0 0 2px #4271ae,0 0 0 2px rgba(66,113,174,.2)}.k-topbar-crumbs a:not(:last-child){max-width:15vw}.k-topbar-breadcrumb-menu{-ms-flex-negative:0;flex-shrink:0}@media screen and (min-width:30em){.k-topbar-crumbs a{display:block}.k-topbar-breadcrumb-menu{display:none}}.k-topbar-signals{position:absolute;top:0;background:#16171a;height:2.5rem;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}[dir=ltr] .k-topbar-signals{right:0}[dir=rtl] .k-topbar-signals{left:0}.k-topbar-signals:before{position:absolute;content:"";top:0;bottom:0;width:.5rem}[dir=ltr] .k-topbar-signals:before{left:-.5rem;background:-webkit-linear-gradient(left,rgba(22,23,26,0),#16171a)}[dir=rtl] .k-topbar-signals:before{right:-.5rem;background:-webkit-linear-gradient(right,rgba(22,23,26,0),#16171a)}.k-topbar-signals .k-button{line-height:1}.k-topbar-notification{font-weight:600;line-height:1;display:-webkit-box;display:-ms-flexbox;display:flex}.k-topbar .k-button[data-theme=positive]{color:#a7bd68}.k-topbar .k-button[data-theme=negative]{color:#d16464}.k-topbar .k-button[data-theme=negative] .k-button-text{display:none}@media screen and (min-width:30em){.k-topbar .k-button[data-theme=negative] .k-button-text{display:inline}}.k-topbar .k-button[data-theme] .k-button-text{opacity:1}.k-topbar .k-dropdown-content{color:#16171a;background:#fff}.k-topbar .k-dropdown-content hr:after{opacity:.1}.k-topbar-menu [aria-current] .k-link{color:#4271ae;font-weight:500}.k-registration{display:inline-block;margin-right:1rem;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.k-registration p{color:#d16464;font-size:.875rem;margin-right:1rem;font-weight:600;display:none}@media screen and (min-width:90em){.k-registration p{display:block}}.k-registration .k-button{color:#fff}.k-section,.k-sections{padding-bottom:3rem}.k-section-header{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline;z-index:1}.k-section-header .k-headline{line-height:1.25rem;padding-bottom:.75rem;min-height:2rem}.k-section-header .k-button-group{position:absolute;top:-.875rem}[dir=ltr] .k-section-header .k-button-group{right:0}[dir=rtl] .k-section-header .k-button-group{left:0}.k-fields-issue-headline,.k-info-section-headline{margin-bottom:.5rem}.k-fields-section input[type=submit]{display:none}[data-locked] .k-fields-section{opacity:.2;pointer-events:none}.k-browser-view .k-error-view-content{text-align:left}.k-error-view{position:absolute;top:0;right:0;bottom:0;left:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.k-error-view-content{line-height:1.5em;max-width:25rem;text-align:center}.k-error-view-icon{color:#c82829;display:inline-block}.k-error-view-content p:not(:last-child){margin-bottom:.75rem}.k-installation-view .k-button{display:block;margin-top:1.5rem}.k-installation-view .k-headline{margin-bottom:.75rem}.k-installation-issues{line-height:1.5em;font-size:.875rem}.k-installation-issues li{position:relative;padding:1.5rem;background:#fff}[dir=ltr] .k-installation-issues li{padding-left:3.5rem}[dir=rtl] .k-installation-issues li{padding-right:3.5rem}.k-installation-issues .k-icon{position:absolute;top:calc(1.5rem + 2px)}[dir=ltr] .k-installation-issues .k-icon{left:1.5rem}[dir=rtl] .k-installation-issues .k-icon{right:1.5rem}.k-installation-issues .k-icon svg *{fill:#c82829}.k-installation-issues li:not(:last-child){margin-bottom:2px}.k-installation-issues li code{font:inherit;color:#c82829}.k-installation-view .k-button[type=submit]{padding:1rem}[dir=ltr] .k-installation-view .k-button[type=submit]{margin-left:-1rem}[dir=rtl] .k-installation-view .k-button[type=submit]{margin-right:-1rem}.k-login-form label abbr{visibility:hidden}.k-login-buttons{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:1.5rem 0}.k-login-button{padding:.5rem 1rem;font-weight:500;-webkit-transition:opacity .3s;transition:opacity .3s}[dir=ltr] .k-login-button{margin-right:-1rem}[dir=rtl] .k-login-button{margin-left:-1rem}.k-login-button span{opacity:1}.k-login-button[disabled]{opacity:.25}.k-login-checkbox{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:.5rem 0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;font-size:.875rem;cursor:pointer}.k-login-checkbox .k-checkbox-text{opacity:.75;-webkit-transition:opacity .3s;transition:opacity .3s}.k-login-checkbox:focus span,.k-login-checkbox:hover span{opacity:1}.k-login-alert{padding:.5rem .75rem;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;min-height:38px;margin-bottom:2rem;background:#c82829;color:#fff;font-size:.875rem;border-radius:1px;-webkit-box-shadow:rgba(22,23,26,.2) 0 2px 10px;box-shadow:0 2px 10px rgba(22,23,26,.2);cursor:pointer}.k-status-flag svg{width:14px;height:14px}.k-status-flag-listed .k-icon{color:#a7bd68}.k-status-flag-unlisted .k-icon{color:#81a2be}.k-status-flag-draft .k-icon{color:#d16464}.k-status-flag[disabled]{opacity:1}.k-settings-view section{margin-bottom:3rem}.k-settings-view .k-header{margin-bottom:1.5rem}.k-settings-view header{margin-bottom:.5rem;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.k-settings-view header,.k-system-info-box{display:-webkit-box;display:-ms-flexbox;display:flex}.k-system-info-box{background:#fff;padding:.75rem}.k-system-info-box li{-ms-flex-negative:0;flex-shrink:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-preferred-size:0;flex-basis:0}.k-system-info-box dt{font-size:.875rem;color:#777;margin-bottom:.25rem}.k-system-unregistered{color:#c82829}.k-languages-section{margin-bottom:2rem}.k-user-profile{background:#fff}.k-user-profile>.k-view{padding-top:3rem;padding-bottom:3rem;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;line-height:0}.k-user-profile .k-button-group{overflow:hidden}[dir=ltr] .k-user-profile .k-button-group{margin-left:.75rem}[dir=rtl] .k-user-profile .k-button-group{margin-right:.75rem}.k-user-profile .k-button-group .k-button{display:block;padding-top:.25rem;padding-bottom:.25rem;overflow:hidden;white-space:nowrap}.k-user-profile .k-button-group .k-button[disabled]{opacity:1}.k-user-profile .k-dropdown-content{margin-top:.5rem;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}.k-user-view-image .k-image{display:block;width:4rem;height:4rem;line-height:0}.k-user-view-image .k-button-text{opacity:1}.k-user-view-image .k-icon{width:4rem;height:4rem;background:#16171a;color:#999}.k-user-name-placeholder{color:#999;-webkit-transition:color .3s;transition:color .3s}.k-header[data-editable] .k-user-name-placeholder:hover{color:#16171a} \ No newline at end of file diff --git a/kirby/panel/dist/favicon.png b/kirby/panel/dist/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..ecf0cf8d60e963463e558d3aefbbd532d676566e GIT binary patch literal 539 zcmV+$0_6RPP)Fp|=ykZ#eI$qt+0+(tJc@4BUyZNbtdTnW7Ue zj`Lb*_W2_5Tx=kw**3Vd^FhyE1y^2TprZL8M>ro&OrP(BlAjb@aMnLvJNdawU;7E)&0!b~~Qh?=C z%B_HJYw=s;T42I`DG!4Ey*|ozV5iKQoIu^rM7aXIzwBD)ww@HwrUfP`y7pc`_SYD^ z=0;%aY>G}`uQO|&U@o_ZYpQsDojZA+);n-5{2ljxA>p-{iQcYc6kzG?QtAS{a;4^K z-JFl;x)xo#h=rrGFdxku?oc*b2GF_fxKMnFDJr={xx$Jjuaiad!KXVCqSn-7i|X`t dTw&!t{{jk>)XmVGi~axr002ovPDHLkV1jM>_2d8m literal 0 HcmV?d00001 diff --git a/kirby/panel/dist/favicon.svg b/kirby/panel/dist/favicon.svg new file mode 100644 index 0000000..5b59d61 --- /dev/null +++ b/kirby/panel/dist/favicon.svg @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/kirby/panel/dist/img/icons.svg b/kirby/panel/dist/img/icons.svg new file mode 100644 index 0000000..090d7ab --- /dev/null +++ b/kirby/panel/dist/img/icons.svg @@ -0,0 +1,424 @@ + diff --git a/kirby/panel/dist/js/app.js b/kirby/panel/dist/js/app.js new file mode 100644 index 0000000..92be119 --- /dev/null +++ b/kirby/panel/dist/js/app.js @@ -0,0 +1 @@ +(function(t){function e(e){for(var i,a,o=e[0],u=e[1],l=e[2],p=0,d=[];p1&&void 0!==arguments[1])||arguments[1],i=t.parents.map((function(t){return{label:t.title,link:e.link(t.id)}}));return!0===n&&i.push({label:t.title,link:this.link(t.id)}),i},changeSlug:function(e,n){var i=this;return Object(j["a"])(regeneratorRuntime.mark((function r(){return regeneratorRuntime.wrap((function(r){while(1)switch(r.prev=r.next){case 0:return r.abrupt("return",t.patch("pages/"+i.id(e)+"/slug",{slug:n}));case 1:case"end":return r.stop()}}),r)})))()},changeStatus:function(e,n,i){var r=this;return Object(j["a"])(regeneratorRuntime.mark((function s(){return regeneratorRuntime.wrap((function(s){while(1)switch(s.prev=s.next){case 0:return s.abrupt("return",t.patch("pages/"+r.id(e)+"/status",{status:n,position:i}));case 1:case"end":return s.stop()}}),s)})))()},changeTemplate:function(e,n){var i=this;return Object(j["a"])(regeneratorRuntime.mark((function r(){return regeneratorRuntime.wrap((function(r){while(1)switch(r.prev=r.next){case 0:return r.abrupt("return",t.patch("pages/"+i.id(e)+"/template",{template:n}));case 1:case"end":return r.stop()}}),r)})))()},changeTitle:function(e,n){var i=this;return Object(j["a"])(regeneratorRuntime.mark((function r(){return regeneratorRuntime.wrap((function(r){while(1)switch(r.prev=r.next){case 0:return r.abrupt("return",t.patch("pages/"+i.id(e)+"/title",{title:n}));case 1:case"end":return r.stop()}}),r)})))()},children:function(e,n){var i=this;return Object(j["a"])(regeneratorRuntime.mark((function r(){return regeneratorRuntime.wrap((function(r){while(1)switch(r.prev=r.next){case 0:return r.abrupt("return",t.post("pages/"+i.id(e)+"/children/search",n));case 1:case"end":return r.stop()}}),r)})))()},create:function(e,n){var i=this;return Object(j["a"])(regeneratorRuntime.mark((function r(){return regeneratorRuntime.wrap((function(r){while(1)switch(r.prev=r.next){case 0:if(null!==e&&"/"!==e){r.next=2;break}return r.abrupt("return",t.post("site/children",n));case 2:return r.abrupt("return",t.post("pages/"+i.id(e)+"/children",n));case 3:case"end":return r.stop()}}),r)})))()},delete:function(e,n){var i=this;return Object(j["a"])(regeneratorRuntime.mark((function r(){return regeneratorRuntime.wrap((function(r){while(1)switch(r.prev=r.next){case 0:return r.abrupt("return",t.delete("pages/"+i.id(e),n));case 1:case"end":return r.stop()}}),r)})))()},duplicate:function(e,n,i){var r=this;return Object(j["a"])(regeneratorRuntime.mark((function s(){return regeneratorRuntime.wrap((function(s){while(1)switch(s.prev=s.next){case 0:return s.abrupt("return",t.post("pages/"+r.id(e)+"/duplicate",{slug:n,children:i.children||!1,files:i.files||!1}));case 1:case"end":return s.stop()}}),s)})))()},get:function(e,n){var i=this;return Object(j["a"])(regeneratorRuntime.mark((function r(){var s;return regeneratorRuntime.wrap((function(r){while(1)switch(r.prev=r.next){case 0:return r.next=2,t.get("pages/"+i.id(e),n);case 2:return s=r.sent,!0===Array.isArray(s.content)&&(s.content={}),r.abrupt("return",s);case 5:case"end":return r.stop()}}),r)})))()},id:function(t){return t.replace(/\//g,"+")},files:function(e,n){var i=this;return Object(j["a"])(regeneratorRuntime.mark((function r(){return regeneratorRuntime.wrap((function(r){while(1)switch(r.prev=r.next){case 0:return r.abrupt("return",t.post("pages/"+i.id(e)+"/files/search",n));case 1:case"end":return r.stop()}}),r)})))()},link:function(t){return"/"+this.url(t)},options:function(e){var n=arguments,i=this;return Object(j["a"])(regeneratorRuntime.mark((function r(){var s,a,o,u;return regeneratorRuntime.wrap((function(r){while(1)switch(r.prev=r.next){case 0:return s=n.length>1&&void 0!==n[1]?n[1]:"view",r.next=3,t.get(i.url(e),{select:"options"});case 3:return a=r.sent,o=a.options,u=[],"list"===s&&(u.push({click:"preview",icon:"open",text:F["a"].i18n.translate("open"),disabled:!1===o.preview}),u.push("-")),u.push({click:"rename",icon:"title",text:F["a"].i18n.translate("rename"),disabled:!o.changeTitle}),u.push({click:"duplicate",icon:"copy",text:F["a"].i18n.translate("duplicate"),disabled:!o.duplicate}),u.push("-"),u.push({click:"url",icon:"url",text:F["a"].i18n.translate("page.changeSlug"),disabled:!o.changeSlug}),u.push({click:"status",icon:"preview",text:F["a"].i18n.translate("page.changeStatus"),disabled:!o.changeStatus}),u.push({click:"template",icon:"template",text:F["a"].i18n.translate("page.changeTemplate"),disabled:!o.changeTemplate}),u.push("-"),u.push({click:"remove",icon:"trash",text:F["a"].i18n.translate("delete"),disabled:!o.delete}),r.abrupt("return",u);case 16:case"end":return r.stop()}}),r)})))()},preview:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){var i;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.next=2,e.get(e.id(t),{select:"previewUrl"});case 2:return i=n.sent,n.abrupt("return",i.previewUrl);case 4:case"end":return n.stop()}}),n)})))()},search:function(e,n){var i=this;return Object(j["a"])(regeneratorRuntime.mark((function r(){return regeneratorRuntime.wrap((function(r){while(1)switch(r.prev=r.next){case 0:if(!e){r.next=2;break}return r.abrupt("return",t.post("pages/"+i.id(e)+"/children/search?select=id,title,hasChildren",n));case 2:return r.abrupt("return",t.post("site/children/search?select=id,title,hasChildren",n));case 3:case"end":return r.stop()}}),r)})))()},update:function(e,n){var i=this;return Object(j["a"])(regeneratorRuntime.mark((function r(){return regeneratorRuntime.wrap((function(r){while(1)switch(r.prev=r.next){case 0:return r.abrupt("return",t.patch("pages/"+i.id(e),n));case 1:case"end":return r.stop()}}),r)})))()},url:function(t,e){var n=null===t?"pages":"pages/"+t.replace(/\//g,"+");return e&&(n+="/"+e),n}}},K=(n("a15b"),n("b64b"),n("d3b7"),n("8a79"),n("2ca0"),function(t){return{running:0,request:function(e,n){var i=arguments,r=this;return Object(j["a"])(regeneratorRuntime.mark((function s(){var a,o,u,l,c,p;return regeneratorRuntime.wrap((function(s){while(1)switch(s.prev=s.next){case 0:return a=i.length>2&&void 0!==i[2]&&i[2],n=Object.assign(n||{},{credentials:"same-origin",cache:"no-store",headers:Object(I["a"])({"x-requested-with":"xmlhttprequest","content-type":"application/json"},n.headers)}),t.methodOverwrite&&"GET"!==n.method&&"POST"!==n.method&&(n.headers["x-http-method-override"]=n.method,n.method="POST"),n=t.onPrepare(n),o=e+"/"+JSON.stringify(n),t.onStart(o,a),r.running++,s.next=9,fetch([t.endpoint,e].join(t.endpoint.endsWith("/")||e.startsWith("/")?"":"/"),n);case 9:return u=s.sent,s.next=12,u.text();case 12:l=s.sent,s.prev=13,s.prev=14,c=JSON.parse(l),s.next=21;break;case 18:throw s.prev=18,s.t0=s["catch"](14),new Error("The JSON response from the API could not be parsed. Please check your API connection.");case 21:if(!(u.status<200||u.status>299)){s.next=23;break}throw c;case 23:if(!c.status||"error"!==c.status){s.next=25;break}throw c;case 25:return p=c,c.data&&c.type&&"model"===c.type&&(p=c.data),r.running--,t.onComplete(o),t.onSuccess(c),s.abrupt("return",p);case 33:throw s.prev=33,s.t1=s["catch"](13),r.running--,t.onComplete(o),t.onError(s.t1),s.t1;case 39:case"end":return s.stop()}}),s,null,[[13,33],[14,18]])})))()},get:function(t,e,n){var i=arguments,r=this;return Object(j["a"])(regeneratorRuntime.mark((function s(){var a;return regeneratorRuntime.wrap((function(s){while(1)switch(s.prev=s.next){case 0:return a=i.length>3&&void 0!==i[3]&&i[3],e&&(t+="?"+Object.keys(e).filter((function(t){return void 0!==e[t]&&null!==e[t]})).map((function(t){return t+"="+e[t]})).join("&")),s.abrupt("return",r.request(t,Object.assign(n||{},{method:"GET"}),a));case 3:case"end":return s.stop()}}),s)})))()},post:function(t,e,n){var i=arguments,r=this;return Object(j["a"])(regeneratorRuntime.mark((function s(){var a,o;return regeneratorRuntime.wrap((function(s){while(1)switch(s.prev=s.next){case 0:return a=i.length>3&&void 0!==i[3]?i[3]:"POST",o=i.length>4&&void 0!==i[4]&&i[4],s.abrupt("return",r.request(t,Object.assign(n||{},{method:a,body:JSON.stringify(e)}),o));case 3:case"end":return s.stop()}}),s)})))()},patch:function(t,e,n){var i=arguments,r=this;return Object(j["a"])(regeneratorRuntime.mark((function s(){var a;return regeneratorRuntime.wrap((function(s){while(1)switch(s.prev=s.next){case 0:return a=i.length>3&&void 0!==i[3]&&i[3],s.abrupt("return",r.post(t,e,n,"PATCH",a));case 2:case"end":return s.stop()}}),s)})))()},delete:function(t,e,n){var i=arguments,r=this;return Object(j["a"])(regeneratorRuntime.mark((function s(){var a;return regeneratorRuntime.wrap((function(s){while(1)switch(s.prev=s.next){case 0:return a=i.length>3&&void 0!==i[3]&&i[3],s.abrupt("return",r.post(t,e,n,"DELETE",a));case 2:case"end":return s.stop()}}),s)})))()}}}),V=(n("a4d3"),n("e01a"),function(t){return{list:function(e){return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.abrupt("return",t.get("roles",e));case 1:case"end":return n.stop()}}),n)})))()},get:function(e){return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.abrupt("return",t.get("roles/"+e));case 1:case"end":return n.stop()}}),n)})))()},options:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){var i;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.next=2,e.list(t);case 2:return i=n.sent,n.abrupt("return",i.data.map((function(t){return{info:t.description||"(".concat(F["a"].i18n.translate("role.description.placeholder"),")"),text:t.title,value:t.name}})));case 4:case"end":return n.stop()}}),n)})))()}}}),Y=function(t){return{get:function(){var e=arguments;return Object(j["a"])(regeneratorRuntime.mark((function n(){var i;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return i=e.length>0&&void 0!==e[0]?e[0]:{view:"panel"},n.abrupt("return",t.get("system",i));case 2:case"end":return n.stop()}}),n)})))()},install:function(e){return Object(j["a"])(regeneratorRuntime.mark((function n(){var i;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.next=2,t.post("system/install",e);case 2:return i=n.sent,n.abrupt("return",i.user);case 4:case"end":return n.stop()}}),n)})))()},register:function(e){return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.abrupt("return",t.post("system/register",e));case 1:case"end":return n.stop()}}),n)})))()}}},W=function(t){return{blueprint:function(){return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.abrupt("return",t.get("site/blueprint"));case 1:case"end":return e.stop()}}),e)})))()},blueprints:function(){return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.abrupt("return",t.get("site/blueprints"));case 1:case"end":return e.stop()}}),e)})))()},changeTitle:function(e){return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.abrupt("return",t.patch("site/title",{title:e}));case 1:case"end":return n.stop()}}),n)})))()},children:function(e){return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.abrupt("return",t.post("site/children/search",e));case 1:case"end":return n.stop()}}),n)})))()},get:function(){var e=arguments;return Object(j["a"])(regeneratorRuntime.mark((function n(){var i;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return i=e.length>0&&void 0!==e[0]?e[0]:{view:"panel"},n.abrupt("return",t.get("site",i));case 2:case"end":return n.stop()}}),n)})))()},options:function(){return Object(j["a"])(regeneratorRuntime.mark((function e(){var n,i,r;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.next=2,t.get("site",{select:"options"});case 2:return n=e.sent,i=n.options,r=[],r.push({click:"rename",icon:"title",text:F["a"].i18n.translate("rename"),disabled:!i.changeTitle}),e.abrupt("return",r);case 7:case"end":return e.stop()}}),e)})))()},update:function(e){return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.abrupt("return",t.post("site",e));case 1:case"end":return n.stop()}}),n)})))()}}},J=function(t){return{list:function(){return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.abrupt("return",t.get("translations"));case 1:case"end":return e.stop()}}),e)})))()},get:function(e){return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.abrupt("return",t.get("translations/"+e));case 1:case"end":return n.stop()}}),n)})))()},options:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){var n,i;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.next=2,t.list();case 2:return n=e.sent,i=n.data.map((function(t){return{value:t.id,text:t.name}})),e.abrupt("return",i);case 5:case"end":return e.stop()}}),e)})))()}}},G=function(t){return{blueprint:function(e){return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.abrupt("return",t.get("users/"+e+"/blueprint"));case 1:case"end":return n.stop()}}),n)})))()},blueprints:function(e,n){return Object(j["a"])(regeneratorRuntime.mark((function i(){return regeneratorRuntime.wrap((function(i){while(1)switch(i.prev=i.next){case 0:return i.abrupt("return",t.get("users/"+e+"/blueprints",{section:n}));case 1:case"end":return i.stop()}}),i)})))()},breadcrumb:function(t){return[{link:"/users/"+t.id,label:t.username}]},changeEmail:function(e,n){return Object(j["a"])(regeneratorRuntime.mark((function i(){return regeneratorRuntime.wrap((function(i){while(1)switch(i.prev=i.next){case 0:return i.abrupt("return",t.patch("users/"+e+"/email",{email:n}));case 1:case"end":return i.stop()}}),i)})))()},changeLanguage:function(e,n){return Object(j["a"])(regeneratorRuntime.mark((function i(){return regeneratorRuntime.wrap((function(i){while(1)switch(i.prev=i.next){case 0:return i.abrupt("return",t.patch("users/"+e+"/language",{language:n}));case 1:case"end":return i.stop()}}),i)})))()},changeName:function(e,n){return Object(j["a"])(regeneratorRuntime.mark((function i(){return regeneratorRuntime.wrap((function(i){while(1)switch(i.prev=i.next){case 0:return i.abrupt("return",t.patch("users/"+e+"/name",{name:n}));case 1:case"end":return i.stop()}}),i)})))()},changePassword:function(e,n){return Object(j["a"])(regeneratorRuntime.mark((function i(){return regeneratorRuntime.wrap((function(i){while(1)switch(i.prev=i.next){case 0:return i.abrupt("return",t.patch("users/"+e+"/password",{password:n}));case 1:case"end":return i.stop()}}),i)})))()},changeRole:function(e,n){return Object(j["a"])(regeneratorRuntime.mark((function i(){return regeneratorRuntime.wrap((function(i){while(1)switch(i.prev=i.next){case 0:return i.abrupt("return",t.patch("users/"+e+"/role",{role:n}));case 1:case"end":return i.stop()}}),i)})))()},create:function(e){return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.abrupt("return",t.post("users",e));case 1:case"end":return n.stop()}}),n)})))()},delete:function(e){return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.abrupt("return",t.delete("users/"+e));case 1:case"end":return n.stop()}}),n)})))()},deleteAvatar:function(e){return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.abrupt("return",t.delete("users/"+e+"/avatar"));case 1:case"end":return n.stop()}}),n)})))()},link:function(t,e){return"/"+this.url(t,e)},list:function(e){var n=this;return Object(j["a"])(regeneratorRuntime.mark((function i(){return regeneratorRuntime.wrap((function(i){while(1)switch(i.prev=i.next){case 0:return i.abrupt("return",t.post(n.url(null,"search"),e));case 1:case"end":return i.stop()}}),i)})))()},get:function(e,n){return Object(j["a"])(regeneratorRuntime.mark((function i(){return regeneratorRuntime.wrap((function(i){while(1)switch(i.prev=i.next){case 0:return i.abrupt("return",t.get("users/"+e,n));case 1:case"end":return i.stop()}}),i)})))()},options:function(e){var n=this;return Object(j["a"])(regeneratorRuntime.mark((function i(){var r,s,a;return regeneratorRuntime.wrap((function(i){while(1)switch(i.prev=i.next){case 0:return i.next=2,t.get(n.url(e),{select:"options"});case 2:return r=i.sent,s=r.options,a=[],a.push({click:"rename",icon:"title",text:F["a"].i18n.translate("user.changeName"),disabled:!s.changeName}),a.push({click:"email",icon:"email",text:F["a"].i18n.translate("user.changeEmail"),disabled:!s.changeEmail}),a.push({click:"role",icon:"bolt",text:F["a"].i18n.translate("user.changeRole"),disabled:!s.changeRole}),a.push({click:"password",icon:"key",text:F["a"].i18n.translate("user.changePassword"),disabled:!s.changePassword}),a.push({click:"language",icon:"globe",text:F["a"].i18n.translate("user.changeLanguage"),disabled:!s.changeLanguage}),a.push({click:"remove",icon:"trash",text:F["a"].i18n.translate("user.delete"),disabled:!s.delete}),i.abrupt("return",a);case 12:case"end":return i.stop()}}),i)})))()},roles:function(e){var n=this;return Object(j["a"])(regeneratorRuntime.mark((function i(){var r;return regeneratorRuntime.wrap((function(i){while(1)switch(i.prev=i.next){case 0:return i.next=2,t.get(n.url(e,"roles"));case 2:return r=i.sent,i.abrupt("return",r.data.map((function(t){return{info:t.description||"(".concat(F["a"].i18n.translate("role.description.placeholder"),")"),text:t.title,value:t.name}})));case 4:case"end":return i.stop()}}),i)})))()},search:function(e){return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.abrupt("return",t.post("users/search",e));case 1:case"end":return n.stop()}}),n)})))()},update:function(e,n){return Object(j["a"])(regeneratorRuntime.mark((function i(){return regeneratorRuntime.wrap((function(i){while(1)switch(i.prev=i.next){case 0:return i.abrupt("return",t.patch("users/"+e,n));case 1:case"end":return i.stop()}}),i)})))()},url:function(t,e){var n=t?"users/"+t:"users";return e&&(n+="/"+e),n}}},Z=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},e={endpoint:"/api",methodOverwrite:!0,onPrepare:function(t){return t},onStart:function(){},onComplete:function(){},onSuccess:function(){},onError:function(t){throw window.console.log(t.message),t}},n=Object(I["a"])(Object(I["a"])({},e),t.config||{}),i=Object(I["a"])(Object(I["a"])(Object(I["a"])({},n),K(n)),t);return i.auth=M(i),i.files=U(i),i.languages=z(i),i.pages=H(i),i.roles=V(i),i.system=Y(i),i.site=W(i),i.translations=J(i),i.users=G(i),i.files.rename=i.files.changeName,i.pages.slug=i.pages.changeSlug,i.pages.status=i.pages.changeStatus,i.pages.template=i.pages.changeTemplate,i.pages.title=i.pages.changeTitle,i.site.title=i.site.changeTitle,i.system.info=i.system.get,i},X={install:function(t,e){t.prototype.$api=t.$api=Z({config:{endpoint:B.api,onComplete:function(n){t.$api.requests=t.$api.requests.filter((function(t){return t!==n})),0===t.$api.requests.length&&e.dispatch("isLoading",!1)},onError:function(t){B.debug&&window.console.error(t),403!==t.code||"Unauthenticated"!==t.message&&"access.panel"!==t.key||e.dispatch("user/logout",!0)},onPrepare:function(t){return e.state.languages.current&&(t.headers["x-language"]=e.state.languages.current.code),t.headers["x-csrf"]=window.panel.csrf,t},onStart:function(n){var i=arguments.length>1&&void 0!==arguments[1]&&arguments[1];!1===i&&e.dispatch("isLoading",!0),t.$api.requests.push(n)},onSuccess:function(){clearInterval(t.$api.ping),t.$api.ping=setInterval(t.$api.auth.user,3e5)}},ping:null,requests:[]}),t.$api.ping=setInterval(t.$api.auth.user,3e5)}},Q={install:function(t){t.filter("t",(function(t){return t}))}},tt=(n("caad"),{install:function(t){t.prototype.$events=new t({data:function(){return{entered:null}},created:function(){window.addEventListener("online",this.online),window.addEventListener("offline",this.offline),window.addEventListener("dragenter",this.dragenter,!1),window.addEventListener("dragover",this.prevent,!1),window.addEventListener("dragexit",this.prevent,!1),window.addEventListener("dragleave",this.dragleave,!1),window.addEventListener("drop",this.drop,!1),window.addEventListener("keydown",this.keydown,!1),window.addEventListener("keyup",this.keyup,!1),document.addEventListener("click",this.click,!1)},destroyed:function(){window.removeEventListener("online",this.online),window.removeEventListener("offline",this.offline),window.removeEventListener("dragenter",this.dragenter,!1),window.removeEventListener("dragover",this.prevent,!1),window.removeEventListener("dragexit",this.prevent,!1),window.removeEventListener("dragleave",this.dragleave,!1),window.removeEventListener("drop",this.drop,!1),window.removeEventListener("keydown",this.keydown,!1),window.removeEventListener("keyup",this.keyup,!1),document.removeEventListener("click",this.click,!1)},methods:{click:function(t){this.$emit("click",t)},drop:function(t){this.prevent(t),this.$emit("drop",t)},dragenter:function(t){this.entered=t.target,this.prevent(t),this.$emit("dragenter",t)},dragleave:function(t){this.prevent(t),this.entered===t.target&&this.$emit("dragleave",t)},keydown:function(t){var e=["keydown"];(t.metaKey||t.ctrlKey)&&e.push("cmd"),!0===t.altKey&&e.push("alt"),!0===t.shiftKey&&e.push("shift");var n=this.$helper.string.lcfirst(t.key),i={escape:"esc",arrowUp:"up",arrowDown:"down",arrowLeft:"left",arrowRight:"right"};i[n]&&(n=i[n]),!1===["alt","control","shift","meta"].includes(n)&&e.push(n),this.$emit(e.join("."),t),this.$emit("keydown",t)},keyup:function(t){this.$emit("keyup",t)},online:function(t){this.$emit("online",t)},offline:function(t){this.$emit("offline",t)},prevent:function(t){t.stopPropagation(),t.preventDefault()}}})}}),et=n("1dce"),nt=n.n(et),it=(n("4d63"),n("25f0"),n("1276"),function(t){if(void 0!==t)return JSON.parse(JSON.stringify(t))}),rt=function(t,e){var n=null;return function(){var i=this,r=arguments;clearTimeout(n),n=setTimeout((function(){t.apply(i,r)}),e)}},st=function(t){return void 0!==F["a"].options.components[t]},at=(n("2532"),function(t){return!!t.dataTransfer&&(!!t.dataTransfer.types&&(!0===t.dataTransfer.types.includes("Files")&&!1===t.dataTransfer.types.includes("text/plain")))}),ot=function(t,e){t=String(t);var n="";e=(e||2)-t.length;while(n.length0&&void 0!==arguments[0]?arguments[0]:"3/2",e=String(t).split("/");if(2!==e.length)return"100%";var n=Number(e[0]),i=Number(e[1]),r=100;return 0!==n&&0!==i&&(r=100/n*i),r+"%"}),lt=(n("498a"),function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[],n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"",i="-";return n="a-z0-9"+n,t=t.trim().toLowerCase(),e.forEach((function(e){e&&Object.keys(e).forEach((function(n){var i="/"!==n.substr(0,1),r=n.substring(1,n.length-1),s=i?n:r;t=t.replace(new RegExp(RegExp.escape(s),"g"),e[n])}))})),t=t.replace("/[^\t\n\r -~]/",""),t=t.replace(new RegExp("[^"+n+"]","ig"),i),t=t.replace(new RegExp("["+RegExp.escape(i)+"]{2,}","g"),i),t=t.replace("/",i),t=t.replace(new RegExp("^[^"+n+"]+","g"),""),t=t.replace(new RegExp("[^"+n+"]+$","g"),""),t}),ct=(n("466d"),function(t){t=t||{};var e=t.desc?-1:1,n=-e,i=/^0/,r=/\s+/g,s=/^\s+|\s+$/g,a=/[^\x00-\x80]/,o=/^0x[0-9a-f]+$/i,u=/(0x[\da-fA-F]+|(^[\+\-]?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?(?=\D|\s|$))|\d+)/g,l=/(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/,c=t.insensitive?function(t){return p(""+t).replace(s,"")}:function(t){return(""+t).replace(s,"")};function p(t){return t.toLocaleLowerCase?t.toLocaleLowerCase():t.toLowerCase()}function d(t){return t.replace(u,"\0$1\0").replace(/\0$/,"").replace(/^\0/,"").split("\0")}function f(t,e){return(!t.match(i)||1===e)&&parseFloat(t)||t.replace(r," ").replace(s,"")||0}return function(t,i){var r=c(t),s=c(i);if(!r&&!s)return 0;if(!r&&s)return n;if(r&&!s)return e;var u=d(r),p=d(s),h=parseInt(r.match(o),16)||1!==u.length&&Date.parse(r),m=parseInt(s.match(o),16)||h&&s.match(l)&&Date.parse(s)||null;if(m){if(hm)return e}for(var g=u.length,b=p.length,v=0,k=Math.max(g,b);v0)return e;if(_<0)return n;if(v===k-1)return 0}else{if($y)return e}}return 0}}),pt={camelToKebab:function(t){return t.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()},hasEmoji:function(t){if("string"!==typeof t)return!1;var e=t.match(/(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|[\ud83c\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c\ude32-\ude3a]|[\ud83c\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/i);return null!==e&&null!==e.length},lcfirst:function(t){var e=String(t);return e.charAt(0).toLowerCase()+e.substr(1)},ucfirst:function(t){var e=String(t);return e.charAt(0).toUpperCase()+e.substr(1)}},dt=function(t,e){var n={url:"/",field:"file",method:"POST",accept:"text",attributes:{},complete:function(){},error:function(){},success:function(){},progress:function(){}},i=Object.assign(n,e),r=new FormData;r.append(i.field,t,t.name),i.attributes&&Object.keys(i.attributes).forEach((function(t){r.append(t,i.attributes[t])}));var s=new XMLHttpRequest,a=function(e){if(e.lengthComputable&&i.progress){var n=Math.max(0,Math.min(100,e.loaded/e.total*100));i.progress(s,t,Math.ceil(n))}};s.upload.addEventListener("loadstart",a),s.upload.addEventListener("progress",a),s.addEventListener("load",(function(e){var n=null;try{n=JSON.parse(e.target.response)}catch(r){n={status:"error",message:"The file could not be uploaded"}}n.status&&"error"===n.status?i.error(s,t,n):(i.success(s,t,n),i.progress(s,t,100))})),s.addEventListener("error",(function(e){var n=JSON.parse(e.target.response);i.error(s,t,n),i.progress(s,t,100)})),s.open("POST",i.url,!0),i.headers&&Object.keys(i.headers).forEach((function(t){var e=i.headers[t];s.setRequestHeader(t,e)})),s.send(r)},ft={install:function(t){Array.prototype.sortBy=function(e){var n=t.prototype.$helper.sort(),i=e.split(" "),r=i[0],s=i[1]||"asc";return this.sort((function(t,e){var i=String(t[r]).toLowerCase(),a=String(e[r]).toLowerCase();return"desc"===s?n(a,i):n(i,a)}))},RegExp.escape=function(t){return t.replace(new RegExp("[-/\\\\^$*+?.()[\\]{}]","gu"),"\\$&")},t.prototype.$helper={clone:it,isComponent:st,isUploadEvent:at,debounce:rt,pad:ot,ratio:ut,slug:lt,sort:ct,string:pt,upload:dt}}},ht=(n("64e4"),{}),mt=Object(y["a"])(ht,i,r,!1,null,null,null),gt=(mt.exports,function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.isOpen?n("div",{staticClass:"k-dialog",on:{mousedown:t.cancel}},[n("div",{staticClass:"k-dialog-box",attrs:{"data-size":t.size},on:{mousedown:function(t){t.stopPropagation()}}},[t.notification?n("div",{staticClass:"k-dialog-notification",attrs:{"data-theme":t.notification.type}},[n("p",[t._v(t._s(t.notification.message))]),n("k-button",{attrs:{icon:"cancel"},on:{click:function(e){t.notification=null}}})],1):t._e(),n("div",{staticClass:"k-dialog-body"},[t._t("default")],2),t.$slots["footer"]||t.cancelButton||t.submitButton?n("footer",{staticClass:"k-dialog-footer"},[t._t("footer",[n("k-button-group",[n("span",[t.cancelButton?n("k-button",{staticClass:"k-dialog-button-cancel",attrs:{icon:"cancel"},on:{click:t.cancel}},[t._v("\n "+t._s(t.cancelButtonLabel)+"\n ")]):t._e()],1),n("span",[t.submitButtonConfig?n("k-button",{staticClass:"k-dialog-button-submit",attrs:{icon:t.icon,theme:t.theme},on:{click:t.submit}},[t._v("\n "+t._s(t.submitButtonLabel)+"\n ")]):t._e()],1)])])],2):t._e()])]):t._e()}),bt=[],vt={props:{cancelButton:{type:[String,Boolean],default:!0},icon:{type:String,default:"check"},size:{type:String,default:"default"},submitButton:{type:[String,Boolean],default:!0},theme:String,visible:Boolean},data:function(){return{notification:null,isOpen:this.visible,scrollTop:0}},computed:{cancelButtonLabel:function(){return!1!==this.cancelButton&&(!0===this.cancelButton||0===this.cancelButton.length?this.$t("cancel"):this.cancelButton)},submitButtonConfig:function(){return void 0!==this.$attrs["button"]?this.$attrs["button"]:void 0===this.submitButton||this.submitButton},submitButtonLabel:function(){return!0===this.submitButton||0===this.submitButton.length?this.$t("confirm"):this.submitButton}},created:function(){this.$events.$on("keydown.esc",this.close,!1)},destroyed:function(){this.$events.$off("keydown.esc",this.close,!1)},mounted:function(){!0===this.isOpen&&this.$emit("open")},methods:{storeScrollPosition:function(){var t=document.querySelector(".k-panel-view");t&&t.scrollTop?this.scrollTop=t.scrollTop:this.scrollTop=0},restoreScrollPosition:function(){var t=document.querySelector(".k-panel-view");t&&t.scrollTop&&(t.scrollTop=this.scrollTop)},open:function(){var t=this;this.storeScrollPosition(),this.$store.dispatch("dialog",!0),this.notification=null,this.isOpen=!0,this.$emit("open"),this.$events.$on("keydown.esc",this.close),this.$nextTick((function(){t.$el&&(t.focus(),document.body.addEventListener("focus",(function(e){!1===t.$el.contains(e.target)&&t.focus()}),!0))}))},close:function(){this.notification=null,this.isOpen=!1,this.$emit("close"),this.$events.$off("keydown.esc",this.close),this.$store.dispatch("dialog",null),this.restoreScrollPosition()},cancel:function(){this.$emit("cancel"),this.close()},focus:function(){if(this.$el&&this.$el.querySelector){var t=this.$el.querySelector("[autofocus], [data-autofocus], input, textarea, select, .k-dialog-button-submit");if(t||(t=this.$el.querySelector(".k-dialog-button-cancel")),t)return void t.focus()}},error:function(t){this.notification={message:t,type:"error"}},submit:function(){this.$emit("submit")},success:function(t){this.notification={message:t,type:"success"}}}},kt=vt,$t=(n("a5f3"),Object(y["a"])(kt,gt,bt,!1,null,null,null)),yt=$t.exports,_t=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.notification?n("k-dialog",{ref:"dialog",staticClass:"k-error-dialog",attrs:{visible:!0},on:{close:t.exit,open:t.enter}},[n("k-text",[t._v(t._s(t.notification.message))]),t.notification.details&&Object.keys(t.notification.details).length?n("dl",{staticClass:"k-error-details"},[t._l(t.notification.details,(function(e,i){return[n("dt",{key:"detail-label-"+i},[t._v(t._s(e.label))]),n("dd",{key:"detail-message-"+i},["object"===typeof e.message?[n("ul",t._l(e.message,(function(e,i){return n("li",{key:i},[t._v("\n "+t._s(e)+"\n ")])})),0)]:[t._v("\n "+t._s(e.message)+"\n ")]],2)]}))],2):t._e(),n("k-button-group",{attrs:{slot:"footer"},slot:"footer"},[n("k-button",{attrs:{icon:"check"},on:{click:t.close}},[t._v("\n "+t._s(t.$t("confirm"))+"\n ")])],1)],1):t._e()},wt=[],xt={mixins:[S],computed:{notification:function(){var t=this.$store.state.notification;return"error"===t.type?t:null}},methods:{enter:function(){var t=this;this.$nextTick((function(){t.$el.querySelector(".k-dialog-footer .k-button").focus()}))},exit:function(){this.$store.dispatch("notification/close")}}},Ot=xt,jt=(n("7737"),Object(y["a"])(Ot,_t,wt,!1,null,null,null)),St=jt.exports,Ct=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-remove-dialog",{ref:"dialog",attrs:{text:t.$t("file.delete.confirm",{filename:t.filename})},on:{submit:t.submit}})},Et=[],Rt={mixins:[S],data:function(){return{id:null,parent:null,filename:null}},methods:{open:function(t,e){var n=this;return Object(j["a"])(regeneratorRuntime.mark((function i(){var r;return regeneratorRuntime.wrap((function(i){while(1)switch(i.prev=i.next){case 0:return i.prev=0,i.next=3,n.$api.files.get(t,e);case 3:r=i.sent,n.id=r.id,n.filename=r.filename,n.parent=t,n.$refs.dialog.open(),i.next=13;break;case 10:i.prev=10,i.t0=i["catch"](0),n.$store.dispatch("notification/error",i.t0);case 13:case"end":return i.stop()}}),i,null,[[0,10]])})))()},submit:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,t.$api.files.delete(t.parent,t.filename);case 3:t.$store.dispatch("content/remove","files/"+t.id),t.$store.dispatch("notification/success",":)"),t.$events.$emit("file.delete",t.id),t.$emit("success"),t.$refs.dialog.close(),e.next=13;break;case 10:e.prev=10,e.t0=e["catch"](0),t.$refs.dialog.error(e.t0.message);case 13:case"end":return e.stop()}}),e,null,[[0,10]])})))()}}},Tt=Rt,It=Object(y["a"])(Tt,Ct,Et,!1,null,null,null),Lt=It.exports,At=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-form-dialog",{ref:"dialog",attrs:{fields:t.fields,"submit-button":t.$t("rename")},on:{input:function(e){t.file.name=t.sluggify(t.file.name)},submit:t.submit},model:{value:t.file,callback:function(e){t.file=e},expression:"file"}})},Bt=[],qt={mixins:[S],data:function(){return{parent:null,file:{id:null,name:null,filename:null,extension:null}}},computed:{fields:function(){return{name:{label:this.$t("name"),type:"text",required:!0,icon:"title",after:"."+this.file.extension,preselect:!0}}},slugs:function(){return this.$store.state.languages.default?this.$store.state.languages.default.rules:this.system.slugs},system:function(){return this.$store.state.system.info}},methods:{open:function(t,e){var n=this;return Object(j["a"])(regeneratorRuntime.mark((function i(){return regeneratorRuntime.wrap((function(i){while(1)switch(i.prev=i.next){case 0:return i.prev=0,i.next=3,n.$api.files.get(t,e,{select:["id","filename","name","extension"]});case 3:n.file=i.sent,n.parent=t,n.$refs.dialog.open(),i.next=11;break;case 8:i.prev=8,i.t0=i["catch"](0),n.$store.dispatch("notification/error",i.t0);case 11:case"end":return i.stop()}}),i,null,[[0,8]])})))()},sluggify:function(t){return this.$helper.slug(t,[this.slugs,this.system.ascii],"@._-")},submit:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){var n;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:if(t.file.name=t.file.name.trim(),0!==t.file.name.length){e.next=4;break}return t.$refs.dialog.error(t.$t("error.file.changeName.empty")),e.abrupt("return");case 4:return e.prev=4,e.next=7,t.$api.files.changeName(t.parent,t.file.filename,t.file.name);case 7:n=e.sent,t.$store.dispatch("content/move",["files/"+t.file.id,"files/"+n.id]),t.$store.dispatch("notification/success",":)"),t.$emit("success",n),t.$events.$emit("file.changeName",n),t.$refs.dialog.close(),e.next=18;break;case 15:e.prev=15,e.t0=e["catch"](4),t.$refs.dialog.error(e.t0.message);case 18:case"end":return e.stop()}}),e,null,[[4,15]])})))()}}},Nt=qt,Pt=Object(y["a"])(Nt,At,Bt,!1,null,null,null),Dt=Pt.exports,Mt=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",staticClass:"k-files-dialog",attrs:{size:"medium"},on:{cancel:function(e){return t.$emit("cancel")},submit:t.submit}},[t.issue?[n("k-box",{attrs:{text:t.issue,theme:"negative"}})]:[t.options.search?n("k-input",{staticClass:"k-dialog-search",attrs:{autofocus:!0,placeholder:t.$t("search")+" …",type:"text",icon:"search"},model:{value:t.search,callback:function(e){t.search=e},expression:"search"}}):t._e(),t.models.length?[n("k-list",t._l(t.models,(function(e){return n("k-list-item",{key:e.id,attrs:{text:e.text,info:e.info,image:e.image,icon:e.icon},on:{click:function(n){return t.toggle(e)}}},[t.isSelected(e)?n("k-button",{attrs:{slot:"options",autofocus:!0,icon:t.checkedIcon,tooltip:t.$t("remove"),theme:"positive"},slot:"options"}):n("k-button",{attrs:{slot:"options",autofocus:!0,tooltip:t.$t("select"),icon:"circle-outline"},slot:"options"})],1)})),1),n("k-pagination",t._b({staticClass:"k-dialog-pagination",attrs:{details:!0,dropdown:!1,align:"center"},on:{paginate:t.paginate}},"k-pagination",t.pagination,!1))]:n("k-empty",{attrs:{icon:"image"}},[t._v("\n "+t._s(t.$t("dialog.files.empty"))+"\n ")])]],2)},Ft=[],Ut=(n("07ac"),{data:function(){return{models:[],issue:null,selected:{},options:{endpoint:null,max:null,multiple:!0,parent:null,selected:[],search:!0},search:null,pagination:{limit:20,page:1,total:0}}},computed:{multiple:function(){return!0===this.options.multiple&&1!==this.options.max},checkedIcon:function(){return!0===this.multiple?"check":"circle-filled"}},watch:{search:rt((function(){this.pagination.page=0,this.fetch()}),200)},methods:{fetch:function(){var t=this,e=Object(I["a"])({page:this.pagination.page,search:this.search},this.fetchData||{});return this.$api.get(this.options.endpoint,e).then((function(e){t.models=e.data,t.pagination=e.pagination,t.onFetched&&t.onFetched(e)})).catch((function(e){t.models=[],t.issue=e.message}))},open:function(t,e){var n=this;this.pagination.page=0,this.search=null;var i=!0;Array.isArray(t)?(this.models=t,i=!1):(this.models=[],e=t),this.options=Object(I["a"])(Object(I["a"])({},this.options),e),this.selected={},this.options.selected.forEach((function(t){n.$set(n.selected,t,{id:t})})),i?this.fetch().then((function(){n.$refs.dialog.open()})):this.$refs.dialog.open()},paginate:function(t){this.pagination.page=t.page,this.pagination.limit=t.limit,this.fetch()},submit:function(){this.$emit("submit",Object.values(this.selected)),this.$refs.dialog.close()},isSelected:function(t){return void 0!==this.selected[t.id]},toggle:function(t){!1!==this.options.multiple&&1!==this.options.max||(this.selected={}),!0!==this.isSelected(t)?this.options.max&&this.options.max<=Object.keys(this.selected).length||this.$set(this.selected,t.id,t):this.$delete(this.selected,t.id)}}}),zt={mixins:[Ut]},Ht=zt,Kt=(n("bf53"),Object(y["a"])(Ht,Mt,Ft,!1,null,null,null)),Vt=Kt.exports,Yt=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",t._g(t._b({ref:"dialog"},"k-dialog",t.$props,!1),t.$listeners),[n("k-form",{ref:"form",attrs:{fields:t.fields,novalidate:t.novalidate},on:{input:function(e){return t.$emit("input",e)},submit:function(e){return t.$emit("submit",e)}},model:{value:t.value,callback:function(e){t.value=e},expression:"value"}})],1)},Wt=[],Jt={mixins:[S],props:{fields:{type:[Array,Object],default:function(){return[]}},novalidate:{type:Boolean,default:!0},size:{type:String,default:"medium"},submitButton:{type:[String,Boolean],default:function(){return this.$t("save")}},theme:{type:String,default:"positive"},value:{type:Object,default:function(){return{}}}}},Gt=Jt,Zt=Object(y["a"])(Gt,Yt,Wt,!1,null,null,null),Xt=Zt.exports,Qt=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-form-dialog",{ref:"dialog",attrs:{fields:t.fields,"submit-button":t.$t("language.create")},on:{submit:t.submit},model:{value:t.language,callback:function(e){t.language=e},expression:"language"}})},te=[],ee={mixins:[S],data:function(){return{language:{name:"",code:"",direction:"ltr",locale:""}}},computed:{fields:function(){return{name:{label:this.$t("language.name"),type:"text",required:!0,icon:"title"},code:{label:this.$t("language.code"),type:"text",required:!0,counter:!1,icon:"globe",width:"1/2"},direction:{label:this.$t("language.direction"),type:"select",required:!0,empty:!1,options:[{value:"ltr",text:this.$t("language.direction.ltr")},{value:"rtl",text:this.$t("language.direction.rtl")}],width:"1/2"},locale:{label:this.$t("language.locale"),type:"text",placeholder:"en_US"}}},system:function(){return this.$store.state.system.info}},watch:{"language.name":function(t){this.onNameChanges(t)},"language.code":function(t){this.language.code=this.$helper.slug(t,[this.system.ascii])}},methods:{onNameChanges:function(t){this.language.code=this.$helper.slug(t,[this.language.rules,this.system.ascii]).substr(0,2)},open:function(){this.language={name:"",code:"",direction:"ltr"},this.$refs.dialog.open()},submit:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return t.language.locale&&(t.language.locale=t.language.locale.trim()||null),e.prev=1,e.next=4,t.$api.languages.create({name:t.language.name,code:t.language.code,direction:t.language.direction,locale:t.language.locale});case 4:t.$store.dispatch("languages/load"),t.success({message:":)",event:"language.create"}),e.next=11;break;case 8:e.prev=8,e.t0=e["catch"](1),t.$refs.dialog.error(e.t0.message);case 11:case"end":return e.stop()}}),e,null,[[1,8]])})))()}}},ne=ee,ie=Object(y["a"])(ne,Qt,te,!1,null,null,null),re=ie.exports,se=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-remove-dialog",{ref:"dialog",attrs:{text:t.$t("language.delete.confirm",{name:t.language.name})},on:{submit:t.submit}})},ae=[],oe={mixins:[S],data:function(){return{language:{name:null}}},methods:{open:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.prev=0,n.next=3,e.$api.languages.get(t);case 3:e.language=n.sent,e.$refs.dialog.open(),n.next=10;break;case 7:n.prev=7,n.t0=n["catch"](0),e.$store.dispatch("notification/error",n.t0);case 10:case"end":return n.stop()}}),n,null,[[0,7]])})))()},submit:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,t.$api.languages.delete(t.language.code);case 3:t.$store.dispatch("languages/load"),t.success({message:t.$t("language.deleted"),event:"language.delete"}),e.next=10;break;case 7:e.prev=7,e.t0=e["catch"](0),t.$refs.dialog.error(e.t0.message);case 10:case"end":return e.stop()}}),e,null,[[0,7]])})))()}}},ue=oe,le=Object(y["a"])(ue,se,ae,!1,null,null,null),ce=le.exports,pe=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-form-dialog",{ref:"dialog",attrs:{fields:t.fields,size:"medium"},on:{submit:t.submit},model:{value:t.language,callback:function(e){t.language=e},expression:"language"}})},de=[],fe=n("53ca"),he={mixins:[re],computed:{fields:function(){var t=re.computed.fields.apply(this);return t.code.disabled=!0,"object"===Object(fe["a"])(this.language.locale)&&(t.locale={label:t.locale.label,type:"info",text:this.$t("language.locale.warning")}),t}},methods:{onNameChanges:function(){return!1},open:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){var i;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.prev=0,n.next=3,e.$api.languages.get(t);case 3:e.language=n.sent,i=Object.keys(e.language.locale),1===i.length&&(e.language.locale=e.language.locale[i[0]]),e.$refs.dialog.open(),n.next=12;break;case 9:n.prev=9,n.t0=n["catch"](0),e.$store.dispatch("notification/error",n.t0);case 12:case"end":return n.stop()}}),n,null,[[0,9]])})))()},submit:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:if(0!==t.language.name.length){e.next=2;break}return e.abrupt("return",t.$refs.dialog.error(t.$t("error.language.name")));case 2:return"string"===typeof t.language.locale&&(t.language.locale=t.language.locale.trim()||null),e.prev=3,e.next=6,t.$api.languages.update(t.language.code,{name:t.language.name,direction:t.language.direction,locale:t.language.locale});case 6:t.$store.dispatch("languages/load"),t.success({message:t.$t("language.updated"),event:"language.update"}),e.next=13;break;case 10:e.prev=10,e.t0=e["catch"](3),t.$refs.dialog.error(e.t0.message);case 13:case"end":return e.stop()}}),e,null,[[3,10]])})))()}}},me=he,ge=Object(y["a"])(me,pe,de,!1,null,null,null),be=ge.exports,ve=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-form-dialog",{ref:"dialog",attrs:{fields:t.fields,"submit-button":t.$t("page.draft.create")},on:{submit:t.submit,close:t.reset},model:{value:t.page,callback:function(e){t.page=e},expression:"page"}})},ke=[],$e={mixins:[S],data:function(){return{parent:null,section:null,templates:[],page:this.emptyForm()}},computed:{fields:function(){return{title:{label:this.$t("title"),type:"text",required:!0,icon:"title"},slug:{label:this.$t("slug"),type:"text",required:!0,counter:!1,icon:"url"},template:{name:"template",label:this.$t("template"),type:"select",disabled:1===this.templates.length,required:!0,icon:"code",empty:!1,options:this.templates}}},slugs:function(){return this.$store.state.languages.default?this.$store.state.languages.default.rules:this.system.slugs},system:function(){return this.$store.state.system.info}},watch:{"page.title":function(t){this.page.slug=this.$helper.slug(t,[this.slugs,this.system.ascii])}},methods:{emptyForm:function(){return{title:"",slug:"",template:null}},open:function(t,e,n){var i=this;return Object(j["a"])(regeneratorRuntime.mark((function r(){var s;return regeneratorRuntime.wrap((function(r){while(1)switch(r.prev=r.next){case 0:return i.parent=t,i.section=n,r.prev=2,r.next=5,i.$api.get(e,{section:n});case 5:s=r.sent,i.templates=s.map((function(t){return{value:t.name,text:t.title}})),i.templates[0]&&(i.page.template=i.templates[0].value),i.$refs.dialog.open(),r.next=14;break;case 11:r.prev=11,r.t0=r["catch"](2),i.$store.dispatch("notification/error",r.t0);case 14:case"end":return r.stop()}}),r,null,[[2,11]])})))()},submit:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){var n,i;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:if(t.page.title=t.page.title.trim(),0!==t.page.title.length){e.next=4;break}return t.$refs.dialog.error(t.$t("error.page.changeTitle.empty")),e.abrupt("return");case 4:return n={template:t.page.template,slug:t.page.slug,content:{title:t.page.title}},e.prev=5,e.next=8,t.$api.post(t.parent+"/children",n);case 8:i=e.sent,t.success({route:t.$api.pages.link(i.id),message:":)",event:"page.create"}),e.next=15;break;case 12:e.prev=12,e.t0=e["catch"](5),t.$refs.dialog.error(e.t0.message);case 15:case"end":return e.stop()}}),e,null,[[5,12]])})))()},reset:function(){this.page=this.emptyForm()}}},ye=$e,_e=Object(y["a"])(ye,ve,ke,!1,null,null,null),we=_e.exports,xe=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-form-dialog",{ref:"dialog",attrs:{fields:t.fields,"submit-button":t.$t("duplicate")},on:{submit:t.submit},model:{value:t.page,callback:function(e){t.page=e},expression:"page"}})},Oe=[],je={mixins:[S],data:function(){return{page:{children:!1,files:!1,hasChildren:!1,hasDrafts:!1,hasFiles:!1,id:null,slug:""}}},computed:{fields:function(){var t=this.page.hasChildren||this.page.hasDrafts,e=this.page.hasFiles,n={slug:{label:this.$t("slug"),type:"text",required:!0,counter:!1,spellcheck:!1,icon:"url"}};return e&&(n.files={label:this.$t("page.duplicate.files"),type:"toggle",required:!0,width:t?"1/2":null}),t&&(n.children={label:this.$t("page.duplicate.pages"),type:"toggle",required:!0,width:e?"1/2":null}),n},slugs:function(){return this.$store.state.languages.default?this.$store.state.languages.default.rules:this.system.slugs},system:function(){return this.$store.state.system.info}},watch:{"page.slug":function(t){this.page.slug=this.$helper.slug(t,[this.slugs,this.system.ascii])}},methods:{open:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){var i;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.prev=0,n.next=3,e.$api.pages.get(t,{language:"@default",select:"id,slug,hasChildren,hasDrafts,hasFiles,title"});case 3:i=n.sent,e.page.id=i.id,e.page.slug=i.slug+"-"+e.$helper.slug(e.$t("page.duplicate.appendix")),e.page.hasChildren=i.hasChildren,e.page.hasDrafts=i.hasDrafts,e.page.hasFiles=i.hasFiles,e.$refs.dialog.open(),n.next=15;break;case 12:n.prev=12,n.t0=n["catch"](0),e.$store.dispatch("notification/error",n.t0);case 15:case"end":return n.stop()}}),n,null,[[0,12]])})))()},submit:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){var n;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,t.$api.pages.duplicate(t.page.id,t.page.slug,{children:t.page.children,files:t.page.files});case 3:n=e.sent,t.success({route:t.$api.pages.link(n.id),message:":)",event:"page.duplicate"}),e.next=10;break;case 7:e.prev=7,e.t0=e["catch"](0),t.$refs.dialog.error(e.t0.message);case 10:case"end":return e.stop()}}),e,null,[[0,7]])})))()}}},Se=je,Ce=Object(y["a"])(Se,xe,Oe,!1,null,null,null),Ee=Ce.exports,Re=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-remove-dialog",{ref:"dialog",attrs:{size:t.hasSubpages?"medium":"small"},on:{submit:t.submit,close:t.reset}},[t.page.hasChildren||t.page.hasDrafts?[n("k-text",{domProps:{innerHTML:t._s(t.$t("page.delete.confirm",{title:t.page.title}))}}),n("div",{staticClass:"k-page-remove-warning"},[n("k-box",{attrs:{theme:"negative"},domProps:{innerHTML:t._s(t.$t("page.delete.confirm.subpages"))}})],1),t.hasSubpages?n("k-form",{attrs:{fields:t.fields},on:{submit:t.submit},model:{value:t.model,callback:function(e){t.model=e},expression:"model"}}):t._e()]:[n("k-text",{domProps:{innerHTML:t._s(t.$t("page.delete.confirm",{title:t.page.title}))},on:{keydown:function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:t.submit(e)}}})]],2)},Te=[],Ie={mixins:[S],data:function(){return{page:{title:null,hasChildren:!1,hasDrafts:!1},model:this.emptyForm()}},computed:{hasSubpages:function(){return this.page.hasChildren||this.page.hasDrafts},fields:function(){return{check:{label:this.$t("page.delete.confirm.title"),type:"text",counter:!1}}}},methods:{emptyForm:function(){return{check:null}},open:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.prev=0,n.next=3,e.$api.pages.get(t,{select:"id, title, hasChildren, hasDrafts, parent"});case 3:e.page=n.sent,e.$refs.dialog.open(),n.next=10;break;case 7:n.prev=7,n.t0=n["catch"](0),e.$store.dispatch("notification/error",n.t0);case 10:case"end":return n.stop()}}),n,null,[[0,7]])})))()},submit:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){var n;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:if(!t.hasSubpages||t.model.check===t.page.title){e.next=2;break}return e.abrupt("return",t.$refs.dialog.error(t.$t("error.page.delete.confirm")));case 2:return e.prev=2,e.next=5,t.$api.pages.delete(t.page.id,{force:!0});case 5:t.$store.dispatch("content/remove","pages/"+t.page.id),n={message:":)",event:"page.delete"},t.$route.params.path&&t.page.id===t.$route.params.path.replace(/\+/g,"/")&&(t.page.parent?n.route=t.$api.pages.link(t.page.parent.id):n.route="/pages"),t.success(n),e.next=14;break;case 11:e.prev=11,e.t0=e["catch"](2),t.$refs.dialog.error(e.t0.message);case 14:case"end":return e.stop()}}),e,null,[[2,11]])})))()},reset:function(){this.model=this.emptyForm()}}},Le=Ie,Ae=(n("12fb"),Object(y["a"])(Le,Re,Te,!1,null,null,null)),Be=Ae.exports,qe=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-form-dialog",{ref:"dialog",attrs:{fields:t.fields,"submit-button":t.$t("rename")},on:{submit:t.submit},model:{value:t.page,callback:function(e){t.page=e},expression:"page"}})},Ne=[],Pe={mixins:[S],data:function(){return{page:{id:null,title:null}}},computed:{fields:function(){return{title:{label:this.$t("title"),type:"text",required:!0,icon:"title",preselect:!0}}}},methods:{open:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.prev=0,n.next=3,e.$api.pages.get(t,{select:["id","title"]});case 3:e.page=n.sent,e.$refs.dialog.open(),n.next=10;break;case 7:n.prev=7,n.t0=n["catch"](0),e.$store.dispatch("notification/error",n.t0);case 10:case"end":return n.stop()}}),n,null,[[0,7]])})))()},submit:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:if(t.page.title=t.page.title.trim(),0!==t.page.title.length){e.next=3;break}return e.abrupt("return",t.$refs.dialog.error(t.$t("error.page.changeTitle.empty")));case 3:return e.prev=3,e.next=6,t.$api.pages.changeTitle(t.page.id,t.page.title);case 6:t.success({message:":)",event:"page.changeTitle"}),e.next=12;break;case 9:e.prev=9,e.t0=e["catch"](3),t.$refs.dialog.error(e.t0.message);case 12:case"end":return e.stop()}}),e,null,[[3,9]])})))()}}},De=Pe,Me=Object(y["a"])(De,qe,Ne,!1,null,null,null),Fe=Me.exports,Ue=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-form-dialog",{ref:"dialog",attrs:{fields:t.fields,"submit-button":t.$t("change")},on:{submit:t.submit},model:{value:t.form,callback:function(e){t.form=e},expression:"form"}})},ze=[],He={mixins:[S],data:function(){return{page:{id:null},isBlocked:!1,isIncomplete:!1,form:{status:null,position:null},states:{}}},computed:{fields:function(){var t=this,e={status:{name:"status",label:this.$t("page.changeStatus.select"),type:"radio",required:!0,options:Object.keys(this.states).map((function(e){return{value:e,text:t.states[e].label,info:t.states[e].text}}))}};return"listed"===this.form.status&&"default"===this.page.blueprint.num&&(e.position={name:"position",label:this.$t("page.changeStatus.position"),type:"select",empty:!1,options:this.sortingOptions()}),e}},methods:{sortingOptions:function(){var t=this,e=[],n=0;return this.page.siblings.forEach((function(i){if(i.id===t.page.id||i.num<1)return!1;n++,e.push({value:n,text:n}),e.push({value:i.id,text:i.title,disabled:!0})})),e.push({value:n+1,text:n+1}),e},open:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){var i,r;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.prev=0,n.next=3,e.$api.pages.get(t,{select:["id","status","num","errors","blueprint"]});case 3:if(i=n.sent,!1!==i.blueprint.options.changeStatus){n.next=6;break}return n.abrupt("return",e.$store.dispatch("notification/error",{message:e.$t("error.page.changeStatus.permission")}));case 6:if(!("draft"===i.status&&Object.keys(i.errors).length>0)){n.next=8;break}return n.abrupt("return",e.$store.dispatch("notification/error",{message:e.$t("error.page.changeStatus.incomplete"),details:i.errors}));case 8:if("default"!==i.blueprint.num){n.next=21;break}return n.prev=9,n.next=12,e.$api.pages.get(t,{select:["siblings"]});case 12:r=n.sent,e.setup(Object(I["a"])(Object(I["a"])({},i),{},{siblings:r.siblings})),n.next=19;break;case 16:n.prev=16,n.t0=n["catch"](9),e.$store.dispatch("notification/error",n.t0);case 19:n.next=22;break;case 21:e.setup(Object(I["a"])(Object(I["a"])({},i),{},{siblings:[]}));case 22:n.next=27;break;case 24:n.prev=24,n.t1=n["catch"](0),e.$store.dispatch("notification/error",n.t1);case 27:case"end":return n.stop()}}),n,null,[[0,24],[9,16]])})))()},setup:function(t){this.page=t,this.form.position=t.num||t.siblings.length+1,this.form.status=t.status,this.states=t.blueprint.status,this.$refs.dialog.open()},submit:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,t.$api.pages.changeStatus(t.page.id,t.form.status,t.form.position||1);case 3:t.success({message:":)",event:"page.changeStatus"}),e.next=9;break;case 6:e.prev=6,e.t0=e["catch"](0),t.$refs.dialog.error(e.t0.message);case 9:case"end":return e.stop()}}),e,null,[[0,6]])})))()}}},Ke=He,Ve=Object(y["a"])(Ke,Ue,ze,!1,null,null,null),Ye=Ve.exports,We=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-form-dialog",{ref:"dialog",attrs:{fields:t.fields,"submit-button":t.$t("change")},on:{submit:t.submit},model:{value:t.page,callback:function(e){t.page=e},expression:"page"}})},Je=[],Ge={mixins:[S],data:function(){return{blueprints:[],page:{id:null,template:null}}},computed:{fields:function(){return{template:{label:this.$t("template"),type:"select",required:!0,empty:!1,options:this.page.blueprints,icon:"template"}}}},methods:{open:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){var i;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.prev=0,n.next=3,e.$api.pages.get(t,{select:["id","template","blueprints"]});case 3:if(i=n.sent,!(i.blueprints.length<=1)){n.next=6;break}return n.abrupt("return",e.$store.dispatch("notification/error",{message:e.$t("error.page.changeTemplate.invalid",{slug:i.id})}));case 6:e.page=i,e.page.blueprints=e.page.blueprints.map((function(t){return{text:t.title,value:t.name}})),e.$refs.dialog.open(),n.next=14;break;case 11:n.prev=11,n.t0=n["catch"](0),e.$store.dispatch("notification/error",n.t0);case 14:case"end":return n.stop()}}),n,null,[[0,11]])})))()},submit:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return t.$events.$emit("keydown.cmd.s"),e.prev=1,e.next=4,t.$api.pages.changeTemplate(t.page.id,t.page.template);case 4:t.success({message:":)",event:"page.changeTemplate"}),e.next=10;break;case 7:e.prev=7,e.t0=e["catch"](1),t.$refs.dialog.error(e.t0.message);case 10:case"end":return e.stop()}}),e,null,[[1,7]])})))()}}},Ze=Ge,Xe=Object(y["a"])(Ze,We,Je,!1,null,null,null),Qe=Xe.exports,tn=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{"submit-button":t.$t("change"),size:"medium",theme:"positive"},on:{submit:function(e){return t.$refs.form.submit()}}},[n("k-form",{ref:"form",on:{submit:t.submit}},[n("k-text-field",t._b({attrs:{value:t.slug},on:{input:function(e){return t.sluggify(e)}}},"k-text-field",t.field,!1),[n("k-button",{attrs:{slot:"options",icon:"wand","data-options":""},on:{click:function(e){return t.sluggify(t.page.title)}},slot:"options"},[t._v("\n "+t._s(t.$t("page.changeSlug.fromTitle"))+"\n ")])],1)],1)],1)},en=[],nn=(n("99af"),{mixins:[S],data:function(){return{slug:null,url:null,page:{id:null,parent:null,title:null}}},computed:{field:function(){return{name:"slug",label:this.$t("slug"),type:"text",required:!0,icon:"url",help:"/"+this.url,counter:!1,preselect:!0}},slugs:function(){return this.$store.state.languages.current?this.$store.state.languages.current.rules:this.system.slugs},system:function(){return this.$store.state.system.info}},methods:{sluggify:function(t){this.slug=this.$helper.slug(t,[this.slugs,this.system.ascii]),this.page.parents?this.url=this.page.parents.map((function(t){return t.slug})).concat([this.slug]).join("/"):this.url=this.slug},open:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.prev=0,n.next=3,e.$api.pages.get(t,{view:"panel"});case 3:e.page=n.sent,e.sluggify(e.page.slug),e.$refs.dialog.open(),n.next=11;break;case 8:n.prev=8,n.t0=n["catch"](0),e.$store.dispatch("notification/error",n.t0);case 11:case"end":return n.stop()}}),n,null,[[0,8]])})))()},submit:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){var n,i;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:if(t.slug!==t.page.slug){e.next=4;break}return t.$refs.dialog.close(),t.$store.dispatch("notification/success",":)"),e.abrupt("return");case 4:if(0!==t.slug.length){e.next=6;break}return e.abrupt("return",t.$refs.dialog.error(t.$t("error.page.slug.invalid")));case 6:return e.prev=6,e.next=9,t.$api.pages.changeSlug(t.page.id,t.slug);case 9:n=e.sent,t.$store.dispatch("content/move",["pages/"+t.page.id,"pages/"+n.id]),i={message:":)",event:"page.changeSlug"},!t.$route.params.path||t.page.id!==t.$route.params.path.replace(/\+/g,"/")||t.$store.state.languages.current&&!0!==t.$store.state.languages.current.default||(i.route=t.$api.pages.link(n.id),delete i.event),t.success(i),e.next=19;break;case 16:e.prev=16,e.t0=e["catch"](6),t.$refs.dialog.error(e.t0.message);case 19:case"end":return e.stop()}}),e,null,[[6,16]])})))()}}}),rn=nn,sn=Object(y["a"])(rn,tn,en,!1,null,null,null),an=sn.exports,on=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",staticClass:"k-pages-dialog",attrs:{size:"medium"},on:{cancel:function(e){return t.$emit("cancel")},submit:t.submit}},[t.issue?[n("k-box",{attrs:{text:t.issue,theme:"negative"}})]:[t.model?n("header",{staticClass:"k-pages-dialog-navbar"},[n("k-button",{attrs:{disabled:!t.model.id,tooltip:t.$t("back"),icon:"angle-left"},on:{click:t.back}}),n("k-headline",[t._v(t._s(t.model.title))])],1):t._e(),t.options.search?n("k-input",{staticClass:"k-dialog-search",attrs:{autofocus:!0,placeholder:t.$t("search")+" …",type:"text",icon:"search"},model:{value:t.search,callback:function(e){t.search=e},expression:"search"}}):t._e(),t.models.length?[n("k-list",t._l(t.models,(function(e){return n("k-list-item",{key:e.id,attrs:{text:e.text,info:e.info,image:e.image,icon:e.icon},on:{click:function(n){return t.toggle(e)}}},[n("template",{slot:"options"},[t.isSelected(e)?n("k-button",{attrs:{slot:"options",autofocus:!0,icon:t.checkedIcon,tooltip:t.$t("remove"),theme:"positive"},slot:"options"}):n("k-button",{attrs:{slot:"options",autofocus:!0,tooltip:t.$t("select"),icon:"circle-outline"},slot:"options"}),t.model?n("k-button",{attrs:{disabled:!e.hasChildren,tooltip:t.$t("open"),icon:"angle-right"},on:{click:function(n){return n.stopPropagation(),t.go(e)}}}):t._e()],1)],2)})),1),n("k-pagination",t._b({staticClass:"k-dialog-pagination",attrs:{details:!0,dropdown:!1,align:"center"},on:{paginate:t.paginate}},"k-pagination",t.pagination,!1))]:n("k-empty",{attrs:{icon:"page"}},[t._v("\n "+t._s(t.$t("dialog.pages.empty"))+"\n ")])]],2)},un=[],ln={mixins:[Ut],data:function(){var t=Ut.data();return Object(I["a"])(Object(I["a"])({},t),{},{model:{title:null,parent:null},options:Object(I["a"])(Object(I["a"])({},t.options),{},{parent:null})})},computed:{fetchData:function(){return{parent:this.options.parent}}},methods:{back:function(){this.options.parent=this.model.parent,this.pagination.page=1,this.fetch()},go:function(t){this.options.parent=t.id,this.pagination.page=1,this.fetch()},onFetched:function(t){this.model=t.model}}},cn=ln,pn=(n("ac27"),Object(y["a"])(cn,on,un,!1,null,null,null)),dn=pn.exports,fn=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-text-dialog",t._g(t._b({ref:"dialog"},"k-text-dialog",t.$props,!1),t.$listeners),[t._t("default")],2)},hn=[],mn={mixins:[S],props:{icon:{type:String,default:"trash"},submitButton:{type:[String,Boolean],default:function(){return this.$t("delete")}},text:String,theme:{type:String,default:"negative"}}},gn=mn,bn=Object(y["a"])(gn,fn,hn,!1,null,null,null),vn=bn.exports,kn={extends:Fe,methods:{open:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,t.$api.site.get({select:["title"]});case 3:t.page=e.sent,t.$refs.dialog.open(),e.next=10;break;case 7:e.prev=7,e.t0=e["catch"](0),t.$store.dispatch("notification/error",e.t0);case 10:case"end":return e.stop()}}),e,null,[[0,7]])})))()},submit:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:if(t.page.title=t.page.title.trim(),0!==t.page.title.length){e.next=4;break}return t.$refs.dialog.error(t.$t("error.site.changeTitle.empty")),e.abrupt("return");case 4:return e.prev=4,e.next=7,t.$api.site.changeTitle(t.page.title);case 7:t.$store.dispatch("system/title",t.page.title),t.success({message:":)",event:"site.changeTitle"}),e.next=14;break;case 11:e.prev=11,e.t0=e["catch"](4),t.$refs.dialog.error(e.t0.message);case 14:case"end":return e.stop()}}),e,null,[[4,11]])})))()}}},$n=kn,yn=Object(y["a"])($n,s,a,!1,null,null,null),_n=yn.exports,wn=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",t._g(t._b({ref:"dialog"},"k-dialog",t.$props,!1),t.$listeners),[t._t("default",[n("k-text",{domProps:{innerHTML:t._s(t.text)}})])],2)},xn=[],On={mixins:[S],props:{text:String}},jn=On,Sn=Object(y["a"])(jn,wn,xn,!1,null,null,null),Cn=Sn.exports,En=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-form-dialog",{ref:"dialog",attrs:{fields:t.fields,"submit-button":t.$t("create")},on:{submit:t.create,close:t.reset},model:{value:t.user,callback:function(e){t.user=e},expression:"user"}})},Rn=[],Tn={mixins:[S],data:function(){return{user:this.emptyForm(),languages:[],roles:[]}},computed:{fields:function(){return{name:{label:this.$t("name"),type:"text",icon:"user"},email:{label:this.$t("email"),type:"email",icon:"email",link:!1,required:!0},password:{label:this.$t("password"),type:"password",icon:"key"},language:{label:this.$t("language"),type:"select",icon:"globe",options:this.languages,required:!0,empty:!1},role:{label:this.$t("role"),type:1===this.roles.length?"hidden":"radio",required:!0,options:this.roles}}}},methods:{create:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,t.$api.users.create(t.user);case 3:t.success({message:":)",event:"user.create"}),e.next=9;break;case 6:e.prev=6,e.t0=e["catch"](0),t.$refs.dialog.error(e.t0.message);case 9:case"end":return e.stop()}}),e,null,[[0,6]])})))()},emptyForm:function(){return{name:"",email:"",password:"",language:this.$store.state.system.info.defaultLanguage||"en",role:this.$user.role.name}},open:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,t.$api.roles.options({canBe:"created"});case 3:t.roles=e.sent,"admin"!==t.$user.role.name&&(t.roles=t.roles.filter((function(t){return"admin"!==t.value}))),e.next=10;break;case 7:e.prev=7,e.t0=e["catch"](0),t.$store.dispatch("notification/error",e.t0);case 10:return e.prev=10,e.next=13,t.$api.translations.options();case 13:t.languages=e.sent,e.next=19;break;case 16:e.prev=16,e.t1=e["catch"](10),t.$store.dispatch("notification/error",e.t1);case 19:t.$refs.dialog.open();case 20:case"end":return e.stop()}}),e,null,[[0,7],[10,16]])})))()},reset:function(){this.user=this.emptyForm()}}},In=Tn,Ln=Object(y["a"])(In,En,Rn,!1,null,null,null),An=Ln.exports,Bn=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-form-dialog",{ref:"dialog",attrs:{fields:t.fields,"submit-button":t.$t("change")},on:{submit:t.submit},model:{value:t.user,callback:function(e){t.user=e},expression:"user"}})},qn=[],Nn={mixins:[S],data:function(){return{user:{id:null,email:null}}},computed:{fields:function(){return{email:{label:this.$t("email"),preselect:!0,required:!0,type:"email"}}}},methods:{open:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.prev=0,n.next=3,e.$api.users.get(t,{select:["id","email"]});case 3:e.user=n.sent,e.$refs.dialog.open(),n.next=10;break;case 7:n.prev=7,n.t0=n["catch"](0),e.$store.dispatch("notification/error",n.t0);case 10:case"end":return n.stop()}}),n,null,[[0,7]])})))()},submit:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){var n;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,t.$api.users.changeEmail(t.user.id,t.user.email);case 3:t.$store.dispatch("content/revert","users/"+t.user.id),t.$user.id===t.user.id&&t.$store.dispatch("user/email",t.user.email),n={message:":)",event:"user.changeEmail"},t.success(n),e.next=12;break;case 9:e.prev=9,e.t0=e["catch"](0),t.$refs.dialog.error(e.t0.message);case 12:case"end":return e.stop()}}),e,null,[[0,9]])})))()}}},Pn=Nn,Dn=Object(y["a"])(Pn,Bn,qn,!1,null,null,null),Mn=Dn.exports,Fn=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-form-dialog",{ref:"dialog",attrs:{fields:t.fields,"submit-button":t.$t("change")},on:{submit:t.submit},model:{value:t.user,callback:function(e){t.user=e},expression:"user"}})},Un=[],zn={mixins:[S],data:function(){return{user:{language:"en"},languages:[]}},computed:{fields:function(){return{language:{label:this.$t("language"),type:"select",icon:"globe",options:this.languages,required:!0,empty:!1}}}},created:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.next=2,t.$api.translations.options();case 2:t.languages=e.sent;case 3:case"end":return e.stop()}}),e)})))()},methods:{open:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.prev=0,n.next=3,e.$api.users.get(t,{view:"compact"});case 3:e.user=n.sent,e.$refs.dialog.open(),n.next=10;break;case 7:n.prev=7,n.t0=n["catch"](0),e.$store.dispatch("notification/error",n.t0);case 10:case"end":return n.stop()}}),n,null,[[0,7]])})))()},submit:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,t.$api.users.changeLanguage(t.user.id,t.user.language);case 3:t.user=e.sent,t.$user.id===t.user.id&&t.$store.dispatch("user/language",t.user.language),t.success({message:":)",event:"user.changeLanguage"}),e.next=11;break;case 8:e.prev=8,e.t0=e["catch"](0),t.$refs.dialog.error(e.t0.message);case 11:case"end":return e.stop()}}),e,null,[[0,8]])})))()}}},Hn=zn,Kn=Object(y["a"])(Hn,Fn,Un,!1,null,null,null),Vn=Kn.exports,Yn=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-form-dialog",{ref:"dialog",attrs:{fields:t.fields,"submit-button":t.$t("change")},on:{submit:t.submit},model:{value:t.values,callback:function(e){t.values=e},expression:"values"}})},Wn=[],Jn=(n("ddb0"),{mixins:[S],data:function(){return{user:null,values:{password:null,passwordConfirmation:null}}},computed:{fields:function(){return{password:{label:this.$t("user.changePassword.new"),type:"password",icon:"key"},passwordConfirmation:{label:this.$t("user.changePassword.new.confirm"),icon:"key",type:"password"}}}},methods:{open:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.prev=0,n.next=3,e.$api.users.get(t);case 3:e.user=n.sent,e.$refs.dialog.open(),n.next=10;break;case 7:n.prev=7,n.t0=n["catch"](0),e.$store.dispatch("notification/error",n.t0);case 10:case"end":return n.stop()}}),n,null,[[0,7]])})))()},submit:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:if(t.values.password&&!(t.values.password.length<8)){e.next=3;break}return t.$refs.dialog.error(t.$t("error.user.password.invalid")),e.abrupt("return",!1);case 3:if(t.values.password===t.values.passwordConfirmation){e.next=6;break}return t.$refs.dialog.error(t.$t("error.user.password.notSame")),e.abrupt("return",!1);case 6:return e.prev=6,e.next=9,t.$api.users.changePassword(t.user.id,t.values.password);case 9:t.success({message:":)",event:"user.changePassword"}),e.next=15;break;case 12:e.prev=12,e.t0=e["catch"](6),t.$refs.dialog.error(e.t0.message);case 15:case"end":return e.stop()}}),e,null,[[6,12]])})))()}}}),Gn=Jn,Zn=Object(y["a"])(Gn,Yn,Wn,!1,null,null,null),Xn=Zn.exports,Qn=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-remove-dialog",{ref:"dialog",attrs:{text:t.$t("user.delete.confirm",{email:t.user.email})},on:{submit:t.submit}})},ti=[],ei={mixins:[S],data:function(){return{user:{email:null}}},methods:{open:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.prev=0,n.next=3,e.$api.users.get(t);case 3:e.user=n.sent,e.$refs.dialog.open(),n.next=10;break;case 7:n.prev=7,n.t0=n["catch"](0),e.$store.dispatch("notification/error",n.t0);case 10:case"end":return n.stop()}}),n,null,[[0,7]])})))()},submit:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,t.$api.users.delete(t.user.id);case 3:t.$store.dispatch("content/remove","users/"+t.user.id),t.success({message:":)",event:"user.delete"}),"User"===t.$route.name&&t.$go("/users"),e.next=11;break;case 8:e.prev=8,e.t0=e["catch"](0),t.$refs.dialog.error(e.t0.message);case 11:case"end":return e.stop()}}),e,null,[[0,8]])})))()}}},ni=ei,ii=Object(y["a"])(ni,Qn,ti,!1,null,null,null),ri=ii.exports,si=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-form-dialog",{ref:"dialog",attrs:{fields:t.fields,"submit-button":t.$t("rename")},on:{submit:t.submit},model:{value:t.user,callback:function(e){t.user=e},expression:"user"}})},ai=[],oi={mixins:[S],data:function(){return{user:{id:null,name:null}}},computed:{fields:function(){return{name:{label:this.$t("name"),type:"text",icon:"user",preselect:!0}}}},methods:{open:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.prev=0,n.next=3,e.$api.users.get(t,{select:["id","name"]});case 3:e.user=n.sent,e.$refs.dialog.open(),n.next=10;break;case 7:n.prev=7,n.t0=n["catch"](0),e.$store.dispatch("notification/error",n.t0);case 10:case"end":return n.stop()}}),n,null,[[0,7]])})))()},submit:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return t.user.name=t.user.name.trim(),e.prev=1,e.next=4,t.$api.users.changeName(t.user.id,t.user.name);case 4:t.$user.id===t.user.id&&t.$store.dispatch("user/name",t.user.name),t.success({message:":)",event:"user.changeName"}),e.next=11;break;case 8:e.prev=8,e.t0=e["catch"](1),t.$refs.dialog.error(e.t0.message);case 11:case"end":return e.stop()}}),e,null,[[1,8]])})))()}}},ui=oi,li=Object(y["a"])(ui,si,ai,!1,null,null,null),ci=li.exports,pi=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-form-dialog",{ref:"dialog",attrs:{fields:t.fields,"submit-button":t.$t("user.changeRole")},on:{submit:t.submit},model:{value:t.user,callback:function(e){t.user=e},expression:"user"}})},di=[],fi={mixins:[S],data:function(){return{roles:[],user:{id:null,role:"visitor"}}},computed:{fields:function(){return{role:{label:this.$t("user.changeRole.select"),type:"radio",required:!0,options:this.roles}}}},methods:{open:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return e.id=t,n.prev=1,n.next=4,e.$api.users.get(t);case 4:return e.user=n.sent,e.user.role=e.user.role.name,n.next=8,e.$api.users.roles(t);case 8:e.roles=n.sent,"admin"!==e.$user.role.name&&(e.roles=e.roles.filter((function(t){return"admin"!==t.value}))),e.$refs.dialog.open(),n.next=16;break;case 13:n.prev=13,n.t0=n["catch"](1),e.$store.dispatch("notification/error",n.t0);case 16:case"end":return n.stop()}}),n,null,[[1,13]])})))()},submit:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,t.$api.users.changeRole(t.user.id,t.user.role);case 3:t.$user.id===t.user.id&&t.$store.dispatch("user/load"),t.success({message:":)",event:"user.changeRole"}),e.next=10;break;case 7:e.prev=7,e.t0=e["catch"](0),t.$refs.dialog.error(e.t0.message);case 10:case"end":return e.stop()}}),e,null,[[0,7]])})))()}}},hi=fi,mi=Object(y["a"])(hi,pi,di,!1,null,null,null),gi=mi.exports,bi=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",staticClass:"k-users-dialog",attrs:{size:"medium"},on:{cancel:function(e){return t.$emit("cancel")},submit:t.submit}},[t.issue?[n("k-box",{attrs:{text:t.issue,theme:"negative"}})]:[t.options.search?n("k-input",{staticClass:"k-dialog-search",attrs:{autofocus:!0,placeholder:t.$t("search")+" …",type:"text",icon:"search"},model:{value:t.search,callback:function(e){t.search=e},expression:"search"}}):t._e(),t.models.length?[n("k-list",t._l(t.models,(function(e){return n("k-list-item",{key:e.email,attrs:{text:e.text,info:e.info!==e.text?e.info:null,image:e.image,icon:e.icon},on:{click:function(n){return t.toggle(e)}}},[t.isSelected(e)?n("k-button",{attrs:{slot:"options",autofocus:!0,icon:t.checkedIcon,tooltip:t.$t("remove"),theme:"positive"},slot:"options"}):n("k-button",{attrs:{slot:"options",autofocus:!0,tooltip:t.$t("select"),icon:"circle-outline"},slot:"options"})],1)})),1),n("k-pagination",t._b({staticClass:"k-dialog-pagination",attrs:{details:!0,dropdown:!1,align:"center"},on:{paginate:t.paginate}},"k-pagination",t.pagination,!1))]:n("k-empty",{attrs:{icon:"users"}},[t._v("\n "+t._s(t.$t("dialog.users.empty"))+"\n ")])]],2)},vi=[],ki={mixins:[Ut]},$i=ki,yi=(n("7568"),Object(y["a"])($i,bi,vi,!1,null,null,null)),_i=yi.exports,wi=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dropdown",{staticClass:"k-autocomplete"},[t._t("default"),n("k-dropdown-content",t._g({ref:"dropdown",attrs:{autofocus:!0}},t.$listeners),t._l(t.matches,(function(e,i){return n("k-dropdown-item",t._b({key:i,on:{mousedown:function(n){return t.onSelect(e)},keydown:[function(n){return!n.type.indexOf("key")&&t._k(n.keyCode,"tab",9,n.key,"Tab")?null:(n.preventDefault(),t.onSelect(e))},function(n){return!n.type.indexOf("key")&&t._k(n.keyCode,"enter",13,n.key,"Enter")?null:(n.preventDefault(),t.onSelect(e))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"left",37,e.key,["Left","ArrowLeft"])?null:"button"in e&&0!==e.button?null:(e.preventDefault(),t.close(e))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"backspace",void 0,e.key,void 0)?null:(e.preventDefault(),t.close(e))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"delete",[8,46],e.key,["Backspace","Delete","Del"])?null:(e.preventDefault(),t.close(e))}]}},"k-dropdown-item",e,!1),[t._v("\n "+t._s(e.text)+"\n ")])})),1),t._v("\n "+t._s(t.query)+"\n")],2)},xi=[],Oi=(n("c975"),n("fb6a"),{props:{limit:{type:Number,default:10},skip:{type:Array,default:function(){return[]}},options:Array,query:String},data:function(){return{matches:[],selected:{text:null}}},methods:{close:function(){this.$refs.dropdown.close()},onSelect:function(t){this.$refs.dropdown.close(),this.$emit("select",t)},search:function(t){var e=this;if(!(t.length<1)){var n=new RegExp(RegExp.escape(t),"ig");this.matches=this.options.filter((function(t){return!!t.text&&(-1===e.skip.indexOf(t.value)&&null!==t.text.match(n))})).slice(0,this.limit),this.$emit("search",t,this.matches),this.$refs.dropdown.open()}}}}),ji=Oi,Si=Object(y["a"])(ji,wi,xi,!1,null,null,null),Ci=Si.exports,Ei=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-calendar-input"},[n("nav",[n("k-button",{attrs:{icon:"angle-left"},on:{click:t.prev}}),n("span",{staticClass:"k-calendar-selects"},[n("k-select-input",{attrs:{options:t.months,disabled:t.disabled,required:!0},model:{value:t.month,callback:function(e){t.month=t._n(e)},expression:"month"}}),n("k-select-input",{attrs:{options:t.years,disabled:t.disabled,required:!0},model:{value:t.year,callback:function(e){t.year=t._n(e)},expression:"year"}})],1),n("k-button",{attrs:{icon:"angle-right"},on:{click:t.next}})],1),n("table",{staticClass:"k-calendar-table"},[n("thead",[n("tr",t._l(t.weekdays,(function(e){return n("th",{key:"weekday_"+e},[t._v(t._s(e))])})),0)]),n("tbody",t._l(t.numberOfWeeks,(function(e){return n("tr",{key:"week_"+e},t._l(t.days(e),(function(e,i){return n("td",{key:"day_"+i,staticClass:"k-calendar-day",attrs:{"aria-current":!!t.isToday(e)&&"date","aria-selected":!!t.isCurrent(e)&&"date"}},[e?n("k-button",{on:{click:function(n){return t.select(e)}}},[t._v(t._s(e))]):t._e()],1)})),0)})),0),n("tfoot",[n("tr",[n("td",{staticClass:"k-calendar-today",attrs:{colspan:"7"}},[n("k-button",{on:{click:t.selectToday}},[t._v(t._s(t.$t("today")))])],1)])])])])},Ri=[],Ti={props:{value:String,disabled:Boolean},data:function(){var t=this.value?this.$library.dayjs(this.value):this.$library.dayjs();return{day:t.date(),month:t.month(),year:t.year(),today:this.$library.dayjs(),current:t}},computed:{date:function(){return this.$library.dayjs("".concat(this.year,"-").concat(this.month+1,"-").concat(this.day))},numberOfDays:function(){return this.date.daysInMonth()},numberOfWeeks:function(){return Math.ceil((this.numberOfDays+this.firstWeekday-1)/7)},firstWeekday:function(){var t=this.date.clone().startOf("month").day();return t>0?t:7},weekdays:function(){return[this.$t("days.mon"),this.$t("days.tue"),this.$t("days.wed"),this.$t("days.thu"),this.$t("days.fri"),this.$t("days.sat"),this.$t("days.sun")]},monthnames:function(){return[this.$t("months.january"),this.$t("months.february"),this.$t("months.march"),this.$t("months.april"),this.$t("months.may"),this.$t("months.june"),this.$t("months.july"),this.$t("months.august"),this.$t("months.september"),this.$t("months.october"),this.$t("months.november"),this.$t("months.december")]},months:function(){var t=[];return this.monthnames.forEach((function(e,n){t.push({value:n,text:e})})),t},years:function(){for(var t=[],e=this.year-10;e<=this.year+10;e++)t.push({value:e,text:this.$helper.pad(e)});return t}},watch:{value:function(t){var e=this.$library.dayjs(t);this.day=e.date(),this.month=e.month(),this.year=e.year(),this.current=e}},methods:{days:function(t){for(var e=[],n=7*(t-1)+1,i=n;ithis.numberOfDays?e.push(""):e.push(r)}return e},next:function(){var t=this.date.clone().add(1,"month");this.set(t)},isToday:function(t){return this.month===this.today.month()&&this.year===this.today.year()&&t===this.today.date()},isCurrent:function(t){return this.month===this.current.month()&&this.year===this.current.year()&&t===this.current.date()},prev:function(){var t=this.date.clone().subtract(1,"month");this.set(t)},go:function(t,e){"today"===t&&(t=this.today.year(),e=this.today.month()),this.year=t,this.month=e},set:function(t){this.day=t.date(),this.month=t.month(),this.year=t.year()},selectToday:function(){this.set(this.$library.dayjs()),this.select(this.day)},select:function(t){t&&(this.day=t);var e=this.$library.dayjs(new Date(this.year,this.month,this.day,this.current.hour(),this.current.minute()));this.$emit("input",e.toISOString())}}},Ii=Ti,Li=(n("ee15"),Object(y["a"])(Ii,Ei,Ri,!1,null,null,null)),Ai=Li.exports,Bi=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",{staticClass:"k-counter",attrs:{"data-invalid":!t.valid}},[n("span",[t._v(t._s(t.count))]),t.min&&t.max?n("span",{staticClass:"k-counter-rules"},[t._v("("+t._s(t.min)+"–"+t._s(t.max)+")")]):t.min?n("span",{staticClass:"k-counter-rules"},[t._v("≥ "+t._s(t.min))]):t.max?n("span",{staticClass:"k-counter-rules"},[t._v("≤ "+t._s(t.max))]):t._e()])},qi=[],Ni={props:{count:Number,min:Number,max:Number,required:{type:Boolean,default:!1}},computed:{valid:function(){return!1===this.required&&0===this.count||(!0!==this.required||0!==this.count)&&(!(this.min&&this.countthis.max))}}},Pi=Ni,Di=(n("fc0f"),Object(y["a"])(Pi,Bi,qi,!1,null,null,null)),Mi=Di.exports,Fi=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("form",{ref:"form",staticClass:"k-form",attrs:{method:"POST",autocomplete:"off",novalidate:""},on:{submit:function(e){return e.preventDefault(),t.onSubmit(e)}}},[t._t("header"),t._t("default",[n("k-fieldset",t._g({ref:"fields",attrs:{disabled:t.disabled,fields:t.fields,novalidate:t.novalidate},model:{value:t.value,callback:function(e){t.value=e},expression:"value"}},t.listeners))]),t._t("footer"),n("input",{ref:"submitter",staticClass:"k-form-submitter",attrs:{type:"submit"}})],2)},Ui=[],zi={props:{disabled:Boolean,config:Object,fields:{type:[Array,Object],default:function(){return{}}},novalidate:{type:Boolean,default:!1},value:{type:Object,default:function(){return{}}}},data:function(){return{errors:{},listeners:Object(I["a"])(Object(I["a"])({},this.$listeners),{},{submit:this.onSubmit})}},methods:{focus:function(t){this.$refs.fields&&this.$refs.fields.focus&&this.$refs.fields.focus(t)},onSubmit:function(){this.$emit("submit",this.value)},submit:function(){this.$refs.submitter.click()}}},Hi=zi,Ki=(n("5d33"),Object(y["a"])(Hi,Fi,Ui,!1,null,null,null)),Vi=Ki.exports,Yi=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("nav",{staticClass:"k-form-buttons",attrs:{"data-theme":t.mode}},["unlock"===t.mode?n("k-view",[n("p",{staticClass:"k-form-lock-info"},[t._v("\n "+t._s(t.$t("lock.isUnlocked"))+"\n ")]),n("span",{staticClass:"k-form-lock-buttons"},[n("k-button",{staticClass:"k-form-button",attrs:{icon:"download"},on:{click:t.onDownload}},[t._v("\n "+t._s(t.$t("download"))+"\n ")]),n("k-button",{staticClass:"k-form-button",attrs:{icon:"check"},on:{click:t.onResolve}},[t._v("\n "+t._s(t.$t("confirm"))+"\n ")])],1)]):"lock"===t.mode?n("k-view",[n("p",{staticClass:"k-form-lock-info"},[n("k-icon",{attrs:{type:"lock"}}),n("span",{domProps:{innerHTML:t._s(t.$t("lock.isLocked",{email:t.form.lock.email}))}})],1),t.form.lock.unlockable?n("k-button",{staticClass:"k-form-button",attrs:{icon:"unlock"},on:{click:t.setUnlock}},[t._v("\n "+t._s(t.$t("lock.unlock"))+"\n ")]):n("k-icon",{staticClass:"k-form-lock-loader",attrs:{type:"loader"}})],1):"changes"===t.mode?n("k-view",[n("k-button",{staticClass:"k-form-button",attrs:{disabled:t.isDisabled,icon:"undo"},on:{click:function(e){return t.$refs.revert.open()}}},[t._v("\n "+t._s(t.$t("revert"))+"\n ")]),n("k-button",{staticClass:"k-form-button",attrs:{disabled:t.isDisabled,icon:"check"},on:{click:t.onSave}},[t._v("\n "+t._s(t.$t("save"))+"\n ")])],1):t._e(),n("k-dialog",{ref:"revert",attrs:{"submit-button":t.$t("revert"),icon:"undo",theme:"negative"},on:{submit:t.onRevert}},[n("k-text",{domProps:{innerHTML:t._s(t.$t("revert.confirm"))}})],1)],1)},Wi=[],Ji=n("2909"),Gi={data:function(){return{supportsLocking:!0}},computed:{api:function(){return{lock:[this.$route.path+"/lock",null,null,!0],unlock:[this.$route.path+"/unlock",null,null,!0]}},hasChanges:function(){return this.$store.getters["content/hasChanges"]()},form:function(){return{lock:this.$store.state.content.status.lock,unlock:this.$store.state.content.status.unlock}},id:function(){return this.$store.state.content.current},isDisabled:function(){return!1===this.$store.state.content.status.enabled},isLocked:function(){return null!==this.form.lock},isUnlocked:function(){return null!==this.form.unlock},mode:function(){return!0===this.isUnlocked?"unlock":!0===this.isLocked?"lock":!0===this.hasChanges?"changes":void 0}},watch:{hasChanges:function(t,e){if(!1===e&&!0===t)return this.$store.dispatch("heartbeat/remove",this.getLock),void this.$store.dispatch("heartbeat/add",[this.setLock,30]);this.id&&!0===e&&!1===t&&this.removeLock()},id:function(){this.id&&!1===this.hasChanges&&this.$store.dispatch("heartbeat/add",[this.getLock,10])}},created:function(){this.$events.$on("keydown.cmd.s",this.onSave)},destroyed:function(){this.$events.$off("keydown.cmd.s",this.onSave)},methods:{getLock:function(){var t,e=this;return(t=this.$api).get.apply(t,Object(Ji["a"])(this.api.lock)).then((function(t){if(!1===t.supported)return e.supportsLocking=!1,void e.$store.dispatch("heartbeat/remove",e.getLock);!1===t.locked?(e.isLocked&&e.form.lock.user!==e.$store.state.user.current.id&&e.$events.$emit("model.reload"),e.$store.dispatch("content/lock",null)):e.$store.dispatch("content/lock",t.locked)})).catch((function(){}))},setLock:function(){var t,e=this;!0===this.supportsLocking&&(t=this.$api).patch.apply(t,Object(Ji["a"])(this.api.lock)).catch((function(t){if("error.lock.notImplemented"===t.key)return e.supportsLocking=!1,e.$store.dispatch("heartbeat/remove",e.setLock),!1;e.$store.dispatch("content/revert",e.id),e.$store.dispatch("heartbeat/remove",e.setLock),e.$store.dispatch("heartbeat/add",[e.getLock,10])}))},removeLock:function(){var t,e=this;!0===this.supportsLocking&&(this.$store.dispatch("heartbeat/remove",this.setLock),(t=this.$api).delete.apply(t,Object(Ji["a"])(this.api.lock)).then((function(){e.$store.dispatch("content/lock",null),e.$store.dispatch("heartbeat/add",[e.getLock,10])})).catch((function(){})))},setUnlock:function(){var t,e=this;!0===this.supportsLocking&&(this.$store.dispatch("heartbeat/remove",this.setLock),(t=this.$api).patch.apply(t,Object(Ji["a"])(this.api.unlock)).then((function(){e.$store.dispatch("content/lock",null),e.$store.dispatch("heartbeat/add",[e.getLock,10])})).catch((function(){})))},removeUnlock:function(){var t,e=this;!0===this.supportsLocking&&(this.$store.dispatch("heartbeat/remove",this.setLock),(t=this.$api).delete.apply(t,Object(Ji["a"])(this.api.unlock)).then((function(){e.$store.dispatch("content/unlock",null),e.$store.dispatch("heartbeat/add",[e.getLock,10])})).catch((function(){})))},onDownload:function(){var t=this,e="";Object.keys(this.form.unlock).forEach((function(n){e+=n+": \n\n"+t.form.unlock[n],e+="\n\n----\n\n"}));var n=document.createElement("a");n.setAttribute("href","data:text/plain;charset=utf-8,"+encodeURIComponent(e)),n.setAttribute("download",this.id+".txt"),n.style.display="none",document.body.appendChild(n),n.click(),document.body.removeChild(n)},onResolve:function(){this.$store.dispatch("content/revert"),this.removeUnlock()},onRevert:function(){this.$store.dispatch("content/revert"),this.$refs.revert.close()},onSave:function(t){var e=this;return!!t&&(t.preventDefault&&t.preventDefault(),!1===this.hasChanges||void this.$store.dispatch("content/save").then((function(){e.$events.$emit("model.update"),e.$store.dispatch("notification/success",":)")})).catch((function(t){403!==t.code&&(t.details&&Object.keys(t.details).length>0?e.$store.dispatch("notification/error",{message:e.$t("error.form.incomplete"),details:t.details}):e.$store.dispatch("notification/error",{message:e.$t("error.form.notSaved"),details:[{label:"Exception: "+t.exception,message:t.message}]}))})))}}},Zi=Gi,Xi=(n("18dd"),Object(y["a"])(Zi,Yi,Wi,!1,null,null,null)),Qi=Xi.exports,tr=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.hasChanges?n("k-dropdown",{staticClass:"k-form-indicator"},[n("k-button",{staticClass:"k-topbar-button",on:{click:t.toggle}},[n("k-icon",{staticClass:"k-form-indicator-icon",attrs:{type:"edit"}})],1),n("k-dropdown-content",{ref:"list",attrs:{align:"right"}},[n("p",{staticClass:"k-form-indicator-info"},[t._v("\n "+t._s(t.$t("lock.unsaved"))+":\n ")]),n("hr"),t._l(t.entries,(function(e){return n("k-dropdown-item",{key:e.id,attrs:{icon:e.icon},nativeOn:{click:function(n){return n.stopPropagation(),t.go(e.target)}}},[t._v("\n "+t._s(e.label)+"\n ")])}))],2)],1):t._e()},er=[],nr=(n("3ca3"),{data:function(){return{isOpen:!1,entries:[]}},computed:{store:function(){return this.$store.state.content.models},models:function(){var t=this,e=Object.keys(this.store).filter((function(e){return!!t.store[e]})),n=e.map((function(e){return Object(I["a"])({id:e},t.store[e])}));return n.filter((function(t){return Object.keys(t.changes).length>0}))},hasChanges:function(){return this.models.length>0}},methods:{go:function(t){if(t.language&&this.$store.state.languages.current.code!==t.language){var e=this.$store.state.languages.all.filter((function(e){return e.code===t.language}))[0];this.$store.dispatch("languages/current",e)}this.$go(t.link)},load:function(){var t=this,e=this.models.map((function(e){return t.$api.get(e.api,{view:"compact"},null,!0).then((function(n){var i;if(i=!0===e.id.startsWith("pages/")?{icon:"page",label:n.title,target:{link:t.$api.pages.link(n.id)}}:!0===e.id.startsWith("files/")?{icon:"image",label:n.filename,target:{link:n.link}}:!0===e.id.startsWith("users/")?{icon:"user",label:n.email,target:{link:t.$api.users.link(n.id)}}:{icon:"home",label:n.title,target:{link:"/site"}},t.$store.state.languages.current){var r=e.id.split("/").pop();i.label=i.label+" ("+r+")",i.target.language=r}return i})).catch((function(){return t.$store.dispatch("content/remove",e.id),null}))}));return Promise.all(e).then((function(e){t.entries=e.filter((function(t){return null!==t})),0===t.entries.length&&t.$store.dispatch("notification/success",t.$t("lock.unsaved.empty"))}))},toggle:function(){var t=this;!1===this.$refs.list.isOpen?this.load().then((function(){t.$refs.list&&t.$refs.list.toggle()})):this.$refs.list.toggle()}}}),ir=nr,rr=(n("9e26"),Object(y["a"])(ir,tr,er,!1,null,null,null)),sr=rr.exports,ar=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{class:"k-field k-field-name-"+t.name,attrs:{"data-disabled":t.disabled,"data-translate":t.translate},on:{focusin:function(e){return t.$emit("focus",e)},focusout:function(e){return t.$emit("blur",e)}}},[t._t("header",[n("header",{staticClass:"k-field-header"},[t._t("label",[n("label",{staticClass:"k-field-label",attrs:{for:t.input}},[t._v(t._s(t.labelText)+" "),t.required?n("abbr",{attrs:{title:t.$t("field.required")}},[t._v("*")]):t._e()])]),t._t("options"),t._t("counter",[t.counter?n("k-counter",t._b({staticClass:"k-field-counter",attrs:{required:t.required}},"k-counter",t.counter,!1)):t._e()])],2)]),t._t("default"),t._t("footer",[t.help||t.$slots.help?n("footer",{staticClass:"k-field-footer"},[t._t("help",[t.help?n("k-text",{staticClass:"k-field-help",attrs:{theme:"help"},domProps:{innerHTML:t._s(t.help)}}):t._e()])],2):t._e()])],2)},or=[],ur={inheritAttrs:!1,props:{counter:[Boolean,Object],disabled:Boolean,endpoints:Object,help:String,input:[String,Number],label:String,name:[String,Number],required:Boolean,translate:Boolean,type:String},computed:{labelText:function(){return this.label||" "}}},lr=ur,cr=(n("a134"),Object(y["a"])(lr,ar,or,!1,null,null,null)),pr=cr.exports,dr=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("fieldset",{staticClass:"k-fieldset"},[n("k-grid",[t._l(t.fields,(function(e,i){return["hidden"!==e.type&&t.meetsCondition(e)?n("k-column",{key:e.signature,attrs:{width:e.width}},[n("k-error-boundary",[t.hasFieldType(e.type)?n("k-"+e.type+"-field",t._b({ref:i,refInFor:!0,tag:"component",attrs:{name:i,novalidate:t.novalidate,disabled:t.disabled||e.disabled},on:{input:function(n){return t.$emit("input",t.value,e,i)},focus:function(n){return t.$emit("focus",n,e,i)},invalid:function(n,r){return t.onInvalid(n,r,e,i)},submit:function(n){return t.$emit("submit",n,e,i)}},model:{value:t.value[i],callback:function(e){t.$set(t.value,i,e)},expression:"value[fieldName]"}},"component",e,!1)):n("k-box",{attrs:{theme:"negative"}},[n("k-text",{attrs:{size:"small"}},[t._v("\n The field type "),n("strong",[t._v('"'+t._s(i)+'"')]),t._v(" does not exist\n ")])],1)],1)],1):t._e()]}))],2)],1)},fr=[],hr={props:{config:Object,disabled:Boolean,fields:{type:[Array,Object],default:function(){return[]}},novalidate:{type:Boolean,default:!1},value:{type:Object,default:function(){return{}}}},data:function(){return{errors:{}}},methods:{focus:function(t){if(t)this.hasField(t)&&"function"===typeof this.$refs[t][0].focus&&this.$refs[t][0].focus();else{var e=Object.keys(this.$refs)[0];this.focus(e)}},hasFieldType:function(t){return this.$helper.isComponent("k-".concat(t,"-field"))},hasField:function(t){return this.$refs[t]&&this.$refs[t][0]},meetsCondition:function(t){var e=this;if(!t.when)return!0;var n=!0;return Object.keys(t.when).forEach((function(i){var r=e.value[i.toLowerCase()],s=t.when[i];r!==s&&(n=!1)})),n},onInvalid:function(t,e,n,i){this.errors[i]=e,this.$emit("invalid",this.errors)},hasErrors:function(){return Object.keys(this.errors).length}}},mr=hr,gr=(n("862b"),Object(y["a"])(mr,dr,fr,!1,null,null,null)),br=gr.exports,vr=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-input",attrs:{"data-disabled":t.disabled,"data-invalid":!t.novalidate&&t.isInvalid,"data-theme":t.theme,"data-type":t.type}},[t.$slots.before||t.before?n("span",{staticClass:"k-input-before",on:{click:t.focus}},[t._t("before",[t._v(t._s(t.before))])],2):t._e(),n("span",{staticClass:"k-input-element",on:{click:function(e){return e.stopPropagation(),t.focus(e)}}},[t._t("default",[n("k-"+t.type+"-input",t._g(t._b({ref:"input",tag:"component",attrs:{value:t.value}},"component",t.inputProps,!1),t.listeners))])],2),t.$slots.after||t.after?n("span",{staticClass:"k-input-after",on:{click:t.focus}},[t._t("after",[t._v(t._s(t.after))])],2):t._e(),t.$slots.icon||t.icon?n("span",{staticClass:"k-input-icon",on:{click:t.focus}},[t._t("icon",[n("k-icon",{attrs:{type:t.icon}})])],2):t._e()])},kr=[],$r={inheritAttrs:!1,props:{after:String,before:String,disabled:Boolean,type:String,icon:[String,Boolean],invalid:Boolean,theme:String,novalidate:{type:Boolean,default:!1},value:{type:[String,Boolean,Number,Object,Array],default:null}},data:function(){var t=this;return{isInvalid:this.invalid,listeners:Object(I["a"])(Object(I["a"])({},this.$listeners),{},{invalid:function(e,n){t.isInvalid=e,t.$emit("invalid",e,n)}})}},computed:{inputProps:function(){return Object(I["a"])(Object(I["a"])({},this.$props),this.$attrs)}},methods:{blur:function(t){t.relatedTarget&&!1===this.$el.contains(t.relatedTarget)&&this.$refs.input.blur&&this.$refs.input.blur()},focus:function(t){if(t&&t.target&&"INPUT"===t.target.tagName)t.target.focus();else if(this.$refs.input&&this.$refs.input.focus)this.$refs.input.focus();else{var e=this.$el.querySelector("input, select, textarea");e&&e.focus()}}}},yr=$r,_r=(n("c7c8"),Object(y["a"])(yr,vr,kr,!1,null,null,null)),wr=_r.exports,xr=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-upload"},[n("input",{ref:"input",attrs:{accept:t.options.accept,multiple:t.options.multiple,"aria-hidden":"true",type:"file",tabindex:"-1"},on:{change:t.select,click:function(t){t.stopPropagation()}}}),n("k-dialog",{ref:"dialog",attrs:{"cancel-button":!1,"submit-button":!1,size:"medium"}},[t.errors.length>0?[n("k-headline",[t._v(t._s(t.$t("upload.errors")))]),n("ul",{staticClass:"k-upload-error-list"},t._l(t.errors,(function(e,i){return n("li",{key:"error-"+i},[n("p",{staticClass:"k-upload-error-filename"},[t._v(t._s(e.file.name))]),n("p",{staticClass:"k-upload-error-message"},[t._v(t._s(e.message))])])})),0)]:[n("k-headline",[t._v(t._s(t.$t("upload.progress")))]),n("ul",{staticClass:"k-upload-list"},t._l(t.files,(function(e,i){return n("li",{key:"file-"+i},[n("k-progress",{ref:e.name,refInFor:!0}),n("p",{staticClass:"k-upload-list-filename"},[t._v(t._s(e.name))]),n("p",[t._v(t._s(t.errors[e.name]))])],1)})),0)],n("template",{slot:"footer"},[t.errors.length>0?[n("k-button-group",[n("k-button",{attrs:{icon:"check"},on:{click:function(e){return t.$refs.dialog.close()}}},[t._v("\n "+t._s(t.$t("confirm"))+"\n ")])],1)]:t._e()],2)],2)],1)},Or=[],jr={props:{url:{type:String},accept:{type:String,default:"*"},attributes:{type:Object},multiple:{type:Boolean,default:!0},max:{type:Number}},data:function(){return{options:this.$props,completed:{},errors:[],files:[],total:0}},methods:{open:function(t){var e=this;this.params(t),setTimeout((function(){e.$refs.input.click()}),1)},params:function(t){this.options=Object.assign({},this.$props,t)},select:function(t){this.upload(t.target.files)},drop:function(t,e){this.params(e),this.upload(t)},upload:function(t){var e=this;this.$refs.dialog.open(),this.files=Object(Ji["a"])(t),this.completed={},this.errors=[],this.hasErrors=!1,this.options.max&&(this.files=this.files.slice(0,this.options.max)),this.total=this.files.length,this.files.forEach((function(t){e.$helper.upload(t,{url:e.options.url,attributes:e.options.attributes,headers:{"X-CSRF":window.panel.csrf},progress:function(t,n,i){e.$refs[n.name]&&e.$refs[n.name][0]&&e.$refs[n.name][0].set(i)},success:function(t,n,i){e.complete(n,i.data)},error:function(t,n,i){e.errors.push({file:n,message:i.message}),e.complete(n,i.data)}})}))},complete:function(t,e){var n=this;if(this.completed[t.name]=e,Object.keys(this.completed).length==this.total){if(this.$refs.input.value="",this.errors.length>0)return this.$forceUpdate(),void this.$emit("error",this.files);setTimeout((function(){n.$refs.dialog.close(),n.$emit("success",n.files,Object.values(n.completed))}),250)}}}},Sr=jr,Cr=(n("5aee"),Object(y["a"])(Sr,xr,Or,!1,null,null,null)),Er=Cr.exports,Rr=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("label",{staticClass:"k-checkbox-input",on:{click:function(t){t.stopPropagation()}}},[n("input",{ref:"input",staticClass:"k-checkbox-input-native",attrs:{disabled:t.disabled,id:t.id,type:"checkbox"},domProps:{checked:t.value},on:{change:function(e){return t.onChange(e.target.checked)}}}),n("span",{staticClass:"k-checkbox-input-icon",attrs:{"aria-hidden":"true"}},[n("svg",{attrs:{width:"12",height:"10",viewBox:"0 0 12 10",xmlns:"http://www.w3.org/2000/svg"}},[n("path",{attrs:{d:"M1 5l3.3 3L11 1","stroke-width":"2",fill:"none","fill-rule":"evenodd"}})])]),n("span",{staticClass:"k-checkbox-input-label",domProps:{innerHTML:t._s(t.label)}})])},Tr=[],Ir=n("b5ae"),Lr={inheritAttrs:!1,props:{autofocus:Boolean,disabled:Boolean,id:[Number,String],label:String,required:Boolean,value:Boolean},watch:{value:function(){this.onInvalid()}},mounted:function(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus:function(){this.$refs.input.focus()},onChange:function(t){this.$emit("input",t)},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},select:function(){this.focus()}},validations:function(){return{value:{required:!this.required||Ir["required"]}}}},Ar=Lr,Br=(n("42e4"),Object(y["a"])(Ar,Rr,Tr,!1,null,null,null)),qr=Br.exports,Nr=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("ul",{staticClass:"k-checkboxes-input",style:"--columns:"+t.columns},t._l(t.options,(function(e,i){return n("li",{key:i},[n("k-checkbox-input",{attrs:{id:t.id+"-"+i,label:e.text,value:-1!==t.selected.indexOf(e.value)},on:{input:function(n){return t.onInput(e.value,n)}}})],1)})),0)},Pr=[],Dr=(n("a434"),{inheritAttrs:!1,props:{autofocus:Boolean,columns:Number,disabled:Boolean,id:{type:[Number,String],default:function(){return this._uid}},max:Number,min:Number,options:Array,required:Boolean,value:{type:[Array,Object],default:function(){return[]}}},data:function(){return{selected:this.valueToArray(this.value)}},watch:{value:function(t){this.selected=this.valueToArray(t)},selected:function(){this.onInvalid()}},mounted:function(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus:function(){this.$el.querySelector("input").focus()},onInput:function(t,e){if(!0===e)this.selected.push(t);else{var n=this.selected.indexOf(t);-1!==n&&this.selected.splice(n,1)}this.$emit("input",this.selected)},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},select:function(){this.focus()},valueToArray:function(t){return!0===Array.isArray(t)?t:"string"===typeof t?String(t).split(","):"object"===Object(fe["a"])(t)?Object.values(t):void 0}},validations:function(){return{selected:{required:!this.required||Ir["required"],min:!this.min||Object(Ir["minLength"])(this.min),max:!this.max||Object(Ir["maxLength"])(this.max)}}}}),Mr=Dr,Fr=Object(y["a"])(Mr,Nr,Pr,!1,null,null,null),Ur=Fr.exports,zr=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-date-input"},[n("k-select-input",{ref:"years",attrs:{"aria-label":t.$t("year"),options:t.years,disabled:t.disabled,required:t.required,value:t.year,placeholder:"––––"},on:{input:t.setYear,invalid:t.onInvalid}}),n("span",{staticClass:"k-date-input-separator"},[t._v("-")]),n("k-select-input",{ref:"months",attrs:{"aria-label":t.$t("month"),options:t.months,disabled:t.disabled,required:t.required,value:t.month,placeholder:"––"},on:{input:t.setMonth,invalid:t.onInvalid}}),n("span",{staticClass:"k-date-input-separator"},[t._v("-")]),n("k-select-input",{ref:"days",attrs:{"aria-label":t.$t("day"),autofocus:t.autofocus,id:t.id,options:t.days,disabled:t.disabled,required:t.required,value:t.day,placeholder:"––"},on:{input:t.setDay,invalid:t.onInvalid}})],1)},Hr=[],Kr={inheritAttrs:!1,props:{autofocus:Boolean,disabled:Boolean,id:[String,Number],max:String,min:String,required:Boolean,value:String},data:function(){return{date:this.$library.dayjs(this.value),minDate:this.calculate(this.min,"min"),maxDate:this.calculate(this.max,"max")}},computed:{day:function(){return isNaN(this.date.date())?"":this.date.date()},days:function(){return this.options(1,this.date.daysInMonth()||31,"days")},month:function(){return isNaN(this.date.date())?"":this.date.month()+1},months:function(){return this.options(1,12,"months")},year:function(){return isNaN(this.date.year())?"":this.date.year()},years:function(){var t=this.date.isBefore(this.minDate)?this.date.year():this.minDate.year(),e=this.date.isAfter(this.maxDate)?this.date.year():this.maxDate.year();return this.options(t,e)}},watch:{value:function(t){this.date=this.$library.dayjs(t)}},methods:{calculate:function(t,e){var n={min:{run:"subtract",take:"startOf"},max:{run:"add",take:"endOf"}}[e],i=t?this.$library.dayjs(t):null;return i&&!1!==i.isValid()||(i=this.$library.dayjs()[n.run](10,"year")[n.take]("year")),i},focus:function(){this.$refs.years.focus()},onInput:function(){!1!==this.date.isValid()?this.$emit("input",this.date.toISOString()):this.$emit("input","")},onInvalid:function(t,e){this.$emit("invalid",t,e)},options:function(t,e){for(var n=[],i=t;i<=e;i++)n.push({value:i,text:this.$helper.pad(i)});return n},set:function(t,e){if(""===e||null===e||!1===e||-1===e)return this.setInvalid(),void this.onInput();if(!1===this.date.isValid())return this.setInitialDate(t,e),void this.onInput();var n=this.date,i=this.date.date();this.date=this.date.set(t,parseInt(e)),"month"===t&&this.date.date()!==i&&(this.date=n.set("date",1).set("month",e).endOf("month")),this.onInput()},setInvalid:function(){this.date=this.$library.dayjs("invalid")},setInitialDate:function(t,e){var n=this.$library.dayjs();return this.date=this.$library.dayjs().set(t,parseInt(e)),"date"===t&&n.month()!==this.date.month()&&(this.date=n.endOf("month")),this.date},setDay:function(t){this.set("date",t)},setMonth:function(t){this.set("month",t-1)},setYear:function(t){this.set("year",t)}}},Vr=Kr,Yr=(n("6ab3"),Object(y["a"])(Vr,zr,Hr,!1,null,null,null)),Wr=Yr.exports,Jr=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-datetime-input"},[n("k-date-input",{ref:"dateInput",attrs:{autofocus:t.autofocus,required:t.required,id:t.id,min:t.min,max:t.max,disabled:t.disabled,value:t.dateValue},on:{input:t.setDate}}),n("k-time-input",t._b({ref:"timeInput",attrs:{required:t.required,disabled:t.disabled,value:t.timeValue},on:{input:t.setTime}},"k-time-input",t.timeOptions,!1))],1)},Gr=[],Zr={inheritAttrs:!1,props:Object(I["a"])(Object(I["a"])({},Wr.props),{},{time:{type:[Boolean,Object],default:function(){return{}}},value:String}),data:function(){return{dateValue:this.parseDate(this.value),timeValue:this.parseTime(this.value),timeOptions:this.setTimeOptions()}},watch:{value:function(t){this.dateValue=this.parseDate(t),this.timeValue=this.parseTime(t),this.onInvalid()}},mounted:function(){this.onInvalid()},methods:{focus:function(){this.$refs.dateInput.focus()},onInput:function(){if(this.timeValue&&this.dateValue){var t=this.dateValue+"T"+this.timeValue+":00";this.$emit("input",t)}else this.$emit("input","")},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},parseDate:function(t){var e=this.$library.dayjs(t);return e.isValid()?e.format("YYYY-MM-DD"):null},parseTime:function(t){var e=this.$library.dayjs(t);return e.isValid()?e.format("HH:mm"):null},setDate:function(t){t&&!this.timeValue&&(this.timeValue=this.$library.dayjs().format("HH:mm")),t?this.dateValue=this.parseDate(t):(this.dateValue=null,this.timeValue=null),this.onInput()},setTime:function(t){t&&!this.dateValue&&(this.dateValue=this.$library.dayjs().format("YYYY-MM-DD")),t?this.timeValue=t:(this.dateValue=null,this.timeValue=null),this.onInput()},setTimeOptions:function(){return!0===this.time?{}:this.time}},validations:function(){return{value:{required:!this.required||Ir["required"]}}}},Xr=Zr,Qr=(n("4433"),Object(y["a"])(Xr,Jr,Gr,!1,null,null,null)),ts=Qr.exports,es=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("input",t._g(t._b({ref:"input",staticClass:"k-text-input",attrs:{dir:t.direction}},"input",{autocomplete:t.autocomplete,autofocus:t.autofocus,disabled:t.disabled,id:t.id,minlength:t.minlength,name:t.name,pattern:t.pattern,placeholder:t.placeholder,required:t.required,spellcheck:t.spellcheck,type:t.type,value:t.value},!1),t.listeners))},ns=[],is=function(t){var e=t.$store.state.languages.default||null,n=t.$store.state.languages.current||null,i=t.$store.state.system.info.multilang||!1,r=t.$store.state.system.info.user?t.$store.state.system.info.user.language:null,s=n?n.direction:null;if(i&&n&&!1===t.disabled&&(n.direction!==e.direction||r!==n.code))return s},rs={inheritAttrs:!1,class:"k-text-input",props:{autocomplete:{type:[Boolean,String],default:"off"},autofocus:Boolean,disabled:Boolean,id:[Number,String],maxlength:Number,minlength:Number,name:[Number,String],pattern:String,placeholder:String,preselect:Boolean,required:Boolean,spellcheck:{type:[Boolean,String],default:"off"},type:{type:String,default:"text"},value:String},computed:{direction:function(){return is(this)}},data:function(){var t=this;return{listeners:Object(I["a"])(Object(I["a"])({},this.$listeners),{},{input:function(e){return t.onInput(e.target.value)}})}},watch:{value:function(){this.onInvalid()}},mounted:function(){this.onInvalid(),this.$props.autofocus&&this.focus(),this.$props.preselect&&this.select()},methods:{focus:function(){this.$refs.input.focus()},onInput:function(t){this.$emit("input",t)},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},select:function(){this.$refs.input.select()}},validations:function(){var t=this,e=function(e){return!t.required&&!e||!t.$refs.input.validity.patternMismatch};return{value:{required:!this.required||Ir["required"],minLength:!this.minlength||Object(Ir["minLength"])(this.minlength),maxLength:!this.maxlength||Object(Ir["maxLength"])(this.maxlength),email:"email"!==this.type||Ir["email"],url:"url"!==this.type||Ir["url"],pattern:!this.pattern||e}}}},ss=rs,as=(n("cb8f"),Object(y["a"])(ss,es,ns,!1,null,null,null)),os=as.exports,us={extends:os,props:Object(I["a"])(Object(I["a"])({},os.props),{},{autocomplete:{type:String,default:"email"},placeholder:{type:String,default:function(){return this.$t("email.placeholder")}},type:{type:String,default:"email"}})},ls=us,cs=Object(y["a"])(ls,o,u,!1,null,null,null),ps=cs.exports,ds=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-draggable",{staticClass:"k-multiselect-input",attrs:{list:t.state,options:t.dragOptions,"data-layout":t.layout,element:"k-dropdown"},on:{end:t.onInput},nativeOn:{click:function(e){return t.$refs.dropdown.toggle(e)}}},[t._l(t.sorted,(function(e){return n("k-tag",{key:e.value,ref:e.value,refInFor:!0,attrs:{removable:!0},on:{remove:function(n){return t.remove(e)}},nativeOn:{click:function(t){t.stopPropagation()},keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"left",37,e.key,["Left","ArrowLeft"])?null:"button"in e&&0!==e.button?null:t.navigate("prev")},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"right",39,e.key,["Right","ArrowRight"])?null:"button"in e&&2!==e.button?null:t.navigate("next")},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"down",40,e.key,["Down","ArrowDown"])?null:t.$refs.dropdown.open(e)}]}},[t._v("\n "+t._s(e.text)+"\n ")])})),n("k-dropdown-content",{ref:"dropdown",attrs:{slot:"footer"},on:{open:t.onOpen,close:t.onClose},nativeOn:{keydown:function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"esc",27,e.key,["Esc","Escape"])?null:(e.stopPropagation(),t.close(e))}},slot:"footer"},[t.search?n("k-dropdown-item",{staticClass:"k-multiselect-search",attrs:{icon:"search"}},[n("input",{directives:[{name:"model",rawName:"v-model",value:t.q,expression:"q"}],ref:"search",attrs:{placeholder:t.search.min?t.$t("search.min",{min:t.search.min}):t.$t("search")+" …"},domProps:{value:t.q},on:{keydown:function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"esc",27,e.key,["Esc","Escape"])?null:(e.stopPropagation(),t.escape(e))},input:function(e){e.target.composing||(t.q=e.target.value)}}})]):t._e(),n("div",{staticClass:"k-multiselect-options"},[t._l(t.visible,(function(e){return n("k-dropdown-item",{key:e.value,class:{"k-multiselect-option":!0,selected:t.isSelected(e),disabled:!t.more},attrs:{icon:t.isSelected(e)?"check":"circle-outline"},on:{click:function(n){return n.preventDefault(),t.select(e)}},nativeOn:{keydown:[function(n){return!n.type.indexOf("key")&&t._k(n.keyCode,"enter",13,n.key,"Enter")?null:(n.preventDefault(),n.stopPropagation(),t.select(e))},function(n){return!n.type.indexOf("key")&&t._k(n.keyCode,"space",32,n.key,[" ","Spacebar"])?null:(n.preventDefault(),n.stopPropagation(),t.select(e))}]}},[n("span",{domProps:{innerHTML:t._s(e.display)}}),n("span",{staticClass:"k-multiselect-value",domProps:{innerHTML:t._s(e.info)}})])})),0===t.filtered.length?n("k-dropdown-item",{staticClass:"k-multiselect-option",attrs:{disabled:!0}},[t._v("\n "+t._s(t.emptyLabel)+"\n ")]):t._e()],2),t.visible.length1&&!this.sort},dragOptions:function(){return{disabled:!this.draggable,draggable:".k-tag",delay:1}},emptyLabel:function(){return this.q?this.$t("search.results.none"):this.$t("options.none")},visible:function(){return this.limit?this.filtered.slice(0,this.search.display||this.filtered.length):this.filtered},filtered:function(){if(this.q&&this.q.length>=(this.search.min||0)){var t=new RegExp("(".concat(RegExp.escape(this.q),")"),"ig");return this.options.filter((function(e){return String(e.text).match(t)||String(e.value).match(t)})).map((function(e){return Object(I["a"])(Object(I["a"])({},e),{},{display:String(e.text).replace(t,"$1"),info:String(e.value).replace(t,"$1")})}))}return this.options.map((function(t){return Object(I["a"])(Object(I["a"])({},t),{},{display:t.text,info:t.value})}))},sorted:function(){var t=this;if(!1===this.sort)return this.state;var e=this.state,n=function(e){return t.options.findIndex((function(t){return t.value===e.value}))};return e.sort((function(t,e){return n(t)-n(e)}))},more:function(){return!this.max||this.state.length1?n[1].length:0;return new Intl.NumberFormat(e,{minimumFractionDigits:i}).format(t)},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},onInput:function(t){this.$emit("input",t)}},validations:function(){return{position:{required:!this.required||Ir["required"],min:!this.min||Object(Ir["minValue"])(this.min),max:!this.max||Object(Ir["maxValue"])(this.max)}}}},Ns=qs,Ps=(n("b5d2"),Object(y["a"])(Ns,As,Bs,!1,null,null,null)),Ds=Ps.exports,Ms=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",{staticClass:"k-select-input",attrs:{"data-disabled":t.disabled,"data-empty":""===t.selected}},[n("select",t._g({ref:"input",staticClass:"k-select-input-native",attrs:{autofocus:t.autofocus,"aria-label":t.ariaLabel,disabled:t.disabled,id:t.id,name:t.name,required:t.required},domProps:{value:t.selected}},t.listeners),[t.hasEmptyOption?n("option",{attrs:{disabled:t.required,value:""}},[t._v("\n "+t._s(t.emptyOption)+"\n ")]):t._e(),t._l(t.options,(function(e){return n("option",{key:e.value,attrs:{disabled:e.disabled},domProps:{value:e.value}},[t._v("\n "+t._s(e.text)+"\n ")])}))],2),t._v("\n "+t._s(t.label)+"\n")])},Fs=[],Us={inheritAttrs:!1,props:{autofocus:Boolean,ariaLabel:String,default:String,disabled:Boolean,empty:{type:[Boolean,String],default:!0},id:[Number,String],name:[Number,String],placeholder:String,options:{type:Array,default:function(){return[]}},required:Boolean,value:{type:[String,Number,Boolean],default:""}},data:function(){var t=this;return{selected:this.value,listeners:Object(I["a"])(Object(I["a"])({},this.$listeners),{},{click:function(e){return t.onClick(e)},change:function(e){return t.onInput(e.target.value)},input:function(){}})}},computed:{emptyOption:function(){return this.placeholder||"—"},hasEmptyOption:function(){return!1!==this.empty&&!(this.required&&this.default)},label:function(){var t=this.text(this.selected);return""===this.selected||null===this.selected||null===t?this.emptyOption:t}},watch:{value:function(t){this.selected=t,this.onInvalid()}},mounted:function(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus:function(){this.$refs.input.focus()},onClick:function(t){t.stopPropagation(),this.$emit("click",t)},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},onInput:function(t){this.selected=t,this.$emit("input",this.selected)},select:function(){this.focus()},text:function(t){var e=null;return this.options.forEach((function(n){n.value==t&&(e=n.text)})),e}},validations:function(){return{selected:{required:!this.required||Ir["required"]}}}},zs=Us,Hs=(n("6a18"),Object(y["a"])(zs,Ms,Fs,!1,null,null,null)),Ks=Hs.exports,Vs=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-draggable",{ref:"box",staticClass:"k-tags-input",attrs:{list:t.tags,"data-layout":t.layout,options:t.dragOptions,dir:t.direction},on:{end:t.onInput}},[t._l(t.tags,(function(e,i){return n("k-tag",{key:i,ref:e.value,refInFor:!0,attrs:{removable:!t.disabled,name:"tag"},on:{remove:function(n){return t.remove(e)}},nativeOn:{click:function(t){t.stopPropagation()},blur:function(e){return t.selectTag(null)},focus:function(n){return t.selectTag(e)},keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"left",37,e.key,["Left","ArrowLeft"])?null:"button"in e&&0!==e.button?null:t.navigate("prev")},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"right",39,e.key,["Right","ArrowRight"])?null:"button"in e&&2!==e.button?null:t.navigate("next")}],dblclick:function(n){return t.edit(e)}}},[t._v("\n "+t._s(e.text)+"\n ")])})),n("span",{staticClass:"k-tags-input-element",attrs:{slot:"footer"},slot:"footer"},[n("k-autocomplete",{ref:"autocomplete",attrs:{options:t.options,skip:t.skip},on:{select:t.addTag,leave:function(e){return t.$refs.input.focus()}}},[n("input",{directives:[{name:"model",rawName:"v-model.trim",value:t.newTag,expression:"newTag",modifiers:{trim:!0}}],ref:"input",attrs:{autofocus:t.autofocus,disabled:t.disabled||t.max&&t.tags.length>=t.max,id:t.id,name:t.name,autocomplete:"off",type:"text"},domProps:{value:t.newTag},on:{input:[function(e){e.target.composing||(t.newTag=e.target.value.trim())},function(e){return t.type(e.target.value)}],blur:[t.blurInput,function(e){return t.$forceUpdate()}],keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"s",void 0,e.key,void 0)?null:e.metaKey?t.blurInput(e):null},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"left",37,e.key,["Left","ArrowLeft"])?null:"button"in e&&0!==e.button?null:e.ctrlKey||e.shiftKey||e.altKey||e.metaKey?null:t.leaveInput(e)},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:e.ctrlKey||e.shiftKey||e.altKey||e.metaKey?null:t.enter(e)},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"tab",9,e.key,"Tab")?null:e.ctrlKey||e.shiftKey||e.altKey||e.metaKey?null:t.tab(e)},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"backspace",void 0,e.key,void 0)?null:e.ctrlKey||e.shiftKey||e.altKey||e.metaKey?null:t.leaveInput(e)}]}})])],1)],2)},Ys=[],Ws={inheritAttrs:!1,props:{autofocus:Boolean,accept:{type:String,default:"all"},disabled:Boolean,icon:{type:[String,Boolean],default:"tag"},id:[Number,String],layout:String,max:Number,min:Number,name:[Number,String],options:{type:Array,default:function(){return[]}},required:Boolean,separator:{type:String,default:","},value:{type:Array,default:function(){return[]}}},data:function(){var t=this;return{tags:this.prepareTags(this.value),selected:null,newTag:null,tagOptions:this.options.map((function(e){return t.icon&&t.icon.length>0&&(e.icon=t.icon),e}),this)}},computed:{direction:function(){return is(this)},dragOptions:function(){return{delay:1,disabled:!this.draggable,draggable:".k-tag"}},draggable:function(){return this.tags.length>1},skip:function(){return this.tags.map((function(t){return t.value}))}},watch:{value:function(t){this.tags=this.prepareTags(t),this.onInvalid()}},mounted:function(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{addString:function(t){var e=this;if(t)if(t=t.trim(),t.includes(this.separator))t.split(this.separator).forEach((function(t){e.addString(t)}));else if(0!==t.length)if("options"===this.accept){var n=this.options.filter((function(e){return e.text===t}))[0];if(!n)return;this.addTag(n)}else this.addTag({text:t,value:t})},addTag:function(t){this.addTagToIndex(t),this.$refs.autocomplete.close(),this.$refs.input.focus()},addTagToIndex:function(t){if("options"===this.accept){var e=this.options.filter((function(e){return e.value===t.value}))[0];if(!e)return}-1===this.index(t)&&(!this.max||this.tags.length=this.tags.length)return;break;case"first":e=0;break;case"last":e=this.tags.length-1;break;default:e=t;break}var i=this.tags[e];if(i){var r=this.$refs[i.value];if(r&&r[0])return{ref:r[0],tag:i,index:e}}return!1},index:function(t){return this.tags.findIndex((function(e){return e.value===t.value}))},onInput:function(){this.$emit("input",this.tags)},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},leaveInput:function(t){0===t.target.selectionStart&&t.target.selectionStart===t.target.selectionEnd&&0!==this.tags.length&&(this.$refs.autocomplete.close(),this.navigate("last"),t.preventDefault())},navigate:function(t){var e=this.get(t);e?(e.ref.focus(),this.selectTag(e.tag)):"next"===t&&(this.$refs.input.focus(),this.selectTag(null))},prepareTags:function(t){return!1===Array.isArray(t)?[]:t.map((function(t){return"string"===typeof t?{text:t,value:t}:t}))},remove:function(t){var e=this.get("prev"),n=this.get("next");this.tags.splice(this.index(t),1),this.onInput(),e?(this.selectTag(e.tag),e.ref.focus()):n?this.selectTag(n.tag):(this.selectTag(null),this.$refs.input.focus())},select:function(){this.focus()},selectTag:function(t){this.selected=t},tab:function(t){this.newTag&&this.newTag.length>0&&(t.preventDefault(),this.addString(this.newTag))},type:function(t){this.newTag=t,this.$refs.autocomplete.search(t)}},validations:function(){return{tags:{required:!this.required||Ir["required"],minLength:!this.min||Object(Ir["minLength"])(this.min),maxLength:!this.max||Object(Ir["maxLength"])(this.max)}}}},Js=Ws,Gs=(n("27c1"),Object(y["a"])(Js,Vs,Ys,!1,null,null,null)),Zs=Gs.exports,Xs={extends:os,props:Object(I["a"])(Object(I["a"])({},os.props),{},{autocomplete:{type:String,default:"tel"},type:{type:String,default:"tel"}})},Qs=Xs,ta=Object(y["a"])(Qs,p,d,!1,null,null,null),ea=ta.exports,na=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-textarea-input",attrs:{"data-theme":t.theme,"data-over":t.over}},[n("div",{staticClass:"k-textarea-input-wrapper"},[t.buttons&&!t.disabled?n("k-toolbar",{ref:"toolbar",attrs:{buttons:t.buttons,disabled:t.disabled,uploads:t.uploads},on:{command:t.onCommand},nativeOn:{mousedown:function(t){t.preventDefault()}}}):t._e(),n("textarea",t._b({ref:"input",staticClass:"k-textarea-input-native",attrs:{"data-font":t.font,"data-size":t.size,dir:t.direction},on:{click:t.onClick,focus:t.onFocus,input:t.onInput,keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:e.metaKey?t.onSubmit(e):null},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:e.ctrlKey?t.onSubmit(e):null},function(e){return e.metaKey?t.onShortcut(e):null},function(e){return e.ctrlKey?t.onShortcut(e):null}],dragover:t.onOver,dragleave:t.onOut,drop:t.onDrop}},"textarea",{autofocus:t.autofocus,disabled:t.disabled,id:t.id,minlength:t.minlength,name:t.name,placeholder:t.placeholder,required:t.required,spellcheck:t.spellcheck,value:t.value},!1))],1),n("k-toolbar-email-dialog",{ref:"emailDialog",on:{cancel:t.cancel,submit:function(e){return t.insert(e)}}}),n("k-toolbar-link-dialog",{ref:"linkDialog",on:{cancel:t.cancel,submit:function(e){return t.insert(e)}}}),n("k-files-dialog",{ref:"fileDialog",on:{cancel:t.cancel,submit:function(e){return t.insertFile(e)}}}),t.uploads?n("k-upload",{ref:"fileUpload",on:{success:t.insertUpload}}):t._e()],1)},ia=[],ra={inheritAttrs:!1,props:{autofocus:Boolean,buttons:{type:[Boolean,Array],default:!0},disabled:Boolean,endpoints:Object,font:String,id:[Number,String],name:[Number,String],maxlength:Number,minlength:Number,placeholder:String,preselect:Boolean,required:Boolean,size:String,spellcheck:{type:[Boolean,String],default:"off"},theme:String,uploads:[Boolean,Object,Array],value:String},data:function(){return{over:!1}},computed:{direction:function(){return is(this)}},watch:{value:function(){var t=this;this.onInvalid(),this.$nextTick((function(){t.resize()}))}},mounted:function(){var t=this;this.$nextTick((function(){t.$library.autosize(t.$refs.input)})),this.onInvalid(),this.$props.autofocus&&this.focus(),this.$props.preselect&&this.select()},methods:{cancel:function(){this.$refs.input.focus()},dialog:function(t){if(!this.$refs[t+"Dialog"])throw"Invalid toolbar dialog";this.$refs[t+"Dialog"].open(this.$refs.input,this.selection())},focus:function(){this.$refs.input.focus()},insert:function(t){var e=this,n=this.$refs.input,i=n.value;setTimeout((function(){if(n.focus(),document.execCommand("insertText",!1,t),n.value===i){var r=n.value.slice(0,n.selectionStart)+t+n.value.slice(n.selectionEnd);n.value=r,e.$emit("input",r)}})),this.resize()},insertFile:function(t){t&&t.length>0&&this.insert(t.map((function(t){return t.dragText})).join("\n\n"))},insertUpload:function(t,e){this.insert(e.map((function(t){return t.dragText})).join("\n\n")),this.$events.$emit("model.update")},onClick:function(){this.$refs.toolbar&&this.$refs.toolbar.close()},onCommand:function(t,e){"function"===typeof this[t]?"function"===typeof e?this[t](e(this.$refs.input,this.selection())):this[t](e):window.console.warn(t+" is not a valid command")},onDrop:function(t){if(this.uploads&&this.$helper.isUploadEvent(t))return this.$refs.fileUpload.drop(t.dataTransfer.files,{url:B.api+"/"+this.endpoints.field+"/upload",multiple:!1});var e=this.$store.state.drag;e&&"text"===e.type&&(this.focus(),this.insert(e.data))},onFocus:function(t){this.$emit("focus",t)},onInput:function(t){this.$emit("input",t.target.value)},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},onOut:function(){this.$refs.input.blur(),this.over=!1},onOver:function(t){if(this.uploads&&this.$helper.isUploadEvent(t))return t.dataTransfer.dropEffect="copy",this.focus(),void(this.over=!0);var e=this.$store.state.drag;e&&"text"===e.type&&(t.dataTransfer.dropEffect="copy",this.focus(),this.over=!0)},onShortcut:function(t){!1!==this.buttons&&"Meta"!==t.key&&"Control"!==t.key&&this.$refs.toolbar&&this.$refs.toolbar.shortcut(t.key,t)},onSubmit:function(t){return this.$emit("submit",t)},prepend:function(t){this.insert(t+" "+this.selection())},resize:function(){this.$library.autosize.update(this.$refs.input)},select:function(){this.$refs.select()},selectFile:function(){this.$refs.fileDialog.open({endpoint:this.endpoints.field+"/files",multiple:!1})},selection:function(){var t=this.$refs.input,e=t.selectionStart,n=t.selectionEnd;return t.value.substring(e,n)},uploadFile:function(){this.$refs.fileUpload.open({url:B.api+"/"+this.endpoints.field+"/upload",multiple:!1})},wrap:function(t){this.insert(t+this.selection()+t)}},validations:function(){return{value:{required:!this.required||Ir["required"],minLength:!this.minlength||Object(Ir["minLength"])(this.minlength),maxLength:!this.maxlength||Object(Ir["maxLength"])(this.maxlength)}}}},sa=ra,aa=(n("cca8"),Object(y["a"])(sa,na,ia,!1,null,null,null)),oa=aa.exports,ua=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-time-input"},[n("k-select-input",{ref:"hour",attrs:{id:t.id,"aria-label":t.$t("hour"),autofocus:t.autofocus,options:t.hours,required:t.required,disabled:t.disabled,placeholder:"––"},on:{input:t.setHour,invalid:t.onInvalid},model:{value:t.hour,callback:function(e){t.hour=e},expression:"hour"}}),n("span",{staticClass:"k-time-input-separator"},[t._v(":")]),n("k-select-input",{ref:"minute",attrs:{"aria-label":t.$t("minutes"),options:t.minutes,required:t.required,disabled:t.disabled,placeholder:"––"},on:{input:t.setMinute,invalid:t.onInvalid},model:{value:t.minute,callback:function(e){t.minute=e},expression:"minute"}}),12===t.notation?n("k-select-input",{ref:"meridiem",staticClass:"k-time-input-meridiem",attrs:{"aria-label":t.$t("meridiem"),empty:!1,options:[{value:"AM",text:"AM"},{value:"PM",text:"PM"}],required:t.required,disabled:t.disabled},on:{input:t.onInput},model:{value:t.meridiem,callback:function(e){t.meridiem=e},expression:"meridiem"}}):t._e()],1)},la=[],ca={inheritAttrs:!1,props:{autofocus:Boolean,disabled:Boolean,id:[String,Number],notation:{type:Number,default:24},required:Boolean,step:{type:Number,default:5},value:{type:String}},data:function(){var t=this.toObject(this.value);return{time:this.value,hour:t.hour,minute:t.minute,meridiem:t.meridiem}},computed:{hours:function(){return this.options(24===this.notation?0:1,24===this.notation?23:12)},minutes:function(){return this.options(0,59,this.step)}},watch:{value:function(t){this.time=t},time:function(t){var e=this.toObject(t);this.hour=e.hour,this.minute=e.minute,this.meridiem=e.meridiem}},methods:{focus:function(){this.$refs.hour.focus()},setHour:function(t){t&&!this.minute&&(this.minute=0),t||(this.minute=null),this.onInput()},setMinute:function(t){t&&!this.hour&&(this.hour=0),t||(this.hour=null),this.onInput()},onInput:function(){if(null!==this.hour&&null!==this.minute){var t=this.$helper.pad(this.hour||0),e=this.$helper.pad(this.minute||0),n=String(this.meridiem||"AM").toUpperCase(),i=24===this.notation?"".concat(t,":").concat(e,":00"):"".concat(t,":").concat(e,":00 ").concat(n),r=24===this.notation?"HH:mm:ss":"hh:mm:ss A",s=this.$library.dayjs("2000-01-01 "+i,"YYYY-MM-DD "+r);this.$emit("input",s.format("HH:mm"))}else this.$emit("input","")},onInvalid:function(t,e){this.$emit("invalid",t,e)},options:function(t,e){for(var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1,i=[],r=t;r<=e;r+=n)i.push({value:r,text:this.$helper.pad(r)});return i},reset:function(){this.hour=null,this.minute=null,this.meridiem=null},round:function(t){return Math.floor(t/this.step)*this.step},toObject:function(t){var e=this.$library.dayjs("2001-01-01 "+t+":00","YYYY-MM-DD HH:mm:ss");return t&&!1!==e.isValid()?{hour:e.format(24===this.notation?"H":"h"),minute:this.round(e.format("m")),meridiem:e.format("A")}:{hour:null,minute:null,meridiem:null}}}},pa=ca,da=(n("50da"),Object(y["a"])(pa,ua,la,!1,null,null,null)),fa=da.exports,ha=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("label",{staticClass:"k-toggle-input",attrs:{"data-disabled":t.disabled}},[n("input",{ref:"input",staticClass:"k-toggle-input-native",attrs:{disabled:t.disabled,id:t.id,type:"checkbox"},domProps:{checked:t.value},on:{change:function(e){return t.onInput(e.target.checked)}}}),n("span",{staticClass:"k-toggle-input-label",domProps:{innerHTML:t._s(t.label)}})])},ma=[],ga={inheritAttrs:!1,props:{autofocus:Boolean,disabled:Boolean,id:[Number,String],text:{type:[Array,String],default:function(){return[this.$t("off"),this.$t("on")]}},required:Boolean,value:Boolean},computed:{label:function(){return Array.isArray(this.text)?this.value?this.text[1]:this.text[0]:this.text}},watch:{value:function(){this.onInvalid()}},mounted:function(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus:function(){this.$refs.input.focus()},onEnter:function(t){"Enter"===t.key&&this.$refs.input.click()},onInput:function(t){this.$emit("input",t)},onInvalid:function(){this.$emit("invalid",this.$v.$invalid,this.$v)},select:function(){this.$refs.input.focus()}},validations:function(){return{value:{required:!this.required||Ir["required"]}}}},ba=ga,va=(n("bb41"),Object(y["a"])(ba,ha,ma,!1,null,null,null)),ka=va.exports,$a={extends:os,props:Object(I["a"])(Object(I["a"])({},os.props),{},{autocomplete:{type:String,default:"url"},type:{type:String,default:"url"}})},ya=$a,_a=Object(y["a"])(ya,f,h,!1,null,null,null),wa=_a.exports,xa=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-checkboxes-field",attrs:{counter:t.counterOptions}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"checkboxes"}},"k-input",t.$props,!1),t.$listeners))],1)},Oa=[],ja={inheritAttrs:!1,props:Object(I["a"])(Object(I["a"])(Object(I["a"])(Object(I["a"])({},pr.props),wr.props),Ur.props),{},{counter:{type:Boolean,default:!0}}),computed:{counterOptions:function(){return null!==this.value&&!this.disabled&&!1!==this.counter&&{count:this.value&&Array.isArray(this.value)?this.value.length:0,min:this.min,max:this.max}}},methods:{focus:function(){this.$refs.input.focus()}}},Sa=ja,Ca=Object(y["a"])(Sa,xa,Oa,!1,null,null,null),Ea=Ca.exports,Ra=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-date-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,type:t.inputType,value:t.date,theme:"field"}},"k-input",t.$props,!1),t.listeners),[n("template",{slot:"icon"},[n("k-dropdown",[n("k-button",{staticClass:"k-input-icon-button",attrs:{icon:t.icon,tooltip:t.$t("date.select"),tabindex:"-1"},on:{click:function(e){return t.$refs.dropdown.toggle()}}}),n("k-dropdown-content",{ref:"dropdown",attrs:{align:"right"}},[n("k-calendar",{attrs:{value:t.date},on:{input:function(e){t.onInput(e),t.$refs.dropdown.close()}}})],1)],1)],1)],2)],1)},Ta=[],Ia={inheritAttrs:!1,props:Object(I["a"])(Object(I["a"])(Object(I["a"])(Object(I["a"])({},pr.props),wr.props),ts.props),{},{icon:{type:String,default:"calendar"}}),data:function(){return{date:this.value,listeners:Object(I["a"])(Object(I["a"])({},this.$listeners),{},{input:this.onInput})}},computed:{inputType:function(){return!1===this.time?"date":"datetime"}},watch:{value:function(t){this.date=t}},methods:{focus:function(){this.$refs.input.focus()},onInput:function(t){this.date=t,this.$emit("input",t)}}},La=Ia,Aa=Object(y["a"])(La,Ra,Ta,!1,null,null,null),Ba=Aa.exports,qa=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-email-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"email"}},"k-input",t.$props,!1),t.$listeners),[t.link?n("k-button",{staticClass:"k-input-icon-button",attrs:{slot:"icon",icon:t.icon,link:t.mailto,tooltip:t.$t("open"),tabindex:"-1",target:"_blank"},slot:"icon"}):t._e()],1)],1)},Na=[],Pa={inheritAttrs:!1,props:Object(I["a"])(Object(I["a"])(Object(I["a"])(Object(I["a"])({},pr.props),wr.props),ps.props),{},{link:{type:Boolean,default:!0},icon:{type:String,default:"email"}}),computed:{mailto:function(){return this.value&&this.value.length>0?"mailto:"+this.value:null}},methods:{focus:function(){this.$refs.input.focus()}}},Da=Pa,Ma=Object(y["a"])(Da,qa,Na,!1,null,null,null),Fa=Ma.exports,Ua=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-files-field"},"k-field",t.$props,!1),[t.more&&!t.disabled?n("template",{slot:"options"},[n("k-button-group",{staticClass:"k-field-options"},[t.uploads?[n("k-dropdown",[n("k-button",{ref:"pickerToggle",staticClass:"k-field-options-button",attrs:{icon:t.btnIcon},on:{click:function(e){return t.$refs.picker.toggle()}}},[t._v("\n "+t._s(t.btnLabel)+"\n ")]),n("k-dropdown-content",{ref:"picker",attrs:{align:"right"}},[n("k-dropdown-item",{attrs:{icon:"check"},on:{click:t.open}},[t._v(t._s(t.$t("select")))]),n("k-dropdown-item",{attrs:{icon:"upload"},on:{click:t.upload}},[t._v(t._s(t.$t("upload")))])],1)],1)]:[n("k-button",{staticClass:"k-field-options-button",attrs:{icon:"add"},on:{click:t.open}},[t._v(t._s(t.$t("add")))])]],2)],1):t._e(),t.selected.length?[n("k-draggable",{attrs:{element:t.elements.list,list:t.selected,"data-size":t.size,handle:!0,"data-invalid":t.isInvalid},on:{end:t.onInput}},t._l(t.selected,(function(e,i){return n(t.elements.item,{key:e.id,tag:"component",attrs:{sortable:!t.disabled&&t.selected.length>1,text:e.text,link:t.link?e.link:null,info:e.info,image:e.image,icon:e.icon}},[t.disabled?t._e():n("k-button",{attrs:{slot:"options",tooltip:t.$t("remove"),icon:"remove"},on:{click:function(e){return t.remove(i)}},slot:"options"})],1)})),1)]:n("k-empty",{attrs:{layout:t.layout,"data-invalid":t.isInvalid,icon:"image"},on:{click:t.open}},[t._v("\n "+t._s(t.empty||t.$t("field.files.empty"))+"\n ")]),n("k-files-dialog",{ref:"selector",on:{submit:t.select}}),n("k-upload",{ref:"fileUpload",on:{success:t.selectUpload}})],2)},za=[],Ha={inheritAttrs:!1,props:Object(I["a"])(Object(I["a"])({},pr.props),{},{empty:String,info:String,link:Boolean,layout:String,max:Number,multiple:Boolean,parent:String,search:Boolean,size:String,text:String,value:{type:Array,default:function(){return[]}}}),data:function(){return{selected:this.value}},computed:{btnIcon:function(){return!this.multiple&&this.selected.length>0?"refresh":"add"},btnLabel:function(){return!this.multiple&&this.selected.length>0?this.$t("change"):this.$t("add")},elements:function(){var t={cards:{list:"k-cards",item:"k-card"},list:{list:"k-list",item:"k-list-item"}};return t[this.layout]?t[this.layout]:t["list"]},isInvalid:function(){return!(!this.required||0!==this.selected.length)||(!!(this.min&&this.selected.lengththis.max))},more:function(){return!this.max||this.max>this.selected.length}},watch:{value:function(t){this.selected=t}},methods:{focus:function(){},onInput:function(){this.$emit("input",this.selected)},remove:function(t){this.selected.splice(t,1),this.onInput()},removeById:function(t){this.selected=this.selected.filter((function(e){return e.id!==t})),this.onInput()},select:function(t){var e=this;0!==t.length?(this.selected=this.selected.filter((function(e){return t.filter((function(t){return t.id===e.id})).length>0})),t.forEach((function(t){0===e.selected.filter((function(e){return t.id===e.id})).length&&e.selected.push(t)})),this.onInput()):this.selected=[]}}},Ka={mixins:[Ha],props:{uploads:[Boolean,Object,Array]},created:function(){this.$events.$on("file.delete",this.removeById)},destroyed:function(){this.$events.$off("file.delete",this.removeById)},methods:{prompt:function(t){t.stopPropagation(),this.uploads?this.$refs.picker.toggle():this.open()},open:function(){if(this.disabled)return!1;this.$refs.selector.open({endpoint:this.endpoints.field,max:this.max,multiple:this.multiple,search:this.search,selected:this.selected.map((function(t){return t.id}))})},selectUpload:function(t,e){var n=this;!1===this.multiple&&(this.selected=[]),e.forEach((function(t){n.selected.push(t)})),this.onInput(),this.$events.$emit("model.update")},upload:function(){this.$refs.fileUpload.open({url:B.api+"/"+this.endpoints.field+"/upload",multiple:this.multiple,accept:this.uploads.accept})}}},Va=Ka,Ya=(n("4a4b"),Object(y["a"])(Va,Ua,za,!1,null,null,null)),Wa=Ya.exports,Ja=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-field k-gap-field"})},Ga=[],Za={},Xa=Object(y["a"])(Za,Ja,Ga,!1,null,null,null),Qa=Xa.exports,to=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-headline",{staticClass:"k-headline-field",attrs:{"data-numbered":t.numbered,size:"large"}},[t._v("\n "+t._s(t.label)+"\n")])},eo=[],no={props:{label:String,numbered:Boolean}},io=no,ro=(n("19d7"),Object(y["a"])(io,to,eo,!1,null,null,null)),so=ro.exports,ao=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-field k-info-field"},[n("k-headline",[t._v(t._s(t.label))]),n("k-box",{attrs:{theme:t.theme}},[n("k-text",{domProps:{innerHTML:t._s(t.text)}})],1)],1)},oo=[],uo={props:{label:String,text:String,theme:{type:String,default:"info"}}},lo=uo,co=(n("ddfd"),Object(y["a"])(lo,ao,oo,!1,null,null,null)),po=co.exports,fo=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("hr",{staticClass:"k-line-field"})},ho=[],mo=(n("718c"),{}),go=Object(y["a"])(mo,fo,ho,!1,null,null,null),bo=go.exports,vo=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-multiselect-field",attrs:{input:t._uid,counter:t.counterOptions},on:{blur:t.blur},nativeOn:{keydown:function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:(e.preventDefault(),t.focus(e))}}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"multiselect"}},"k-input",t.$props,!1),t.$listeners))],1)},ko=[],$o={inheritAttrs:!1,props:Object(I["a"])(Object(I["a"])(Object(I["a"])(Object(I["a"])({},pr.props),wr.props),bs.props),{},{counter:{type:Boolean,default:!0},icon:{type:String,default:"angle-down"}}),computed:{counterOptions:function(){return null!==this.value&&!this.disabled&&!1!==this.counter&&{count:this.value&&Array.isArray(this.value)?this.value.length:0,min:this.min,max:this.max}}},mounted:function(){this.$refs.input.$el.setAttribute("tabindex",0)},methods:{blur:function(t){this.$refs.input.blur(t)},focus:function(){this.$refs.input.focus()}}},yo=$o,_o=Object(y["a"])(yo,vo,ko,!1,null,null,null),wo=_o.exports,xo=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-number-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"number"}},"k-input",t.$props,!1),t.$listeners))],1)},Oo=[],jo={inheritAttrs:!1,props:Object(I["a"])(Object(I["a"])(Object(I["a"])({},pr.props),wr.props),ws.props),methods:{focus:function(){this.$refs.input.focus()}}},So=jo,Co=Object(y["a"])(So,xo,Oo,!1,null,null,null),Eo=Co.exports,Ro=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-pages-field"},"k-field",t.$props,!1),[n("k-button-group",{staticClass:"k-field-options",attrs:{slot:"options"},slot:"options"},[t.more&&!t.disabled?n("k-button",{staticClass:"k-field-options-button",attrs:{icon:t.btnIcon},on:{click:t.open}},[t._v("\n "+t._s(t.btnLabel)+"\n ")]):t._e()],1),t.selected.length?[n("k-draggable",{attrs:{element:t.elements.list,handle:!0,list:t.selected,"data-size":t.size,"data-invalid":t.isInvalid},on:{end:t.onInput}},t._l(t.selected,(function(e,i){return n(t.elements.item,{key:e.id,tag:"component",attrs:{sortable:!t.disabled&&t.selected.length>1,text:e.text,info:e.info,link:t.link?e.link:null,icon:e.icon,image:e.image}},[t.disabled?t._e():n("k-button",{attrs:{slot:"options",icon:"remove"},on:{click:function(e){return t.remove(i)}},slot:"options"})],1)})),1)]:n("k-empty",{attrs:{layout:t.layout,"data-invalid":t.isInvalid,icon:"page"},on:{click:t.open}},[t._v("\n "+t._s(t.empty||t.$t("field.pages.empty"))+"\n ")]),n("k-pages-dialog",{ref:"selector",on:{submit:t.select}})],2)},To=[],Io={mixins:[Ha],methods:{open:function(){if(this.disabled)return!1;this.$refs.selector.open({endpoint:this.endpoints.field,max:this.max,multiple:this.multiple,search:this.search,selected:this.selected.map((function(t){return t.id}))})}}},Lo=Io,Ao=(n("7e85"),Object(y["a"])(Lo,Ro,To,!1,null,null,null)),Bo=Ao.exports,qo=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-password-field",attrs:{input:t._uid,counter:t.counterOptions}},"k-field",t.$props,!1),[t._t("options",null,{slot:"options"}),n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"password"}},"k-input",t.$props,!1),t.$listeners))],2)},No=[],Po={inheritAttrs:!1,props:Object(I["a"])(Object(I["a"])(Object(I["a"])(Object(I["a"])({},pr.props),wr.props),Ss.props),{},{counter:{type:Boolean,default:!0},minlength:{type:Number,default:8},icon:{type:String,default:"key"}}),computed:{counterOptions:function(){return null!==this.value&&!this.disabled&&!1!==this.counter&&{count:this.value?String(this.value).length:0,min:this.minlength,max:this.maxlength}}},methods:{focus:function(){this.$refs.input.focus()}}},Do=Po,Mo=Object(y["a"])(Do,qo,No,!1,null,null,null),Fo=Mo.exports,Uo=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-radio-field"},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"radio"}},"k-input",t.$props,!1),t.$listeners))],1)},zo=[],Ho={inheritAttrs:!1,props:Object(I["a"])(Object(I["a"])(Object(I["a"])({},pr.props),wr.props),Ls.props),methods:{focus:function(){this.$refs.input.focus()}}},Ko=Ho,Vo=Object(y["a"])(Ko,Uo,zo,!1,null,null,null),Yo=Vo.exports,Wo=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-range-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"range"}},"k-input",t.$props,!1),t.$listeners))],1)},Jo=[],Go={inheritAttrs:!1,props:Object(I["a"])(Object(I["a"])(Object(I["a"])({},pr.props),wr.props),Ds.props),methods:{focus:function(){this.$refs.input.focus()}}},Zo=Go,Xo=Object(y["a"])(Zo,Wo,Jo,!1,null,null,null),Qo=Xo.exports,tu=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-select-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"select"}},"k-input",t.$props,!1),t.$listeners))],1)},eu=[],nu={inheritAttrs:!1,props:Object(I["a"])(Object(I["a"])(Object(I["a"])(Object(I["a"])({},pr.props),wr.props),Ks.props),{},{icon:{type:String,default:"angle-down"}}),methods:{focus:function(){this.$refs.input.focus()}}},iu=nu,ru=Object(y["a"])(iu,tu,eu,!1,null,null,null),su=ru.exports,au=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-structure-field",nativeOn:{click:function(t){t.stopPropagation()}}},"k-field",t.$props,!1),[n("template",{slot:"options"},[t.more&&null===t.currentIndex?n("k-button",{ref:"add",attrs:{id:t._uid,icon:"add"},on:{click:t.add}},[t._v("\n "+t._s(t.$t("add"))+"\n ")]):t._e()],1),null!==t.currentIndex?[n("div",{staticClass:"k-structure-backdrop",on:{click:t.escape}}),n("section",{staticClass:"k-structure-form"},[n("k-form",{ref:"form",staticClass:"k-structure-form-fields",attrs:{fields:t.formFields},on:{input:t.onInput,submit:t.submit},model:{value:t.currentModel,callback:function(e){t.currentModel=e},expression:"currentModel"}}),n("footer",{staticClass:"k-structure-form-buttons"},[n("k-button",{staticClass:"k-structure-form-cancel-button",attrs:{icon:"cancel"},on:{click:t.close}},[t._v(t._s(t.$t("cancel")))]),"new"!==t.currentIndex?n("k-pagination",{attrs:{dropdown:!1,total:t.items.length,limit:1,page:t.currentIndex+1,details:!0,validate:t.beforePaginate},on:{paginate:t.paginate}}):t._e(),n("k-button",{staticClass:"k-structure-form-submit-button",attrs:{icon:"check"},on:{click:t.submit}},[t._v(t._s(t.$t("new"!==t.currentIndex?"confirm":"add")))])],1)],1)]:0===t.items.length?n("k-empty",{attrs:{"data-invalid":t.isInvalid,icon:"list-bullet"},on:{click:t.add}},[t._v("\n "+t._s(t.empty||t.$t("field.structure.empty"))+"\n ")]):[n("table",{staticClass:"k-structure-table",attrs:{"data-invalid":t.isInvalid,"data-sortable":t.isSortable}},[n("thead",[n("tr",[n("th",{staticClass:"k-structure-table-index"},[t._v("#")]),t._l(t.columns,(function(e,i){return n("th",{key:i+"-header",staticClass:"k-structure-table-column",style:"width:"+t.width(e.width),attrs:{"data-align":e.align}},[t._v("\n "+t._s(e.label)+"\n ")])})),n("th")],2)]),n("k-draggable",{attrs:{list:t.items,"data-disabled":t.disabled,options:t.dragOptions,handle:!0,dir:t.direction,element:"tbody"},on:{end:t.onInput}},t._l(t.paginatedItems,(function(e,i){return n("tr",{key:i,on:{click:function(t){t.stopPropagation()}}},[n("td",{staticClass:"k-structure-table-index"},[t.isSortable?n("k-sort-handle"):t._e(),n("span",{staticClass:"k-structure-table-index-number"},[t._v(t._s(t.indexOf(i)))])],1),t._l(t.columns,(function(r,s){return n("td",{key:s,staticClass:"k-structure-table-column",style:"width:"+t.width(r.width),attrs:{title:r.label,"data-align":r.align},on:{click:function(e){return t.jump(i,s)}}},[!1===t.columnIsEmpty(e[s])?[t.previewExists(r.type)?n("k-"+r.type+"-field-preview",{tag:"component",attrs:{value:e[s],column:r,field:t.fields[s]},on:{input:function(e){return t.update(i,s,e)}}}):[n("p",{staticClass:"k-structure-table-text"},[t._v("\n "+t._s(r.before)+" "+t._s(t.displayText(t.fields[s],e[s])||"–")+" "+t._s(r.after)+"\n ")])]]:t._e()],2)})),n("td",{staticClass:"k-structure-table-options"},[t.duplicate&&t.more&&null===t.currentIndex?[n("k-button",{key:i,ref:"actionsToggle",refInFor:!0,staticClass:"k-structure-table-options-button",attrs:{icon:"dots"},on:{click:function(e){t.$refs[i+"-actions"][0].toggle()}}}),n("k-dropdown-content",{ref:i+"-actions",refInFor:!0,attrs:{align:"right"}},[n("k-dropdown-item",{attrs:{icon:"copy"},on:{click:function(e){return t.duplicateItem(i)}}},[t._v(t._s(t.$t("duplicate")))]),n("k-dropdown-item",{attrs:{icon:"remove"},on:{click:function(e){return t.confirmRemove(i)}}},[t._v(t._s(t.$t("remove")))])],1)]:[n("k-button",{staticClass:"k-structure-table-options-button",attrs:{tooltip:t.$t("remove"),icon:"remove"},on:{click:function(e){return t.confirmRemove(i)}}})]],2)],2)})),0)],1),t.limit?n("k-pagination",t._b({on:{paginate:t.paginateItems}},"k-pagination",t.pagination,!1)):t._e(),t.disabled?t._e():n("k-dialog",{ref:"remove",attrs:{"submit-button":t.$t("delete"),theme:"negative"},on:{submit:t.remove}},[n("k-text",[t._v(t._s(t.$t("field.structure.delete.confirm")))])],1)]],2)},ou=[],uu={inheritAttrs:!1,props:Object(I["a"])(Object(I["a"])({},pr.props),{},{columns:Object,duplicate:{type:Boolean,default:!0},empty:String,fields:Object,limit:Number,max:Number,min:Number,prepend:{type:Boolean,default:!1},sortable:{type:Boolean,default:!0},sortBy:String,value:{type:Array,default:function(){return[]}}}),data:function(){return{items:this.makeItems(this.value),currentIndex:null,currentModel:null,trash:null,page:1}},computed:{direction:function(){return is(this)},dragOptions:function(){return{disabled:!this.isSortable,fallbackClass:"k-sortable-row-fallback"}},formFields:function(){var t=this,e={};return Object.keys(this.fields).forEach((function(n){var i=t.fields[n];i.section=t.name,i.endpoints={field:t.endpoints.field+"+"+n,section:t.endpoints.section,model:t.endpoints.model},e[n]=i})),e},more:function(){return!0!==this.disabled&&!(this.max&&this.items.length>=this.max)},isInvalid:function(){return!0!==this.disabled&&(!!(this.min&&this.items.lengththis.max))},isSortable:function(){return!this.sortBy&&(!this.limit&&(!0!==this.disabled&&(!(this.items.length<=1)&&!1!==this.sortable)))},pagination:function(){var t=0;return this.limit&&(t=(this.page-1)*this.limit),{page:this.page,offset:t,limit:this.limit,total:this.items.length,align:"center",details:!0}},paginatedItems:function(){return this.limit?this.items.slice(this.pagination.offset,this.pagination.offset+this.limit):this.items}},watch:{value:function(t){t!=this.items&&(this.items=this.makeItems(t))}},methods:{add:function(){var t=this;if(!0===this.disabled)return!1;if(null!==this.currentIndex)return this.escape(),!1;var e={};Object.keys(this.fields).forEach((function(n){var i=t.fields[n];null!==i.default?e[n]=t.$helper.clone(i.default):e[n]=null})),this.currentIndex="new",this.currentModel=e,this.createForm()},addItem:function(t){!0===this.prepend?this.items.unshift(t):this.items.push(t)},beforePaginate:function(){return this.save(this.currentModel)},close:function(){this.currentIndex=null,this.currentModel=null,this.$events.$off("keydown.esc",this.escape),this.$events.$off("keydown.cmd.s",this.submit),this.$store.dispatch("content/enable")},columnIsEmpty:function(t){return void 0===t||null===t||""===t||("object"===Object(fe["a"])(t)&&0===Object.keys(t).length&&t.constructor===Object||void 0!==t.length&&0===t.length)},confirmRemove:function(t){this.close(),this.trash=t,this.$refs.remove.open()},createForm:function(t){var e=this;this.$events.$on("keydown.esc",this.escape),this.$events.$on("keydown.cmd.s",this.submit),this.$store.dispatch("content/disable"),this.$nextTick((function(){e.$refs.form&&e.$refs.form.focus(t)}))},displayText:function(t,e){switch(t.type){case"user":return e.email;case"date":var n=this.$library.dayjs(e),i=!0===t.time?"YYYY-MM-DD HH:mm":"YYYY-MM-DD";return n.isValid()?n.format(i):"";case"tags":case"multiselect":return e.map((function(t){return t.text})).join(", ");case"checkboxes":return e.map((function(e){var n=e;return t.options.forEach((function(t){t.value===e&&(n=t.text)})),n})).join(", ");case"radio":case"select":var r=t.options.filter((function(t){return t.value===e}))[0];return r?r.text:null}return"object"===Object(fe["a"])(e)&&null!==e?"…":e.toString()},duplicateItem:function(t){this.addItem(this.items[t]),this.onInput()},escape:function(){var t=this;if("new"===this.currentIndex){var e=Object.values(this.currentModel),n=!0;if(e.forEach((function(e){!1===t.columnIsEmpty(e)&&(n=!1)})),!0===n)return void this.close()}this.submit()},focus:function(){this.$refs.add&&this.$refs.add.focus&&this.$refs.add.focus()},indexOf:function(t){return this.limit?(this.page-1)*this.limit+t+1:t+1},isActive:function(t){return this.currentIndex===t},jump:function(t,e){this.open(t+this.pagination.offset,e)},makeItems:function(t){return!1===Array.isArray(t)?[]:this.sort(t)},onInput:function(){this.$emit("input",this.items)},open:function(t,e){this.currentIndex=t,this.currentModel=this.$helper.clone(this.items[t]),this.createForm(e)},paginate:function(t){this.open(t.offset)},paginateItems:function(t){this.page=t.page},previewExists:function(t){return this.$helper.isComponent("k-".concat(t,"-field-preview"))},remove:function(){if(null===this.trash)return!1;this.items.splice(this.trash,1),this.trash=null,this.$refs.remove.close(),this.onInput(),0===this.paginatedItems.length&&this.page>1&&this.page--,this.items=this.sort(this.items)},sort:function(t){return this.sortBy?t.sortBy(this.sortBy):t},save:function(){var t=this;return null!==this.currentIndex&&void 0!==this.currentIndex?this.validate(this.currentModel).then((function(){return"new"===t.currentIndex?t.addItem(t.currentModel):t.items[t.currentIndex]=t.currentModel,t.items=t.sort(t.items),t.onInput(),!0})).catch((function(e){throw t.$store.dispatch("notification/error",{message:t.$t("error.form.incomplete"),details:e}),e})):Promise.resolve()},submit:function(){this.save().then(this.close).catch((function(){}))},validate:function(t){return this.$api.post(this.endpoints.field+"/validate",t).then((function(t){if(t.length>0)throw t;return!0}))},width:function(t){if(!t)return"auto";var e=t.toString().split("/");if(2!==e.length)return"auto";var n=Number(e[0]),i=Number(e[1]);return parseFloat(100/i*n,2).toFixed(2)+"%"},update:function(t,e,n){this.items[t][e]=n,this.onInput()}}},lu=uu,cu=(n("088c"),Object(y["a"])(lu,au,ou,!1,null,null,null)),pu=cu.exports,du=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-tags-field",attrs:{input:t._uid,counter:t.counterOptions}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"tags"}},"k-input",t.$props,!1),t.$listeners))],1)},fu=[],hu={inheritAttrs:!1,props:Object(I["a"])(Object(I["a"])(Object(I["a"])(Object(I["a"])({},pr.props),wr.props),Zs.props),{},{counter:{type:Boolean,default:!0}}),computed:{counterOptions:function(){return null!==this.value&&!this.disabled&&!1!==this.counter&&{count:this.value&&Array.isArray(this.value)?this.value.length:0,min:this.min,max:this.max}}},methods:{focus:function(){this.$refs.input.focus()}}},mu=hu,gu=Object(y["a"])(mu,du,fu,!1,null,null,null),bu=gu.exports,vu=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-tel-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"tel"}},"k-input",t.$props,!1),t.$listeners))],1)},ku=[],$u={inheritAttrs:!1,props:Object(I["a"])(Object(I["a"])(Object(I["a"])(Object(I["a"])({},pr.props),wr.props),ea.props),{},{icon:{type:String,default:"phone"}}),methods:{focus:function(){this.$refs.input.focus()}}},yu=$u,_u=Object(y["a"])(yu,vu,ku,!1,null,null,null),wu=_u.exports,xu=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-text-field",attrs:{input:t._uid,counter:t.counterOptions}},"k-field",t.$props,!1),[t._t("options",null,{slot:"options"}),n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field"}},"k-input",t.$props,!1),t.$listeners))],2)},Ou=[],ju={inheritAttrs:!1,props:Object(I["a"])(Object(I["a"])(Object(I["a"])(Object(I["a"])({},pr.props),wr.props),os.props),{},{counter:{type:Boolean,default:!0}}),computed:{counterOptions:function(){return null!==this.value&&!this.disabled&&!1!==this.counter&&{count:this.value?String(this.value).length:0,min:this.minlength,max:this.maxlength}}},methods:{focus:function(){this.$refs.input.focus()}}},Su=ju,Cu=(n("b746"),Object(y["a"])(Su,xu,Ou,!1,null,null,null)),Eu=Cu.exports,Ru=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-textarea-field",attrs:{input:t._uid,counter:t.counterOptions}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,type:"textarea",theme:"field"}},"k-input",t.$props,!1),t.$listeners))],1)},Tu=[],Iu={inheritAttrs:!1,props:Object(I["a"])(Object(I["a"])(Object(I["a"])(Object(I["a"])({},pr.props),wr.props),oa.props),{},{counter:{type:Boolean,default:!0}}),computed:{counterOptions:function(){return null!==this.value&&!this.disabled&&!1!==this.counter&&{count:this.value?this.value.length:0,min:this.minlength,max:this.maxlength}}},methods:{focus:function(){this.$refs.input.focus()}}},Lu=Iu,Au=Object(y["a"])(Lu,Ru,Tu,!1,null,null,null),Bu=Au.exports,qu=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-time-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"time"}},"k-input",t.$props,!1),t.$listeners))],1)},Nu=[],Pu={inheritAttrs:!1,props:Object(I["a"])(Object(I["a"])(Object(I["a"])(Object(I["a"])({},pr.props),wr.props),fa.props),{},{icon:{type:String,default:"clock"}}),methods:{focus:function(){this.$refs.input.focus()}}},Du=Pu,Mu=Object(y["a"])(Du,qu,Nu,!1,null,null,null),Fu=Mu.exports,Uu=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-toggle-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"toggle"}},"k-input",t.$props,!1),t.$listeners))],1)},zu=[],Hu={inheritAttrs:!1,props:Object(I["a"])(Object(I["a"])(Object(I["a"])({},pr.props),wr.props),ka.props),methods:{focus:function(){this.$refs.input.focus()}}},Ku=Hu,Vu=Object(y["a"])(Ku,Uu,zu,!1,null,null,null),Yu=Vu.exports,Wu=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-url-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"url"}},"k-input",t.$props,!1),t.$listeners),[t.link?n("k-button",{staticClass:"k-input-icon-button",attrs:{slot:"icon",icon:t.icon,link:t.value,tooltip:t.$t("open"),tabindex:"-1",target:"_blank"},slot:"icon"}):t._e()],1)],1)},Ju=[],Gu={inheritAttrs:!1,props:Object(I["a"])(Object(I["a"])(Object(I["a"])(Object(I["a"])({},pr.props),wr.props),wa.props),{},{link:{type:Boolean,default:!0},icon:{type:String,default:"url"}}),methods:{focus:function(){this.$refs.input.focus()}}},Zu=Gu,Xu=Object(y["a"])(Zu,Wu,Ju,!1,null,null,null),Qu=Xu.exports,tl=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-users-field"},"k-field",t.$props,!1),[n("k-button-group",{staticClass:"k-field-options",attrs:{slot:"options"},slot:"options"},[t.more&&!t.disabled?n("k-button",{staticClass:"k-field-options-button",attrs:{icon:t.btnIcon},on:{click:t.open}},[t._v("\n "+t._s(t.btnLabel)+"\n ")]):t._e()],1),t.selected.length?[n("k-draggable",{attrs:{element:t.elements.list,list:t.selected,handle:!0,"data-invalid":t.isInvalid},on:{end:t.onInput}},t._l(t.selected,(function(e,i){return n(t.elements.item,{key:e.email,tag:"component",attrs:{sortable:!t.disabled&&t.selected.length>1,text:e.text,info:e.info,link:t.link?t.$api.users.link(e.id):null,image:e.image,icon:e.icon}},[t.disabled?t._e():n("k-button",{attrs:{slot:"options",icon:"remove"},on:{click:function(e){return t.remove(i)}},slot:"options"})],1)})),1)]:n("k-empty",{attrs:{"data-invalid":t.isInvalid,icon:"users"},on:{click:t.open}},[t._v("\n "+t._s(t.empty||t.$t("field.users.empty"))+"\n ")]),n("k-users-dialog",{ref:"selector",on:{submit:t.select}})],2)},el=[],nl={mixins:[Ha],methods:{open:function(){if(this.disabled)return!1;this.$refs.selector.open({endpoint:this.endpoints.field,max:this.max,multiple:this.multiple,search:this.search,selected:this.selected.map((function(t){return t.id}))})}}},il=nl,rl=(n("7f6e"),Object(y["a"])(il,tl,el,!1,null,null,null)),sl=rl.exports,al=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("nav",{staticClass:"k-toolbar"},[n("div",{staticClass:"k-toolbar-wrapper"},[n("div",{staticClass:"k-toolbar-buttons"},[t._l(t.layout,(function(e,i){return[e.divider?[n("span",{key:i,staticClass:"k-toolbar-divider"})]:e.dropdown?[n("k-dropdown",{key:i},[n("k-button",{key:i,staticClass:"k-toolbar-button",attrs:{icon:e.icon,tooltip:e.label,tabindex:"-1"},on:{click:function(e){t.$refs[i+"-dropdown"][0].toggle()}}}),n("k-dropdown-content",{ref:i+"-dropdown",refInFor:!0},t._l(e.dropdown,(function(e,i){return n("k-dropdown-item",{key:i,attrs:{icon:e.icon},on:{click:function(n){return t.command(e.command,e.args)}}},[t._v("\n "+t._s(e.label)+"\n ")])})),1)],1)]:[n("k-button",{key:i,staticClass:"k-toolbar-button",attrs:{icon:e.icon,tooltip:e.label,tabindex:"-1"},on:{click:function(n){return t.command(e.command,e.args)}}})]]}))],2)])])},ol=[],ul=function(t){this.command("insert",(function(e,n){var i=[];return n.split("\n").forEach((function(e,n){var r="ol"===t?n+1+".":"-";i.push(r+" "+e)})),i.join("\n")}))},ll={layout:["headlines","bold","italic","|","link","email","file","|","code","ul","ol"],props:{buttons:{type:[Boolean,Array],default:!0},uploads:[Boolean,Object,Array]},data:function(){var t={},e={},n=[],i=this.commands();return!1===this.buttons?t:(Array.isArray(this.buttons)&&(n=this.buttons),!0!==Array.isArray(this.buttons)&&(n=this.$options.layout),n.forEach((function(n,r){if("|"===n)t["divider-"+r]={divider:!0};else if(i[n]){var s=i[n];t[n]=s,s.shortcut&&(e[s.shortcut]=n)}})),{layout:t,shortcuts:e})},methods:{command:function(t,e){"function"===typeof t?t.apply(this):this.$emit("command",t,e)},close:function(){var t=this;Object.keys(this.$refs).forEach((function(e){var n=t.$refs[e][0];n.close&&"function"===typeof n.close&&n.close()}))},fileCommandSetup:function(){var t={label:this.$t("toolbar.button.file"),icon:"attachment"};return!1===this.uploads?t.command="selectFile":t.dropdown={select:{label:this.$t("toolbar.button.file.select"),icon:"check",command:"selectFile"},upload:{label:this.$t("toolbar.button.file.upload"),icon:"upload",command:"uploadFile"}},t},commands:function(){return{headlines:{label:this.$t("toolbar.button.headings"),icon:"title",dropdown:{h1:{label:this.$t("toolbar.button.heading.1"),icon:"title",command:"prepend",args:"#"},h2:{label:this.$t("toolbar.button.heading.2"),icon:"title",command:"prepend",args:"##"},h3:{label:this.$t("toolbar.button.heading.3"),icon:"title",command:"prepend",args:"###"}}},bold:{label:this.$t("toolbar.button.bold"),icon:"bold",command:"wrap",args:"**",shortcut:"b"},italic:{label:this.$t("toolbar.button.italic"),icon:"italic",command:"wrap",args:"*",shortcut:"i"},link:{label:this.$t("toolbar.button.link"),icon:"url",shortcut:"k",command:"dialog",args:"link"},email:{label:this.$t("toolbar.button.email"),icon:"email",shortcut:"e",command:"dialog",args:"email"},file:this.fileCommandSetup(),code:{label:this.$t("toolbar.button.code"),icon:"code",command:"wrap",args:"`"},ul:{label:this.$t("toolbar.button.ul"),icon:"list-bullet",command:function(){return ul.apply(this,["ul"])}},ol:{label:this.$t("toolbar.button.ol"),icon:"list-numbers",command:function(){return ul.apply(this,["ol"])}}}},shortcut:function(t,e){if(this.shortcuts[t]){var n=this.layout[this.shortcuts[t]];if(!n)return!1;e.preventDefault(),this.command(n.command,n.args)}}}},cl=ll,pl=(n("df0d"),Object(y["a"])(cl,al,ol,!1,null,null,null)),dl=pl.exports,fl=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{"submit-button":t.$t("insert")},on:{close:t.cancel,submit:function(e){return t.$refs.form.submit()}}},[n("k-form",{ref:"form",attrs:{fields:t.fields},on:{submit:t.submit},model:{value:t.value,callback:function(e){t.value=e},expression:"value"}})],1)},hl=[],ml={data:function(){return{value:{email:null,text:null},fields:{email:{label:this.$t("email"),type:"email"},text:{label:this.$t("link.text"),type:"text"}}}},computed:{kirbytext:function(){return this.$store.state.system.info.kirbytext}},methods:{open:function(t,e){this.value.text=e,this.$refs.dialog.open()},cancel:function(){this.$emit("cancel")},createKirbytext:function(){var t=this.value.email||"";return this.value.text&&this.value.text.length>0?"(email: ".concat(t," text: ").concat(this.value.text,")"):"(email: ".concat(t,")")},createMarkdown:function(){var t=this.value.email||"";return this.value.text&&this.value.text.length>0?"[".concat(this.value.text,"](mailto:").concat(t,")"):"<".concat(t,">")},submit:function(){this.$emit("submit",this.kirbytext?this.createKirbytext():this.createMarkdown()),this.value={email:null,text:null},this.$refs.dialog.close()}}},gl=ml,bl=Object(y["a"])(gl,fl,hl,!1,null,null,null),vl=bl.exports,kl=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{"submit-button":t.$t("insert")},on:{close:t.cancel,submit:function(e){return t.$refs.form.submit()}}},[n("k-form",{ref:"form",attrs:{fields:t.fields},on:{submit:t.submit},model:{value:t.value,callback:function(e){t.value=e},expression:"value"}})],1)},$l=[],yl={data:function(){return{value:{url:null,text:null},fields:{url:{label:this.$t("link"),type:"text",placeholder:this.$t("url.placeholder"),icon:"url"},text:{label:this.$t("link.text"),type:"text"}}}},computed:{kirbytext:function(){return this.$store.state.system.info.kirbytext}},methods:{open:function(t,e){this.value.text=e,this.$refs.dialog.open()},cancel:function(){this.$emit("cancel")},createKirbytext:function(){return this.value.text.length>0?"(link: ".concat(this.value.url," text: ").concat(this.value.text,")"):"(link: ".concat(this.value.url,")")},createMarkdown:function(){return this.value.text.length>0?"[".concat(this.value.text,"](").concat(this.value.url,")"):"<".concat(this.value.url,">")},submit:function(){this.$emit("submit",this.kirbytext?this.createKirbytext():this.createMarkdown()),this.value={url:null,text:null},this.$refs.dialog.close()}}},_l=yl,wl=Object(y["a"])(_l,kl,$l,!1,null,null,null),xl=wl.exports,Ol=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.value?n("ul",{staticClass:"k-files-field-preview"},t._l(t.value,(function(e){return n("li",{key:e.url},[n("k-link",{attrs:{title:e.filename,to:e.link},nativeOn:{click:function(t){t.stopPropagation()}}},["image"===e.type?n("k-image",t._b({},"k-image",t.imageOptions(e),!1)):n("k-icon",t._b({},"k-icon",e.icon,!1))],1)],1)})),0):t._e()},jl=[],Sl=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"list",n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"1/1";if(!t||0===t.length)return!1;var i=null,r=null;if(t.list?(i=t[e].url,r=t[e].srcset):(i=t.url,r=t.srcset),!i)return!1;var s={src:i,srcset:r,back:t.back||"black",cover:t.cover};return"cards"===e&&(s.ratio=t.ratio||"3/2",s.sizes=Cl(n)),s};function Cl(t){switch(t){case"1/2":case"2/4":return"(min-width: 30em) and (max-width: 65em) 59em, (min-width: 65em) 44em, 27em";case"1/3":return"(min-width: 30em) and (max-width: 65em) 59em, (min-width: 65em) 29.333em, 27em";case"1/4":return"(min-width: 30em) and (max-width: 65em) 59em, (min-width: 65em) 22em, 27em";case"2/3":return"(min-width: 30em) and (max-width: 65em) 59em, (min-width: 65em) 27em, 27em";case"3/4":return"(min-width: 30em) and (max-width: 65em) 59em, (min-width: 65em) 66em, 27em";default:return"(min-width: 30em) and (max-width: 65em) 59em, (min-width: 65em) 88em, 27em"}}var El,Rl,Tl,Il,Ll={props:{value:Array,field:Object},methods:{imageOptions:function(t){var e=Sl(t.image);return e.src?Object(I["a"])(Object(I["a"])({},e),{},{back:"pattern",cover:!1},this.field.image||{}):{src:t.url}}}},Al=Ll,Bl=(n("21dc"),Object(y["a"])(Al,Ol,jl,!1,null,null,null)),ql=Bl.exports,Nl=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("p",{staticClass:"k-url-field-preview"},[t._v("\n "+t._s(t.column.before)+"\n "),n("k-link",{attrs:{to:t.link,target:"_blank"},nativeOn:{click:function(t){t.stopPropagation()}}},[t._v(t._s(t.value))]),t._v("\n "+t._s(t.column.after)+"\n")],1)},Pl=[],Dl={props:{column:{type:Object,default:function(){return{}}},value:String},computed:{link:function(){return this.value}}},Ml=Dl,Fl=(n("977f"),Object(y["a"])(Ml,Nl,Pl,!1,null,null,null)),Ul=Fl.exports,zl={extends:Ul,computed:{link:function(){return this.value&&this.value.length>0?"mailto:"+this.value:null}}},Hl=zl,Kl=Object(y["a"])(Hl,El,Rl,!1,null,null,null),Vl=Kl.exports,Yl=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.value?n("ul",{staticClass:"k-pages-field-preview"},t._l(t.value,(function(e){return n("li",{key:e.id},[n("figure",[n("k-link",{attrs:{title:e.id,to:t.$api.pages.link(e.id)},nativeOn:{click:function(t){t.stopPropagation()}}},[n("k-icon",{staticClass:"k-pages-field-preview-image",attrs:{type:"page",back:"pattern"}}),n("figcaption",[t._v("\n "+t._s(e.text)+"\n ")])],1)],1)])})),0):t._e()},Wl=[],Jl={props:{value:Array}},Gl=Jl,Zl=(n("d0c1"),Object(y["a"])(Gl,Yl,Wl,!1,null,null,null)),Xl=Zl.exports,Ql=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-input",{staticClass:"k-toggle-field-preview",attrs:{text:t.text,type:"toggle"},on:{input:function(e){return t.$emit("input",e)}},model:{value:t.value,callback:function(e){t.value=e},expression:"value"}})},tc=[],ec={props:{field:Object,value:Boolean,column:Object},computed:{text:function(){return!1!==this.column.text?this.field.text:null}}},nc=ec,ic=(n("1c4e"),Object(y["a"])(nc,Ql,tc,!1,null,null,null)),rc=ic.exports,sc=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.value?n("ul",{staticClass:"k-users-field-preview"},t._l(t.value,(function(e){return n("li",{key:e.email},[n("figure",[n("k-link",{attrs:{title:e.email,to:t.$api.users.link(e.id)},nativeOn:{click:function(t){t.stopPropagation()}}},[e.avatar?n("k-image",{staticClass:"k-users-field-preview-avatar",attrs:{src:e.avatar.url,back:"pattern"}}):n("k-icon",{staticClass:"k-users-field-preview-avatar",attrs:{type:"user",back:"pattern"}}),n("figcaption",[t._v("\n "+t._s(e.username)+"\n ")])],1)],1)])})),0):t._e()},ac=[],oc={props:{value:Array}},uc=oc,lc=(n("3a85"),Object(y["a"])(uc,sc,ac,!1,null,null,null)),cc=lc.exports,pc=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-bar"},[t.$slots.left?n("div",{staticClass:"k-bar-slot",attrs:{"data-position":"left"}},[t._t("left")],2):t._e(),t.$slots.center?n("div",{staticClass:"k-bar-slot",attrs:{"data-position":"center"}},[t._t("center")],2):t._e(),t.$slots.right?n("div",{staticClass:"k-bar-slot",attrs:{"data-position":"right"}},[t._t("right")],2):t._e()])},dc=[],fc=(n("6f7b"),{}),hc=Object(y["a"])(fc,pc,dc,!1,null,null,null),mc=hc.exports,gc=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",t._g({staticClass:"k-box",attrs:{"data-theme":t.theme}},t.$listeners),[t._t("default",[n("k-text",{domProps:{innerHTML:t._s(t.text)}})])],2)},bc=[],vc={props:{theme:String,text:String}},kc=vc,$c=(n("7dc7"),Object(y["a"])(kc,gc,bc,!1,null,null,null)),yc=$c.exports,_c=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("figure",t._g({staticClass:"k-card"},t.$listeners),[t.sortable?n("k-sort-handle"):t._e(),n(t.wrapper,{tag:"component",attrs:{to:t.link,target:t.target}},[t.imageOptions?n("k-image",t._b({staticClass:"k-card-image"},"k-image",t.imageOptions,!1)):n("span",{staticClass:"k-card-icon",style:"padding-bottom:"+t.ratioPadding},[n("k-icon",t._b({},"k-icon",t.icon,!1))],1),n("figcaption",{staticClass:"k-card-content"},[n("span",{staticClass:"k-card-text",attrs:{"data-noinfo":!t.info}},[t._v(t._s(t.text))]),t.info?n("span",{staticClass:"k-card-info",domProps:{innerHTML:t._s(t.info)}}):t._e()])],1),n("nav",{staticClass:"k-card-options"},[t.flag?n("k-button",t._b({staticClass:"k-card-options-button",on:{click:t.flag.click}},"k-button",t.flag,!1)):t._e(),t._t("options",[t.options?n("k-button",{staticClass:"k-card-options-button",attrs:{tooltip:t.$t("options"),icon:"dots"},on:{click:function(e){return e.stopPropagation(),t.$refs.dropdown.toggle()}}}):t._e(),n("k-dropdown-content",{ref:"dropdown",staticClass:"k-card-options-dropdown",attrs:{options:t.options,align:"right"},on:{action:function(e){return t.$emit("action",e)}}})])],2)],1)},wc=[],xc={inheritAttrs:!1,props:{column:String,flag:Object,icon:{type:Object,default:function(){return{type:"file",back:"black"}}},image:[Object,Boolean],info:String,link:[String,Function],options:[Array,Function],sortable:Boolean,target:String,text:String},computed:{wrapper:function(){return this.link?"k-link":"div"},ratioPadding:function(){return this.icon&&this.icon.ratio?this.$helper.ratio(this.icon.ratio):this.$helper.ratio("3/2")},imageOptions:function(){return Sl(this.image,"cards",this.column)}}},Oc=xc,jc=(n("c119"),Object(y["a"])(Oc,_c,wc,!1,null,null,null)),Sc=jc.exports,Cc=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-cards"},[t._t("default",t._l(t.cards,(function(e,i){return n("k-card",t._g(t._b({key:i},"k-card",e,!1),t.$listeners))})))],2)},Ec=[],Rc={props:{cards:Array}},Tc=Rc,Ic=(n("f56d"),Object(y["a"])(Tc,Cc,Ec,!1,null,null,null)),Lc=Ic.exports,Ac=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-collection",attrs:{"data-layout":t.layout}},[n("k-draggable",{attrs:{list:t.items,options:t.dragOptions,element:t.elements.list,"data-size":t.size,handle:!0},on:{change:function(e){return t.$emit("change",e)},end:t.onEnd}},t._l(t.items,(function(e,i){return n(t.elements.item,t._b({key:i,tag:"component",class:{"k-draggable-item":e.sortable},on:{action:function(n){return t.$emit("action",e,n)},dragstart:function(n){return t.onDragStart(n,e.dragText)}}},"component",e,!1))})),1),t.hasFooter?n("footer",{staticClass:"k-collection-footer"},[t.help?n("k-text",{staticClass:"k-collection-help",attrs:{theme:"help"},domProps:{innerHTML:t._s(t.help)}}):t._e(),n("div",{staticClass:"k-collection-pagination"},[t.hasPagination?n("k-pagination",t._b({on:{paginate:function(e){return t.$emit("paginate",e)}}},"k-pagination",t.paginationOptions,!1)):t._e()],1)],1):t._e()],1)},Bc=[],qc={props:{help:String,items:{type:[Array,Object],default:function(){return[]}},layout:{type:String,default:"list"},size:String,sortable:Boolean,pagination:{type:[Boolean,Object],default:function(){return!1}}},computed:{hasPagination:function(){return!1!==this.pagination&&(!0!==this.paginationOptions.hide&&!(this.pagination.total<=this.pagination.limit))},hasFooter:function(){return!(!this.hasPagination&&!this.help)},dragOptions:function(){return{sort:this.sortable,disabled:!1===this.sortable,draggable:".k-draggable-item"}},elements:function(){var t={cards:{list:"k-cards",item:"k-card"},list:{list:"k-list",item:"k-list-item"}};return t[this.layout]?t[this.layout]:t["list"]},paginationOptions:function(){var t="object"!==Object(fe["a"])(this.pagination)?{}:this.pagination;return Object(I["a"])({limit:10,details:!0,keys:!1,total:0,hide:!1},t)}},watch:{$props:function(){this.$forceUpdate()}},over:null,methods:{onEnd:function(){this.over&&this.over.removeAttribute("data-over"),this.$emit("sort",this.items)},onDragStart:function(t,e){this.$store.dispatch("drag",{type:"text",data:e})}}},Nc=qc,Pc=(n("8c28"),Object(y["a"])(Nc,Ac,Bc,!1,null,null,null)),Dc=Pc.exports,Mc=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-column",attrs:{"data-width":t.width,"data-sticky":t.sticky}},[n("div",[t._t("default")],2)])},Fc=[],Uc={props:{width:String,sticky:Boolean}},zc=Uc,Hc=(n("c9cb"),Object(y["a"])(zc,Mc,Fc,!1,null,null,null)),Kc=Hc.exports,Vc=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-dropzone",attrs:{"data-dragging":t.dragging,"data-over":t.over},on:{dragenter:t.onEnter,dragleave:t.onLeave,dragover:t.onOver,drop:t.onDrop}},[t._t("default")],2)},Yc=[],Wc={props:{label:{type:String,default:"Drop to upload"},disabled:{type:Boolean,default:!1}},data:function(){return{files:[],dragging:!1,over:!1}},methods:{cancel:function(){this.reset()},reset:function(){this.dragging=!1,this.over=!1},onDrop:function(t){return!0===this.disabled?this.reset():!1===this.$helper.isUploadEvent(t)?this.reset():(this.$events.$emit("dropzone.drop"),this.files=t.dataTransfer.files,this.$emit("drop",this.files),void this.reset())},onEnter:function(t){!1===this.disabled&&this.$helper.isUploadEvent(t)&&(this.dragging=!0)},onLeave:function(){this.reset()},onOver:function(t){!1===this.disabled&&this.$helper.isUploadEvent(t)&&(t.dataTransfer.dropEffect="copy",this.over=!0)}}},Jc=Wc,Gc=(n("414d"),Object(y["a"])(Jc,Vc,Yc,!1,null,null,null)),Zc=Gc.exports,Xc=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",t._g({staticClass:"k-empty",attrs:{"data-layout":t.layout}},t.$listeners),[t.icon?n("k-icon",{attrs:{type:t.icon}}):t._e(),n("p",[t._t("default")],2)],1)},Qc=[],tp={props:{text:String,icon:String,layout:{type:String,default:"list"}}},ep=tp,np=(n("ba8f"),Object(y["a"])(ep,Xc,Qc,!1,null,null,null)),ip=np.exports,rp=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-file-preview"},[n("k-view",{staticClass:"k-file-preview-layout"},[n("div",{staticClass:"k-file-preview-image"},[n("k-link",{staticClass:"k-file-preview-image-link",attrs:{to:t.file.url,title:t.$t("open"),target:"_blank"}},[t.file.panelImage&&t.file.panelImage.cards&&t.file.panelImage.cards.url?n("k-image",{attrs:{src:t.file.panelImage.cards.url,srcset:t.file.panelImage.cards.srcset,back:"none"}}):t.file.panelIcon?n("k-icon",{staticClass:"k-file-preview-icon",style:{color:t.file.panelIcon.color},attrs:{type:t.file.panelIcon.type}}):n("span",{staticClass:"k-file-preview-placeholder"})],1)],1),n("div",{staticClass:"k-file-preview-details"},[n("ul",[n("li",[n("h3",[t._v(t._s(t.$t("template")))]),n("p",[t._v(t._s(t.file.template||"—"))])]),n("li",[n("h3",[t._v(t._s(t.$t("mime")))]),n("p",[t._v(t._s(t.file.mime))])]),n("li",[n("h3",[t._v(t._s(t.$t("url")))]),n("p",[n("k-link",{attrs:{to:t.file.url,tabindex:"-1",target:"_blank"}},[t._v("/"+t._s(t.file.id))])],1)]),n("li",[n("h3",[t._v(t._s(t.$t("size")))]),n("p",[t._v(t._s(t.file.niceSize))])]),n("li",[n("h3",[t._v(t._s(t.$t("dimensions")))]),t.file.dimensions&&(t.file.dimensions.width||t.file.dimensions.height)?n("p",[t._v("\n "+t._s(t.file.dimensions.width)+"×"+t._s(t.file.dimensions.height)+" "+t._s(t.$t("pixel"))+"\n ")]):n("p",[t._v("—")])]),n("li",[n("h3",[t._v(t._s(t.$t("orientation")))]),t.file.dimensions&&t.file.dimensions.orientation?n("p",[t._v("\n "+t._s(t.$t("orientation."+t.file.dimensions.orientation))+"\n ")]):n("p",[t._v("—")])])])])])],1)},sp=[],ap={props:{file:Object}},op=ap,up=(n("696b"),Object(y["a"])(op,rp,sp,!1,null,null,null)),lp=up.exports,cp=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-grid",attrs:{"data-gutter":t.gutter}},[t._t("default")],2)},pp=[],dp={props:{gutter:String}},fp=dp,hp=(n("5b23"),Object(y["a"])(fp,cp,pp,!1,null,null,null)),mp=hp.exports,gp=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("header",{staticClass:"k-header",attrs:{"data-editable":t.editable}},[n("k-headline",{attrs:{tag:"h1",size:"huge"}},[t.editable&&t.$listeners.edit?n("span",{staticClass:"k-headline-editable",on:{click:function(e){return t.$emit("edit")}}},[t._t("default"),n("k-icon",{attrs:{type:"edit"}})],2):t._t("default")],2),t.$slots.left||t.$slots.right?n("k-bar",{staticClass:"k-header-buttons"},[t._t("left",null,{slot:"left"}),t._t("right",null,{slot:"right"})],2):t._e(),t.tabs&&t.tabs.length>1?n("div",{staticClass:"k-header-tabs"},[n("nav",[t._l(t.visibleTabs,(function(e,i){return n("k-button",{key:t.$route.fullPath+"-tab-"+i,staticClass:"k-tab-button",attrs:{link:"#"+e.name,current:t.currentTab&&t.currentTab.name===e.name,icon:e.icon,tooltip:e.label}},[t._v("\n "+t._s(e.label)+"\n ")])})),t.invisibleTabs.length?n("k-button",{staticClass:"k-tab-button k-tabs-dropdown-button",attrs:{icon:"dots"},on:{click:function(e){return e.stopPropagation(),t.$refs.more.toggle()}}},[t._v("\n "+t._s(t.$t("more"))+"\n ")]):t._e()],2),t.invisibleTabs.length?n("k-dropdown-content",{ref:"more",staticClass:"k-tabs-dropdown",attrs:{align:"right"}},t._l(t.invisibleTabs,(function(e,i){return n("k-dropdown-item",{key:"more-"+i,attrs:{link:"#"+e.name,current:t.currentTab&&t.currentTab.name===e.name,icon:e.icon,tooltip:e.label}},[t._v("\n "+t._s(e.label)+"\n ")])})),1):t._e()],1):t._e()],1)},bp=[],vp={props:{editable:Boolean,tabs:Array,tab:Object},data:function(){return{size:null,currentTab:this.tab,visibleTabs:this.tabs,invisibleTabs:[]}},watch:{tab:function(){this.currentTab=this.tab},tabs:function(t){this.visibleTabs=t,this.invisibleTabs=[],this.resize(!0)}},created:function(){window.addEventListener("resize",this.resize)},destroyed:function(){window.removeEventListener("resize",this.resize)},methods:{resize:function(t){if(this.tabs&&!(this.tabs.length<=1)){if(this.tabs.length<=3)return this.visibleTabs=this.tabs,void(this.invisibleTabs=[]);if(window.innerWidth>=700){if("large"===this.size&&!t)return;this.visibleTabs=this.tabs,this.invisibleTabs=[],this.size="large"}else{if("small"===this.size&&!t)return;this.visibleTabs=this.tabs.slice(0,2),this.invisibleTabs=this.tabs.slice(2),this.size="small"}}}}},kp=vp,$p=(n("53c5"),Object(y["a"])(kp,gp,bp,!1,null,null,null)),yp=$p.exports,_p=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("ul",{staticClass:"k-list"},[t._t("default",t._l(t.items,(function(e,i){return n("k-list-item",t._g(t._b({key:i},"k-list-item",e,!1),t.$listeners))})))],2)},wp=[],xp={props:{items:Array}},Op=xp,jp=(n("c857"),Object(y["a"])(Op,_p,wp,!1,null,null,null)),Sp=jp.exports,Cp=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n(t.element,t._g({tag:"component",staticClass:"k-list-item"},t.$listeners),[t.sortable?n("k-sort-handle"):t._e(),n("k-link",{staticClass:"k-list-item-content",attrs:{to:t.link,target:t.target}},[t.image?n("span",{staticClass:"k-list-item-image"},[t.imageOptions?n("k-image",t._b({},"k-image",t.imageOptions,!1)):n("k-icon",t._b({},"k-icon",t.icon,!1))],1):t._e(),n("span",{staticClass:"k-list-item-text"},[n("em",[t._v(t._s(t.text))]),t.info?n("small",{domProps:{innerHTML:t._s(t.info)}}):t._e()])]),n("nav",{staticClass:"k-list-item-options"},[t._t("options",[t.flag?n("k-button",t._b({staticClass:"k-list-item-status",on:{click:t.flag.click}},"k-button",t.flag,!1)):t._e(),t.options?n("k-button",{staticClass:"k-list-item-toggle",attrs:{tooltip:t.$t("options"),icon:"dots",alt:"Options"},on:{click:function(e){return e.stopPropagation(),t.$refs.options.toggle()}}}):t._e(),n("k-dropdown-content",{ref:"options",attrs:{options:t.options,align:"right"},on:{action:function(e){return t.$emit("action",e)}}})])],2)],1)},Ep=[],Rp={inheritAttrs:!1,props:{element:{type:String,default:"li"},image:[Object,Boolean],icon:{type:Object,default:function(){return{type:"file",back:"black"}}},sortable:Boolean,text:String,target:String,info:String,link:[String,Function],flag:Object,options:[Array,Function]},computed:{imageOptions:function(){return Sl(this.image)}}},Tp=Rp,Ip=(n("fa6a"),Object(y["a"])(Tp,Cp,Ep,!1,null,null,null)),Lp=Ip.exports,Ap=function(){var t=this,e=t.$createElement,n=t._self._c||e;return 0===t.tabs.length?n("k-box",{attrs:{text:"This page has no blueprint setup yet",theme:"info"}}):t.tab?n("k-sections",{attrs:{parent:t.parent,blueprint:t.blueprint,columns:t.tab.columns},on:{submit:function(e){return t.$emit("submit",e)}}}):t._e()},Bp=[],qp={props:{parent:String,blueprint:String,tabs:Array},data:function(){return{tab:null}},watch:{$route:function(){this.open()},blueprint:function(){this.open()}},mounted:function(){this.open()},methods:{open:function(t){if(0!==this.tabs.length){t||(t=this.$route.hash.replace("#","")),t||(t=this.tabs[0].name);var e=null;this.tabs.forEach((function(n){n.name===t&&(e=n)})),e||(e=this.tabs[0]),this.tab=e,this.$emit("tab",this.tab)}}}},Np=qp,Pp=Object(y["a"])(Np,Ap,Bp,!1,null,null,null),Dp=Pp.exports,Mp=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-view",attrs:{"data-align":t.align}},[t._t("default")],2)},Fp=[],Up={props:{align:String}},zp=Up,Hp=(n("daa8"),Object(y["a"])(zp,Mp,Fp,!1,null,null,null)),Kp=Hp.exports,Vp=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("draggable",t._g(t._b({staticClass:"k-draggable",attrs:{tag:t.element,list:t.list,move:t.move}},"draggable",t.dragOptions,!1),t.listeners),[t._t("default"),t._t("footer",null,{slot:"footer"})],2)},Yp=[],Wp=n("310e"),Jp=n.n(Wp),Gp={components:{draggable:Jp.a},props:{element:String,handle:[String,Boolean],list:[Array,Object],move:Function,options:Object},data:function(){var t=this;return{listeners:Object(I["a"])(Object(I["a"])({},this.$listeners),{},{start:function(e){t.$store.dispatch("drag",{}),t.$listeners.start&&t.$listeners.start(e)},end:function(e){t.$store.dispatch("drag",null),t.$listeners.end&&t.$listeners.end(e)}})}},computed:{dragOptions:function(){var t=!1;return t=!0===this.handle?".k-sort-handle":this.handle,Object(I["a"])({fallbackClass:"k-sortable-fallback",fallbackOnBody:!0,forceFallback:!0,ghostClass:"k-sortable-ghost",handle:t,scroll:document.querySelector(".k-panel-view")},this.options)}}},Zp=Gp,Xp=Object(y["a"])(Zp,Vp,Yp,!1,null,null,null),Qp=Xp.exports,td={data:function(){return{error:null}},errorCaptured:function(t){return B.debug&&window.console.warn(t),this.error=t,!1},render:function(t){return this.error?this.$slots.error?this.$slots.error[0]:this.$scopedSlots.error?this.$scopedSlots.error({error:this.error}):t("k-box",{attrs:{theme:"negative"}},this.error.message||this.error):this.$slots.default[0]}},ed=td,nd=Object(y["a"])(ed,Tl,Il,!1,null,null,null),id=nd.exports,rd=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n(t.tag,t._g({tag:"component",staticClass:"k-headline",attrs:{"data-theme":t.theme,"data-size":t.size}},t.$listeners),[t.link?n("k-link",{attrs:{to:t.link}},[t._t("default")],2):t._t("default")],2)},sd=[],ad={props:{link:String,size:{type:String},tag:{type:String,default:"h2"},theme:{type:String}}},od=ad,ud=(n("f8a7"),Object(y["a"])(od,rd,sd,!1,null,null,null)),ld=ud.exports,cd=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",{class:"k-icon k-icon-"+t.type,attrs:{"aria-label":t.alt,role:t.alt?"img":null,"aria-hidden":!t.alt,"data-back":t.back,"data-size":t.size}},[t.isEmoji?n("span",{staticClass:"k-icon-emoji"},[t._v(t._s(t.type))]):n("svg",{style:{color:t.color},attrs:{viewBox:"0 0 16 16"}},[n("use",{attrs:{"xlink:href":"#icon-"+t.type}})])])},pd=[],dd={props:{alt:String,color:String,back:String,size:String,type:String},computed:{isEmoji:function(){return this.$helper.string.hasEmoji(this.type)}}},fd=dd,hd=(n("3342"),Object(y["a"])(fd,cd,pd,!1,null,null,null)),md=hd.exports,gd=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",t._g({staticClass:"k-image",attrs:{"data-ratio":t.ratio,"data-back":t.back,"data-cover":t.cover}},t.$listeners),[n("span",{style:"padding-bottom:"+t.ratioPadding},[t.loaded?n("img",{key:t.src,attrs:{alt:t.alt||"",src:t.src,srcset:t.srcset,sizes:t.sizes},on:{dragstart:function(t){t.preventDefault()}}}):t._e(),t.loaded||t.error?t._e():n("k-loader",{attrs:{position:"center",theme:"light"}}),!t.loaded&&t.error?n("k-icon",{staticClass:"k-image-error",attrs:{type:"cancel"}}):t._e()],1)])},bd=[],vd={props:{alt:String,back:String,cover:Boolean,ratio:String,sizes:String,src:String,srcset:String},data:function(){return{loaded:{type:Boolean,default:!1},error:{type:Boolean,default:!1}}},computed:{ratioPadding:function(){return this.$helper.ratio(this.ratio||"1/1")}},created:function(){var t=this,e=new Image;e.onload=function(){t.loaded=!0,t.$emit("load")},e.onerror=function(){t.error=!0,t.$emit("error")},e.src=this.src}},kd=vd,$d=(n("0d56"),Object(y["a"])(kd,gd,bd,!1,null,null,null)),yd=$d.exports,_d=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("progress",{staticClass:"k-progress",attrs:{max:"100"},domProps:{value:t.state}},[t._v("\n "+t._s(t.state)+"%\n")])},wd=[],xd={props:{value:{type:Number,default:0}},data:function(){return{state:this.value}},methods:{set:function(t){this.state=t}}},Od=xd,jd=(n("9799"),Object(y["a"])(Od,_d,wd,!1,null,null,null)),Sd=jd.exports,Cd=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",{staticClass:"k-sort-handle",attrs:{"aria-hidden":"true"}},[n("svg",{attrs:{viewBox:"0 0 16 16"}},[n("use",{attrs:{"xlink:href":"#icon-sort"}})])])},Ed=[],Rd=(n("35cb"),{}),Td=Object(y["a"])(Rd,Cd,Ed,!1,null,null,null),Id=Td.exports,Ld=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-text",attrs:{"data-align":t.align,"data-size":t.size,"data-theme":t.theme}},[t._t("default")],2)},Ad=[],Bd={props:{align:String,size:String,theme:String}},qd=Bd,Nd=(n("b0d6"),Object(y["a"])(qd,Ld,Ad,!1,null,null,null)),Pd=Nd.exports,Dd=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n(t.component,t._g(t._b({ref:"button",tag:"component"},"component",t.$props,!1),t.$listeners),[t._t("default")],2)},Md=[],Fd={inheritAttrs:!1,props:{autofocus:Boolean,current:[String,Boolean],disabled:Boolean,icon:String,id:[String,Number],link:String,responsive:Boolean,rel:String,role:String,target:String,tabindex:String,theme:String,tooltip:String,type:{type:String,default:"button"}},computed:{component:function(){return!0===this.disabled?"k-button-disabled":this.link?"k-button-link":"k-button-native"}},methods:{focus:function(){this.$refs.button.focus&&this.$refs.button.focus()},tab:function(){this.$refs.button.tab&&this.$refs.button.tab()},untab:function(){this.$refs.button.untab&&this.$refs.button.untab()}}},Ud=Fd,zd=(n("3787"),Object(y["a"])(Ud,Dd,Md,!1,null,null,null)),Hd=zd.exports,Kd=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",{staticClass:"k-button",attrs:{id:t.id,"data-disabled":!0,"data-responsive":t.responsive,"data-theme":t.theme,title:t.tooltip}},[t.icon?n("k-icon",{staticClass:"k-button-icon",attrs:{type:t.icon,alt:t.tooltip}}):t._e(),t.$slots.default?n("span",{staticClass:"k-button-text"},[t._t("default")],2):t._e()],1)},Vd=[],Yd={inheritAttrs:!1,props:{icon:String,id:[String,Number],responsive:Boolean,theme:String,tooltip:String}},Wd=Yd,Jd=(n("16eb"),Object(y["a"])(Wd,Kd,Vd,!1,null,null,null)),Gd=Jd.exports,Zd=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-button-group"},[t._t("default")],2)},Xd=[],Qd=(n("a567"),{}),tf=Object(y["a"])(Qd,Zd,Xd,!1,null,null,null),ef=tf.exports,nf=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-link",t._g({staticClass:"k-button",attrs:{"aria-current":t.current,autofocus:t.autofocus,id:t.id,"data-theme":t.theme,"data-responsive":t.responsive,rel:t.rel,role:t.role,tabindex:t.tabindex,target:t.target,title:t.tooltip,to:t.link}},t.$listeners),[t.icon?n("k-icon",{staticClass:"k-button-icon",attrs:{type:t.icon,alt:t.tooltip}}):t._e(),t.$slots.default?n("span",{staticClass:"k-button-text"},[t._t("default")],2):t._e()],1)},rf=[],sf={inheritAttrs:!1,props:{autofocus:Boolean,current:[String,Boolean],icon:String,id:[String,Number],link:String,rel:String,responsive:Boolean,role:String,target:String,tabindex:String,theme:String,tooltip:String}},af=sf,of=Object(y["a"])(af,nf,rf,!1,null,null,null),uf=of.exports,lf=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("button",t._g({staticClass:"k-button",attrs:{"aria-current":t.current,autofocus:t.autofocus,id:t.id,"data-theme":t.theme,"data-responsive":t.responsive,role:t.role,tabindex:t.tabindex,title:t.tooltip,type:t.type}},t.$listeners),[t.icon?n("k-icon",{staticClass:"k-button-icon",attrs:{type:t.icon,alt:t.tooltip}}):t._e(),t.$slots.default?n("span",{staticClass:"k-button-text"},[t._t("default")],2):t._e()],1)},cf=[],pf={mounted:function(){this.$el.addEventListener("keyup",this.onTab,!0),this.$el.addEventListener("blur",this.onUntab,!0)},destroyed:function(){this.$el.removeEventListener("keyup",this.onTab,!0),this.$el.removeEventListener("blur",this.onUntab,!0)},methods:{focus:function(){this.$el.focus&&this.$el.focus()},onTab:function(t){9===t.keyCode&&this.$el.setAttribute("data-tabbed",!0)},onUntab:function(){this.$el.removeAttribute("data-tabbed")},tab:function(){this.$el.focus(),this.$el.setAttribute("data-tabbed",!0)},untab:function(){this.$el.removeAttribute("data-tabbed")}}},df={mixins:[pf],inheritAttrs:!1,props:{autofocus:Boolean,current:[String,Boolean],icon:String,id:[String,Number],responsive:Boolean,role:String,tabindex:String,theme:String,tooltip:String,type:{type:String,default:"button"}}},ff=df,hf=Object(y["a"])(ff,lf,cf,!1,null,null,null),mf=hf.exports,gf=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",{staticClass:"k-dropdown",on:{click:function(t){t.stopPropagation()}}},[t._t("default")],2)},bf=[],vf=(n("f95f"),{}),kf=Object(y["a"])(vf,gf,bf,!1,null,null,null),$f=kf.exports,yf=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.isOpen?n("div",{staticClass:"k-dropdown-content",attrs:{"data-align":t.align}},[t._t("default",[t._l(t.items,(function(e,i){return["-"===e?n("hr",{key:t._uid+"-item-"+i}):n("k-dropdown-item",t._b({key:t._uid+"-item-"+i,ref:t._uid+"-item-"+i,refInFor:!0,on:{click:function(n){return t.$emit("action",e.click)}}},"k-dropdown-item",e,!1),[t._v("\n "+t._s(e.text)+"\n ")])]}))])],2):t._e()},_f=[],wf=null,xf={props:{options:[Array,Function],align:String},data:function(){return{items:[],current:-1,isOpen:!1}},methods:{fetchOptions:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){var i,r;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:if(!e.options){n.next=14;break}if("string"!==typeof e.options){n.next=11;break}return n.next=4,fetch(e.options);case 4:return i=n.sent,n.next=7,i.json();case 7:return r=n.sent,n.abrupt("return",t(r));case 11:"function"===typeof e.options?e.options(t):Array.isArray(e.options)&&t(e.options);case 12:n.next=15;break;case 14:return n.abrupt("return",t(e.items));case 15:case"end":return n.stop()}}),n)})))()},open:function(){var t=this;this.reset(),wf&&wf!==this&&wf.close(),this.fetchOptions((function(e){t.$events.$on("keydown",t.navigate),t.$events.$on("click",t.close),t.items=e,t.isOpen=!0,t.$emit("open"),wf=t}))},reset:function(){this.current=-1,this.$events.$off("keydown",this.navigate),this.$events.$off("click",this.close)},close:function(){this.reset(),this.isOpen=wf=!1,this.$emit("close")},toggle:function(){this.isOpen?this.close():this.open()},focus:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0;this.$children[t]&&this.$children[t].focus&&(this.current=t,this.$children[t].focus())},navigate:function(t){switch(t.code){case"Escape":case"ArrowLeft":this.close(),this.$emit("leave",t.code);break;case"ArrowUp":t.preventDefault();while(1){if(this.current--,this.current<0){this.close(),this.$emit("leave",t.code);break}if(this.$children[this.current]&&!1===this.$children[this.current].disabled){this.focus(this.current);break}}break;case"ArrowDown":t.preventDefault();while(1){if(this.current++,this.current>this.$children.length-1){var e=this.$children.filter((function(t){return!1===t.disabled}));this.current=this.$children.indexOf(e[e.length-1]);break}if(this.$children[this.current]&&!1===this.$children[this.current].disabled){this.focus(this.current);break}}break;case"Tab":while(1){if(this.current++,this.current>this.$children.length-1){this.close(),this.$emit("leave",t.code);break}if(this.$children[this.current]&&!1===this.$children[this.current].disabled)break}break}}}},Of=xf,jf=(n("98a1"),Object(y["a"])(Of,yf,_f,!1,null,null,null)),Sf=jf.exports,Cf=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-button",t._g(t._b({ref:"button",staticClass:"k-dropdown-item"},"k-button",t.$props,!1),t.listeners),[t._t("default")],2)},Ef=[],Rf={inheritAttrs:!1,props:{disabled:Boolean,icon:String,image:[String,Object],link:String,target:String,theme:String,upload:String,current:[String,Boolean]},data:function(){var t=this;return{listeners:Object(I["a"])(Object(I["a"])({},this.$listeners),{},{click:function(e){t.$parent.close(),t.$emit("click",e)}})}},methods:{focus:function(){this.$refs.button.focus()},tab:function(){this.$refs.button.tab()}}},Tf=Rf,If=(n("580a"),Object(y["a"])(Tf,Cf,Ef,!1,null,null,null)),Lf=If.exports,Af=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.to&&!t.disabled?n("a",t._g({ref:"link",staticClass:"k-link",attrs:{href:t.href,rel:t.relAttr,tabindex:t.tabindex,target:t.target,title:t.title}},t.listeners),[t._t("default")],2):n("span",{staticClass:"k-link",attrs:{title:t.title,"data-disabled":""}},[t._t("default")],2)},Bf=[],qf={mixins:[pf],props:{disabled:Boolean,rel:String,tabindex:[String,Number],target:String,title:String,to:[String,Function]},data:function(){return{relAttr:"_blank"===this.target?"noreferrer noopener":this.rel,listeners:Object(I["a"])(Object(I["a"])({},this.$listeners),{},{click:this.onClick})}},computed:{href:function(){return"function"===typeof this.to?"":void 0===this.$route||"/"!==this.to[0]||this.target?this.to:(this.$router.options.url||"")+this.to}},methods:{isRoutable:function(t){return void 0!==this.$route&&(!(t.metaKey||t.altKey||t.ctrlKey||t.shiftKey)&&(!t.defaultPrevented&&((void 0===t.button||0===t.button)&&!this.target)))},onClick:function(t){if(!0===this.disabled)return t.preventDefault(),!1;"function"===typeof this.to&&(t.preventDefault(),this.to()),this.isRoutable(t)&&(t.preventDefault(),this.$go(this.to)),this.$emit("click",t)}}},Nf=qf,Pf=(n("cc79"),Object(y["a"])(Nf,Af,Bf,!1,null,null,null)),Df=Pf.exports,Mf=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.languages.length?n("k-dropdown",[n("k-button",{attrs:{responsive:!0,icon:"globe"},on:{click:function(e){return t.$refs.languages.toggle()}}},[t._v("\n "+t._s(t.language.name)+"\n ")]),t.languages?n("k-dropdown-content",{ref:"languages"},[n("k-dropdown-item",{on:{click:function(e){return t.change(t.defaultLanguage)}}},[t._v(t._s(t.defaultLanguage.name))]),n("hr"),t._l(t.languages,(function(e){return n("k-dropdown-item",{key:e.code,on:{click:function(n){return t.change(e)}}},[t._v("\n "+t._s(e.name)+"\n ")])}))],2):t._e()],1):t._e()},Ff=[],Uf={computed:{defaultLanguage:function(){return this.$store.state.languages.default},language:function(){return this.$store.state.languages.current},languages:function(){return this.$store.state.languages.all.filter((function(t){return!1===t.default}))}},methods:{change:function(t){this.$store.dispatch("languages/current",t),this.$emit("change",t)}}},zf=Uf,Hf=Object(y["a"])(zf,Mf,Ff,!1,null,null,null),Kf=Hf.exports,Vf=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.show?n("nav",{staticClass:"k-pagination",attrs:{"data-align":t.align}},[t.show?n("k-button",{attrs:{disabled:!t.hasPrev,tooltip:t.prevLabel,icon:"angle-left"},on:{click:t.prev}}):t._e(),t.details?[t.dropdown?[n("k-dropdown",[n("k-button",{staticClass:"k-pagination-details",attrs:{disabled:!t.hasPages},on:{click:function(e){return t.$refs.dropdown.toggle()}}},[t.total>1?[t._v(t._s(t.detailsText))]:t._e(),t._v(t._s(t.total)+"\n ")],2),n("k-dropdown-content",{ref:"dropdown",staticClass:"k-pagination-selector",on:{open:function(e){t.$nextTick((function(){return t.$refs.page.focus()}))}}},[n("div",{staticClass:"k-pagination-settings"},[n("label",{attrs:{for:"k-pagination-page"}},[n("span",[t._v(t._s(t.pageLabel)+":")]),n("select",{ref:"page",attrs:{id:"k-pagination-page"}},t._l(t.pages,(function(e){return n("option",{key:e,domProps:{selected:t.page===e,value:e}},[t._v("\n "+t._s(e)+"\n ")])})),0)]),n("k-button",{attrs:{icon:"check"},on:{click:function(e){return t.goTo(t.$refs.page.value)}}})],1)])],1)]:[n("span",{staticClass:"k-pagination-details"},[t.total>1?[t._v(t._s(t.detailsText))]:t._e(),t._v(t._s(t.total)+"\n ")],2)]]:t._e(),t.show?n("k-button",{attrs:{disabled:!t.hasNext,tooltip:t.nextLabel,icon:"angle-right"},on:{click:t.next}}):t._e()],2):t._e()},Yf=[],Wf={props:{align:{type:String,default:"left"},details:{type:Boolean,default:!1},dropdown:{type:Boolean,default:!0},validate:{type:Function,default:function(){return Promise.resolve()}},page:{type:Number,default:1},total:{type:Number,default:0},limit:{type:Number,default:10},keys:{type:Boolean,default:!1},pageLabel:{type:String,default:function(){return this.$t("pagination.page")}},prevLabel:{type:String,default:function(){return this.$t("prev")}},nextLabel:{type:String,default:function(){return this.$t("next")}}},data:function(){return{currentPage:this.page}},computed:{show:function(){return this.pages>1},start:function(){return(this.currentPage-1)*this.limit+1},end:function(){var t=this.start-1+this.limit;return t>this.total?this.total:t},detailsText:function(){return 1===this.limit?this.start+" / ":this.start+"-"+this.end+" / "},pages:function(){return Math.ceil(this.total/this.limit)},hasPrev:function(){return this.start>1},hasNext:function(){return this.endthis.limit},offset:function(){return this.start-1}},watch:{page:function(t){this.currentPage=parseInt(t)}},created:function(){!0===this.keys&&window.addEventListener("keydown",this.navigate,!1)},destroyed:function(){window.removeEventListener("keydown",this.navigate,!1)},methods:{goTo:function(t){var e=this;this.validate(t).then((function(){t<1&&(t=1),t>e.pages&&(t=e.pages),e.currentPage=t,e.$refs.dropdown&&e.$refs.dropdown.close(),e.$emit("paginate",{page:e.currentPage,start:e.start,end:e.end,limit:e.limit,offset:e.offset})})).catch((function(){}))},prev:function(){this.goTo(this.currentPage-1)},next:function(){this.goTo(this.currentPage+1)},navigate:function(t){switch(t.code){case"ArrowLeft":this.prev();break;case"ArrowRight":this.next();break}}}},Jf=Wf,Gf=(n("a66d"),Object(y["a"])(Jf,Vf,Yf,!1,null,null,null)),Zf=Gf.exports,Xf=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-button-group",{staticClass:"k-prev-next"},[n("k-button",t._b({attrs:{icon:"angle-left"}},"k-button",t.prev,!1)),n("k-button",t._b({attrs:{icon:"angle-right"}},"k-button",t.next,!1))],1)},Qf=[],th={props:{prev:{type:Object,default:function(){return{disabled:!0,link:"#"}}},next:{type:Object,default:function(){return{disabled:!0,link:"#"}}}}},eh=th,nh=(n("7a7d"),Object(y["a"])(eh,Xf,Qf,!1,null,null,null)),ih=nh.exports,rh=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-search",attrs:{role:"search"},on:{click:t.close}},[n("div",{staticClass:"k-search-box",on:{click:function(t){t.stopPropagation()}}},[n("div",{staticClass:"k-search-input"},[n("k-dropdown",{staticClass:"k-search-types"},[n("k-button",{attrs:{icon:t.type.icon},on:{click:function(e){return t.$refs.types.toggle()}}},[t._v(t._s(t.type.label)+":")]),n("k-dropdown-content",{ref:"types"},t._l(t.types,(function(e,i){return n("k-dropdown-item",{key:i,attrs:{icon:e.icon},on:{click:function(e){t.currentType=i}}},[t._v("\n "+t._s(e.label)+"\n ")])})),1)],1),n("input",{directives:[{name:"model",rawName:"v-model",value:t.q,expression:"q"}],ref:"input",attrs:{placeholder:t.$t("search")+" …","aria-label":"$t('search')",type:"text"},domProps:{value:t.q},on:{keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"down",40,e.key,["Down","ArrowDown"])?null:(e.preventDefault(),t.down(e))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"up",38,e.key,["Up","ArrowUp"])?null:(e.preventDefault(),t.up(e))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"tab",9,e.key,"Tab")?null:(e.preventDefault(),t.tab(e))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:t.enter(e)},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"esc",27,e.key,["Esc","Escape"])?null:t.close(e)}],input:function(e){e.target.composing||(t.q=e.target.value)}}}),n("k-button",{staticClass:"k-search-close",attrs:{tooltip:t.$t("close"),icon:"cancel"},on:{click:t.close}})],1),n("ul",t._l(t.items,(function(e,i){return n("li",{key:e.id,attrs:{"data-selected":t.selected===i},on:{mouseover:function(e){t.selected=i}}},[n("k-link",{attrs:{to:e.link},on:{click:t.close}},[n("strong",[t._v(t._s(e.title))]),n("small",[t._v(t._s(e.info))])])],1)})),0)])])},sh=[],ah={data:function(){return{items:[],q:null,selected:-1,currentType:"users"===this.$store.state.view?"users":"pages"}},computed:{type:function(){return this.types[this.currentType]||this.types["pages"]},types:function(){return{pages:{label:this.$t("pages"),icon:"page",endpoint:"site/search"},files:{label:this.$t("files"),icon:"image",endpoint:"files/search"},users:{label:this.$t("users"),icon:"users",endpoint:"users/search"}}}},watch:{q:rt((function(t){this.search(t)}),200),currentType:function(){this.search(this.q)}},mounted:function(){var t=this;this.$nextTick((function(){t.$refs.input.focus()}))},methods:{open:function(t){t.preventDefault(),this.$store.dispatch("search",!0)},click:function(t){this.selected=t,this.tab()},close:function(){this.$store.dispatch("search",!1)},down:function(){this.selected=0&&this.selected--}}},oh=ah,uh=(n("4cb2"),Object(y["a"])(oh,rh,sh,!1,null,null,null)),lh=uh.exports,ch=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",{ref:"button",staticClass:"k-tag",attrs:{"data-size":t.size,tabindex:"0"},on:{keydown:function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"delete",[8,46],e.key,["Backspace","Delete","Del"])?null:(e.preventDefault(),t.remove(e))}}},[n("span",{staticClass:"k-tag-text"},[t._t("default")],2),t.removable?n("span",{staticClass:"k-tag-toggle",on:{click:t.remove}},[t._v("×")]):t._e()])},ph=[],dh={props:{removable:Boolean,size:String},methods:{remove:function(){this.removable&&this.$emit("remove")},focus:function(){this.$refs.button.focus()}}},fh=dh,hh=(n("021f"),Object(y["a"])(fh,ch,ph,!1,null,null,null)),mh=hh.exports,gh=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.user&&t.view?n("div",{staticClass:"k-topbar"},[n("k-view",[n("div",{staticClass:"k-topbar-wrapper"},[n("k-dropdown",{staticClass:"k-topbar-menu"},[n("k-button",{staticClass:"k-topbar-button k-topbar-menu-button",attrs:{tooltip:t.$t("menu"),icon:"bars"},on:{click:function(e){return t.$refs.menu.toggle()}}},[n("k-icon",{attrs:{type:"angle-down"}})],1),n("k-dropdown-content",{ref:"menu",staticClass:"k-topbar-menu"},[n("ul",[t._l(t.views,(function(e,i){return[e.menu?n("li",{key:"menu-item-"+i,attrs:{"aria-current":t.$store.state.view===i}},[n("k-dropdown-item",{attrs:{disabled:!1===t.$permissions.access[i],icon:e.icon,link:e.link}},[t._v("\n "+t._s(t.menuTitle(e,i))+"\n ")])],1):t._e()]})),n("li",[n("hr")]),n("li",{attrs:{"aria-current":"account"===t.$route.meta.view}},[n("k-dropdown-item",{attrs:{icon:"account",link:"/account"}},[t._v("\n "+t._s(t.$t("view.account"))+"\n ")])],1),n("li",[n("hr")]),n("li",[n("k-dropdown-item",{attrs:{icon:"logout",link:"/logout"}},[t._v("\n "+t._s(t.$t("logout"))+"\n ")])],1)],2)])],1),t.view?n("k-link",{staticClass:"k-topbar-button k-topbar-view-button",attrs:{to:t.view.link}},[n("k-icon",{attrs:{type:t.view.icon}}),t._v(" "+t._s(t.breadcrumbTitle)+"\n ")],1):t._e(),t.$store.state.breadcrumb.length>1?n("k-dropdown",{staticClass:"k-topbar-breadcrumb-menu"},[n("k-button",{staticClass:"k-topbar-button",on:{click:function(e){return t.$refs.crumb.toggle()}}},[t._v("\n …\n "),n("k-icon",{attrs:{type:"angle-down"}})],1),n("k-dropdown-content",{ref:"crumb"},[n("k-dropdown-item",{attrs:{icon:t.view.icon,link:t.view.link}},[t._v("\n "+t._s(t.$t("view."+t.$store.state.view,t.view.label))+"\n ")]),t._l(t.$store.state.breadcrumb,(function(e,i){return n("k-dropdown-item",{key:"crumb-"+i+"-dropdown",attrs:{icon:t.view.icon,link:e.link}},[t._v("\n "+t._s(e.label)+"\n ")])}))],2)],1):t._e(),n("nav",{staticClass:"k-topbar-crumbs"},t._l(t.$store.state.breadcrumb,(function(e,i){return n("k-link",{key:"crumb-"+i,attrs:{to:e.link}},[t._v("\n "+t._s(e.label)+"\n ")])})),1),n("div",{staticClass:"k-topbar-signals"},[n("span",{directives:[{name:"show",rawName:"v-show",value:t.$store.state.isLoading,expression:"$store.state.isLoading"}],staticClass:"k-topbar-loader"},[n("svg",{attrs:{viewBox:"0 0 16 18"}},[n("path",{attrs:{fill:"white",d:"M8,0 L16,4.50265232 L16,13.5112142 L8,18.0138665 L0,13.5112142 L0,4.50265232 L8,0 Z M2.10648757,5.69852516 L2.10648757,12.3153414 L8,15.632396 L13.8935124,12.3153414 L13.8935124,5.69852516 L8,2.38147048 L2.10648757,5.69852516 Z"}})])]),t.notification?[n("k-button",{staticClass:"k-topbar-notification k-topbar-signals-button",attrs:{theme:"positive"},on:{click:function(e){return t.$store.dispatch("notification/close")}}},[t._v("\n "+t._s(t.notification.message)+"\n ")])]:t.unregistered?[n("div",{staticClass:"k-registration"},[n("p",[t._v(t._s(t.$t("license.unregistered")))]),n("k-button",{staticClass:"k-topbar-signals-button",attrs:{responsive:!0,tooltip:t.$t("license.unregistered"),icon:"key"},on:{click:function(e){return t.$emit("register")}}},[t._v("\n "+t._s(t.$t("license.register"))+"\n ")]),n("k-button",{staticClass:"k-topbar-signals-button",attrs:{responsive:!0,link:"https://getkirby.com/buy",target:"_blank",icon:"cart"}},[t._v("\n "+t._s(t.$t("license.buy"))+"\n ")])],1)]:t._e(),[n("k-form-indicator")],n("k-button",{staticClass:"k-topbar-signals-button",attrs:{tooltip:t.$t("search"),icon:"search"},on:{click:function(e){return t.$store.dispatch("search",!0)}}})],2)],1)])],1):t._e()},bh=[],vh=Object(I["a"])({site:{link:"/site",icon:"page",menu:!0},users:{link:"/users",icon:"users",menu:!0},settings:{link:"/settings",icon:"settings",menu:!0},account:{link:"/account",icon:"users",menu:!1}},window.panel.plugins.views),kh={computed:{breadcrumbTitle:function(){var t=this.$t("view.".concat(this.$store.state.view),this.view.label);return"site"===this.$store.state.view&&this.$store.state.system.info.title||t},view:function(){return vh[this.$store.state.view]},views:function(){return vh},user:function(){return this.$store.state.user.current},notification:function(){return this.$store.state.notification.type&&"error"!==this.$store.state.notification.type?this.$store.state.notification:null},unregistered:function(){return!this.$store.state.system.info.license}},methods:{menuTitle:function(t,e){var n=this.$t("view."+e,t.label);return"site"===e&&this.$store.state.system.info.site||n}}},$h=kh,yh=(n("1e3b"),Object(y["a"])($h,gh,bh,!1,null,null,null)),_h=yh.exports,wh=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-grid",{staticClass:"k-sections",attrs:{gutter:"large"}},t._l(t.columns,(function(e,i){return n("k-column",{key:t.parent+"-column-"+i,attrs:{width:e.width,sticky:e.sticky}},[t._l(e.sections,(function(r,s){return[t.meetsCondition(r)?[t.exists(r.type)?n("k-"+r.type+"-section",t._b({key:t.parent+"-column-"+i+"-section-"+s+"-"+t.blueprint,tag:"component",class:"k-section k-section-name-"+r.name,attrs:{name:r.name,parent:t.parent,blueprint:t.blueprint,column:e.width},on:{submit:function(e){return t.$emit("submit",e)}}},"component",r,!1)):[n("k-box",{key:t.parent+"-column-"+i+"-section-"+s,attrs:{text:t.$t("error.section.type.invalid",{type:r.type}),theme:"negative"}})]]:t._e()]}))],2)})),1)},xh=[],Oh={props:{parent:String,blueprint:String,columns:[Array,Object]},computed:{content:function(){return this.$store.getters["content/values"]()}},methods:{exists:function(t){return this.$helper.isComponent("k-".concat(t,"-section"))},meetsCondition:function(t){var e=this;if(!t.when)return!0;var n=!0;return Object.keys(t.when).forEach((function(i){var r=e.content[i.toLowerCase()],s=t.when[i];r!==s&&(n=!1)})),n}}},jh=Oh,Sh=(n("6bcd"),Object(y["a"])(jh,wh,xh,!1,null,null,null)),Ch=Sh.exports,Eh=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("section",{staticClass:"k-info-section"},[n("k-headline",{staticClass:"k-info-section-headline"},[t._v(t._s(t.headline))]),n("k-box",{attrs:{theme:t.theme}},[n("k-text",{domProps:{innerHTML:t._s(t.text)}})],1)],1)},Rh=[],Th={props:{blueprint:String,help:String,name:String,parent:String},methods:{load:function(){return this.$api.get(this.parent+"/sections/"+this.name)}}},Ih={mixins:[Th],data:function(){return{headline:null,issue:null,text:null,theme:null}},created:function(){var t=this;this.load().then((function(e){t.headline=e.options.headline,t.text=e.options.text,t.theme=e.options.theme||"info"})).catch((function(e){t.issue=e}))}},Lh=Ih,Ah=(n("4333"),Object(y["a"])(Lh,Eh,Rh,!1,null,null,null)),Bh=Ah.exports,qh=function(){var t=this,e=t.$createElement,n=t._self._c||e;return!1===t.isLoading?n("section",{staticClass:"k-pages-section"},[n("header",{staticClass:"k-section-header"},[n("k-headline",{attrs:{link:t.options.link}},[t._v("\n "+t._s(t.headline)+" "),t.options.min?n("abbr",{attrs:{title:t.$t("section.required")}},[t._v("*")]):t._e()]),t.add?n("k-button-group",[n("k-button",{attrs:{icon:"add"},on:{click:t.create}},[t._v(t._s(t.$t("add")))])],1):t._e()],1),t.error?[n("k-box",{attrs:{theme:"negative"}},[n("k-text",{attrs:{size:"small"}},[n("strong",[t._v("\n "+t._s(t.$t("error.section.notLoaded",{name:t.name}))+":\n ")]),t._v("\n "+t._s(t.error)+"\n ")])],1)]:[t.data.length?n("k-collection",{attrs:{layout:t.options.layout,help:t.help,items:t.data,pagination:t.pagination,sortable:t.options.sortable,size:t.options.size,"data-invalid":t.isInvalid},on:{change:t.sort,paginate:t.paginate,action:t.action}}):[n("k-empty",{attrs:{layout:t.options.layout,"data-invalid":t.isInvalid,icon:"page"},on:{click:t.create}},[t._v("\n "+t._s(t.options.empty||t.$t("pages.empty"))+"\n ")]),n("footer",{staticClass:"k-collection-footer"},[t.help?n("k-text",{staticClass:"k-collection-help",attrs:{theme:"help"},domProps:{innerHTML:t._s(t.help)}}):t._e()],1)],n("k-page-create-dialog",{ref:"create"}),n("k-page-duplicate-dialog",{ref:"duplicate"}),n("k-page-rename-dialog",{ref:"rename",on:{success:t.update}}),n("k-page-url-dialog",{ref:"url",on:{success:t.update}}),n("k-page-status-dialog",{ref:"status",on:{success:t.update}}),n("k-page-template-dialog",{ref:"template",on:{success:t.update}}),n("k-page-remove-dialog",{ref:"remove",on:{success:t.update}})]],2):t._e()},Nh=[],Ph={inheritAttrs:!1,props:{blueprint:String,column:String,parent:String,name:String},data:function(){return{data:[],error:null,isLoading:!1,options:{empty:null,headline:null,help:null,layout:"list",link:null,max:null,min:null,size:null,sortable:null},pagination:{page:null}}},computed:{headline:function(){return this.options.headline||" "},help:function(){return this.options.help},isInvalid:function(){return!!(this.options.min&&this.data.lengththis.options.max)},language:function(){return this.$store.state.languages.current},paginationId:function(){return"kirby$pagination$"+this.parent+"/"+this.name}},watch:{language:function(){this.reload()}},methods:{items:function(t){return t},load:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){var i;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return t||(e.isLoading=!0),null===e.pagination.page&&(e.pagination.page=localStorage.getItem(e.paginationId)||1),n.prev=2,n.next=5,e.$api.get(e.parent+"/sections/"+e.name,{page:e.pagination.page});case 5:i=n.sent,e.options=i.options,e.pagination=i.pagination,e.data=e.items(i.data),n.next=14;break;case 11:n.prev=11,n.t0=n["catch"](2),e.error=n.t0.message;case 14:return n.prev=14,e.isLoading=!1,n.finish(14);case 17:case"end":return n.stop()}}),n,null,[[2,11,14,17]])})))()},paginate:function(t){localStorage.setItem(this.paginationId,t.page),this.pagination=t,this.reload()},reload:function(){this.load(!0)}}},Dh={mixins:[Ph],computed:{add:function(){return this.options.add&&this.$permissions.pages.create}},created:function(){this.load(),this.$events.$on("page.changeStatus",this.reload)},destroyed:function(){this.$events.$off("page.changeStatus",this.reload)},methods:{create:function(){this.add&&this.$refs.create.open(this.options.link||this.parent,this.parent+"/blueprints",this.name)},action:function(t,e){var n=this;switch(e){case"duplicate":this.$refs.duplicate.open(t.id);break;case"preview":var i=window.open("","_blank");i.document.write="...",this.$api.pages.preview(t.id).then((function(t){i.location.href=t})).catch((function(t){n.$store.dispatch("notification/error",t)}));break;case"rename":this.$refs.rename.open(t.id);break;case"url":this.$refs.url.open(t.id);break;case"status":this.$refs.status.open(t.id);break;case"template":this.$refs.template.open(t.id);break;case"remove":if(this.data.length<=this.options.min){var r=this.options.min>1?"plural":"singular";this.$store.dispatch("notification/error",{message:this.$t("error.section.pages.min."+r,{section:this.options.headline||this.name,min:this.options.min})});break}this.$refs.remove.open(t.id);break;default:throw new Error("Invalid action")}},items:function(t){var e=this;return t.map((function(t){var n=!1!==t.permissions.changeStatus;return t.flag={class:"k-status-flag k-status-flag-"+t.status,tooltip:n?e.$t("page.status"):"".concat(e.$t("page.status")," (").concat(e.$t("disabled"),")"),icon:n?"circle":"protected",disabled:!n,click:function(){e.action(t,"status")}},t.options=function(n){e.$api.pages.options(t.id,"list").then((function(t){return n(t)})).catch((function(t){e.$store.dispatch("notification/error",t)}))},t.sortable=t.permissions.sort&&e.options.sortable,t.column=e.column,t}))},sort:function(t){var e=this,n=null;if(t.added&&(n="added"),t.moved&&(n="moved"),n){var i=t[n].element,r=t[n].newIndex+1+this.pagination.offset;this.$api.pages.status(i.id,"listed",r).then((function(){e.$store.dispatch("notification/success",":)")})).catch((function(t){e.$store.dispatch("notification/error",{message:t.message,details:t.details}),e.reload()}))}},update:function(){this.reload(),this.$events.$emit("model.update")}}},Mh=Dh,Fh=Object(y["a"])(Mh,qh,Nh,!1,null,null,null),Uh=Fh.exports,zh=function(){var t=this,e=t.$createElement,n=t._self._c||e;return!1===t.isLoading?n("section",{staticClass:"k-files-section"},[n("header",{staticClass:"k-section-header"},[n("k-headline",[t._v("\n "+t._s(t.headline)+" "),t.options.min?n("abbr",{attrs:{title:t.$t("section.required")}},[t._v("*")]):t._e()]),t.add?n("k-button-group",[n("k-button",{attrs:{icon:"upload"},on:{click:t.upload}},[t._v(t._s(t.$t("add")))])],1):t._e()],1),t.error?[n("k-box",{attrs:{theme:"negative"}},[n("k-text",{attrs:{size:"small"}},[n("strong",[t._v(t._s(t.$t("error.section.notLoaded",{name:t.name}))+":")]),t._v("\n "+t._s(t.error)+"\n ")])],1)]:[n("k-dropzone",{attrs:{disabled:!1===t.add},on:{drop:t.drop}},[t.data.length?n("k-collection",{attrs:{help:t.help,items:t.data,layout:t.options.layout,pagination:t.pagination,sortable:t.options.sortable,size:t.options.size,"data-invalid":t.isInvalid},on:{sort:t.sort,paginate:t.paginate,action:t.action}}):[n("k-empty",{attrs:{layout:t.options.layout,"data-invalid":t.isInvalid,icon:"image"},on:{click:function(e){t.add&&t.upload()}}},[t._v("\n "+t._s(t.options.empty||t.$t("files.empty"))+"\n ")]),n("footer",{staticClass:"k-collection-footer"},[t.help?n("k-text",{staticClass:"k-collection-help",attrs:{theme:"help"},domProps:{innerHTML:t._s(t.help)}}):t._e()],1)]],2),n("k-file-rename-dialog",{ref:"rename",on:{success:t.update}}),n("k-file-remove-dialog",{ref:"remove",on:{success:t.update}}),n("k-upload",{ref:"upload",on:{success:t.uploaded,error:t.reload}})]],2):t._e()},Hh=[],Kh={mixins:[Ph],computed:{add:function(){return!(!this.$permissions.files.create||!1===this.options.upload)&&this.options.upload}},created:function(){this.load(),this.$events.$on("model.update",this.reload)},destroyed:function(){this.$events.$off("model.update",this.reload)},methods:{action:function(t,e){switch(e){case"edit":this.$go(t.link);break;case"download":window.open(t.url);break;case"rename":this.$refs.rename.open(t.parent,t.filename);break;case"replace":this.$refs.upload.open({url:B.api+"/"+this.$api.files.url(t.parent,t.filename),accept:"."+t.extension+","+t.mime,multiple:!1});break;case"remove":if(this.data.length<=this.options.min){var n=this.options.min>1?"plural":"singular";this.$store.dispatch("notification/error",{message:this.$t("error.section.files.min."+n,{section:this.options.headline||this.name,min:this.options.min})});break}this.$refs.remove.open(t.parent,t.filename);break}},drop:function(t){if(!1===this.add)return!1;this.$refs.upload.drop(t,Object(I["a"])(Object(I["a"])({},this.add),{},{url:B.api+"/"+this.add.api}))},items:function(t){var e=this;return t.map((function(t){return t.options=function(){var n=Object(j["a"])(regeneratorRuntime.mark((function n(i){var r;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.prev=0,n.next=3,e.$api.files.options(t.parent,t.filename,"list");case 3:r=n.sent,i(r),n.next=10;break;case 7:n.prev=7,n.t0=n["catch"](0),e.$store.dispatch("notification/error",n.t0);case 10:case"end":return n.stop()}}),n,null,[[0,7]])})));return function(t){return n.apply(this,arguments)}}(),t.sortable=e.options.sortable,t.column=e.column,t}))},replace:function(t){this.$refs.upload.open({url:B.api+"/"+this.$api.files.url(t.parent,t.filename),accept:t.mime,multiple:!1})},sort:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:if(!1!==e.options.sortable){n.next=2;break}return n.abrupt("return",!1);case 2:return t=t.map((function(t){return t.id})),n.prev=3,n.next=6,e.$api.patch(e.options.apiUrl+"/files/sort",{files:t,index:e.pagination.offset});case 6:e.$store.dispatch("notification/success",":)"),n.next=13;break;case 9:n.prev=9,n.t0=n["catch"](3),e.reload(),e.$store.dispatch("notification/error",n.t0.message);case 13:case"end":return n.stop()}}),n,null,[[3,9]])})))()},update:function(){this.$events.$emit("model.update")},upload:function(){if(!1===this.add)return!1;this.$refs.upload.open(Object(I["a"])(Object(I["a"])({},this.add),{},{url:B.api+"/"+this.add.api}))},uploaded:function(){this.$events.$emit("file.create"),this.$events.$emit("model.update"),this.$store.dispatch("notification/success",":)")}}},Vh=Kh,Yh=Object(y["a"])(Vh,zh,Hh,!1,null,null,null),Wh=Yh.exports,Jh=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.isLoading?t._e():n("section",{staticClass:"k-fields-section"},[t.issue?[n("k-headline",{staticClass:"k-fields-issue-headline"},[t._v("Error")]),n("k-box",{attrs:{text:t.issue.message,theme:"negative"}})]:t._e(),n("k-form",{attrs:{fields:t.fields,validate:!0,value:t.values,disabled:null!==t.$store.state.content.status.lock},on:{input:t.input,submit:t.onSubmit}})],2)},Gh=[],Zh={mixins:[Th],inheritAttrs:!1,data:function(){return{fields:{},isLoading:!0,issue:null}},computed:{language:function(){return this.$store.state.languages.current},values:function(){return this.$store.getters["content/values"]()}},watch:{language:function(){this.fetch()}},created:function(){this.fetch()},methods:{input:function(t,e,n){this.$store.dispatch("content/update",[n,t[n]])},fetch:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){var n;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,t.load();case 3:n=e.sent,t.fields=n.fields,Object.keys(t.fields).forEach((function(e){t.fields[e].section=t.name,t.fields[e].endpoints={field:t.parent+"/fields/"+e,section:t.parent+"/sections/"+t.name,model:t.parent}})),e.next=11;break;case 8:e.prev=8,e.t0=e["catch"](0),t.issue=e.t0;case 11:return e.prev=11,t.isLoading=!1,e.finish(11);case 14:case"end":return e.stop()}}),e,null,[[0,8,11,14]])})))()},onSubmit:function(t){this.$events.$emit("keydown.cmd.s",t)}}},Xh=Zh,Qh=(n("7d5d"),Object(y["a"])(Xh,Jh,Gh,!1,null,null,null)),tm=Qh.exports,em=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-error-view",{staticClass:"k-browser-view"},[n("p",[t._v("\n We are really sorry, but your browser does not support\n all features required for the Kirby Panel.\n ")]),!1===t.hasFetchSupport?[n("p",[n("strong",[t._v("Fetch")]),n("br"),t._v("\n We use Javascript's new Fetch API. You can find a list of supported browsers for this feature on\n "),n("strong",[n("a",{attrs:{href:"https://caniuse.com/#feat=fetch"}},[t._v("caniuse.com")])])])]:t._e(),!1===t.hasGridSupport?[n("p",[n("strong",[t._v("CSS Grid")]),n("br"),t._v("\n We use CSS Grids for all our layouts. You can find a list of supported browsers for this feature on\n "),n("strong",[n("a",{attrs:{href:"https://caniuse.com/#feat=css-grid"}},[t._v("caniuse.com")])])])]:t._e()],2)},nm=[],im={grid:function(){return!(!window.CSS||!window.CSS.supports("display","grid"))},fetch:function(){return void 0!==window.fetch},all:function(){return this.fetch()&&this.grid()}},rm={computed:{hasFetchSupport:function(){return im.fetch()},hasGridSupport:function(){return im.grid()}},created:function(){this.$store.dispatch("content/current",null),im.all()&&this.$go("/")}},sm=rm,am=(n("d6fc"),Object(y["a"])(sm,em,nm,!1,null,null,null)),om=am.exports,um=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-error-boundary",{key:t.plugin,scopedSlots:t._u([{key:"error",fn:function(e){var i=e.error;return n("k-error-view",{},[t._v("\n "+t._s(i.message||i)+"\n ")])}}])},[n("k-"+t.plugin+"-plugin-view",{tag:"component"})],1)},lm=[],cm={props:{plugin:String},beforeRouteEnter:function(t,e,n){n((function(t){t.$store.dispatch("breadcrumb",[]),t.$store.dispatch("content/current",null)}))},watch:{plugin:{handler:function(){this.$store.dispatch("view",this.plugin)},immediate:!0}}},pm=cm,dm=Object(y["a"])(pm,um,lm,!1,null,null,null),fm=dm.exports,hm=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-view",{staticClass:"k-error-view"},[n("div",{staticClass:"k-error-view-content"},[n("k-text",[n("p",[n("k-icon",{staticClass:"k-error-view-icon",attrs:{type:"alert"}})],1),n("p",[t._t("default")],2)])],1)])},mm=[],gm=(n("d221"),{}),bm=Object(y["a"])(gm,hm,mm,!1,null,null,null),vm=bm.exports,km=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.issue?n("k-error-view",[t._v("\n "+t._s(t.issue.message)+"\n")]):n("div",{staticClass:"k-file-view"},[n("k-file-preview",{attrs:{file:t.file}}),n("k-view",{staticClass:"k-file-content",attrs:{"data-locked":t.isLocked}},[n("k-header",{attrs:{editable:t.permissions.changeName&&!t.isLocked,tabs:t.tabs,tab:t.tab},on:{edit:function(e){return t.action("rename")}}},[t._v("\n\n "+t._s(t.file.filename)+"\n\n "),n("k-button-group",{attrs:{slot:"left"},slot:"left"},[n("k-button",{attrs:{responsive:!0,icon:"open"},on:{click:function(e){return t.action("download")}}},[t._v("\n "+t._s(t.$t("open"))+"\n ")]),n("k-dropdown",[n("k-button",{attrs:{responsive:!0,disabled:t.isLocked,icon:"cog"},on:{click:function(e){return t.$refs.settings.toggle()}}},[t._v("\n "+t._s(t.$t("settings"))+"\n ")]),n("k-dropdown-content",{ref:"settings",attrs:{options:t.options},on:{action:t.action}})],1),n("k-languages-dropdown")],1),t.file.id?n("k-prev-next",{attrs:{slot:"right",prev:t.prev,next:t.next},slot:"right"}):t._e()],1),t.file.id?n("k-tabs",{key:t.tabsKey,ref:"tabs",attrs:{parent:t.parent,tabs:t.tabs,blueprint:t.file.blueprint.name},on:{tab:function(e){t.tab=e}}}):t._e(),n("k-file-rename-dialog",{ref:"rename",on:{success:t.renamed}}),n("k-file-remove-dialog",{ref:"remove",on:{success:t.deleted}}),n("k-upload",{ref:"upload",attrs:{url:t.uploadApi,accept:t.file.mime,multiple:!1},on:{success:t.uploaded}})],1)],1)},$m=[],ym={computed:{isLocked:function(){return null!==this.$store.state.content.status.lock}},created:function(){this.fetch(),this.$events.$on("model.reload",this.fetch),this.$events.$on("keydown.left",this.toPrev),this.$events.$on("keydown.right",this.toNext)},destroyed:function(){this.$events.$off("model.reload",this.fetch),this.$events.$off("keydown.left",this.toPrev),this.$events.$off("keydown.right",this.toNext)},methods:{toPrev:function(t){this.prev&&"body"===t.target.localName&&this.$router.push(this.prev.link)},toNext:function(t){this.next&&"body"===t.target.localName&&this.$router.push(this.next.link)}}},_m={mixins:[ym],props:{path:{type:String},filename:{type:String,required:!0}},data:function(){return{file:{id:null,parent:null,filename:"",url:"",prev:null,next:null,panelIcon:null,panelImage:null,mime:null,content:{}},parent:null,permissions:{changeName:!1,delete:!1},issue:null,tabs:[],tab:null,options:null}},computed:{uploadApi:function(){return B.api+"/"+this.path+"/files/"+this.filename},prev:function(){if(this.file.prev)return{link:this.$api.files.link(this.path,this.file.prev.filename),tooltip:this.file.prev.filename}},tabsKey:function(){return"file-"+this.file.id+"-tabs"},language:function(){return this.$store.state.languages.current},next:function(){if(this.file.next)return{link:this.$api.files.link(this.path,this.file.next.filename),tooltip:this.file.next.filename}}},watch:{language:function(){this.fetch()},filename:function(){this.fetch()}},methods:{fetch:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){var n;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,t.$api.files.get(t.path,t.filename,{view:"panel"});case 3:n=e.sent,t.file=Object(I["a"])(Object(I["a"])({},n),{},{next:n.nextWithTemplate,prev:n.prevWithTemplate,url:n.url}),t.parent=t.$api.files.url(t.path,n.filename),t.tabs=n.blueprint.tabs,t.permissions=n.options,t.options=function(){var e=Object(j["a"])(regeneratorRuntime.mark((function e(n){var i;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.next=2,t.$api.files.options(t.path,t.file.filename);case 2:i=e.sent,n(i);case 4:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}(),t.$store.dispatch("breadcrumb",t.$api.files.breadcrumb(t.file,t.$route.name)),t.$store.dispatch("title",t.filename),t.$store.dispatch("content/create",{id:"files/"+n.id,api:t.$api.files.link(t.path,t.filename),content:n.content}),e.next=18;break;case 14:e.prev=14,e.t0=e["catch"](0),window.console.error(e.t0),t.issue=e.t0;case 18:case"end":return e.stop()}}),e,null,[[0,14]])})))()},action:function(t){switch(t){case"download":window.open(this.file.url);break;case"rename":this.$refs.rename.open(this.path,this.file.filename);break;case"replace":this.$refs.upload.open({url:B.api+"/"+this.$api.files.url(this.path,this.file.filename),accept:"."+this.file.extension+","+this.file.mime});break;case"remove":this.$refs.remove.open(this.path,this.file.filename);break}},deleted:function(){this.path?this.$go("/"+this.path):this.$go("/site")},renamed:function(t){this.$go(this.$api.files.link(this.path,t.filename))},uploaded:function(){this.fetch(),this.$store.dispatch("notification/success",":)")}}},wm=_m,xm=Object(y["a"])(wm,km,$m,!1,null,null,null),Om=xm.exports,jm=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.system?n("k-view",{staticClass:"k-installation-view",attrs:{align:"center"}},["install"===t.state?n("form",{on:{submit:function(e){return e.preventDefault(),t.install(e)}}},[n("h1",{staticClass:"k-offscreen"},[t._v(t._s(t.$t("installation")))]),n("k-fieldset",{attrs:{fields:t.fields,novalidate:!0},model:{value:t.user,callback:function(e){t.user=e},expression:"user"}}),n("k-button",{attrs:{type:"submit",icon:"check"}},[t._v(t._s(t.$t("install")))])],1):"completed"===t.state?n("k-text",[n("k-headline",[t._v(t._s(t.$t("installation.completed")))]),n("k-link",{attrs:{to:"/login"}},[t._v(t._s(t.$t("login")))])],1):n("div",[t.system.isInstalled?t._e():n("k-headline",[t._v(t._s(t.$t("installation.issues.headline")))]),n("ul",{staticClass:"k-installation-issues"},[!1===t.system.isInstallable?n("li",[n("k-icon",{attrs:{type:"alert"}}),n("span",{domProps:{innerHTML:t._s(t.$t("installation.disabled"))}})],1):t._e(),!1===t.requirements.php?n("li",[n("k-icon",{attrs:{type:"alert"}}),n("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.php"))}})],1):t._e(),!1===t.requirements.server?n("li",[n("k-icon",{attrs:{type:"alert"}}),n("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.server"))}})],1):t._e(),!1===t.requirements.mbstring?n("li",[n("k-icon",{attrs:{type:"alert"}}),n("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.mbstring"))}})],1):t._e(),!1===t.requirements.curl?n("li",[n("k-icon",{attrs:{type:"alert"}}),n("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.curl"))}})],1):t._e(),!1===t.requirements.accounts?n("li",[n("k-icon",{attrs:{type:"alert"}}),n("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.accounts"))}})],1):t._e(),!1===t.requirements.content?n("li",[n("k-icon",{attrs:{type:"alert"}}),n("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.content"))}})],1):t._e(),!1===t.requirements.media?n("li",[n("k-icon",{attrs:{type:"alert"}}),n("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.media"))}})],1):t._e(),!1===t.requirements.sessions?n("li",[n("k-icon",{attrs:{type:"alert"}}),n("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.sessions"))}})],1):t._e()]),n("k-button",{attrs:{icon:"refresh"},on:{click:t.check}},[n("span",{domProps:{innerHTML:t._s(t.$t("retry"))}})])],1)],1):t._e()},Sm=[],Cm={data:function(){return{user:{name:"",email:"",language:"",password:"",role:"admin"},languages:[],system:null}},computed:{state:function(){return this.system.isOk&&this.system.isInstallable&&!this.system.isInstalled?"install":this.system.isOk&&this.system.isInstallable&&this.system.isInstalled?"completed":void 0},translation:function(){return this.$store.state.translation.current},requirements:function(){return this.system&&this.system.requirements?this.system.requirements:{}},fields:function(){return{email:{label:this.$t("email"),type:"email",link:!1,required:!0},password:{label:this.$t("password"),type:"password",placeholder:this.$t("password")+" …",required:!0},language:{label:this.$t("language"),type:"select",options:this.languages,icon:"globe",empty:!1,required:!0}}}},watch:{translation:{handler:function(t){this.user.language=t},immediate:!0},"user.language":function(t){this.$store.dispatch("translation/activate",t)}},created:function(){this.$store.dispatch("content/current",null),this.check()},methods:{install:function(){var t=this;this.$api.system.install(this.user).then((function(e){t.$store.dispatch("user/current",e),t.$store.dispatch("notification/success",t.$t("welcome")+"!"),t.$go("/")})).catch((function(e){t.$store.dispatch("notification/error",e)}))},check:function(){var t=this;this.$store.dispatch("system/load",!0).then((function(e){!0===e.isInstalled&&e.isReady?t.$go("/login"):t.$api.translations.options().then((function(n){t.languages=n,t.system=e,t.$store.dispatch("title",t.$t("view.installation"))}))}))}}},Em=Cm,Rm=(n("146c"),Object(y["a"])(Em,jm,Sm,!1,null,null,null)),Tm=Rm.exports,Im=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.issue?n("k-error-view",[t._v("\n "+t._s(t.issue.message)+"\n")]):t.ready?n("k-view",{staticClass:"k-login-view",attrs:{align:"center"}},[n("k-login-form")],1):t._e()},Lm=[],Am=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("form",{staticClass:"k-login-form",on:{submit:function(e){return e.preventDefault(),t.login(e)}}},[n("h1",{staticClass:"k-offscreen"},[t._v(t._s(t.$t("login")))]),t.issue?n("div",{staticClass:"k-login-alert",on:{click:function(e){t.issue=null}}},[n("span",[t._v(t._s(t.issue))]),n("k-icon",{attrs:{type:"alert"}})],1):t._e(),n("k-fieldset",{attrs:{novalidate:!0,fields:t.fields},model:{value:t.user,callback:function(e){t.user=e},expression:"user"}}),n("div",{staticClass:"k-login-buttons"},[n("span",{staticClass:"k-login-checkbox"},[n("k-checkbox-input",{attrs:{value:t.user.remember,label:t.$t("login.remember")},on:{input:function(e){t.user.remember=e}}})],1),n("k-button",{staticClass:"k-login-button",attrs:{icon:"check",type:"submit"}},[t._v("\n "+t._s(t.$t("login"))+" "),t.isLoading?[t._v("…")]:t._e()],2)],1)],1)},Bm=[],qm={data:function(){return{isLoading:!1,issue:"",user:{email:"",password:"",remember:!1}}},computed:{fields:function(){return{email:{autofocus:!0,label:this.$t("email"),type:"email",required:!0,link:!1},password:{label:this.$t("password"),type:"password",minLength:8,required:!0,autocomplete:"current-password",counter:!1}}}},methods:{login:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return t.issue=null,t.isLoading=!0,e.prev=2,e.next=5,t.$store.dispatch("user/login",t.user);case 5:return e.next=7,t.$store.dispatch("system/load",!0);case 7:t.$store.dispatch("notification/success",t.$t("welcome")),e.next=13;break;case 10:e.prev=10,e.t0=e["catch"](2),t.issue=t.$t("error.access.login");case 13:return e.prev=13,t.isLoading=!1,e.finish(13);case 16:case"end":return e.stop()}}),e,null,[[2,10,13,16]])})))()}}},Nm=qm,Pm=Object(y["a"])(Nm,Am,Bm,!1,null,null,null),Dm=Pm.exports,Mm={components:{"k-login-form":window.panel.plugins.login||Dm},data:function(){return{ready:!1,issue:null}},created:function(){var t=this;this.$store.dispatch("content/current",null),this.$store.dispatch("system/load").then((function(e){e.isReady||t.$go("/installation"),e.user&&e.user.id&&t.$go("/"),t.ready=!0,t.$store.dispatch("title",t.$t("login"))})).catch((function(e){t.issue=e}))}},Fm=Mm,Um=(n("24c1"),Object(y["a"])(Fm,Im,Lm,!1,null,null,null)),zm=Um.exports,Hm=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.issue?n("k-error-view",[t._v("\n "+t._s(t.issue.message)+"\n")]):n("k-view",{staticClass:"k-page-view",attrs:{"data-locked":t.isLocked}},[n("k-header",{attrs:{tabs:t.tabs,tab:t.tab,editable:t.permissions.changeTitle&&!t.isLocked},on:{edit:function(e){return t.action("rename")}}},[t._v("\n "+t._s(t.page.title)+"\n "),n("k-button-group",{attrs:{slot:"left"},slot:"left"},[t.permissions.preview&&t.page.previewUrl?n("k-button",{attrs:{responsive:!0,link:t.page.previewUrl,target:"_blank",icon:"open"}},[t._v("\n "+t._s(t.$t("open"))+"\n ")]):t._e(),t.status?n("k-button",{class:["k-status-flag","k-status-flag-"+t.page.status],attrs:{disabled:!t.permissions.changeStatus||t.isLocked,icon:!t.permissions.changeStatus||t.isLocked?"protected":"circle",responsive:!0,tooltip:t.status.label},on:{click:function(e){return t.action("status")}}},[t._v("\n "+t._s(t.status.label)+"\n ")]):t._e(),n("k-dropdown",[n("k-button",{attrs:{responsive:!0,disabled:!0===t.isLocked,icon:"cog"},on:{click:function(e){return t.$refs.settings.toggle()}}},[t._v("\n "+t._s(t.$t("settings"))+"\n ")]),n("k-dropdown-content",{ref:"settings",attrs:{options:t.options},on:{action:t.action}})],1),n("k-languages-dropdown")],1),t.page.id?n("k-prev-next",{attrs:{slot:"right",prev:t.prev,next:t.next},slot:"right"}):t._e()],1),t.page.id?n("k-tabs",{key:t.tabsKey,ref:"tabs",attrs:{parent:t.$api.pages.url(t.page.id),blueprint:t.blueprint,tabs:t.tabs},on:{tab:t.onTab}}):t._e(),n("k-page-rename-dialog",{ref:"rename",on:{success:t.update}}),n("k-page-duplicate-dialog",{ref:"duplicate"}),n("k-page-url-dialog",{ref:"url"}),n("k-page-status-dialog",{ref:"status",on:{success:t.update}}),n("k-page-template-dialog",{ref:"template",on:{success:t.update}}),n("k-page-remove-dialog",{ref:"remove"})],1)},Km=[],Vm={mixins:[ym],props:{path:{type:String,required:!0}},data:function(){return{page:{title:"",id:null,prev:null,next:null,status:null},blueprint:null,preview:!0,permissions:{changeTitle:!1,changeStatus:!1},icon:"page",issue:null,tab:null,tabs:[],options:null}},computed:{language:function(){return this.$store.state.languages.current},next:function(){if(this.page.next)return{link:this.$api.pages.link(this.page.next.id),tooltip:this.page.next.title}},prev:function(){if(this.page.prev)return{link:this.$api.pages.link(this.page.prev.id),tooltip:this.page.prev.title}},status:function(){return null!==this.page.status?this.page.blueprint.status[this.page.status]:null},tabsKey:function(){return"page-"+this.page.id+"-tabs"}},watch:{language:function(){this.fetch()},path:function(){this.fetch()}},created:function(){this.$events.$on("page.changeSlug",this.update)},destroyed:function(){this.$events.$off("page.changeSlug",this.update)},methods:{action:function(t){switch(t){case"duplicate":this.$refs.duplicate.open(this.page.id);break;case"rename":this.$refs.rename.open(this.page.id);break;case"url":this.$refs.url.open(this.page.id);break;case"status":this.$refs.status.open(this.page.id);break;case"template":this.$refs.template.open(this.page.id);break;case"remove":this.$refs.remove.open(this.page.id);break;default:this.$store.dispatch("notification/error",this.$t("notification.notImplemented"));break}},fetch:function(){var t=this;this.$api.pages.get(this.path,{view:"panel"}).then((function(e){t.page=e,t.blueprint=e.blueprint.name,t.permissions=e.options,t.tabs=e.blueprint.tabs,t.options=function(e){t.$api.pages.options(t.page.id).then((function(t){e(t)}))},t.$store.dispatch("breadcrumb",t.$api.pages.breadcrumb(e)),t.$store.dispatch("title",t.page.title),t.$store.dispatch("content/create",{id:"pages/"+t.page.id,api:t.$api.pages.link(t.page.id),content:t.page.content})})).catch((function(e){t.issue=e}))},onTab:function(t){this.tab=t},update:function(){this.fetch(),this.$emit("model.update")}}},Ym=Vm,Wm=(n("202d"),Object(y["a"])(Ym,Hm,Km,!1,null,null,null)),Jm=Wm.exports,Gm=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-view",{staticClass:"k-settings-view"},[n("k-header",[t._v("\n "+t._s(t.$t("view.settings"))+"\n ")]),n("section",{staticClass:"k-system-info"},[n("header",[n("k-headline",[t._v("Kirby")])],1),n("ul",{staticClass:"k-system-info-box"},[n("li",[n("dl",[n("dt",[t._v(t._s(t.$t("license")))]),n("dd",[t.license?[t._v("\n "+t._s(t.license)+"\n ")]:n("p",[n("strong",{staticClass:"k-system-unregistered"},[t._v(t._s(t.$t("license.unregistered")))])])],2)])]),n("li",[n("dl",[n("dt",[t._v(t._s(t.$t("version")))]),n("dd",[t._v(t._s(t.$store.state.system.info.version))])])])])]),t.multilang?n("section",{staticClass:"k-languages"},[t.languages.length>0?[n("section",{staticClass:"k-languages-section"},[n("header",[n("k-headline",[t._v(t._s(t.$t("languages.default")))])],1),n("k-collection",{attrs:{items:t.defaultLanguage},on:{action:t.action}})],1),n("section",{staticClass:"k-languages-section"},[n("header",[n("k-headline",[t._v(t._s(t.$t("languages.secondary")))]),n("k-button",{attrs:{icon:"add"},on:{click:function(e){return t.$refs.create.open()}}},[t._v(t._s(t.$t("language.create")))])],1),t.translations.length?n("k-collection",{attrs:{items:t.translations},on:{action:t.action}}):n("k-empty",{attrs:{icon:"globe"},on:{click:function(e){return t.$refs.create.open()}}},[t._v(t._s(t.$t("languages.secondary.empty")))])],1)]:0===t.languages.length?[n("header",[n("k-headline",[t._v(t._s(t.$t("languages")))]),n("k-button",{attrs:{icon:"add"},on:{click:function(e){return t.$refs.create.open()}}},[t._v(t._s(t.$t("language.create")))])],1),n("k-empty",{attrs:{icon:"globe"},on:{click:function(e){return t.$refs.create.open()}}},[t._v(t._s(t.$t("languages.empty")))])]:t._e(),n("k-language-create-dialog",{ref:"create",on:{success:t.fetch}}),n("k-language-update-dialog",{ref:"update",on:{success:t.fetch}}),n("k-language-remove-dialog",{ref:"remove",on:{success:t.fetch}})],2):t._e()],1)},Zm=[],Xm={data:function(){return{languages:[]}},computed:{defaultLanguage:function(){return this.languages.filter((function(t){return t.default}))},multilang:function(){return this.$store.state.system.info.multilang},license:function(){return this.$store.state.system.info.license},translations:function(){return this.languages.filter((function(t){return!1===t.default}))}},created:function(){this.$store.dispatch("content/current",null),this.$store.dispatch("title",this.$t("view.settings")),this.$store.dispatch("breadcrumb",[]),this.fetch()},methods:{fetch:function(){var t=this;!0===this.multilang?this.$api.get("languages").then((function(e){t.languages=e.data.map((function(n){return{id:n.code,default:n.default,icon:{type:"globe",back:"black"},image:!0,text:n.name,info:n.code,link:function(){t.$refs.update.open(n.code)},options:[{icon:"edit",text:t.$t("edit"),click:"update"},{icon:"trash",text:t.$t("delete"),disabled:n.default&&1!==e.data.length,click:"remove"}]}}))})):this.languages=[]},action:function(t,e){switch(e){case"update":this.$refs.update.open(t.id);break;case"remove":this.$refs.remove.open(t.id);break}}}},Qm=Xm,tg=(n("9bd5"),Object(y["a"])(Qm,Gm,Zm,!1,null,null,null)),eg=tg.exports,ng=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.issue?n("k-error-view",[t._v("\n "+t._s(t.issue.message)+"\n")]):n("k-view",{key:"site-view",staticClass:"k-site-view",attrs:{"data-locked":t.isLocked}},[n("k-header",{attrs:{tabs:t.tabs,tab:t.tab,editable:t.permissions.changeTitle&&!t.isLocked},on:{edit:function(e){return t.action("rename")}}},[t._v("\n "+t._s(t.site.title)+"\n "),n("k-button-group",{attrs:{slot:"left"},slot:"left"},[n("k-button",{attrs:{responsive:!0,link:t.site.previewUrl,target:"_blank",icon:"open"}},[t._v("\n "+t._s(t.$t("open"))+"\n ")]),n("k-languages-dropdown")],1)],1),t.site.url?n("k-tabs",{ref:"tabs",attrs:{tabs:t.tabs,blueprint:t.site.blueprint.name,parent:"site"},on:{tab:function(e){t.tab=e}}}):t._e(),n("k-site-rename-dialog",{ref:"rename",on:{success:t.fetch}})],1)},ig=[],rg={data:function(){return{site:{title:null,url:null},issue:null,tab:null,tabs:[],options:null,permissions:{changeTitle:!0}}},computed:{isLocked:function(){return null!==this.$store.state.content.status.lock},language:function(){return this.$store.state.languages.current}},watch:{language:function(){this.fetch()}},created:function(){this.fetch()},methods:{fetch:function(){var t=this;this.$api.site.get({view:"panel"}).then((function(e){t.site=e,t.tabs=e.blueprint.tabs,t.permissions=e.options,t.options=function(e){t.$api.site.options().then((function(t){e(t)}))},t.$store.dispatch("breadcrumb",[]),t.$store.dispatch("title",null),t.$store.dispatch("content/create",{id:"site",api:"site",content:e.content})})).catch((function(e){t.issue=e}))},action:function(t){switch(t){case"languages":this.$refs.languages.open();break;case"rename":this.$refs.rename.open();break;default:this.$store.dispatch("notification/error",this.$t("notification.notImplemented"));break}}}},sg=rg,ag=Object(y["a"])(sg,ng,ig,!1,null,null,null),og=ag.exports,ug=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.issue?n("k-error-view",[t._v("\n "+t._s(t.issue.message)+"\n")]):n("k-view",{staticClass:"k-users-view"},[n("k-header",[t._v("\n "+t._s(t.$t("view.users"))+"\n "),n("k-button-group",{attrs:{slot:"left"},slot:"left"},[n("k-button",{attrs:{disabled:!1===t.$permissions.users.create,icon:"add"},on:{click:function(e){return t.$refs.create.open()}}},[t._v(t._s(t.$t("user.create")))])],1),n("k-button-group",{attrs:{slot:"right"},slot:"right"},[n("k-dropdown",[n("k-button",{attrs:{responsive:!0,icon:"funnel"},on:{click:function(e){return t.$refs.roles.toggle()}}},[t._v("\n "+t._s(t.$t("role"))+": "+t._s(t.role?t.role.text:t.$t("role.all"))+"\n ")]),n("k-dropdown-content",{ref:"roles",attrs:{align:"right"}},[n("k-dropdown-item",{attrs:{icon:"bolt"},on:{click:function(e){return t.filter(!1)}}},[t._v("\n "+t._s(t.$t("role.all"))+"\n ")]),n("hr"),t._l(t.roles,(function(e){return n("k-dropdown-item",{key:e.value,attrs:{icon:"bolt"},on:{click:function(n){return t.filter(e)}}},[t._v("\n "+t._s(e.text)+"\n ")])}))],2)],1)],1)],1),t.users.length>0?[n("k-collection",{attrs:{items:t.users,pagination:t.pagination},on:{paginate:t.paginate,action:t.action}})]:0===t.total?[n("k-empty",{attrs:{icon:"users"}},[t._v(t._s(t.$t("role.empty")))])]:t._e(),n("k-user-create-dialog",{ref:"create",on:{success:t.fetch}}),n("k-user-email-dialog",{ref:"email",on:{success:t.fetch}}),n("k-user-language-dialog",{ref:"language",on:{success:t.fetch}}),n("k-user-password-dialog",{ref:"password"}),n("k-user-remove-dialog",{ref:"remove",on:{success:t.fetch}}),n("k-user-rename-dialog",{ref:"rename",on:{success:t.fetch}}),n("k-user-role-dialog",{ref:"role",on:{success:t.fetch}})],2)},lg=[],cg={data:function(){return{page:1,limit:20,total:null,users:[],roles:[],issue:null}},computed:{pagination:function(){return{page:this.page,limit:this.limit,total:this.total}},role:function(){var t=this,e=null;return this.$route.params.role&&this.roles.forEach((function(n){n.value===t.$route.params.role&&(e=n)})),e}},watch:{$route:function(){this.fetch()}},created:function(){var t=this;this.$store.dispatch("content/current",null),this.$api.roles.options().then((function(e){t.roles=e,t.fetch()}))},methods:{fetch:function(){var t=this;this.$store.dispatch("title",this.$t("view.users"));var e={paginate:{page:this.page,limit:this.limit},sortBy:"username asc"};this.role&&(e.filterBy=[{field:"role",operator:"==",value:this.role.value}]),this.$api.users.list(e).then((function(e){t.users=e.data.map((function(e){var n={id:e.id,icon:{type:"user",back:"black"},text:e.name||e.email,info:e.role.title,link:"/users/"+e.id,options:function(n){t.$api.users.options(e.id,"list").then((function(t){return n(t)})).catch((function(e){t.$store.dispatch("notification/error",e)}))},image:!0};return e.avatar&&(n.image={url:e.avatar.url,cover:!0}),n})),t.role?t.$store.dispatch("breadcrumb",[{link:"/users/role/"+t.role.value,label:t.$t("role")+": "+t.role.text}]):t.$store.dispatch("breadcrumb",[]),t.total=e.pagination.total})).catch((function(e){t.issue=e}))},paginate:function(t){this.page=t.page,this.limit=t.limit,this.fetch()},action:function(t,e){switch(e){case"edit":this.$go("/users/"+t.id);break;case"email":this.$refs.email.open(t.id);break;case"role":this.$refs.role.open(t.id);break;case"rename":this.$refs.rename.open(t.id);break;case"password":this.$refs.password.open(t.id);break;case"language":this.$refs.language.open(t.id);break;case"remove":this.$refs.remove.open(t.id);break}},filter:function(t){!1===t?this.$go("/users"):this.$go("/users/role/"+t.value),this.$refs.roles.close()}}},pg=cg,dg=Object(y["a"])(pg,ug,lg,!1,null,null,null),fg=dg.exports,hg=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.issue?n("k-error-view",[t._v("\n "+t._s(t.issue.message)+"\n")]):t.ready?n("div",{staticClass:"k-user-view",attrs:{"data-locked":t.isLocked}},[n("div",{staticClass:"k-user-profile"},[n("k-view",[t.avatar?[n("k-dropdown",[n("k-button",{staticClass:"k-user-view-image",attrs:{tooltip:t.$t("avatar"),disabled:t.isLocked},on:{click:function(e){return t.$refs.picture.toggle()}}},[t.avatar?n("k-image",{attrs:{cover:!0,src:t.avatar,ratio:"1/1"}}):t._e()],1),n("k-dropdown-content",{ref:"picture"},[n("k-dropdown-item",{attrs:{icon:"upload"},on:{click:function(e){return t.$refs.upload.open()}}},[t._v("\n "+t._s(t.$t("change"))+"\n ")]),n("k-dropdown-item",{attrs:{icon:"trash"},on:{click:function(e){return t.action("picture.delete")}}},[t._v("\n "+t._s(t.$t("delete"))+"\n ")])],1)],1)]:[n("k-button",{staticClass:"k-user-view-image",attrs:{tooltip:t.$t("avatar")},on:{click:function(e){return t.$refs.upload.open()}}},[n("k-icon",{attrs:{type:"user"}})],1)],n("k-button-group",[n("k-button",{attrs:{disabled:!t.permissions.changeEmail||t.isLocked,icon:"email"},on:{click:function(e){return t.action("email")}}},[t._v(t._s(t.$t("email"))+": "+t._s(t.user.email))]),n("k-button",{attrs:{disabled:!t.permissions.changeRole||t.isLocked,icon:"bolt"},on:{click:function(e){return t.action("role")}}},[t._v(t._s(t.$t("role"))+": "+t._s(t.user.role.title))]),n("k-button",{attrs:{disabled:!t.permissions.changeLanguage||t.isLocked,icon:"globe"},on:{click:function(e){return t.action("language")}}},[t._v(t._s(t.$t("language"))+": "+t._s(t.user.language))])],1)],2)],1),n("k-view",[n("k-header",{attrs:{editable:t.permissions.changeName&&!t.isLocked,tabs:t.tabs,tab:t.tab},on:{edit:function(e){return t.action("rename")}}},[t.user.name&&0!==t.user.name.length?[t._v(t._s(t.user.name))]:n("span",{staticClass:"k-user-name-placeholder"},[t._v(t._s(t.$t("name"))+" …")]),n("k-button-group",{attrs:{slot:"left"},slot:"left"},[n("k-dropdown",[n("k-button",{attrs:{disabled:t.isLocked,icon:"cog"},on:{click:function(e){return t.$refs.settings.toggle()}}},[t._v("\n "+t._s(t.$t("settings"))+"\n ")]),n("k-dropdown-content",{ref:"settings",attrs:{options:t.options},on:{action:t.action}})],1),n("k-languages-dropdown")],1),t.user.id&&"User"===t.$route.name?n("k-prev-next",{attrs:{slot:"right",prev:t.prev,next:t.next},slot:"right"}):t._e()],2),t.user&&t.tabs.length?n("k-tabs",{key:t.tabsKey,ref:"tabs",attrs:{parent:"users/"+t.user.id,blueprint:t.user.blueprint.name,tabs:t.tabs},on:{tab:function(e){t.tab=e}}}):t.ready?n("k-box",{attrs:{text:t.$t("user.blueprint",{role:t.user.role.name}),theme:"info"}}):t._e(),n("k-user-email-dialog",{ref:"email",on:{success:t.fetch}}),n("k-user-language-dialog",{ref:"language",on:{success:t.fetch}}),n("k-user-password-dialog",{ref:"password"}),n("k-user-remove-dialog",{ref:"remove"}),n("k-user-rename-dialog",{ref:"rename",on:{success:t.fetch}}),n("k-user-role-dialog",{ref:"role",on:{success:t.fetch}}),n("k-upload",{ref:"upload",attrs:{url:t.uploadApi,multiple:!1,accept:"image/*"},on:{success:t.uploadedAvatar}})],1)],1):t._e()},mg=[],gg={mixins:[ym],props:{id:{type:[Boolean,String],required:!0}},data:function(){return{tab:null,tabs:[],ready:!1,user:{role:{name:null},name:null,language:null,prev:null,next:null},permissions:{changeEmail:!0,changeName:!0,changeLanguage:!0,changeRole:!0},issue:null,avatar:null,options:null}},computed:{language:function(){return this.$store.state.languages.current},next:function(){if(this.user.next)return{link:this.$api.users.link(this.user.next.id),tooltip:this.user.next.name}},prev:function(){if(this.user.prev)return{link:this.$api.users.link(this.user.prev.id),tooltip:this.user.prev.name}},tabsKey:function(){return"user-"+this.user.id+"-tabs"},uploadApi:function(){return B.api+"/users/"+this.user.id+"/avatar"}},watch:{"$route.name":{handler:function(t){"Account"===t&&this.$store.dispatch("breadcrumb",[])},immediate:!0},language:function(){this.fetch()},id:function(){this.fetch()}},methods:{action:function(t){var e=this;return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:n.t0=t,n.next="email"===n.t0?3:"language"===n.t0?5:"password"===n.t0?7:"picture.delete"===n.t0?9:"remove"===n.t0?14:"rename"===n.t0?16:"role"===n.t0?18:20;break;case 3:return e.$refs.email.open(e.user.id),n.abrupt("break",21);case 5:return e.$refs.language.open(e.user.id),n.abrupt("break",21);case 7:return e.$refs.password.open(e.user.id),n.abrupt("break",21);case 9:return n.next=11,e.$api.users.deleteAvatar(e.id);case 11:return e.avatar=null,e.$store.dispatch("notification/success",":)"),n.abrupt("break",21);case 14:return e.$refs.remove.open(e.user.id),n.abrupt("break",21);case 16:return e.$refs.rename.open(e.user.id),n.abrupt("break",21);case 18:return e.$refs.role.open(e.user.id),n.abrupt("break",21);case 20:e.$store.dispatch("notification/error","Not yet implemented");case 21:case"end":return n.stop()}}),n)})))()},fetch:function(){var t=this;return Object(j["a"])(regeneratorRuntime.mark((function e(){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:if(t.id){e.next=2;break}return e.abrupt("return");case 2:return e.prev=2,e.next=5,t.$api.users.get(t.id,{view:"panel"});case 5:t.user=e.sent,t.tabs=t.user.blueprint.tabs,t.ready=!0,t.permissions=t.user.options,t.options=function(){var e=Object(j["a"])(regeneratorRuntime.mark((function e(n){var i;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.next=2,t.$api.users.options(t.user.id);case 2:i=e.sent,n(i);case 4:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}(),t.user.avatar?t.avatar=t.user.avatar.url:t.avatar=null,"User"===t.$route.name&&t.$store.dispatch("breadcrumb",t.$api.users.breadcrumb(t.user)),t.$store.dispatch("title",t.user.name||t.user.email),t.$store.dispatch("content/create",{id:"users/"+t.user.id,api:t.$api.users.link(t.user.id),content:t.user.content}),e.next=19;break;case 16:e.prev=16,e.t0=e["catch"](2),t.issue=e.t0;case 19:case"end":return e.stop()}}),e,null,[[2,16]])})))()},uploadedAvatar:function(){this.$store.dispatch("notification/success",":)"),this.fetch()}}},bg=gg,vg=(n("bd96"),Object(y["a"])(bg,hg,mg,!1,null,null,null)),kg=vg.exports;F["a"].component("k-dialog",yt),F["a"].component("k-error-dialog",St),F["a"].component("k-file-rename-dialog",Dt),F["a"].component("k-file-remove-dialog",Lt),F["a"].component("k-files-dialog",Vt),F["a"].component("k-form-dialog",Xt),F["a"].component("k-language-create-dialog",re),F["a"].component("k-language-remove-dialog",ce),F["a"].component("k-language-update-dialog",be),F["a"].component("k-page-create-dialog",we),F["a"].component("k-page-duplicate-dialog",Ee),F["a"].component("k-page-rename-dialog",Fe),F["a"].component("k-page-remove-dialog",Be),F["a"].component("k-page-status-dialog",Ye),F["a"].component("k-page-template-dialog",Qe),F["a"].component("k-page-url-dialog",an),F["a"].component("k-pages-dialog",dn),F["a"].component("k-remove-dialog",vn),F["a"].component("k-site-rename-dialog",_n),F["a"].component("k-text-dialog",Cn),F["a"].component("k-user-create-dialog",An),F["a"].component("k-user-email-dialog",Mn),F["a"].component("k-user-language-dialog",Vn),F["a"].component("k-user-password-dialog",Xn),F["a"].component("k-user-remove-dialog",ri),F["a"].component("k-user-rename-dialog",ci),F["a"].component("k-user-role-dialog",gi),F["a"].component("k-users-dialog",_i),F["a"].component("k-calendar",Ai),F["a"].component("k-counter",Mi),F["a"].component("k-autocomplete",Ci),F["a"].component("k-form",Vi),F["a"].component("k-form-buttons",Qi),F["a"].component("k-form-indicator",sr),F["a"].component("k-field",pr),F["a"].component("k-fieldset",br),F["a"].component("k-input",wr),F["a"].component("k-upload",Er),F["a"].component("k-checkbox-input",qr),F["a"].component("k-checkboxes-input",Ur),F["a"].component("k-date-input",Wr),F["a"].component("k-datetime-input",ts),F["a"].component("k-email-input",ps),F["a"].component("k-multiselect-input",bs),F["a"].component("k-number-input",ws),F["a"].component("k-password-input",Ss),F["a"].component("k-radio-input",Ls),F["a"].component("k-range-input",Ds),F["a"].component("k-select-input",Ks),F["a"].component("k-tags-input",Zs),F["a"].component("k-tel-input",ea),F["a"].component("k-text-input",os),F["a"].component("k-textarea-input",oa),F["a"].component("k-time-input",fa),F["a"].component("k-toggle-input",ka),F["a"].component("k-url-input",wa),F["a"].component("k-checkboxes-field",Ea),F["a"].component("k-date-field",Ba),F["a"].component("k-email-field",Fa),F["a"].component("k-files-field",Wa),F["a"].component("k-gap-field",Qa),F["a"].component("k-headline-field",so),F["a"].component("k-info-field",po),F["a"].component("k-line-field",bo),F["a"].component("k-multiselect-field",wo),F["a"].component("k-number-field",Eo),F["a"].component("k-pages-field",Bo),F["a"].component("k-password-field",Fo),F["a"].component("k-radio-field",Yo),F["a"].component("k-range-field",Qo),F["a"].component("k-select-field",su),F["a"].component("k-structure-field",pu),F["a"].component("k-tags-field",bu),F["a"].component("k-text-field",Eu),F["a"].component("k-textarea-field",Bu),F["a"].component("k-tel-field",wu),F["a"].component("k-time-field",Fu),F["a"].component("k-toggle-field",Yu),F["a"].component("k-url-field",Qu),F["a"].component("k-users-field",sl),F["a"].component("k-toolbar",dl),F["a"].component("k-toolbar-email-dialog",vl),F["a"].component("k-toolbar-link-dialog",xl),F["a"].component("k-email-field-preview",Vl),F["a"].component("k-files-field-preview",ql),F["a"].component("k-pages-field-preview",Xl),F["a"].component("k-toggle-field-preview",rc),F["a"].component("k-url-field-preview",Ul),F["a"].component("k-users-field-preview",cc),F["a"].component("k-bar",mc),F["a"].component("k-box",yc),F["a"].component("k-card",Sc),F["a"].component("k-cards",Lc),F["a"].component("k-collection",Dc),F["a"].component("k-column",Kc),F["a"].component("k-dropzone",Zc),F["a"].component("k-empty",ip),F["a"].component("k-file-preview",lp),F["a"].component("k-grid",mp),F["a"].component("k-header",yp),F["a"].component("k-list",Sp),F["a"].component("k-list-item",Lp),F["a"].component("k-tabs",Dp),F["a"].component("k-view",Kp),F["a"].component("k-draggable",Qp),F["a"].component("k-error-boundary",id),F["a"].component("k-headline",ld),F["a"].component("k-icon",md),F["a"].component("k-image",yd),F["a"].component("k-progress",Sd),F["a"].component("k-sort-handle",Id),F["a"].component("k-text",Pd),F["a"].component("k-button",Hd),F["a"].component("k-button-disabled",Gd),F["a"].component("k-button-group",ef),F["a"].component("k-button-link",uf),F["a"].component("k-button-native",mf),F["a"].component("k-dropdown",$f),F["a"].component("k-dropdown-content",Sf),F["a"].component("k-dropdown-item",Lf),F["a"].component("k-languages-dropdown",Kf),F["a"].component("k-link",Df),F["a"].component("k-pagination",Zf),F["a"].component("k-prev-next",ih),F["a"].component("k-search",lh),F["a"].component("k-tag",mh),F["a"].component("k-topbar",_h),F["a"].component("k-sections",Ch),F["a"].component("k-info-section",Bh),F["a"].component("k-pages-section",Uh),F["a"].component("k-files-section",Wh),F["a"].component("k-fields-section",tm),F["a"].component("k-browser-view",om),F["a"].component("k-custom-view",fm),F["a"].component("k-error-view",vm),F["a"].component("k-file-view",Om),F["a"].component("k-installation-view",Tm),F["a"].component("k-login-view",zm),F["a"].component("k-page-view",Jm),F["a"].component("k-settings-view",eg),F["a"].component("k-site-view",og),F["a"].component("k-users-view",fg),F["a"].component("k-user-view",kg);var $g=n("2f62"),yg=n("3835"),_g=function(t,e){localStorage.setItem("kirby$content$"+t,JSON.stringify(e))},wg={namespaced:!0,state:{current:null,models:{},status:{enabled:!0,lock:null,unlock:null}},getters:{exists:function(t){return function(e){return t.models.hasOwnProperty(e)}},hasChanges:function(t,e){return function(t){var n=e.model(t).changes;return Object.keys(n).length>0}},isCurrent:function(t){return function(e){return t.current===e}},id:function(t,e,n){return function(e){return e=e||t.current,n.languages.current?e+"/"+n.languages.current.code:e}},model:function(t,e){return function(n){return n=n||t.current,!0===e.exists(n)?t.models[n]:{api:null,originals:{},values:{},changes:{}}}},originals:function(t,e){return function(t){return it(e.model(t).originals)}},values:function(t,e){return function(t){return Object(I["a"])(Object(I["a"])({},e.originals(t)),e.changes(t))}},changes:function(t,e){return function(t){return it(e.model(t).changes)}}},mutations:{CREATE:function(t,e){var n=Object(yg["a"])(e,2),i=n[0],r=n[1];if(!r)return!1;var s=t.models[i]?t.models[i].changes:r.changes;F["a"].set(t.models,i,{api:r.api,originals:r.originals,changes:s||{}})},CURRENT:function(t,e){t.current=e},LOCK:function(t,e){F["a"].set(t.status,"lock",e)},MOVE:function(t,e){var n=Object(yg["a"])(e,2),i=n[0],r=n[1],s=it(t.models[i]);F["a"].delete(t.models,i),F["a"].set(t.models,r,s);var a=localStorage.getItem("kirby$content$"+i);localStorage.removeItem("kirby$content$"+i),localStorage.setItem("kirby$content$"+r,a)},REMOVE:function(t,e){F["a"].delete(t.models,e),localStorage.removeItem("kirby$content$"+e)},REVERT:function(t,e){t.models[e]&&(F["a"].set(t.models[e],"changes",{}),localStorage.removeItem("kirby$content$"+e))},STATUS:function(t,e){F["a"].set(t.status,"enabled",e)},UNLOCK:function(t,e){e&&F["a"].set(t.models[t.current],"changes",{}),F["a"].set(t.status,"unlock",e)},UPDATE:function(t,e){var n=Object(yg["a"])(e,3),i=n[0],r=n[1],s=n[2];if(!t.models[i])return!1;s=it(s);var a=JSON.stringify(s),o=JSON.stringify(t.models[i].originals[r]);o===a?F["a"].delete(t.models[i].changes,r):F["a"].set(t.models[i].changes,r,s),_g(i,{api:t.models[i].api,originals:t.models[i].originals,changes:t.models[i].changes})}},actions:{init:function(t){Object.keys(localStorage).filter((function(t){return t.startsWith("kirby$content$")})).map((function(t){return t.split("kirby$content$")[1]})).forEach((function(e){var n=localStorage.getItem("kirby$content$"+e);t.commit("CREATE",[e,JSON.parse(n)])})),Object.keys(localStorage).filter((function(t){return t.startsWith("kirby$form$")})).map((function(t){return t.split("kirby$form$")[1]})).forEach((function(e){var n=localStorage.getItem("kirby$form$"+e),i=null;try{i=JSON.parse(n)}catch(s){}if(!i||!i.api)return localStorage.removeItem("kirby$form$"+e),!1;var r={api:i.api,originals:i.originals,changes:i.values};t.commit("CREATE",[e,r]),_g(e,r),localStorage.removeItem("kirby$form$"+e)}))},create:function(t,e){e.id=t.getters.id(e.id),(e.id.startsWith("pages/")||e.id.startsWith("site"))&&delete e.content.title;var n={api:e.api,originals:it(e.content),changes:{}};F["a"].$api.get(e.api+"/unlock").then((function(n){!0===n.supported&&!0===n.unlocked&&t.commit("UNLOCK",t.state.models[e.id].changes)})).catch((function(){})),t.commit("CREATE",[e.id,n]),t.dispatch("current",e.id)},current:function(t,e){t.commit("CURRENT",e)},disable:function(t){t.commit("STATUS",!1)},enable:function(t){t.commit("STATUS",!0)},lock:function(t,e){t.commit("LOCK",e)},move:function(t,e){var n=Object(yg["a"])(e,2),i=n[0],r=n[1];i=t.getters.id(i),r=t.getters.id(r),t.commit("MOVE",[i,r])},remove:function(t,e){t.commit("REMOVE",e),t.getters.isCurrent(e)&&t.commit("CURRENT",null)},revert:function(t,e){e=e||t.state.current,t.commit("REVERT",e)},save:function(t,e){if(e=e||t.state.current,t.getters.isCurrent(e)&&!1===t.state.status.enabled)return!1;t.dispatch("disable");var n=t.getters.model(e),i=Object(I["a"])(Object(I["a"])({},n.originals),n.changes);return F["a"].$api.patch(n.api,i).then((function(){t.commit("CREATE",[e,Object(I["a"])(Object(I["a"])({},n),{},{originals:i})]),t.dispatch("revert",e),t.dispatch("enable")})).catch((function(e){throw t.dispatch("enable"),e}))},unlock:function(t,e){t.commit("UNLOCK",e)},update:function(t,e){var n=Object(yg["a"])(e,3),i=n[0],r=n[1],s=n[2];s=s||t.state.current,t.commit("UPDATE",[s,i,r])}}},xg={namespaced:!0,state:{instance:null,clock:0,step:5,beats:[]},mutations:{ADD:function(t,e){t.beats.push(e)},CLEAR:function(t){clearInterval(t.instance),t.clock=0},CLOCK:function(t){t.clock+=t.step},INITIALIZE:function(t,e){t.instance=e},REMOVE:function(t,e){var n=t.beats.map((function(t){return t.handler})).indexOf(e);-1!==n&&F["a"].delete(t.beats,n)}},actions:{add:function(t,e){e={handler:e[0]||e,interval:e[1]||t.state.step},e.handler(),t.commit("ADD",e),1===t.state.beats.length&&t.dispatch("run")},clear:function(t){t.commit("CLEAR")},remove:function(t,e){t.commit("REMOVE",e),t.state.beats.length<1&&t.commit("CLEAR")},run:function(t){t.commit("CLEAR"),t.commit("INITIALIZE",setInterval((function(){t.commit("CLOCK"),t.state.beats.forEach((function(e){t.state.clock%e.interval===0&&e.handler()}))}),1e3*t.state.step))}}},Og={namespaced:!0,state:{all:[],current:null,default:null},mutations:{SET_ALL:function(t,e){t.all=e.map((function(t){return{code:t.code,default:t.default,direction:t.direction,locale:t.locale,name:t.name,rules:t.rules,url:t.url}}))},SET_CURRENT:function(t,e){t.current=e,e&&e.code&&localStorage.setItem("kirby$language",e.code)},SET_DEFAULT:function(t,e){t.default=e}},actions:{current:function(t,e){t.commit("SET_CURRENT",e)},install:function(t,e){var n=e.filter((function(t){return t.default}))[0];t.commit("SET_ALL",e),t.commit("SET_DEFAULT",n);var i=localStorage.getItem("kirby$language");if(i){var r=e.filter((function(t){return t.code===i}))[0];if(r)return void t.dispatch("current",r)}t.dispatch("current",n||e[0]||null)},load:function(t){return Object(j["a"])(regeneratorRuntime.mark((function e(){var n;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.next=2,F["a"].$api.languages.list();case 2:n=e.sent,t.dispatch("install",n.data);case 4:case"end":return e.stop()}}),e)})))()}}},jg={timer:null,namespaced:!0,state:{type:null,message:null,details:null,timeout:null},mutations:{SET:function(t,e){t.type=e.type,t.message=e.message,t.details=e.details,t.timeout=e.timeout},UNSET:function(t){t.type=null,t.message=null,t.details=null,t.timeout=null}},actions:{close:function(t){clearTimeout(this.timer),t.commit("UNSET")},open:function(t,e){t.dispatch("close"),t.commit("SET",e),e.timeout&&(this.timer=setTimeout((function(){t.dispatch("close")}),e.timeout))},success:function(t,e){"string"===typeof e&&(e={message:e}),t.dispatch("open",Object(I["a"])({type:"success",timeout:4e3},e))},error:function(t,e){"string"===typeof e&&(e={message:e}),t.dispatch("open",Object(I["a"])({type:"error"},e))}}},Sg={namespaced:!0,state:{info:{title:null}},mutations:{SET_INFO:function(t,e){t.info=e},SET_LICENSE:function(t,e){t.info.license=e},SET_TITLE:function(t,e){t.info.title=e}},actions:{load:function(t,e){return Object(j["a"])(regeneratorRuntime.mark((function n(){var i;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:if(e||!t.state.info.isReady||!t.rootState.user.current){n.next=2;break}return n.abrupt("return",new Promise((function(e){e(t.state.info)})));case 2:return n.prev=2,n.next=5,F["a"].$api.system.get();case 5:return i=n.sent,t.commit("SET_INFO",Object(I["a"])({isReady:i.isInstalled&&i.isOk},i)),i.languages&&t.dispatch("languages/install",i.languages,{root:!0}),t.dispatch("translation/install",i.translation,{root:!0}),t.dispatch("translation/activate",i.translation.id,{root:!0}),i.user&&t.dispatch("user/current",i.user,{root:!0}),n.abrupt("return",t.state.info);case 14:n.prev=14,n.t0=n["catch"](2),t.commit("SET_INFO",{isBroken:!0,error:n.t0.message});case 17:case"end":return n.stop()}}),n,null,[[2,14]])})))()},register:function(t,e){t.commit("SET_LICENSE",e)},title:function(t,e){t.commit("SET_TITLE",e)}}},Cg={namespaced:!0,state:{current:null,installed:[]},mutations:{SET_CURRENT:function(t,e){t.current=e},INSTALL:function(t,e){t.installed[e.id]=e}},actions:{load:function(t,e){return F["a"].$api.translations.get(e)},install:function(t,e){t.commit("INSTALL",e),F["a"].i18n.add(e.id,e.data)},activate:function(t,e){return Object(j["a"])(regeneratorRuntime.mark((function n(){var i,r;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:if(i=t.state.installed[e],i){n.next=8;break}return n.next=4,t.dispatch("load",e);case 4:return r=n.sent,t.dispatch("install",r),t.dispatch("activate",e),n.abrupt("return");case 8:F["a"].i18n.set(e),t.commit("SET_CURRENT",e),document.dir=i.direction,document.documentElement.lang=e;case 12:case"end":return n.stop()}}),n)})))()}}},Eg=n("8c4f"),Rg=function(t,e,n){Bg.dispatch("system/load").then((function(){var e=Bg.state.user.current;if(!e)return Bg.dispatch("user/visit",t.path),Bg.dispatch("user/logout"),!1;var i=e.permissions.access;return!1===i.panel?(window.location.href=B.site,!1):!1===i[t.meta.view]?(Bg.dispatch("notification/error",{message:F["a"].i18n.translate("error.access.view")}),n(!1===i.site?"/account":"/")):void n()}))},Tg=[{path:"/",name:"Home",redirect:"/site"},{path:"/browser",name:"Browser",component:F["a"].component("k-browser-view"),meta:{outside:!0}},{path:"/login",component:F["a"].component("k-login-view"),meta:{outside:!0}},{path:"/logout",beforeEnter:function(){Object.keys(localStorage).forEach((function(t){t.startsWith("kirby$content$")&&localStorage.removeItem(t)})),Bg.dispatch("user/logout")},meta:{outside:!0}},{path:"/installation",component:F["a"].component("k-installation-view"),meta:{outside:!0}},{path:"/site",name:"Site",meta:{view:"site"},component:F["a"].component("k-site-view"),beforeEnter:Rg},{path:"/site/files/:filename",name:"SiteFile",meta:{view:"site"},component:F["a"].component("k-file-view"),beforeEnter:Rg,props:function(t){return{path:"site",filename:t.params.filename}}},{path:"/pages/:path/files/:filename",name:"PageFile",meta:{view:"site"},component:F["a"].component("k-file-view"),beforeEnter:Rg,props:function(t){return{path:"pages/"+t.params.path,filename:t.params.filename}}},{path:"/users/:path/files/:filename",name:"UserFile",meta:{view:"users"},component:F["a"].component("k-file-view"),beforeEnter:Rg,props:function(t){return{path:"users/"+t.params.path,filename:t.params.filename}}},{path:"/pages/:path",name:"Page",meta:{view:"site"},component:F["a"].component("k-page-view"),beforeEnter:Rg,props:function(t){return{path:t.params.path}}},{path:"/settings",name:"Settings",meta:{view:"settings"},component:F["a"].component("k-settings-view"),beforeEnter:Rg},{path:"/users/role/:role",name:"UsersByRole",meta:{view:"users"},component:F["a"].component("k-users-view"),beforeEnter:Rg,props:function(t){return{role:t.params.role}}},{path:"/users",name:"Users",meta:{view:"users"},beforeEnter:Rg,component:F["a"].component("k-users-view")},{path:"/users/:id",name:"User",meta:{view:"users"},component:F["a"].component("k-user-view"),beforeEnter:Rg,props:function(t){return{id:t.params.id}}},{path:"/account",name:"Account",meta:{view:"account"},component:F["a"].component("k-user-view"),beforeEnter:Rg,props:function(){return{id:!!Bg.state.user.current&&Bg.state.user.current.id}}},{path:"/plugins/:id",name:"Plugin",meta:{view:"plugin"},props:function(t){return{plugin:t.params.id}},beforeEnter:Rg,component:F["a"].component("k-custom-view")},{path:"*",name:"NotFound",beforeEnter:function(t,e,n){n("/")}}];F["a"].use(Eg["a"]);var Ig=new Eg["a"]({mode:"history",routes:Tg,url:"/"===B.url?"":B.url});Ig.beforeEach((function(t,e,n){"Browser"!==t.name&&!1===im.all()&&n("/browser"),t.meta.outside||Bg.dispatch("user/visit",t.path),Bg.dispatch("view",t.meta.view),Bg.dispatch("content/lock",null),Bg.dispatch("content/unlock",null),Bg.dispatch("heartbeat/clear"),n()}));var Lg=Ig,Ag={namespaced:!0,state:{current:null,path:null},mutations:{SET_CURRENT:function(t,e){t.current=e,e&&e.permissions?(F["a"].prototype.$user=e,F["a"].prototype.$permissions=e.permissions):(F["a"].prototype.$user=null,F["a"].prototype.$permissions=null)},SET_PATH:function(t,e){t.path=e}},actions:{current:function(t,e){t.commit("SET_CURRENT",e)},email:function(t,e){t.commit("SET_CURRENT",Object(I["a"])(Object(I["a"])({},t.state.current),{},{email:e}))},language:function(t,e){t.dispatch("translation/activate",e,{root:!0}),t.commit("SET_CURRENT",Object(I["a"])(Object(I["a"])({},t.state.current),{},{language:e}))},load:function(t){return Object(j["a"])(regeneratorRuntime.mark((function e(){var n;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.next=2,F["a"].$api.auth.user();case 2:return n=e.sent,t.commit("SET_CURRENT",n),e.abrupt("return",n);case 5:case"end":return e.stop()}}),e)})))()},login:function(t,e){return Object(j["a"])(regeneratorRuntime.mark((function n(){var i;return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:return n.next=2,F["a"].$api.auth.login(e);case 2:return i=n.sent,t.commit("SET_CURRENT",i),t.dispatch("translation/activate",i.language,{root:!0}),Lg.push(t.state.path||"/"),n.abrupt("return",i);case 7:case"end":return n.stop()}}),n)})))()},logout:function(t,e){return Object(j["a"])(regeneratorRuntime.mark((function n(){return regeneratorRuntime.wrap((function(n){while(1)switch(n.prev=n.next){case 0:if(t.commit("SET_CURRENT",null),!e){n.next=4;break}return window.location.href=(window.panel.url||"")+"/login",n.abrupt("return");case 4:return n.prev=4,n.next=7,F["a"].$api.auth.logout();case 7:return n.prev=7,Lg.push("/login"),n.finish(7);case 10:case"end":return n.stop()}}),n,null,[[4,,7,10]])})))()},name:function(t,e){t.commit("SET_CURRENT",Object(I["a"])(Object(I["a"])({},t.state.current),{},{name:e}))},visit:function(t,e){t.commit("SET_PATH",e)}}};F["a"].use($g["a"]);var Bg=new $g["a"].Store({strict:!1,state:{breadcrumb:[],dialog:null,drag:null,isLoading:!1,search:!1,title:null,view:null},mutations:{SET_BREADCRUMB:function(t,e){t.breadcrumb=e},SET_DIALOG:function(t,e){t.dialog=e},SET_DRAG:function(t,e){t.drag=e},SET_SEARCH:function(t,e){!0===e&&(e={}),t.search=e},SET_TITLE:function(t,e){t.title=e},SET_VIEW:function(t,e){t.view=e},START_LOADING:function(t){t.isLoading=!0},STOP_LOADING:function(t){t.isLoading=!1}},actions:{breadcrumb:function(t,e){t.commit("SET_BREADCRUMB",e)},dialog:function(t,e){t.commit("SET_DIALOG",e)},drag:function(t,e){t.commit("SET_DRAG",e)},isLoading:function(t,e){t.commit(!0===e?"START_LOADING":"STOP_LOADING")},search:function(t,e){t.commit("SET_SEARCH",e)},title:function(t,e){var n;n=t.state.user.current?F["a"].$api.site.get(["title"]):new Promise((function(e){e(t.state.system.info)})),n.then((function(n){t.commit("SET_TITLE",e),t.dispatch("system/title",n.title),document.title=e||"",document.title+=null!==e?" | "+n.title:n.title}))},view:function(t,e){t.commit("SET_VIEW",e)}},modules:{content:wg,heartbeat:xg,languages:Og,notification:jg,system:Sg,translation:Cg,user:Ag}});F["a"].config.errorHandler=function(t){B.debug&&window.console.error(t),Bg.dispatch("notification/error",{message:t.message||"An error occurred. Please reload the Panel."})},window.panel=window.panel||{},window.panel.error=function(t,e){B.debug&&window.console.error(t+": "+e),Bg.dispatch("error",t+". See the console for more information.")};var qg=n("f2f3");F["a"].use(qg["a"].plugin,Bg);var Ng=n("19e9"),Pg=n.n(Ng),Dg=n("5a0c"),Mg=n.n(Dg),Fg=n("f906"),Ug=n.n(Fg);Mg.a.extend(Ug.a),F["a"].prototype.$library={autosize:Pg.a,dayjs:Mg.a};n("4fad");var zg={};for(var Hg in F["a"].options.components)zg[Hg]=F["a"].options.components[Hg];var Kg=function(t,e){e.template||e.render||e.extends?(e.extends&&"string"===typeof e.extends&&(e.extends=zg[e.extends],e.template&&(e.render=null)),e.mixins&&(e.mixins=e.mixins.map((function(t){return"string"===typeof t?zg[t]:t}))),zg[t]&&window.console.warn('Plugin is replacing "'.concat(t,'"')),F["a"].component(t,e)):Bg.dispatch("notification/error",'Neither template or render method provided nor extending a component when loading plugin component "'.concat(t,'". The component has not been registered.'))};Object.entries(window.panel.plugins.components).forEach((function(t){var e=Object(yg["a"])(t,2),n=e[0],i=e[1];Kg(n,i)})),Object.entries(window.panel.plugins.fields).forEach((function(t){var e=Object(yg["a"])(t,2),n=e[0],i=e[1];Kg(n,i)})),Object.entries(window.panel.plugins.sections).forEach((function(t){var e=Object(yg["a"])(t,2),n=e[0],i=e[1];Kg(n,Object(I["a"])(Object(I["a"])({},i),{},{mixins:[Th].concat(i.mixins||[])}))})),Object.entries(window.panel.plugins.views).forEach((function(t){var e=Object(yg["a"])(t,2),n=e[0],i=e[1];if(!i.component)return Bg.dispatch("notification/error",'No view component provided when loading view "'.concat(n,'". The view has not been registered.')),void delete window.panel.plugins.views[n];i.link="/plugins/"+n,void 0===i.icon&&(i.icon="page"),void 0===i.menu&&(i.menu=!0),window.panel.plugins.views[n]={link:i.link,icon:i.icon,menu:i.menu},F["a"].component("k-"+n+"-plugin-view",i.component)})),window.panel.plugins.use.forEach((function(t){F["a"].use(t)})),F["a"].config.productionTip=!1,F["a"].config.devtools=!0,F["a"].use(ft),F["a"].use(tt),F["a"].use(Q),F["a"].use(nt.a),F["a"].use(X,Bg),F["a"].prototype.$go=function(t){Lg.push(t).catch((function(t){if(t&&t.name&&"NavigationDuplicated"===t.name)return!0;throw t}))},new F["a"]({router:Lg,store:Bg,created:function(){var t=this;window.panel.app=this,window.panel.plugins.created.forEach((function(e){e(t)})),this.$store.dispatch("content/init")},render:function(t){return t(D)}}).$mount("#app")},"56d9":function(t,e,n){},"56ee":function(t,e,n){},"580a":function(t,e,n){"use strict";var i=n("df34"),r=n.n(i);r.a},5933:function(t,e,n){},"5aee":function(t,e,n){"use strict";var i=n("3523"),r=n.n(i);r.a},"5b23":function(t,e,n){"use strict";var i=n("b055"),r=n.n(i);r.a},"5c0b":function(t,e,n){"use strict";var i=n("64b0"),r=n.n(i);r.a},"5c8f":function(t,e,n){},"5d33":function(t,e,n){"use strict";var i=n("5e4b"),r=n.n(i);r.a},"5e4b":function(t,e,n){},6018:function(t,e,n){"use strict";var i=n("517f"),r=n.n(i);r.a},6233:function(t,e,n){},"64b0":function(t,e,n){},"64e4":function(t,e,n){"use strict";var i=n("d735"),r=n.n(i);r.a},6695:function(t,e,n){},"696b":function(t,e,n){"use strict";var i=n("af28"),r=n.n(i);r.a},"6a18":function(t,e,n){"use strict";var i=n("8e2b"),r=n.n(i);r.a},"6ab3":function(t,e,n){"use strict";var i=n("1aa8"),r=n.n(i);r.a},"6bcd":function(t,e,n){"use strict";var i=n("f99d"),r=n.n(i);r.a},"6f7b":function(t,e,n){"use strict";var i=n("7435"),r=n.n(i);r.a},"6fff":function(t,e,n){},"718c":function(t,e,n){"use strict";var i=n("ea6b"),r=n.n(i);r.a},"73f5":function(t,e,n){},7435:function(t,e,n){},7568:function(t,e,n){"use strict";var i=n("6695"),r=n.n(i);r.a},"756d":function(t,e,n){},7737:function(t,e,n){"use strict";var i=n("c366"),r=n.n(i);r.a},"7a7d":function(t,e,n){"use strict";var i=n("cbef"),r=n.n(i);r.a},"7d5d":function(t,e,n){"use strict";var i=n("3afb"),r=n.n(i);r.a},"7dc7":function(t,e,n){"use strict";var i=n("fa59"),r=n.n(i);r.a},"7e85":function(t,e,n){"use strict";var i=n("a925"),r=n.n(i);r.a},"7f6e":function(t,e,n){"use strict";var i=n("0f7a"),r=n.n(i);r.a},8370:function(t,e,n){},"862b":function(t,e,n){"use strict";var i=n("db66"),r=n.n(i);r.a},"863d":function(t,e,n){},"893d":function(t,e,n){"use strict";var i=n("56ee"),r=n.n(i);r.a},"8b71":function(t,e,n){},"8c28":function(t,e,n){"use strict";var i=n("6fff"),r=n.n(i);r.a},"8e2b":function(t,e,n){},9143:function(t,e,n){},"977f":function(t,e,n){"use strict";var i=n("c19c"),r=n.n(i);r.a},9799:function(t,e,n){"use strict";var i=n("f0c3"),r=n.n(i);r.a},"98a1":function(t,e,n){"use strict";var i=n("4c4a"),r=n.n(i);r.a},"9b86":function(t,e,n){},"9bd5":function(t,e,n){"use strict";var i=n("6233"),r=n.n(i);r.a},"9c45":function(t,e,n){},"9c80":function(t,e,n){},"9cce":function(t,e,n){},"9e1f":function(t,e,n){},"9e26":function(t,e,n){"use strict";var i=n("a101"),r=n.n(i);r.a},a101:function(t,e,n){},a134:function(t,e,n){"use strict";var i=n("bed0"),r=n.n(i);r.a},a14d:function(t,e,n){},a17f:function(t,e,n){},a567:function(t,e,n){"use strict";var i=n("34ba"),r=n.n(i);r.a},a5f3:function(t,e,n){"use strict";var i=n("f1ff"),r=n.n(i);r.a},a66d:function(t,e,n){"use strict";var i=n("2dc9"),r=n.n(i);r.a},a803:function(t,e,n){},a925:function(t,e,n){},abde:function(t,e,n){},ac27:function(t,e,n){"use strict";var i=n("f1ce"),r=n.n(i);r.a},af28:function(t,e,n){},b055:function(t,e,n){},b0af:function(t,e,n){},b0d6:function(t,e,n){"use strict";var i=n("3341"),r=n.n(i);r.a},b5d2:function(t,e,n){"use strict";var i=n("0656"),r=n.n(i);r.a},b746:function(t,e,n){"use strict";var i=n("5933"),r=n.n(i);r.a},b822:function(t,e,n){},b9d6:function(t,e,n){},ba8f:function(t,e,n){"use strict";var i=n("c235"),r=n.n(i);r.a},bb41:function(t,e,n){"use strict";var i=n("de06"),r=n.n(i);r.a},bbcc:function(t,e,n){},bd96:function(t,e,n){"use strict";var i=n("b0af"),r=n.n(i);r.a},bed0:function(t,e,n){},bf53:function(t,e,n){"use strict";var i=n("eb2d"),r=n.n(i);r.a},c119:function(t,e,n){"use strict";var i=n("315e"),r=n.n(i);r.a},c19c:function(t,e,n){},c235:function(t,e,n){},c366:function(t,e,n){},c429:function(t,e,n){},c4fb:function(t,e,n){},c7c8:function(t,e,n){"use strict";var i=n("abde"),r=n.n(i);r.a},c857:function(t,e,n){"use strict";var i=n("c4fb"),r=n.n(i);r.a},c9cb:function(t,e,n){"use strict";var i=n("bbcc"),r=n.n(i);r.a},cb8f:function(t,e,n){"use strict";var i=n("9b86"),r=n.n(i);r.a},cbef:function(t,e,n){},cc79:function(t,e,n){"use strict";var i=n("a14d"),r=n.n(i);r.a},cca8:function(t,e,n){"use strict";var i=n("15da"),r=n.n(i);r.a},d07b:function(t,e,n){},d0c1:function(t,e,n){"use strict";var i=n("56d9"),r=n.n(i);r.a},d221:function(t,e,n){"use strict";var i=n("8b71"),r=n.n(i);r.a},d2d1:function(t,e,n){},d343:function(t,e,n){},d34c:function(t,e,n){},d559:function(t,e,n){},d6fc:function(t,e,n){"use strict";var i=n("5c8f"),r=n.n(i);r.a},d735:function(t,e,n){},daa8:function(t,e,n){"use strict";var i=n("337f"),r=n.n(i);r.a},db66:function(t,e,n){},ddfd:function(t,e,n){"use strict";var i=n("b822"),r=n.n(i);r.a},de06:function(t,e,n){},df0d:function(t,e,n){"use strict";var i=n("9c80"),r=n.n(i);r.a},df34:function(t,e,n){},df71:function(t,e,n){},ea2e:function(t,e,n){},ea6b:function(t,e,n){},eb2d:function(t,e,n){},ee15:function(t,e,n){"use strict";var i=n("53cc"),r=n.n(i);r.a},f0c3:function(t,e,n){},f1ce:function(t,e,n){},f1ff:function(t,e,n){},f2c9:function(t,e,n){},f355:function(t,e,n){},f56d:function(t,e,n){"use strict";var i=n("9143"),r=n.n(i);r.a},f8a7:function(t,e,n){"use strict";var i=n("f2c9"),r=n.n(i);r.a},f95f:function(t,e,n){"use strict";var i=n("a803"),r=n.n(i);r.a},f99d:function(t,e,n){},fa59:function(t,e,n){},fa6a:function(t,e,n){"use strict";var i=n("ea2e"),r=n.n(i);r.a},fb36:function(t,e,n){},fc0f:function(t,e,n){"use strict";var i=n("c429"),r=n.n(i);r.a}}); \ No newline at end of file diff --git a/kirby/panel/dist/js/plugins.js b/kirby/panel/dist/js/plugins.js new file mode 100644 index 0000000..25d927b --- /dev/null +++ b/kirby/panel/dist/js/plugins.js @@ -0,0 +1,73 @@ + +window.panel = window.panel || {}; +window.panel.plugins = { + components: {}, + created: [], + fields: {}, + icons: {}, + sections: {}, + routes: [], + use: [], + views: {}, + thirdParty: {} +}; + +window.panel.plugin = function (plugin, parts) { + // Components + resolve(parts, "components", function (name, options) { + window.panel.plugins["components"][name] = options; + }); + + // Fields + resolve(parts, "fields", function (name, options) { + window.panel.plugins["fields"][`k-${name}-field`] = options; + }); + + // Icons + resolve(parts, "icons", function (name, options) { + window.panel.plugins["icons"][name] = options; + }); + + // Sections + resolve(parts, "sections", function (name, options) { + window.panel.plugins["sections"][`k-${name}-section`] = options; + }); + + // Vue.use + resolve(parts, "use", function (name, options) { + window.panel.plugins["use"].push(options); + }); + + // created callback + if (parts["created"]) { + window.panel.plugins["created"].push(parts["created"]); + } + + // Views + resolve(parts, "views", function (name, options) { + window.panel.plugins["views"][name] = options; + }); + + // Login + if (parts.login) { + window.panel.plugins.login = parts.login; + } + + // Third-party plugins + resolve(parts, "thirdParty", function(name, options) { + window.panel.plugins["thirdParty"][name] = options; + }); + +}; + +function resolve(object, type, callback) { + if (object[type]) { + + if (Object.entries) { + Object.entries(object[type]).forEach(function ([name, options]) { + callback(name, options); + }); + } + + } +} diff --git a/kirby/panel/dist/js/vendor.js b/kirby/panel/dist/js/vendor.js new file mode 100644 index 0000000..6236c78 --- /dev/null +++ b/kirby/panel/dist/js/vendor.js @@ -0,0 +1,30 @@ +(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-vendors"],{"00ee":function(t,e,n){var r=n("b622"),o=r("toStringTag"),i={};i[o]="z",t.exports="[object z]"===String(i)},"0234":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=Object.assign||function(t){for(var e=1;e0;(i>>>=1)&&(e+=e))1&i&&(n+=e);return n}},1276:function(t,e,n){"use strict";var r=n("d784"),o=n("44e7"),i=n("825a"),a=n("1d80"),s=n("4840"),c=n("8aa5"),u=n("50c4"),l=n("14c3"),f=n("9263"),d=n("d039"),p=[].push,h=Math.min,v=4294967295,g=!d((function(){return!RegExp(v,"y")}));r("split",2,(function(t,e,n){var r;return r="c"=="abbc".split(/(b)*/)[1]||4!="test".split(/(?:)/,-1).length||2!="ab".split(/(?:ab)*/).length||4!=".".split(/(.?)(.?)/).length||".".split(/()()/).length>1||"".split(/.?/).length?function(t,n){var r=String(a(this)),i=void 0===n?v:n>>>0;if(0===i)return[];if(void 0===t)return[r];if(!o(t))return e.call(r,t,i);var s,c,u,l=[],d=(t.ignoreCase?"i":"")+(t.multiline?"m":"")+(t.unicode?"u":"")+(t.sticky?"y":""),h=0,g=new RegExp(t.source,d+"g");while(s=f.call(g,r)){if(c=g.lastIndex,c>h&&(l.push(r.slice(h,s.index)),s.length>1&&s.index=i))break;g.lastIndex===s.index&&g.lastIndex++}return h===r.length?!u&&g.test("")||l.push(""):l.push(r.slice(h)),l.length>i?l.slice(0,i):l}:"0".split(void 0,0).length?function(t,n){return void 0===t&&0===n?[]:e.call(this,t,n)}:e,[function(e,n){var o=a(this),i=void 0==e?void 0:e[t];return void 0!==i?i.call(e,o,n):r.call(String(o),e,n)},function(t,o){var a=n(r,t,this,o,r!==e);if(a.done)return a.value;var f=i(t),d=String(this),p=s(f,RegExp),m=f.unicode,y=(f.ignoreCase?"i":"")+(f.multiline?"m":"")+(f.unicode?"u":"")+(g?"y":"g"),b=new p(g?f:"^(?:"+f.source+")",y),w=void 0===o?v:o>>>0;if(0===w)return[];if(0===d.length)return null===l(b,d)?[d]:[];var _=0,x=0,S=[];while(x1?arguments[1]:void 0)}},"19aa":function(t,e){t.exports=function(t,e,n){if(!(t instanceof e))throw TypeError("Incorrect "+(n?n+" ":"")+"invocation");return t}},"19e9":function(t,e,n){var r,o,i; +/*! + autosize 4.0.2 + license: MIT + http://www.jacklmoore.com/autosize +*/(function(n,a){o=[t,e],r=a,i="function"===typeof r?r.apply(e,o):r,void 0===i||(t.exports=i)})(0,(function(t,e){"use strict";var n="function"===typeof Map?new Map:function(){var t=[],e=[];return{has:function(e){return t.indexOf(e)>-1},get:function(n){return e[t.indexOf(n)]},set:function(n,r){-1===t.indexOf(n)&&(t.push(n),e.push(r))},delete:function(n){var r=t.indexOf(n);r>-1&&(t.splice(r,1),e.splice(r,1))}}}(),r=function(t){return new Event(t,{bubbles:!0})};try{new Event("test")}catch(c){r=function(t){var e=document.createEvent("Event");return e.initEvent(t,!0,!1),e}}function o(t){if(t&&t.nodeName&&"TEXTAREA"===t.nodeName&&!n.has(t)){var e=null,o=null,i=null,a=function(){t.clientWidth!==o&&d()},s=function(e){window.removeEventListener("resize",a,!1),t.removeEventListener("input",d,!1),t.removeEventListener("keyup",d,!1),t.removeEventListener("autosize:destroy",s,!1),t.removeEventListener("autosize:update",d,!1),Object.keys(e).forEach((function(n){t.style[n]=e[n]})),n.delete(t)}.bind(t,{height:t.style.height,resize:t.style.resize,overflowY:t.style.overflowY,overflowX:t.style.overflowX,wordWrap:t.style.wordWrap});t.addEventListener("autosize:destroy",s,!1),"onpropertychange"in t&&"oninput"in t&&t.addEventListener("keyup",d,!1),window.addEventListener("resize",a,!1),t.addEventListener("input",d,!1),t.addEventListener("autosize:update",d,!1),t.style.overflowX="hidden",t.style.wordWrap="break-word",n.set(t,{destroy:s,update:d}),c()}function c(){var n=window.getComputedStyle(t,null);"vertical"===n.resize?t.style.resize="none":"both"===n.resize&&(t.style.resize="horizontal"),e="content-box"===n.boxSizing?-(parseFloat(n.paddingTop)+parseFloat(n.paddingBottom)):parseFloat(n.borderTopWidth)+parseFloat(n.borderBottomWidth),isNaN(e)&&(e=0),d()}function u(e){var n=t.style.width;t.style.width="0px",t.offsetWidth,t.style.width=n,t.style.overflowY=e}function l(t){var e=[];while(t&&t.parentNode&&t.parentNode instanceof Element)t.parentNode.scrollTop&&e.push({node:t.parentNode,scrollTop:t.parentNode.scrollTop}),t=t.parentNode;return e}function f(){if(0!==t.scrollHeight){var n=l(t),r=document.documentElement&&document.documentElement.scrollTop;t.style.height="",t.style.height=t.scrollHeight+e+"px",o=t.clientWidth,n.forEach((function(t){t.node.scrollTop=t.scrollTop})),r&&(document.documentElement.scrollTop=r)}}function d(){f();var e=Math.round(parseFloat(t.style.height)),n=window.getComputedStyle(t,null),o="content-box"===n.boxSizing?Math.round(parseFloat(n.height)):t.offsetHeight;if(o1?a:a.$sub[0]:null;return{output:o,params:s}}},computed:{run:function(){return this.runRule(this.lazyParentModel())},$params:function(){return this.run.params},proxy:function(){var t=this.run.output;return t[d]?!!t.v:!!t},$pending:function(){var t=this.run.output;return!!t[d]&&t.p}}}),c=e.extend({data:function(){return{dirty:!1,validations:null,lazyModel:null,model:null,prop:null,lazyParentModel:null,rootModel:null}},methods:r({},g,{refProxy:function(t){return this.getRef(t).proxy},getRef:function(t){return this.refs[t]},isNested:function(t){return"function"!==typeof this.validations[t]}}),computed:r({},h,{nestedKeys:function(){return this.keys.filter(this.isNested)},ruleKeys:function(){var t=this;return this.keys.filter((function(e){return!t.isNested(e)}))},keys:function(){return Object.keys(this.validations).filter((function(t){return"$params"!==t}))},proxy:function(){var t=this,e=s(this.keys,(function(e){return{enumerable:!0,configurable:!0,get:function(){return t.refProxy(e)}}})),n=s(m,(function(e){return{enumerable:!0,configurable:!0,get:function(){return t[e]}}})),o=s(y,(function(e){return{enumerable:!1,configurable:!0,get:function(){return t[e]}}}));return Object.defineProperties({},r({},e,n,o))},children:function(){var t=this;return[].concat(this.nestedKeys.map((function(e){return _(t,e)})),this.ruleKeys.map((function(e){return x(t,e)}))).filter(Boolean)}})}),v=c.extend({methods:{isNested:function(t){return"undefined"!==typeof this.validations[t]()},getRef:function(t){var e=this;return{get proxy(){return e.validations[t]()||!1}}}}}),w=c.extend({computed:{keys:function(){var t=this.getModel();return u(t)?Object.keys(t):[]},tracker:function(){var t=this,e=this.validations.$trackBy;return e?function(n){return""+f(t.rootModel,t.getModelKey(n),e)}:function(t){return""+t}},eagerParentModel:function(){var t=this.lazyParentModel();return function(){return t}},children:function(){var t=this,e=this.validations,n=this.getModel(),i=r({},e);delete i["$trackBy"];var a={};return this.keys.map((function(e){var r=t.tracker(e);return a.hasOwnProperty(r)?null:(a[r]=!0,(0,o.h)(c,r,{validations:i,prop:e,lazyParentModel:t.eagerParentModel,model:n[e],rootModel:t.rootModel}))})).filter(Boolean)}},methods:{isNested:function(){return!0},getRef:function(t){return this.refs[this.tracker(t)]}}}),_=function(t,e){if("$each"===e)return(0,o.h)(w,e,{validations:t.validations[e],lazyParentModel:t.lazyParentModel,prop:e,lazyModel:t.getModel,rootModel:t.rootModel});var n=t.validations[e];if(Array.isArray(n)){var r=t.rootModel,i=s(n,(function(t){return function(){return f(r,r.$v,t)}}),(function(t){return Array.isArray(t)?t.join("."):t}));return(0,o.h)(v,e,{validations:i,lazyParentModel:a,prop:e,lazyModel:a,rootModel:r})}return(0,o.h)(c,e,{validations:n,lazyParentModel:t.getModel,prop:e,lazyModel:t.getModelKey,rootModel:t.rootModel})},x=function(t,e){return(0,o.h)(n,e,{rule:t.validations[e],lazyParentModel:t.lazyParentModel,lazyModel:t.getModel,rootModel:t.rootModel})};return b={VBase:e,Validation:c},b},_=null;function x(t){if(_)return _;var e=t.constructor;while(e.super)e=e.super;return _=e,e}var S=function(t,e){var n=x(t),r=w(n),i=r.Validation,s=r.VBase,c=new s({computed:{children:function(){var n="function"===typeof e?e.call(t):e;return[(0,o.h)(i,"$v",{validations:n,lazyParentModel:a,prop:"$v",model:t,rootModel:t})]}}});return c},O={data:function(){var t=this.$options.validations;return t&&(this._vuelidate=S(this,t)),{}},beforeCreate:function(){var t=this.$options,e=t.validations;e&&(t.computed||(t.computed={}),t.computed.$v||(t.computed.$v=function(){return this._vuelidate?this._vuelidate.refs.$v.proxy:null}))},beforeDestroy:function(){this._vuelidate&&(this._vuelidate.$destroy(),this._vuelidate=null)}};function E(t){t.mixin(O)}e.Vuelidate=E,e.validationMixin=O,e.withParams=i.withParams,e.default=E},"1dde":function(t,e,n){var r=n("d039"),o=n("b622"),i=n("2d00"),a=o("species");t.exports=function(t){return i>=51||!r((function(){var e=[],n=e.constructor={};return n[a]=function(){return{foo:1}},1!==e[t](Boolean).foo}))}},2266:function(t,e,n){var r=n("825a"),o=n("e95a"),i=n("50c4"),a=n("0366"),s=n("35a1"),c=n("9bdd"),u=function(t,e){this.stopped=t,this.result=e},l=t.exports=function(t,e,n,l,f){var d,p,h,v,g,m,y,b=a(e,n,l?2:1);if(f)d=t;else{if(p=s(t),"function"!=typeof p)throw TypeError("Target is not iterable");if(o(p)){for(h=0,v=i(t.length);v>h;h++)if(g=l?b(r(y=t[h])[0],y[1]):b(t[h]),g&&g instanceof u)return g;return new u(!1)}d=p.call(t)}m=d.next;while(!(y=m.call(d)).done)if(g=c(d,b,y.value,l),"object"==typeof g&&g&&g instanceof u)return g;return new u(!1)};l.stop=function(t){return new u(!0,t)}},"23cb":function(t,e,n){var r=n("a691"),o=Math.max,i=Math.min;t.exports=function(t,e){var n=r(t);return n<0?o(n+e,0):i(n,e)}},"23e7":function(t,e,n){var r=n("da84"),o=n("06cf").f,i=n("9112"),a=n("6eeb"),s=n("ce4e"),c=n("e893"),u=n("94ca");t.exports=function(t,e){var n,l,f,d,p,h,v=t.target,g=t.global,m=t.stat;if(l=g?r:m?r[v]||s(v,{}):(r[v]||{}).prototype,l)for(f in e){if(p=e[f],t.noTargetGet?(h=o(l,f),d=h&&h.value):d=l[f],n=u(g?f:v+(m?".":"#")+f,t.forced),!n&&void 0!==d){if(typeof p===typeof d)continue;c(p,d)}(t.sham||d&&d.sham)&&i(p,"sham",!0),a(l,f,p,t)}}},"241c":function(t,e,n){var r=n("ca84"),o=n("7839"),i=o.concat("length","prototype");e.f=Object.getOwnPropertyNames||function(t){return r(t,i)}},2532:function(t,e,n){"use strict";var r=n("23e7"),o=n("5a34"),i=n("1d80"),a=n("ab13");r({target:"String",proto:!0,forced:!a("includes")},{includes:function(t){return!!~String(i(this)).indexOf(o(t),arguments.length>1?arguments[1]:void 0)}})},"25f0":function(t,e,n){"use strict";var r=n("6eeb"),o=n("825a"),i=n("d039"),a=n("ad6d"),s="toString",c=RegExp.prototype,u=c[s],l=i((function(){return"/a/b"!=u.call({source:"a",flags:"b"})})),f=u.name!=s;(l||f)&&r(RegExp.prototype,s,(function(){var t=o(this),e=String(t.source),n=t.flags,r=String(void 0===n&&t instanceof RegExp&&!("flags"in c)?a.call(t):n);return"/"+e+"/"+r}),{unsafe:!0})},2626:function(t,e,n){"use strict";var r=n("d066"),o=n("9bf2"),i=n("b622"),a=n("83ab"),s=i("species");t.exports=function(t){var e=r(t),n=o.f;a&&e&&!e[s]&&n(e,s,{configurable:!0,get:function(){return this}})}},2877:function(t,e,n){"use strict";function r(t,e,n,r,o,i,a,s){var c,u="function"===typeof t?t.options:t;if(e&&(u.render=e,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),a?(c=function(t){t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,t||"undefined"===typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),o&&o.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(a)},u._ssrRegister=c):o&&(c=s?function(){o.call(this,this.$root.$options.shadowRoot)}:o),c)if(u.functional){u._injectStyles=c;var l=u.render;u.render=function(t,e){return c.call(e),l(t,e)}}else{var f=u.beforeCreate;u.beforeCreate=f?[].concat(f,c):[c]}return{exports:t,options:u}}n.d(e,"a",(function(){return r}))},2909:function(t,e,n){"use strict";n.d(e,"a",(function(){return c}));var r=n("6b75");function o(t){if(Array.isArray(t))return Object(r["a"])(t)}function i(t){if("undefined"!==typeof Symbol&&Symbol.iterator in Object(t))return Array.from(t)}var a=n("06c5");function s(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function c(t){return o(t)||i(t)||Object(a["a"])(t)||s()}},"2a12":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=function(t){return(0,r.withParams)({type:"maxLength",max:t},(function(e){return!(0,r.req)(e)||(0,r.len)(e)<=t}))}},"2ca0":function(t,e,n){"use strict";var r=n("23e7"),o=n("06cf").f,i=n("50c4"),a=n("5a34"),s=n("1d80"),c=n("ab13"),u=n("c430"),l="".startsWith,f=Math.min,d=c("startsWith"),p=!u&&!d&&!!function(){var t=o(String.prototype,"startsWith");return t&&!t.writable}();r({target:"String",proto:!0,forced:!p&&!d},{startsWith:function(t){var e=String(s(this));a(t);var n=i(f(arguments.length>1?arguments[1]:void 0,e.length)),r=String(t);return l?l.call(e,r,n):e.slice(n,n+r.length)===r}})},"2cf4":function(t,e,n){var r,o,i,a=n("da84"),s=n("d039"),c=n("c6b6"),u=n("0366"),l=n("1be4"),f=n("cc12"),d=n("1cdc"),p=a.location,h=a.setImmediate,v=a.clearImmediate,g=a.process,m=a.MessageChannel,y=a.Dispatch,b=0,w={},_="onreadystatechange",x=function(t){if(w.hasOwnProperty(t)){var e=w[t];delete w[t],e()}},S=function(t){return function(){x(t)}},O=function(t){x(t.data)},E=function(t){a.postMessage(t+"",p.protocol+"//"+p.host)};h&&v||(h=function(t){var e=[],n=1;while(arguments.length>n)e.push(arguments[n++]);return w[++b]=function(){("function"==typeof t?t:Function(t)).apply(void 0,e)},r(b),b},v=function(t){delete w[t]},"process"==c(g)?r=function(t){g.nextTick(S(t))}:y&&y.now?r=function(t){y.now(S(t))}:m&&!d?(o=new m,i=o.port2,o.port1.onmessage=O,r=u(i.postMessage,i,1)):!a.addEventListener||"function"!=typeof postMessage||a.importScripts||s(E)||"file:"===p.protocol?r=_ in f("script")?function(t){l.appendChild(f("script"))[_]=function(){l.removeChild(this),x(t)}}:function(t){setTimeout(S(t),0)}:(r=E,a.addEventListener("message",O,!1))),t.exports={set:h,clear:v}},"2d00":function(t,e,n){var r,o,i=n("da84"),a=n("342f"),s=i.process,c=s&&s.versions,u=c&&c.v8;u?(r=u.split("."),o=r[0]+r[1]):a&&(r=a.match(/Edge\/(\d+)/),(!r||r[1]>=74)&&(r=a.match(/Chrome\/(\d+)/),r&&(o=r[1]))),t.exports=o&&+o},"2f62":function(t,e,n){"use strict";(function(t){ +/** + * vuex v3.1.3 + * (c) 2020 Evan You + * @license MIT + */ +function n(t){var e=Number(t.version.split(".")[0]);if(e>=2)t.mixin({beforeCreate:r});else{var n=t.prototype._init;t.prototype._init=function(t){void 0===t&&(t={}),t.init=t.init?[r].concat(t.init):r,n.call(this,t)}}function r(){var t=this.$options;t.store?this.$store="function"===typeof t.store?t.store():t.store:t.parent&&t.parent.$store&&(this.$store=t.parent.$store)}}var r="undefined"!==typeof window?window:"undefined"!==typeof t?t:{},o=r.__VUE_DEVTOOLS_GLOBAL_HOOK__;function i(t){o&&(t._devtoolHook=o,o.emit("vuex:init",t),o.on("vuex:travel-to-state",(function(e){t.replaceState(e)})),t.subscribe((function(t,e){o.emit("vuex:mutation",t,e)})))}function a(t,e){Object.keys(t).forEach((function(n){return e(t[n],n)}))}function s(t){return null!==t&&"object"===typeof t}function c(t){return t&&"function"===typeof t.then}function u(t,e){return function(){return t(e)}}var l=function(t,e){this.runtime=e,this._children=Object.create(null),this._rawModule=t;var n=t.state;this.state=("function"===typeof n?n():n)||{}},f={namespaced:{configurable:!0}};f.namespaced.get=function(){return!!this._rawModule.namespaced},l.prototype.addChild=function(t,e){this._children[t]=e},l.prototype.removeChild=function(t){delete this._children[t]},l.prototype.getChild=function(t){return this._children[t]},l.prototype.update=function(t){this._rawModule.namespaced=t.namespaced,t.actions&&(this._rawModule.actions=t.actions),t.mutations&&(this._rawModule.mutations=t.mutations),t.getters&&(this._rawModule.getters=t.getters)},l.prototype.forEachChild=function(t){a(this._children,t)},l.prototype.forEachGetter=function(t){this._rawModule.getters&&a(this._rawModule.getters,t)},l.prototype.forEachAction=function(t){this._rawModule.actions&&a(this._rawModule.actions,t)},l.prototype.forEachMutation=function(t){this._rawModule.mutations&&a(this._rawModule.mutations,t)},Object.defineProperties(l.prototype,f);var d=function(t){this.register([],t,!1)};function p(t,e,n){if(e.update(n),n.modules)for(var r in n.modules){if(!e.getChild(r))return void 0;p(t.concat(r),e.getChild(r),n.modules[r])}}d.prototype.get=function(t){return t.reduce((function(t,e){return t.getChild(e)}),this.root)},d.prototype.getNamespace=function(t){var e=this.root;return t.reduce((function(t,n){return e=e.getChild(n),t+(e.namespaced?n+"/":"")}),"")},d.prototype.update=function(t){p([],this.root,t)},d.prototype.register=function(t,e,n){var r=this;void 0===n&&(n=!0);var o=new l(e,n);if(0===t.length)this.root=o;else{var i=this.get(t.slice(0,-1));i.addChild(t[t.length-1],o)}e.modules&&a(e.modules,(function(e,o){r.register(t.concat(o),e,n)}))},d.prototype.unregister=function(t){var e=this.get(t.slice(0,-1)),n=t[t.length-1];e.getChild(n).runtime&&e.removeChild(n)};var h;var v=function(t){var e=this;void 0===t&&(t={}),!h&&"undefined"!==typeof window&&window.Vue&&k(window.Vue);var n=t.plugins;void 0===n&&(n=[]);var r=t.strict;void 0===r&&(r=!1),this._committing=!1,this._actions=Object.create(null),this._actionSubscribers=[],this._mutations=Object.create(null),this._wrappedGetters=Object.create(null),this._modules=new d(t),this._modulesNamespaceMap=Object.create(null),this._subscribers=[],this._watcherVM=new h,this._makeLocalGettersCache=Object.create(null);var o=this,a=this,s=a.dispatch,c=a.commit;this.dispatch=function(t,e){return s.call(o,t,e)},this.commit=function(t,e,n){return c.call(o,t,e,n)},this.strict=r;var u=this._modules.root.state;w(this,u,[],this._modules.root),b(this,u),n.forEach((function(t){return t(e)}));var l=void 0!==t.devtools?t.devtools:h.config.devtools;l&&i(this)},g={state:{configurable:!0}};function m(t,e){return e.indexOf(t)<0&&e.push(t),function(){var n=e.indexOf(t);n>-1&&e.splice(n,1)}}function y(t,e){t._actions=Object.create(null),t._mutations=Object.create(null),t._wrappedGetters=Object.create(null),t._modulesNamespaceMap=Object.create(null);var n=t.state;w(t,n,[],t._modules.root,!0),b(t,n,e)}function b(t,e,n){var r=t._vm;t.getters={},t._makeLocalGettersCache=Object.create(null);var o=t._wrappedGetters,i={};a(o,(function(e,n){i[n]=u(e,t),Object.defineProperty(t.getters,n,{get:function(){return t._vm[n]},enumerable:!0})}));var s=h.config.silent;h.config.silent=!0,t._vm=new h({data:{$$state:e},computed:i}),h.config.silent=s,t.strict&&C(t),r&&(n&&t._withCommit((function(){r._data.$$state=null})),h.nextTick((function(){return r.$destroy()})))}function w(t,e,n,r,o){var i=!n.length,a=t._modules.getNamespace(n);if(r.namespaced&&(t._modulesNamespaceMap[a],t._modulesNamespaceMap[a]=r),!i&&!o){var s=A(e,n.slice(0,-1)),c=n[n.length-1];t._withCommit((function(){h.set(s,c,r.state)}))}var u=r.context=_(t,a,n);r.forEachMutation((function(e,n){var r=a+n;S(t,r,e,u)})),r.forEachAction((function(e,n){var r=e.root?n:a+n,o=e.handler||e;O(t,r,o,u)})),r.forEachGetter((function(e,n){var r=a+n;E(t,r,e,u)})),r.forEachChild((function(r,i){w(t,e,n.concat(i),r,o)}))}function _(t,e,n){var r=""===e,o={dispatch:r?t.dispatch:function(n,r,o){var i=$(n,r,o),a=i.payload,s=i.options,c=i.type;return s&&s.root||(c=e+c),t.dispatch(c,a)},commit:r?t.commit:function(n,r,o){var i=$(n,r,o),a=i.payload,s=i.options,c=i.type;s&&s.root||(c=e+c),t.commit(c,a,s)}};return Object.defineProperties(o,{getters:{get:r?function(){return t.getters}:function(){return x(t,e)}},state:{get:function(){return A(t.state,n)}}}),o}function x(t,e){if(!t._makeLocalGettersCache[e]){var n={},r=e.length;Object.keys(t.getters).forEach((function(o){if(o.slice(0,r)===e){var i=o.slice(r);Object.defineProperty(n,i,{get:function(){return t.getters[o]},enumerable:!0})}})),t._makeLocalGettersCache[e]=n}return t._makeLocalGettersCache[e]}function S(t,e,n,r){var o=t._mutations[e]||(t._mutations[e]=[]);o.push((function(e){n.call(t,r.state,e)}))}function O(t,e,n,r){var o=t._actions[e]||(t._actions[e]=[]);o.push((function(e){var o=n.call(t,{dispatch:r.dispatch,commit:r.commit,getters:r.getters,state:r.state,rootGetters:t.getters,rootState:t.state},e);return c(o)||(o=Promise.resolve(o)),t._devtoolHook?o.catch((function(e){throw t._devtoolHook.emit("vuex:error",e),e})):o}))}function E(t,e,n,r){t._wrappedGetters[e]||(t._wrappedGetters[e]=function(t){return n(r.state,r.getters,t.state,t.getters)})}function C(t){t._vm.$watch((function(){return this._data.$$state}),(function(){0}),{deep:!0,sync:!0})}function A(t,e){return e.reduce((function(t,e){return t[e]}),t)}function $(t,e,n){return s(t)&&t.type&&(n=e,e=t,t=t.type),{type:t,payload:e,options:n}}function k(t){h&&t===h||(h=t,n(h))}g.state.get=function(){return this._vm._data.$$state},g.state.set=function(t){0},v.prototype.commit=function(t,e,n){var r=this,o=$(t,e,n),i=o.type,a=o.payload,s=(o.options,{type:i,payload:a}),c=this._mutations[i];c&&(this._withCommit((function(){c.forEach((function(t){t(a)}))})),this._subscribers.slice().forEach((function(t){return t(s,r.state)})))},v.prototype.dispatch=function(t,e){var n=this,r=$(t,e),o=r.type,i=r.payload,a={type:o,payload:i},s=this._actions[o];if(s){try{this._actionSubscribers.slice().filter((function(t){return t.before})).forEach((function(t){return t.before(a,n.state)}))}catch(u){0}var c=s.length>1?Promise.all(s.map((function(t){return t(i)}))):s[0](i);return c.then((function(t){try{n._actionSubscribers.filter((function(t){return t.after})).forEach((function(t){return t.after(a,n.state)}))}catch(u){0}return t}))}},v.prototype.subscribe=function(t){return m(t,this._subscribers)},v.prototype.subscribeAction=function(t){var e="function"===typeof t?{before:t}:t;return m(e,this._actionSubscribers)},v.prototype.watch=function(t,e,n){var r=this;return this._watcherVM.$watch((function(){return t(r.state,r.getters)}),e,n)},v.prototype.replaceState=function(t){var e=this;this._withCommit((function(){e._vm._data.$$state=t}))},v.prototype.registerModule=function(t,e,n){void 0===n&&(n={}),"string"===typeof t&&(t=[t]),this._modules.register(t,e),w(this,this.state,t,this._modules.get(t),n.preserveState),b(this,this.state)},v.prototype.unregisterModule=function(t){var e=this;"string"===typeof t&&(t=[t]),this._modules.unregister(t),this._withCommit((function(){var n=A(e.state,t.slice(0,-1));h.delete(n,t[t.length-1])})),y(this)},v.prototype.hotUpdate=function(t){this._modules.update(t),y(this,!0)},v.prototype._withCommit=function(t){var e=this._committing;this._committing=!0,t(),this._committing=e},Object.defineProperties(v.prototype,g);var M=N((function(t,e){var n={};return I(e).forEach((function(e){var r=e.key,o=e.val;n[r]=function(){var e=this.$store.state,n=this.$store.getters;if(t){var r=R(this.$store,"mapState",t);if(!r)return;e=r.context.state,n=r.context.getters}return"function"===typeof o?o.call(this,e,n):e[o]},n[r].vuex=!0})),n})),T=N((function(t,e){var n={};return I(e).forEach((function(e){var r=e.key,o=e.val;n[r]=function(){var e=[],n=arguments.length;while(n--)e[n]=arguments[n];var r=this.$store.commit;if(t){var i=R(this.$store,"mapMutations",t);if(!i)return;r=i.context.commit}return"function"===typeof o?o.apply(this,[r].concat(e)):r.apply(this.$store,[o].concat(e))}})),n})),j=N((function(t,e){var n={};return I(e).forEach((function(e){var r=e.key,o=e.val;o=t+o,n[r]=function(){if(!t||R(this.$store,"mapGetters",t))return this.$store.getters[o]},n[r].vuex=!0})),n})),P=N((function(t,e){var n={};return I(e).forEach((function(e){var r=e.key,o=e.val;n[r]=function(){var e=[],n=arguments.length;while(n--)e[n]=arguments[n];var r=this.$store.dispatch;if(t){var i=R(this.$store,"mapActions",t);if(!i)return;r=i.context.dispatch}return"function"===typeof o?o.apply(this,[r].concat(e)):r.apply(this.$store,[o].concat(e))}})),n})),D=function(t){return{mapState:M.bind(null,t),mapGetters:j.bind(null,t),mapMutations:T.bind(null,t),mapActions:P.bind(null,t)}};function I(t){return L(t)?Array.isArray(t)?t.map((function(t){return{key:t,val:t}})):Object.keys(t).map((function(e){return{key:e,val:t[e]}})):[]}function L(t){return Array.isArray(t)||s(t)}function N(t){return function(e,n){return"string"!==typeof e?(n=e,e=""):"/"!==e.charAt(e.length-1)&&(e+="/"),t(e,n)}}function R(t,e,n){var r=t._modulesNamespaceMap[n];return r}var F={Store:v,install:k,version:"3.1.3",mapState:M,mapMutations:T,mapGetters:j,mapActions:P,createNamespacedHelpers:D};e["a"]=F}).call(this,n("c8ba"))},"310e":function(t,e,n){t.exports=function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"===typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t["default"]}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s="fb15")}({"02f4":function(t,e,n){var r=n("4588"),o=n("be13");t.exports=function(t){return function(e,n){var i,a,s=String(o(e)),c=r(n),u=s.length;return c<0||c>=u?t?"":void 0:(i=s.charCodeAt(c),i<55296||i>56319||c+1===u||(a=s.charCodeAt(c+1))<56320||a>57343?t?s.charAt(c):i:t?s.slice(c,c+2):a-56320+(i-55296<<10)+65536)}}},"0390":function(t,e,n){"use strict";var r=n("02f4")(!0);t.exports=function(t,e,n){return e+(n?r(t,e).length:1)}},"07e3":function(t,e){var n={}.hasOwnProperty;t.exports=function(t,e){return n.call(t,e)}},"0bfb":function(t,e,n){"use strict";var r=n("cb7c");t.exports=function(){var t=r(this),e="";return t.global&&(e+="g"),t.ignoreCase&&(e+="i"),t.multiline&&(e+="m"),t.unicode&&(e+="u"),t.sticky&&(e+="y"),e}},"0fc9":function(t,e,n){var r=n("3a38"),o=Math.max,i=Math.min;t.exports=function(t,e){return t=r(t),t<0?o(t+e,0):i(t,e)}},1654:function(t,e,n){"use strict";var r=n("71c1")(!0);n("30f1")(String,"String",(function(t){this._t=String(t),this._i=0}),(function(){var t,e=this._t,n=this._i;return n>=e.length?{value:void 0,done:!0}:(t=r(e,n),this._i+=t.length,{value:t,done:!1})}))},1691:function(t,e){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},"1af6":function(t,e,n){var r=n("63b6");r(r.S,"Array",{isArray:n("9003")})},"1bc3":function(t,e,n){var r=n("f772");t.exports=function(t,e){if(!r(t))return t;var n,o;if(e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;if("function"==typeof(n=t.valueOf)&&!r(o=n.call(t)))return o;if(!e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;throw TypeError("Can't convert object to primitive value")}},"1ec9":function(t,e,n){var r=n("f772"),o=n("e53d").document,i=r(o)&&r(o.createElement);t.exports=function(t){return i?o.createElement(t):{}}},"20fd":function(t,e,n){"use strict";var r=n("d9f6"),o=n("aebd");t.exports=function(t,e,n){e in t?r.f(t,e,o(0,n)):t[e]=n}},"214f":function(t,e,n){"use strict";n("b0c5");var r=n("2aba"),o=n("32e9"),i=n("79e5"),a=n("be13"),s=n("2b4c"),c=n("520a"),u=s("species"),l=!i((function(){var t=/./;return t.exec=function(){var t=[];return t.groups={a:"7"},t},"7"!=="".replace(t,"$
")})),f=function(){var t=/(?:)/,e=t.exec;t.exec=function(){return e.apply(this,arguments)};var n="ab".split(t);return 2===n.length&&"a"===n[0]&&"b"===n[1]}();t.exports=function(t,e,n){var d=s(t),p=!i((function(){var e={};return e[d]=function(){return 7},7!=""[t](e)})),h=p?!i((function(){var e=!1,n=/a/;return n.exec=function(){return e=!0,null},"split"===t&&(n.constructor={},n.constructor[u]=function(){return n}),n[d](""),!e})):void 0;if(!p||!h||"replace"===t&&!l||"split"===t&&!f){var v=/./[d],g=n(a,d,""[t],(function(t,e,n,r,o){return e.exec===c?p&&!o?{done:!0,value:v.call(e,n,r)}:{done:!0,value:t.call(n,e,r)}:{done:!1}})),m=g[0],y=g[1];r(String.prototype,t,m),o(RegExp.prototype,d,2==e?function(t,e){return y.call(t,this,e)}:function(t){return y.call(t,this)})}}},"230e":function(t,e,n){var r=n("d3f4"),o=n("7726").document,i=r(o)&&r(o.createElement);t.exports=function(t){return i?o.createElement(t):{}}},"23c6":function(t,e,n){var r=n("2d95"),o=n("2b4c")("toStringTag"),i="Arguments"==r(function(){return arguments}()),a=function(t,e){try{return t[e]}catch(n){}};t.exports=function(t){var e,n,s;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(n=a(e=Object(t),o))?n:i?r(e):"Object"==(s=r(e))&&"function"==typeof e.callee?"Arguments":s}},"241e":function(t,e,n){var r=n("25eb");t.exports=function(t){return Object(r(t))}},"25eb":function(t,e){t.exports=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}},"294c":function(t,e){t.exports=function(t){try{return!!t()}catch(e){return!0}}},"2aba":function(t,e,n){var r=n("7726"),o=n("32e9"),i=n("69a8"),a=n("ca5a")("src"),s=n("fa5b"),c="toString",u=(""+s).split(c);n("8378").inspectSource=function(t){return s.call(t)},(t.exports=function(t,e,n,s){var c="function"==typeof n;c&&(i(n,"name")||o(n,"name",e)),t[e]!==n&&(c&&(i(n,a)||o(n,a,t[e]?""+t[e]:u.join(String(e)))),t===r?t[e]=n:s?t[e]?t[e]=n:o(t,e,n):(delete t[e],o(t,e,n)))})(Function.prototype,c,(function(){return"function"==typeof this&&this[a]||s.call(this)}))},"2b4c":function(t,e,n){var r=n("5537")("wks"),o=n("ca5a"),i=n("7726").Symbol,a="function"==typeof i,s=t.exports=function(t){return r[t]||(r[t]=a&&i[t]||(a?i:o)("Symbol."+t))};s.store=r},"2d00":function(t,e){t.exports=!1},"2d95":function(t,e){var n={}.toString;t.exports=function(t){return n.call(t).slice(8,-1)}},"2fdb":function(t,e,n){"use strict";var r=n("5ca1"),o=n("d2c8"),i="includes";r(r.P+r.F*n("5147")(i),"String",{includes:function(t){return!!~o(this,t,i).indexOf(t,arguments.length>1?arguments[1]:void 0)}})},"30f1":function(t,e,n){"use strict";var r=n("b8e3"),o=n("63b6"),i=n("9138"),a=n("35e8"),s=n("481b"),c=n("8f60"),u=n("45f2"),l=n("53e2"),f=n("5168")("iterator"),d=!([].keys&&"next"in[].keys()),p="@@iterator",h="keys",v="values",g=function(){return this};t.exports=function(t,e,n,m,y,b,w){c(n,e,m);var _,x,S,O=function(t){if(!d&&t in $)return $[t];switch(t){case h:return function(){return new n(this,t)};case v:return function(){return new n(this,t)}}return function(){return new n(this,t)}},E=e+" Iterator",C=y==v,A=!1,$=t.prototype,k=$[f]||$[p]||y&&$[y],M=k||O(y),T=y?C?O("entries"):M:void 0,j="Array"==e&&$.entries||k;if(j&&(S=l(j.call(new t)),S!==Object.prototype&&S.next&&(u(S,E,!0),r||"function"==typeof S[f]||a(S,f,g))),C&&k&&k.name!==v&&(A=!0,M=function(){return k.call(this)}),r&&!w||!d&&!A&&$[f]||a($,f,M),s[e]=M,s[E]=g,y)if(_={values:C?M:O(v),keys:b?M:O(h),entries:T},w)for(x in _)x in $||i($,x,_[x]);else o(o.P+o.F*(d||A),e,_);return _}},"32a6":function(t,e,n){var r=n("241e"),o=n("c3a1");n("ce7e")("keys",(function(){return function(t){return o(r(t))}}))},"32e9":function(t,e,n){var r=n("86cc"),o=n("4630");t.exports=n("9e1e")?function(t,e,n){return r.f(t,e,o(1,n))}:function(t,e,n){return t[e]=n,t}},"32fc":function(t,e,n){var r=n("e53d").document;t.exports=r&&r.documentElement},"335c":function(t,e,n){var r=n("6b4c");t.exports=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==r(t)?t.split(""):Object(t)}},"355d":function(t,e){e.f={}.propertyIsEnumerable},"35e8":function(t,e,n){var r=n("d9f6"),o=n("aebd");t.exports=n("8e60")?function(t,e,n){return r.f(t,e,o(1,n))}:function(t,e,n){return t[e]=n,t}},"36c3":function(t,e,n){var r=n("335c"),o=n("25eb");t.exports=function(t){return r(o(t))}},3702:function(t,e,n){var r=n("481b"),o=n("5168")("iterator"),i=Array.prototype;t.exports=function(t){return void 0!==t&&(r.Array===t||i[o]===t)}},"3a38":function(t,e){var n=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:n)(t)}},"40c3":function(t,e,n){var r=n("6b4c"),o=n("5168")("toStringTag"),i="Arguments"==r(function(){return arguments}()),a=function(t,e){try{return t[e]}catch(n){}};t.exports=function(t){var e,n,s;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(n=a(e=Object(t),o))?n:i?r(e):"Object"==(s=r(e))&&"function"==typeof e.callee?"Arguments":s}},4588:function(t,e){var n=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:n)(t)}},"45f2":function(t,e,n){var r=n("d9f6").f,o=n("07e3"),i=n("5168")("toStringTag");t.exports=function(t,e,n){t&&!o(t=n?t:t.prototype,i)&&r(t,i,{configurable:!0,value:e})}},4630:function(t,e){t.exports=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}}},"469f":function(t,e,n){n("6c1c"),n("1654"),t.exports=n("7d7b")},"481b":function(t,e){t.exports={}},"4aa6":function(t,e,n){t.exports=n("dc62")},"4bf8":function(t,e,n){var r=n("be13");t.exports=function(t){return Object(r(t))}},"4ee1":function(t,e,n){var r=n("5168")("iterator"),o=!1;try{var i=[7][r]();i["return"]=function(){o=!0},Array.from(i,(function(){throw 2}))}catch(a){}t.exports=function(t,e){if(!e&&!o)return!1;var n=!1;try{var i=[7],s=i[r]();s.next=function(){return{done:n=!0}},i[r]=function(){return s},t(i)}catch(a){}return n}},"50ed":function(t,e){t.exports=function(t,e){return{value:e,done:!!t}}},5147:function(t,e,n){var r=n("2b4c")("match");t.exports=function(t){var e=/./;try{"/./"[t](e)}catch(n){try{return e[r]=!1,!"/./"[t](e)}catch(o){}}return!0}},5168:function(t,e,n){var r=n("dbdb")("wks"),o=n("62a0"),i=n("e53d").Symbol,a="function"==typeof i,s=t.exports=function(t){return r[t]||(r[t]=a&&i[t]||(a?i:o)("Symbol."+t))};s.store=r},5176:function(t,e,n){t.exports=n("51b6")},"51b6":function(t,e,n){n("a3c3"),t.exports=n("584a").Object.assign},"520a":function(t,e,n){"use strict";var r=n("0bfb"),o=RegExp.prototype.exec,i=String.prototype.replace,a=o,s="lastIndex",c=function(){var t=/a/,e=/b*/g;return o.call(t,"a"),o.call(e,"a"),0!==t[s]||0!==e[s]}(),u=void 0!==/()??/.exec("")[1],l=c||u;l&&(a=function(t){var e,n,a,l,f=this;return u&&(n=new RegExp("^"+f.source+"$(?!\\s)",r.call(f))),c&&(e=f[s]),a=o.call(f,t),c&&a&&(f[s]=f.global?a.index+a[0].length:e),u&&a&&a.length>1&&i.call(a[0],n,(function(){for(l=1;l1?arguments[1]:void 0,g=void 0!==v,m=0,y=l(d);if(g&&(v=r(v,h>2?arguments[2]:void 0,2)),void 0==y||p==Array&&s(y))for(e=c(d.length),n=new p(e);e>m;m++)u(n,m,g?v(d[m],m):d[m]);else for(f=y.call(d),n=new p;!(o=f.next()).done;m++)u(n,m,g?a(f,v,[o.value,m],!0):o.value);return n.length=m,n}})},"54a1":function(t,e,n){n("6c1c"),n("1654"),t.exports=n("95d5")},5537:function(t,e,n){var r=n("8378"),o=n("7726"),i="__core-js_shared__",a=o[i]||(o[i]={});(t.exports=function(t,e){return a[t]||(a[t]=void 0!==e?e:{})})("versions",[]).push({version:r.version,mode:n("2d00")?"pure":"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})},5559:function(t,e,n){var r=n("dbdb")("keys"),o=n("62a0");t.exports=function(t){return r[t]||(r[t]=o(t))}},"584a":function(t,e){var n=t.exports={version:"2.6.5"};"number"==typeof __e&&(__e=n)},"5b4e":function(t,e,n){var r=n("36c3"),o=n("b447"),i=n("0fc9");t.exports=function(t){return function(e,n,a){var s,c=r(e),u=o(c.length),l=i(a,u);if(t&&n!=n){while(u>l)if(s=c[l++],s!=s)return!0}else for(;u>l;l++)if((t||l in c)&&c[l]===n)return t||l||0;return!t&&-1}}},"5ca1":function(t,e,n){var r=n("7726"),o=n("8378"),i=n("32e9"),a=n("2aba"),s=n("9b43"),c="prototype",u=function(t,e,n){var l,f,d,p,h=t&u.F,v=t&u.G,g=t&u.S,m=t&u.P,y=t&u.B,b=v?r:g?r[e]||(r[e]={}):(r[e]||{})[c],w=v?o:o[e]||(o[e]={}),_=w[c]||(w[c]={});for(l in v&&(n=e),n)f=!h&&b&&void 0!==b[l],d=(f?b:n)[l],p=y&&f?s(d,r):m&&"function"==typeof d?s(Function.call,d):d,b&&a(b,l,d,t&u.U),w[l]!=d&&i(w,l,p),m&&_[l]!=d&&(_[l]=d)};r.core=o,u.F=1,u.G=2,u.S=4,u.P=8,u.B=16,u.W=32,u.U=64,u.R=128,t.exports=u},"5d73":function(t,e,n){t.exports=n("469f")},"5f1b":function(t,e,n){"use strict";var r=n("23c6"),o=RegExp.prototype.exec;t.exports=function(t,e){var n=t.exec;if("function"===typeof n){var i=n.call(t,e);if("object"!==typeof i)throw new TypeError("RegExp exec method returned something other than an Object or null");return i}if("RegExp"!==r(t))throw new TypeError("RegExp#exec called on incompatible receiver");return o.call(t,e)}},"626a":function(t,e,n){var r=n("2d95");t.exports=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==r(t)?t.split(""):Object(t)}},"62a0":function(t,e){var n=0,r=Math.random();t.exports=function(t){return"Symbol(".concat(void 0===t?"":t,")_",(++n+r).toString(36))}},"63b6":function(t,e,n){var r=n("e53d"),o=n("584a"),i=n("d864"),a=n("35e8"),s=n("07e3"),c="prototype",u=function(t,e,n){var l,f,d,p=t&u.F,h=t&u.G,v=t&u.S,g=t&u.P,m=t&u.B,y=t&u.W,b=h?o:o[e]||(o[e]={}),w=b[c],_=h?r:v?r[e]:(r[e]||{})[c];for(l in h&&(n=e),n)f=!p&&_&&void 0!==_[l],f&&s(b,l)||(d=f?_[l]:n[l],b[l]=h&&"function"!=typeof _[l]?n[l]:m&&f?i(d,r):y&&_[l]==d?function(t){var e=function(e,n,r){if(this instanceof t){switch(arguments.length){case 0:return new t;case 1:return new t(e);case 2:return new t(e,n)}return new t(e,n,r)}return t.apply(this,arguments)};return e[c]=t[c],e}(d):g&&"function"==typeof d?i(Function.call,d):d,g&&((b.virtual||(b.virtual={}))[l]=d,t&u.R&&w&&!w[l]&&a(w,l,d)))};u.F=1,u.G=2,u.S=4,u.P=8,u.B=16,u.W=32,u.U=64,u.R=128,t.exports=u},6762:function(t,e,n){"use strict";var r=n("5ca1"),o=n("c366")(!0);r(r.P,"Array",{includes:function(t){return o(this,t,arguments.length>1?arguments[1]:void 0)}}),n("9c6c")("includes")},6821:function(t,e,n){var r=n("626a"),o=n("be13");t.exports=function(t){return r(o(t))}},"69a8":function(t,e){var n={}.hasOwnProperty;t.exports=function(t,e){return n.call(t,e)}},"6a99":function(t,e,n){var r=n("d3f4");t.exports=function(t,e){if(!r(t))return t;var n,o;if(e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;if("function"==typeof(n=t.valueOf)&&!r(o=n.call(t)))return o;if(!e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;throw TypeError("Can't convert object to primitive value")}},"6b4c":function(t,e){var n={}.toString;t.exports=function(t){return n.call(t).slice(8,-1)}},"6c1c":function(t,e,n){n("c367");for(var r=n("e53d"),o=n("35e8"),i=n("481b"),a=n("5168")("toStringTag"),s="CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,TextTrackList,TouchList".split(","),c=0;c=u?t?"":void 0:(i=s.charCodeAt(c),i<55296||i>56319||c+1===u||(a=s.charCodeAt(c+1))<56320||a>57343?t?s.charAt(c):i:t?s.slice(c,c+2):a-56320+(i-55296<<10)+65536)}}},7726:function(t,e){var n=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},"774e":function(t,e,n){t.exports=n("d2d5")},"77f1":function(t,e,n){var r=n("4588"),o=Math.max,i=Math.min;t.exports=function(t,e){return t=r(t),t<0?o(t+e,0):i(t,e)}},"794b":function(t,e,n){t.exports=!n("8e60")&&!n("294c")((function(){return 7!=Object.defineProperty(n("1ec9")("div"),"a",{get:function(){return 7}}).a}))},"79aa":function(t,e){t.exports=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t}},"79e5":function(t,e){t.exports=function(t){try{return!!t()}catch(e){return!0}}},"7cd6":function(t,e,n){var r=n("40c3"),o=n("5168")("iterator"),i=n("481b");t.exports=n("584a").getIteratorMethod=function(t){if(void 0!=t)return t[o]||t["@@iterator"]||i[r(t)]}},"7d7b":function(t,e,n){var r=n("e4ae"),o=n("7cd6");t.exports=n("584a").getIterator=function(t){var e=o(t);if("function"!=typeof e)throw TypeError(t+" is not iterable!");return r(e.call(t))}},"7e90":function(t,e,n){var r=n("d9f6"),o=n("e4ae"),i=n("c3a1");t.exports=n("8e60")?Object.defineProperties:function(t,e){o(t);var n,a=i(e),s=a.length,c=0;while(s>c)r.f(t,n=a[c++],e[n]);return t}},8378:function(t,e){var n=t.exports={version:"2.6.5"};"number"==typeof __e&&(__e=n)},8436:function(t,e){t.exports=function(){}},"86cc":function(t,e,n){var r=n("cb7c"),o=n("c69a"),i=n("6a99"),a=Object.defineProperty;e.f=n("9e1e")?Object.defineProperty:function(t,e,n){if(r(t),e=i(e,!0),r(n),o)try{return a(t,e,n)}catch(s){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(t[e]=n.value),t}},"8aae":function(t,e,n){n("32a6"),t.exports=n("584a").Object.keys},"8e60":function(t,e,n){t.exports=!n("294c")((function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a}))},"8f60":function(t,e,n){"use strict";var r=n("a159"),o=n("aebd"),i=n("45f2"),a={};n("35e8")(a,n("5168")("iterator"),(function(){return this})),t.exports=function(t,e,n){t.prototype=r(a,{next:o(1,n)}),i(t,e+" Iterator")}},9003:function(t,e,n){var r=n("6b4c");t.exports=Array.isArray||function(t){return"Array"==r(t)}},9138:function(t,e,n){t.exports=n("35e8")},9306:function(t,e,n){"use strict";var r=n("c3a1"),o=n("9aa9"),i=n("355d"),a=n("241e"),s=n("335c"),c=Object.assign;t.exports=!c||n("294c")((function(){var t={},e={},n=Symbol(),r="abcdefghijklmnopqrst";return t[n]=7,r.split("").forEach((function(t){e[t]=t})),7!=c({},t)[n]||Object.keys(c({},e)).join("")!=r}))?function(t,e){var n=a(t),c=arguments.length,u=1,l=o.f,f=i.f;while(c>u){var d,p=s(arguments[u++]),h=l?r(p).concat(l(p)):r(p),v=h.length,g=0;while(v>g)f.call(p,d=h[g++])&&(n[d]=p[d])}return n}:c},9427:function(t,e,n){var r=n("63b6");r(r.S,"Object",{create:n("a159")})},"95d5":function(t,e,n){var r=n("40c3"),o=n("5168")("iterator"),i=n("481b");t.exports=n("584a").isIterable=function(t){var e=Object(t);return void 0!==e[o]||"@@iterator"in e||i.hasOwnProperty(r(e))}},"9aa9":function(t,e){e.f=Object.getOwnPropertySymbols},"9b43":function(t,e,n){var r=n("d8e8");t.exports=function(t,e,n){if(r(t),void 0===e)return t;switch(n){case 1:return function(n){return t.call(e,n)};case 2:return function(n,r){return t.call(e,n,r)};case 3:return function(n,r,o){return t.call(e,n,r,o)}}return function(){return t.apply(e,arguments)}}},"9c6c":function(t,e,n){var r=n("2b4c")("unscopables"),o=Array.prototype;void 0==o[r]&&n("32e9")(o,r,{}),t.exports=function(t){o[r][t]=!0}},"9def":function(t,e,n){var r=n("4588"),o=Math.min;t.exports=function(t){return t>0?o(r(t),9007199254740991):0}},"9e1e":function(t,e,n){t.exports=!n("79e5")((function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a}))},a159:function(t,e,n){var r=n("e4ae"),o=n("7e90"),i=n("1691"),a=n("5559")("IE_PROTO"),s=function(){},c="prototype",u=function(){var t,e=n("1ec9")("iframe"),r=i.length,o="<",a=">";e.style.display="none",n("32fc").appendChild(e),e.src="javascript:",t=e.contentWindow.document,t.open(),t.write(o+"script"+a+"document.F=Object"+o+"/script"+a),t.close(),u=t.F;while(r--)delete u[c][i[r]];return u()};t.exports=Object.create||function(t,e){var n;return null!==t?(s[c]=r(t),n=new s,s[c]=null,n[a]=t):n=u(),void 0===e?n:o(n,e)}},a352:function(t,e){t.exports=n("aa47")},a3c3:function(t,e,n){var r=n("63b6");r(r.S+r.F,"Object",{assign:n("9306")})},a481:function(t,e,n){"use strict";var r=n("cb7c"),o=n("4bf8"),i=n("9def"),a=n("4588"),s=n("0390"),c=n("5f1b"),u=Math.max,l=Math.min,f=Math.floor,d=/\$([$&`']|\d\d?|<[^>]*>)/g,p=/\$([$&`']|\d\d?)/g,h=function(t){return void 0===t?t:String(t)};n("214f")("replace",2,(function(t,e,n,v){return[function(r,o){var i=t(this),a=void 0==r?void 0:r[e];return void 0!==a?a.call(r,i,o):n.call(String(i),r,o)},function(t,e){var o=v(n,t,this,e);if(o.done)return o.value;var f=r(t),d=String(this),p="function"===typeof e;p||(e=String(e));var m=f.global;if(m){var y=f.unicode;f.lastIndex=0}var b=[];while(1){var w=c(f,d);if(null===w)break;if(b.push(w),!m)break;var _=String(w[0]);""===_&&(f.lastIndex=s(d,i(f.lastIndex),y))}for(var x="",S=0,O=0;O=S&&(x+=d.slice(S,C)+T,S=C+E.length)}return x+d.slice(S)}];function g(t,e,r,i,a,s){var c=r+t.length,u=i.length,l=p;return void 0!==a&&(a=o(a),l=d),n.call(s,l,(function(n,o){var s;switch(o.charAt(0)){case"$":return"$";case"&":return t;case"`":return e.slice(0,r);case"'":return e.slice(c);case"<":s=a[o.slice(1,-1)];break;default:var l=+o;if(0===l)return n;if(l>u){var d=f(l/10);return 0===d?n:d<=u?void 0===i[d-1]?o.charAt(1):i[d-1]+o.charAt(1):n}s=i[l-1]}return void 0===s?"":s}))}}))},a4bb:function(t,e,n){t.exports=n("8aae")},a745:function(t,e,n){t.exports=n("f410")},aae3:function(t,e,n){var r=n("d3f4"),o=n("2d95"),i=n("2b4c")("match");t.exports=function(t){var e;return r(t)&&(void 0!==(e=t[i])?!!e:"RegExp"==o(t))}},aebd:function(t,e){t.exports=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}}},b0c5:function(t,e,n){"use strict";var r=n("520a");n("5ca1")({target:"RegExp",proto:!0,forced:r!==/./.exec},{exec:r})},b0dc:function(t,e,n){var r=n("e4ae");t.exports=function(t,e,n,o){try{return o?e(r(n)[0],n[1]):e(n)}catch(a){var i=t["return"];throw void 0!==i&&r(i.call(t)),a}}},b447:function(t,e,n){var r=n("3a38"),o=Math.min;t.exports=function(t){return t>0?o(r(t),9007199254740991):0}},b8e3:function(t,e){t.exports=!0},be13:function(t,e){t.exports=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}},c366:function(t,e,n){var r=n("6821"),o=n("9def"),i=n("77f1");t.exports=function(t){return function(e,n,a){var s,c=r(e),u=o(c.length),l=i(a,u);if(t&&n!=n){while(u>l)if(s=c[l++],s!=s)return!0}else for(;u>l;l++)if((t||l in c)&&c[l]===n)return t||l||0;return!t&&-1}}},c367:function(t,e,n){"use strict";var r=n("8436"),o=n("50ed"),i=n("481b"),a=n("36c3");t.exports=n("30f1")(Array,"Array",(function(t,e){this._t=a(t),this._i=0,this._k=e}),(function(){var t=this._t,e=this._k,n=this._i++;return!t||n>=t.length?(this._t=void 0,o(1)):o(0,"keys"==e?n:"values"==e?t[n]:[n,t[n]])}),"values"),i.Arguments=i.Array,r("keys"),r("values"),r("entries")},c3a1:function(t,e,n){var r=n("e6f3"),o=n("1691");t.exports=Object.keys||function(t){return r(t,o)}},c649:function(t,e,n){"use strict";(function(t){n.d(e,"c",(function(){return f})),n.d(e,"a",(function(){return u})),n.d(e,"b",(function(){return a})),n.d(e,"d",(function(){return l}));n("a481");var r=n("4aa6"),o=n.n(r);function i(){return"undefined"!==typeof window?window.console:t.console}var a=i();function s(t){var e=o()(null);return function(n){var r=e[n];return r||(e[n]=t(n))}}var c=/-(\w)/g,u=s((function(t){return t.replace(c,(function(t,e){return e?e.toUpperCase():""}))}));function l(t){null!==t.parentElement&&t.parentElement.removeChild(t)}function f(t,e,n){var r=0===n?t.children[0]:t.children[n-1].nextSibling;t.insertBefore(e,r)}}).call(this,n("c8ba"))},c69a:function(t,e,n){t.exports=!n("9e1e")&&!n("79e5")((function(){return 7!=Object.defineProperty(n("230e")("div"),"a",{get:function(){return 7}}).a}))},c8ba:function(t,e){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(r){"object"===typeof window&&(n=window)}t.exports=n},c8bb:function(t,e,n){t.exports=n("54a1")},ca5a:function(t,e){var n=0,r=Math.random();t.exports=function(t){return"Symbol(".concat(void 0===t?"":t,")_",(++n+r).toString(36))}},cb7c:function(t,e,n){var r=n("d3f4");t.exports=function(t){if(!r(t))throw TypeError(t+" is not an object!");return t}},ce7e:function(t,e,n){var r=n("63b6"),o=n("584a"),i=n("294c");t.exports=function(t,e){var n=(o.Object||{})[t]||Object[t],a={};a[t]=e(n),r(r.S+r.F*i((function(){n(1)})),"Object",a)}},d2c8:function(t,e,n){var r=n("aae3"),o=n("be13");t.exports=function(t,e,n){if(r(e))throw TypeError("String#"+n+" doesn't accept regex!");return String(o(t))}},d2d5:function(t,e,n){n("1654"),n("549b"),t.exports=n("584a").Array.from},d3f4:function(t,e){t.exports=function(t){return"object"===typeof t?null!==t:"function"===typeof t}},d864:function(t,e,n){var r=n("79aa");t.exports=function(t,e,n){if(r(t),void 0===e)return t;switch(n){case 1:return function(n){return t.call(e,n)};case 2:return function(n,r){return t.call(e,n,r)};case 3:return function(n,r,o){return t.call(e,n,r,o)}}return function(){return t.apply(e,arguments)}}},d8e8:function(t,e){t.exports=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t}},d9f6:function(t,e,n){var r=n("e4ae"),o=n("794b"),i=n("1bc3"),a=Object.defineProperty;e.f=n("8e60")?Object.defineProperty:function(t,e,n){if(r(t),e=i(e,!0),r(n),o)try{return a(t,e,n)}catch(s){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(t[e]=n.value),t}},dbdb:function(t,e,n){var r=n("584a"),o=n("e53d"),i="__core-js_shared__",a=o[i]||(o[i]={});(t.exports=function(t,e){return a[t]||(a[t]=void 0!==e?e:{})})("versions",[]).push({version:r.version,mode:n("b8e3")?"pure":"global",copyright:"© 2019 Denis Pushkarev (zloirock.ru)"})},dc62:function(t,e,n){n("9427");var r=n("584a").Object;t.exports=function(t,e){return r.create(t,e)}},e4ae:function(t,e,n){var r=n("f772");t.exports=function(t){if(!r(t))throw TypeError(t+" is not an object!");return t}},e53d:function(t,e){var n=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},e6f3:function(t,e,n){var r=n("07e3"),o=n("36c3"),i=n("5b4e")(!1),a=n("5559")("IE_PROTO");t.exports=function(t,e){var n,s=o(t),c=0,u=[];for(n in s)n!=a&&r(s,n)&&u.push(n);while(e.length>c)r(s,n=e[c++])&&(~i(u,n)||u.push(n));return u}},f410:function(t,e,n){n("1af6"),t.exports=n("584a").Array.isArray},f559:function(t,e,n){"use strict";var r=n("5ca1"),o=n("9def"),i=n("d2c8"),a="startsWith",s=""[a];r(r.P+r.F*n("5147")(a),"String",{startsWith:function(t){var e=i(this,t,a),n=o(Math.min(arguments.length>1?arguments[1]:void 0,e.length)),r=String(t);return s?s.call(e,r,n):e.slice(n,n+r.length)===r}})},f772:function(t,e){t.exports=function(t){return"object"===typeof t?null!==t:"function"===typeof t}},fa5b:function(t,e,n){t.exports=n("5537")("native-function-to-string",Function.toString)},fb15:function(t,e,n){"use strict";var r;(n.r(e),"undefined"!==typeof window)&&((r=window.document.currentScript)&&(r=r.src.match(/(.+\/)[^/]+\.js(\?.*)?$/))&&(n.p=r[1]));var o=n("5176"),i=n.n(o),a=(n("f559"),n("a4bb")),s=n.n(a),c=n("a745"),u=n.n(c);function l(t){if(u()(t))return t}var f=n("5d73"),d=n.n(f);function p(t,e){var n=[],r=!0,o=!1,i=void 0;try{for(var a,s=d()(t);!(r=(a=s.next()).done);r=!0)if(n.push(a.value),e&&n.length===e)break}catch(c){o=!0,i=c}finally{try{r||null==s["return"]||s["return"]()}finally{if(o)throw i}}return n}function h(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}function v(t,e){return l(t)||p(t,e)||h()}n("6762"),n("2fdb");function g(t){if(u()(t)){for(var e=0,n=new Array(t.length);e=i?o.length:o.indexOf(t)}));return n?a.filter((function(t){return-1!==t})):a}function M(t,e){var n=this;this.$nextTick((function(){return n.$emit(t.toLowerCase(),e)}))}function T(t){var e=this;return function(n){null!==e.realList&&e["onDrag"+t](n),M.call(e,t,n)}}function j(t){return["transition-group","TransitionGroup"].includes(t)}function P(t){if(!t||1!==t.length)return!1;var e=v(t,1),n=e[0].componentOptions;return!!n&&j(n.tag)}function D(t,e,n){return t[n]||(e[n]?e[n]():void 0)}function I(t,e,n){var r=0,o=0,i=D(e,n,"header");i&&(r=i.length,t=t?[].concat(S(i),S(t)):S(i));var a=D(e,n,"footer");return a&&(o=a.length,t=t?[].concat(S(t),S(a)):S(a)),{children:t,headerOffset:r,footerOffset:o}}function L(t,e){var n=null,r=function(t,e){n=A(n,t,e)},o=s()(t).filter((function(t){return"id"===t||t.startsWith("data-")})).reduce((function(e,n){return e[n]=t[n],e}),{});if(r("attrs",o),!e)return n;var a=e.on,c=e.props,u=e.attrs;return r("on",a),r("props",c),i()(n.attrs,u),n}var N=["Start","Add","Remove","Update","End"],R=["Choose","Unchoose","Sort","Filter","Clone"],F=["Move"].concat(N,R).map((function(t){return"on"+t})),z=null,U={options:Object,list:{type:Array,required:!1,default:null},value:{type:Array,required:!1,default:null},noTransitionOnDrag:{type:Boolean,default:!1},clone:{type:Function,default:function(t){return t}},element:{type:String,default:"div"},tag:{type:String,default:null},move:{type:Function,default:null},componentData:{type:Object,required:!1,default:null}},H={name:"draggable",inheritAttrs:!1,props:U,data:function(){return{transitionMode:!1,noneFunctionalComponentMode:!1}},render:function(t){var e=this.$slots.default;this.transitionMode=P(e);var n=I(e,this.$slots,this.$scopedSlots),r=n.children,o=n.headerOffset,i=n.footerOffset;this.headerOffset=o,this.footerOffset=i;var a=L(this.$attrs,this.componentData);return t(this.getTag(),a,r)},created:function(){null!==this.list&&null!==this.value&&C["b"].error("Value and list props are mutually exclusive! Please set one or another."),"div"!==this.element&&C["b"].warn("Element props is deprecated please use tag props instead. See https://github.com/SortableJS/Vue.Draggable/blob/master/documentation/migrate.md#element-props"),void 0!==this.options&&C["b"].warn("Options props is deprecated, add sortable options directly as vue.draggable item, or use v-bind. See https://github.com/SortableJS/Vue.Draggable/blob/master/documentation/migrate.md#options-props")},mounted:function(){var t=this;if(this.noneFunctionalComponentMode=this.getTag().toLowerCase()!==this.$el.nodeName.toLowerCase()&&!this.getIsFunctional(),this.noneFunctionalComponentMode&&this.transitionMode)throw new Error("Transition-group inside component is not supported. Please alter tag value or remove transition-group. Current tag value: ".concat(this.getTag()));var e={};N.forEach((function(n){e["on"+n]=T.call(t,n)})),R.forEach((function(n){e["on"+n]=M.bind(t,n)}));var n=s()(this.$attrs).reduce((function(e,n){return e[Object(C["a"])(n)]=t.$attrs[n],e}),{}),r=i()({},this.options,n,e,{onMove:function(e,n){return t.onDragMove(e,n)}});!("draggable"in r)&&(r.draggable=">*"),this._sortable=new E.a(this.rootContainer,r),this.computeIndexes()},beforeDestroy:function(){void 0!==this._sortable&&this._sortable.destroy()},computed:{rootContainer:function(){return this.transitionMode?this.$el.children[0]:this.$el},realList:function(){return this.list?this.list:this.value}},watch:{options:{handler:function(t){this.updateOptions(t)},deep:!0},$attrs:{handler:function(t){this.updateOptions(t)},deep:!0},realList:function(){this.computeIndexes()}},methods:{getIsFunctional:function(){var t=this._vnode.fnOptions;return t&&t.functional},getTag:function(){return this.tag||this.element},updateOptions:function(t){for(var e in t){var n=Object(C["a"])(e);-1===F.indexOf(n)&&this._sortable.option(n,t[e])}},getChildrenNodes:function(){if(this.noneFunctionalComponentMode)return this.$children[0].$slots.default;var t=this.$slots.default;return this.transitionMode?t[0].child.$slots.default:t},computeIndexes:function(){var t=this;this.$nextTick((function(){t.visibleIndexes=k(t.getChildrenNodes(),t.rootContainer.children,t.transitionMode,t.footerOffset)}))},getUnderlyingVm:function(t){var e=$(this.getChildrenNodes()||[],t);if(-1===e)return null;var n=this.realList[e];return{index:e,element:n}},getUnderlyingPotencialDraggableComponent:function(t){var e=t.__vue__;return e&&e.$options&&j(e.$options._componentTag)?e.$parent:!("realList"in e)&&1===e.$children.length&&"realList"in e.$children[0]?e.$children[0]:e},emitChanges:function(t){var e=this;this.$nextTick((function(){e.$emit("change",t)}))},alterList:function(t){if(this.list)t(this.list);else{var e=S(this.value);t(e),this.$emit("input",e)}},spliceList:function(){var t=arguments,e=function(e){return e.splice.apply(e,S(t))};this.alterList(e)},updatePosition:function(t,e){var n=function(n){return n.splice(e,0,n.splice(t,1)[0])};this.alterList(n)},getRelatedContextFromMoveEvent:function(t){var e=t.to,n=t.related,r=this.getUnderlyingPotencialDraggableComponent(e);if(!r)return{component:r};var o=r.realList,a={list:o,component:r};if(e!==n&&o&&r.getUnderlyingVm){var s=r.getUnderlyingVm(n);if(s)return i()(s,a)}return a},getVmIndex:function(t){var e=this.visibleIndexes,n=e.length;return t>n-1?n:e[t]},getComponent:function(){return this.$slots.default[0].componentInstance},resetTransitionData:function(t){if(this.noTransitionOnDrag&&this.transitionMode){var e=this.getChildrenNodes();e[t].data=null;var n=this.getComponent();n.children=[],n.kept=void 0}},onDragStart:function(t){this.context=this.getUnderlyingVm(t.item),t.item._underlying_vm_=this.clone(this.context.element),z=t.item},onDragAdd:function(t){var e=t.item._underlying_vm_;if(void 0!==e){Object(C["d"])(t.item);var n=this.getVmIndex(t.newIndex);this.spliceList(n,0,e),this.computeIndexes();var r={element:e,newIndex:n};this.emitChanges({added:r})}},onDragRemove:function(t){if(Object(C["c"])(this.rootContainer,t.item,t.oldIndex),"clone"!==t.pullMode){var e=this.context.index;this.spliceList(e,1);var n={element:this.context.element,oldIndex:e};this.resetTransitionData(e),this.emitChanges({removed:n})}else Object(C["d"])(t.clone)},onDragUpdate:function(t){Object(C["d"])(t.item),Object(C["c"])(t.from,t.item,t.oldIndex);var e=this.context.index,n=this.getVmIndex(t.newIndex);this.updatePosition(e,n);var r={element:this.context.element,oldIndex:e,newIndex:n};this.emitChanges({moved:r})},updateProperty:function(t,e){t.hasOwnProperty(e)&&(t[e]+=this.headerOffset)},computeFutureIndex:function(t,e){if(!t.element)return 0;var n=S(e.to.children).filter((function(t){return"none"!==t.style["display"]})),r=n.indexOf(e.related),o=t.component.getVmIndex(r),i=-1!==n.indexOf(z);return i||!e.willInsertAfter?o:o+1},onDragMove:function(t,e){var n=this.move;if(!n||!this.realList)return!0;var r=this.getRelatedContextFromMoveEvent(t),o=this.context,a=this.computeFutureIndex(r,t);i()(o,{futureIndex:a});var s=i()({},t,{relatedContext:r,draggedContext:o});return n(s,e)},onDragEnd:function(){this.computeIndexes(),z=null}}};"undefined"!==typeof window&&"Vue"in window&&window.Vue.component("draggable",H);var B=H;e["default"]=B}})["default"]},3360:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=function(){for(var t=arguments.length,e=Array(t),n=0;n0&&e.reduce((function(e,n){return e&&n.apply(t,r)}),!0)}))}},"342f":function(t,e,n){var r=n("d066");t.exports=r("navigator","userAgent")||""},"35a1":function(t,e,n){var r=n("f5df"),o=n("3f8c"),i=n("b622"),a=i("iterator");t.exports=function(t){if(void 0!=t)return t[a]||t["@@iterator"]||o[r(t)]}},"37e8":function(t,e,n){var r=n("83ab"),o=n("9bf2"),i=n("825a"),a=n("df75");t.exports=r?Object.defineProperties:function(t,e){i(t);var n,r=a(e),s=r.length,c=0;while(s>c)o.f(t,n=r[c++],e[n]);return t}},3835:function(t,e,n){"use strict";function r(t){if(Array.isArray(t))return t}function o(t,e){if("undefined"!==typeof Symbol&&Symbol.iterator in Object(t)){var n=[],r=!0,o=!1,i=void 0;try{for(var a,s=t[Symbol.iterator]();!(r=(a=s.next()).done);r=!0)if(n.push(a.value),e&&n.length===e)break}catch(c){o=!0,i=c}finally{try{r||null==s["return"]||s["return"]()}finally{if(o)throw i}}return n}}n.d(e,"a",(function(){return s}));var i=n("06c5");function a(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function s(t,e){return r(t)||o(t,e)||Object(i["a"])(t,e)||a()}},"3a54":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=(0,r.regex)("alphaNum",/^[a-zA-Z0-9]*$/)},"3bbe":function(t,e,n){var r=n("861d");t.exports=function(t){if(!r(t)&&null!==t)throw TypeError("Can't set "+String(t)+" as a prototype");return t}},"3ca3":function(t,e,n){"use strict";var r=n("6547").charAt,o=n("69f3"),i=n("7dd0"),a="String Iterator",s=o.set,c=o.getterFor(a);i(String,"String",(function(t){s(this,{type:a,string:String(t),index:0})}),(function(){var t,e=c(this),n=e.string,o=e.index;return o>=n.length?{value:void 0,done:!0}:(t=r(n,o),e.index+=t.length,{value:t,done:!1})}))},"3f8c":function(t,e){t.exports={}},"408a":function(t,e,n){var r=n("c6b6");t.exports=function(t){if("number"!=typeof t&&"Number"!=r(t))throw TypeError("Incorrect invocation");return+t}},4160:function(t,e,n){"use strict";var r=n("23e7"),o=n("17c2");r({target:"Array",proto:!0,forced:[].forEach!=o},{forEach:o})},"428f":function(t,e,n){var r=n("da84");t.exports=r},"44ad":function(t,e,n){var r=n("d039"),o=n("c6b6"),i="".split;t.exports=r((function(){return!Object("z").propertyIsEnumerable(0)}))?function(t){return"String"==o(t)?i.call(t,""):Object(t)}:Object},"44d2":function(t,e,n){var r=n("b622"),o=n("7c73"),i=n("9bf2"),a=r("unscopables"),s=Array.prototype;void 0==s[a]&&i.f(s,a,{configurable:!0,value:o(null)}),t.exports=function(t){s[a][t]=!0}},"44de":function(t,e,n){var r=n("da84");t.exports=function(t,e){var n=r.console;n&&n.error&&(1===arguments.length?n.error(t):n.error(t,e))}},"44e7":function(t,e,n){var r=n("861d"),o=n("c6b6"),i=n("b622"),a=i("match");t.exports=function(t){var e;return r(t)&&(void 0!==(e=t[a])?!!e:"RegExp"==o(t))}},"45b8":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=(0,r.regex)("numeric",/^[0-9]*$/)},"466d":function(t,e,n){"use strict";var r=n("d784"),o=n("825a"),i=n("50c4"),a=n("1d80"),s=n("8aa5"),c=n("14c3");r("match",1,(function(t,e,n){return[function(e){var n=a(this),r=void 0==e?void 0:e[t];return void 0!==r?r.call(e,n):new RegExp(e)[t](String(n))},function(t){var r=n(e,t,this);if(r.done)return r.value;var a=o(t),u=String(this);if(!a.global)return c(a,u);var l=a.unicode;a.lastIndex=0;var f,d=[],p=0;while(null!==(f=c(a,u))){var h=String(f[0]);d[p]=h,""===h&&(a.lastIndex=s(u,i(a.lastIndex),l)),p++}return 0===p?null:d}]}))},"46bc":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=function(t){return(0,r.withParams)({type:"maxValue",max:t},(function(e){return!(0,r.req)(e)||(!/\s/.test(e)||e instanceof Date)&&+e<=+t}))}},4840:function(t,e,n){var r=n("825a"),o=n("1c0b"),i=n("b622"),a=i("species");t.exports=function(t,e){var n,i=r(t).constructor;return void 0===i||void 0==(n=r(i)[a])?e:o(n)}},4930:function(t,e,n){var r=n("d039");t.exports=!!Object.getOwnPropertySymbols&&!r((function(){return!String(Symbol())}))},"498a":function(t,e,n){"use strict";var r=n("23e7"),o=n("58a8").trim,i=n("c8d2");r({target:"String",proto:!0,forced:i("trim")},{trim:function(){return o(this)}})},"4d63":function(t,e,n){var r=n("83ab"),o=n("da84"),i=n("94ca"),a=n("7156"),s=n("9bf2").f,c=n("241c").f,u=n("44e7"),l=n("ad6d"),f=n("9f7f"),d=n("6eeb"),p=n("d039"),h=n("69f3").set,v=n("2626"),g=n("b622"),m=g("match"),y=o.RegExp,b=y.prototype,w=/a/g,_=/a/g,x=new y(w)!==w,S=f.UNSUPPORTED_Y,O=r&&i("RegExp",!x||S||p((function(){return _[m]=!1,y(w)!=w||y(_)==_||"/a/i"!=y(w,"i")})));if(O){var E=function(t,e){var n,r=this instanceof E,o=u(t),i=void 0===e;if(!r&&o&&t.constructor===E&&i)return t;x?o&&!i&&(t=t.source):t instanceof E&&(i&&(e=l.call(t)),t=t.source),S&&(n=!!e&&e.indexOf("y")>-1,n&&(e=e.replace(/y/g,"")));var s=a(x?new y(t,e):y(t,e),r?this:b,E);return S&&n&&h(s,{sticky:n}),s},C=function(t){t in E||s(E,t,{configurable:!0,get:function(){return y[t]},set:function(e){y[t]=e}})},A=c(y),$=0;while(A.length>$)C(A[$++]);b.constructor=E,E.prototype=b,d(o,"RegExp",E)}v("RegExp")},"4d64":function(t,e,n){var r=n("fc6a"),o=n("50c4"),i=n("23cb"),a=function(t){return function(e,n,a){var s,c=r(e),u=o(c.length),l=i(a,u);if(t&&n!=n){while(u>l)if(s=c[l++],s!=s)return!0}else for(;u>l;l++)if((t||l in c)&&c[l]===n)return t||l||0;return!t&&-1}};t.exports={includes:a(!0),indexOf:a(!1)}},"4de4":function(t,e,n){"use strict";var r=n("23e7"),o=n("b727").filter,i=n("1dde"),a=n("ae40"),s=i("filter"),c=a("filter");r({target:"Array",proto:!0,forced:!s||!c},{filter:function(t){return o(this,t,arguments.length>1?arguments[1]:void 0)}})},"4fad":function(t,e,n){var r=n("23e7"),o=n("6f53").entries;r({target:"Object",stat:!0},{entries:function(t){return o(t)}})},"50c4":function(t,e,n){var r=n("a691"),o=Math.min;t.exports=function(t){return t>0?o(r(t),9007199254740991):0}},5135:function(t,e){var n={}.hasOwnProperty;t.exports=function(t,e){return n.call(t,e)}},5319:function(t,e,n){"use strict";var r=n("d784"),o=n("825a"),i=n("7b0b"),a=n("50c4"),s=n("a691"),c=n("1d80"),u=n("8aa5"),l=n("14c3"),f=Math.max,d=Math.min,p=Math.floor,h=/\$([$&'`]|\d\d?|<[^>]*>)/g,v=/\$([$&'`]|\d\d?)/g,g=function(t){return void 0===t?t:String(t)};r("replace",2,(function(t,e,n,r){var m=r.REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE,y=r.REPLACE_KEEPS_$0,b=m?"$":"$0";return[function(n,r){var o=c(this),i=void 0==n?void 0:n[t];return void 0!==i?i.call(n,o,r):e.call(String(o),n,r)},function(t,r){if(!m&&y||"string"===typeof r&&-1===r.indexOf(b)){var i=n(e,t,this,r);if(i.done)return i.value}var c=o(t),p=String(this),h="function"===typeof r;h||(r=String(r));var v=c.global;if(v){var _=c.unicode;c.lastIndex=0}var x=[];while(1){var S=l(c,p);if(null===S)break;if(x.push(S),!v)break;var O=String(S[0]);""===O&&(c.lastIndex=u(p,a(c.lastIndex),_))}for(var E="",C=0,A=0;A=C&&(E+=p.slice(C,k)+D,C=k+$.length)}return E+p.slice(C)}];function w(t,n,r,o,a,s){var c=r+t.length,u=o.length,l=v;return void 0!==a&&(a=i(a),l=h),e.call(s,l,(function(e,i){var s;switch(i.charAt(0)){case"$":return"$";case"&":return t;case"`":return n.slice(0,r);case"'":return n.slice(c);case"<":s=a[i.slice(1,-1)];break;default:var l=+i;if(0===l)return e;if(l>u){var f=p(l/10);return 0===f?e:f<=u?void 0===o[f-1]?i.charAt(1):o[f-1]+i.charAt(1):e}s=o[l-1]}return void 0===s?"":s}))}}))},"53ca":function(t,e,n){"use strict";function r(t){return r="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"===typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},r(t)}n.d(e,"a",(function(){return r}))},5530:function(t,e,n){"use strict";function r(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function o(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,r)}return n}function i(t){for(var e=1;e=e?t:""+Array(e+1-r.length).join(n)+t},d={s:f,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),o=n%60;return(e<=0?"+":"-")+f(r,2,"0")+":"+f(o,2,"0")},m:function(t,e){var n=12*(e.year()-t.year())+(e.month()-t.month()),r=t.clone().add(n,a),o=e-r<0,i=t.clone().add(n+(o?-1:1),a);return Number(-(n+(e-r)/(o?r-i:i-r))||0)},a:function(t){return t<0?Math.ceil(t)||0:Math.floor(t)},p:function(u){return{M:a,y:c,w:i,d:o,D:"date",h:r,m:n,s:e,ms:t,Q:s}[u]||String(u||"").toLowerCase().replace(/s$/,"")},u:function(t){return void 0===t}},p={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_")},h="en",v={};v[h]=p;var g=function(t){return t instanceof w},m=function(t,e,n){var r;if(!t)return h;if("string"==typeof t)v[t]&&(r=t),e&&(v[t]=e,r=t);else{var o=t.name;v[o]=t,r=o}return!n&&r&&(h=r),r||!n&&h},y=function(t,e,n){if(g(t))return t.clone();var r=e?"string"==typeof e?{format:e,pl:n}:e:{};return r.date=t,new w(r)},b=d;b.l=m,b.i=g,b.w=function(t,e){return y(t,{locale:e.$L,utc:e.$u,$offset:e.$offset})};var w=function(){function f(t){this.$L=this.$L||m(t.locale,null,!0),this.parse(t)}var d=f.prototype;return d.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(b.u(e))return new Date;if(e instanceof Date)return new Date(e);if("string"==typeof e&&!/Z$/i.test(e)){var r=e.match(u);if(r)return n?new Date(Date.UTC(r[1],r[2]-1,r[3]||1,r[4]||0,r[5]||0,r[6]||0,r[7]||0)):new Date(r[1],r[2]-1,r[3]||1,r[4]||0,r[5]||0,r[6]||0,r[7]||0)}return new Date(e)}(t),this.init()},d.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},d.$utils=function(){return b},d.isValid=function(){return!("Invalid Date"===this.$d.toString())},d.isSame=function(t,e){var n=y(t);return this.startOf(e)<=n&&n<=this.endOf(e)},d.isAfter=function(t,e){return y(t)()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$)/;e.default=(0,r.regex)("email",o)},"5db3":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=function(t){return(0,r.withParams)({type:"minLength",min:t},(function(e){return!(0,r.req)(e)||(0,r.len)(e)>=t}))}},"5e89":function(t,e,n){var r=n("861d"),o=Math.floor;t.exports=function(t){return!r(t)&&isFinite(t)&&o(t)===t}},"60da":function(t,e,n){"use strict";var r=n("83ab"),o=n("d039"),i=n("df75"),a=n("7418"),s=n("d1e7"),c=n("7b0b"),u=n("44ad"),l=Object.assign,f=Object.defineProperty;t.exports=!l||o((function(){if(r&&1!==l({b:1},l(f({},"a",{enumerable:!0,get:function(){f(this,"b",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var t={},e={},n=Symbol(),o="abcdefghijklmnopqrst";return t[n]=7,o.split("").forEach((function(t){e[t]=t})),7!=l({},t)[n]||i(l({},e)).join("")!=o}))?function(t,e){var n=c(t),o=arguments.length,l=1,f=a.f,d=s.f;while(o>l){var p,h=u(arguments[l++]),v=f?i(h).concat(f(h)):i(h),g=v.length,m=0;while(g>m)p=v[m++],r&&!d.call(h,p)||(n[p]=h[p])}return n}:l},6235:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=(0,r.regex)("alpha",/^[a-zA-Z]*$/)},6547:function(t,e,n){var r=n("a691"),o=n("1d80"),i=function(t){return function(e,n){var i,a,s=String(o(e)),c=r(n),u=s.length;return c<0||c>=u?t?"":void 0:(i=s.charCodeAt(c),i<55296||i>56319||c+1===u||(a=s.charCodeAt(c+1))<56320||a>57343?t?s.charAt(c):i:t?s.slice(c,c+2):a-56320+(i-55296<<10)+65536)}};t.exports={codeAt:i(!1),charAt:i(!0)}},"65f0":function(t,e,n){var r=n("861d"),o=n("e8b5"),i=n("b622"),a=i("species");t.exports=function(t,e){var n;return o(t)&&(n=t.constructor,"function"!=typeof n||n!==Array&&!o(n.prototype)?r(n)&&(n=n[a],null===n&&(n=void 0)):n=void 0),new(void 0===n?Array:n)(0===e?0:e)}},"69f3":function(t,e,n){var r,o,i,a=n("7f9a"),s=n("da84"),c=n("861d"),u=n("9112"),l=n("5135"),f=n("f772"),d=n("d012"),p=s.WeakMap,h=function(t){return i(t)?o(t):r(t,{})},v=function(t){return function(e){var n;if(!c(e)||(n=o(e)).type!==t)throw TypeError("Incompatible receiver, "+t+" required");return n}};if(a){var g=new p,m=g.get,y=g.has,b=g.set;r=function(t,e){return b.call(g,t,e),e},o=function(t){return m.call(g,t)||{}},i=function(t){return y.call(g,t)}}else{var w=f("state");d[w]=!0,r=function(t,e){return u(t,w,e),e},o=function(t){return l(t,w)?t[w]:{}},i=function(t){return l(t,w)}}t.exports={set:r,get:o,has:i,enforce:h,getterFor:v}},"6b75":function(t,e,n){"use strict";function r(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,r=new Array(e);nl)n=c[l++],r&&!a.call(s,n)||f.push(t?[n,s[n]]:s[n]);return f}};t.exports={entries:s(!0),values:s(!1)}},7156:function(t,e,n){var r=n("861d"),o=n("d2bb");t.exports=function(t,e,n){var i,a;return o&&"function"==typeof(i=e.constructor)&&i!==n&&r(a=i.prototype)&&a!==n.prototype&&o(t,a),t}},7418:function(t,e){e.f=Object.getOwnPropertySymbols},"746f":function(t,e,n){var r=n("428f"),o=n("5135"),i=n("e538"),a=n("9bf2").f;t.exports=function(t){var e=r.Symbol||(r.Symbol={});o(e,t)||a(e,t,{value:i.f(t)})}},"772d":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef"),o=/^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i;e.default=(0,r.regex)("url",o)},7839:function(t,e){t.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},"78ef":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.regex=e.ref=e.len=e.req=e.withParams=void 0;var r=n("8750"),o=i(r);function i(t){return t&&t.__esModule?t:{default:t}}e.withParams=o.default;var a=e.req=function(t){if(Array.isArray(t))return!!t.length;if(void 0===t||null===t||!1===t)return!1;if(t instanceof Date)return!isNaN(t.getTime());if("object"===typeof t){for(var e in t)return!0;return!1}return!!String(t).length};e.len=function(t){return Array.isArray(t)?t.length:"object"===typeof t?Object.keys(t).length:String(t).length},e.ref=function(t,e,n){return"function"===typeof t?t.call(e,n):n[t]},e.regex=function(t,e){return(0,o.default)({type:t},(function(t){return!a(t)||e.test(t)}))}},"7b0b":function(t,e,n){var r=n("1d80");t.exports=function(t){return Object(r(t))}},"7c73":function(t,e,n){var r,o=n("825a"),i=n("37e8"),a=n("7839"),s=n("d012"),c=n("1be4"),u=n("cc12"),l=n("f772"),f=">",d="<",p="prototype",h="script",v=l("IE_PROTO"),g=function(){},m=function(t){return d+h+f+t+d+"/"+h+f},y=function(t){t.write(m("")),t.close();var e=t.parentWindow.Object;return t=null,e},b=function(){var t,e=u("iframe"),n="java"+h+":";return e.style.display="none",c.appendChild(e),e.src=String(n),t=e.contentWindow.document,t.open(),t.write(m("document.F=Object")),t.close(),t.F},w=function(){try{r=document.domain&&new ActiveXObject("htmlfile")}catch(e){}w=r?y(r):b();var t=a.length;while(t--)delete w[p][a[t]];return w()};s[v]=!0,t.exports=Object.create||function(t,e){var n;return null!==t?(g[p]=o(t),n=new g,g[p]=null,n[v]=t):n=w(),void 0===e?n:i(n,e)}},"7dd0":function(t,e,n){"use strict";var r=n("23e7"),o=n("9ed3"),i=n("e163"),a=n("d2bb"),s=n("d44e"),c=n("9112"),u=n("6eeb"),l=n("b622"),f=n("c430"),d=n("3f8c"),p=n("ae93"),h=p.IteratorPrototype,v=p.BUGGY_SAFARI_ITERATORS,g=l("iterator"),m="keys",y="values",b="entries",w=function(){return this};t.exports=function(t,e,n,l,p,_,x){o(n,e,l);var S,O,E,C=function(t){if(t===p&&T)return T;if(!v&&t in k)return k[t];switch(t){case m:return function(){return new n(this,t)};case y:return function(){return new n(this,t)};case b:return function(){return new n(this,t)}}return function(){return new n(this)}},A=e+" Iterator",$=!1,k=t.prototype,M=k[g]||k["@@iterator"]||p&&k[p],T=!v&&M||C(p),j="Array"==e&&k.entries||M;if(j&&(S=i(j.call(new t)),h!==Object.prototype&&S.next&&(f||i(S)===h||(a?a(S,h):"function"!=typeof S[g]&&c(S,g,w)),s(S,A,!0,!0),f&&(d[A]=w))),p==y&&M&&M.name!==y&&($=!0,T=function(){return M.call(this)}),f&&!x||k[g]===T||c(k,g,T),d[e]=T,p)if(O={values:C(y),keys:_?T:C(m),entries:C(b)},x)for(E in O)!v&&!$&&E in k||u(k,E,O[E]);else r({target:e,proto:!0,forced:v||$},O);return O}},"7f9a":function(t,e,n){var r=n("da84"),o=n("8925"),i=r.WeakMap;t.exports="function"===typeof i&&/native code/.test(o(i))},"825a":function(t,e,n){var r=n("861d");t.exports=function(t){if(!r(t))throw TypeError(String(t)+" is not an object");return t}},"83ab":function(t,e,n){var r=n("d039");t.exports=!r((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]}))},8418:function(t,e,n){"use strict";var r=n("c04e"),o=n("9bf2"),i=n("5c6c");t.exports=function(t,e,n){var a=r(e);a in t?o.f(t,a,i(0,n)):t[a]=n}},"841c":function(t,e,n){"use strict";var r=n("d784"),o=n("825a"),i=n("1d80"),a=n("129f"),s=n("14c3");r("search",1,(function(t,e,n){return[function(e){var n=i(this),r=void 0==e?void 0:e[t];return void 0!==r?r.call(e,n):new RegExp(e)[t](String(n))},function(t){var r=n(e,t,this);if(r.done)return r.value;var i=o(t),c=String(this),u=i.lastIndex;a(u,0)||(i.lastIndex=0);var l=s(i,c);return a(i.lastIndex,u)||(i.lastIndex=u),null===l?-1:l.index}]}))},"857a":function(t,e,n){var r=n("1d80"),o=/"/g;t.exports=function(t,e,n,i){var a=String(r(t)),s="<"+e;return""!==n&&(s+=" "+n+'="'+String(i).replace(o,""")+'"'),s+">"+a+""}},"861d":function(t,e){t.exports=function(t){return"object"===typeof t?null!==t:"function"===typeof t}},8750:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("0234").withParams;e.default=r},8925:function(t,e,n){var r=n("c6cd"),o=Function.toString;"function"!=typeof r.inspectSource&&(r.inspectSource=function(t){return o.call(t)}),t.exports=r.inspectSource},"8a79":function(t,e,n){"use strict";var r=n("23e7"),o=n("06cf").f,i=n("50c4"),a=n("5a34"),s=n("1d80"),c=n("ab13"),u=n("c430"),l="".endsWith,f=Math.min,d=c("endsWith"),p=!u&&!d&&!!function(){var t=o(String.prototype,"endsWith");return t&&!t.writable}();r({target:"String",proto:!0,forced:!p&&!d},{endsWith:function(t){var e=String(s(this));a(t);var n=arguments.length>1?arguments[1]:void 0,r=i(e.length),o=void 0===n?r:f(i(n),r),c=String(t);return l?l.call(e,c,o):e.slice(o-c.length,o)===c}})},"8aa5":function(t,e,n){"use strict";var r=n("6547").charAt;t.exports=function(t,e,n){return e+(n?r(t,e).length:1)}},"8ba4":function(t,e,n){var r=n("23e7"),o=n("5e89");r({target:"Number",stat:!0},{isInteger:o})},"8c4f":function(t,e,n){"use strict"; +/*! + * vue-router v3.1.6 + * (c) 2020 Evan You + * @license MIT + */function r(t,e){0}function o(t){return Object.prototype.toString.call(t).indexOf("Error")>-1}function i(t,e){return e instanceof t||e&&(e.name===t.name||e._name===t._name)}function a(t,e){for(var n in e)t[n]=e[n];return t}var s={name:"RouterView",functional:!0,props:{name:{type:String,default:"default"}},render:function(t,e){var n=e.props,r=e.children,o=e.parent,i=e.data;i.routerView=!0;var s=o.$createElement,u=n.name,l=o.$route,f=o._routerViewCache||(o._routerViewCache={}),d=0,p=!1;while(o&&o._routerRoot!==o){var h=o.$vnode?o.$vnode.data:{};h.routerView&&d++,h.keepAlive&&o._directInactive&&o._inactive&&(p=!0),o=o.$parent}if(i.routerViewDepth=d,p){var v=f[u],g=v&&v.component;return g?(v.configProps&&c(g,i,v.route,v.configProps),s(g,i,r)):s()}var m=l.matched[d],y=m&&m.components[u];if(!m||!y)return f[u]=null,s();f[u]={component:y},i.registerRouteInstance=function(t,e){var n=m.instances[u];(e&&n!==t||!e&&n===t)&&(m.instances[u]=e)},(i.hook||(i.hook={})).prepatch=function(t,e){m.instances[u]=e.componentInstance},i.hook.init=function(t){t.data.keepAlive&&t.componentInstance&&t.componentInstance!==m.instances[u]&&(m.instances[u]=t.componentInstance)};var b=m.props&&m.props[u];return b&&(a(f[u],{route:l,configProps:b}),c(y,i,l,b)),s(y,i,r)}};function c(t,e,n,r){var o=e.props=u(n,r);if(o){o=e.props=a({},o);var i=e.attrs=e.attrs||{};for(var s in o)t.props&&s in t.props||(i[s]=o[s],delete o[s])}}function u(t,e){switch(typeof e){case"undefined":return;case"object":return e;case"function":return e(t);case"boolean":return e?t.params:void 0;default:0}}var l=/[!'()*]/g,f=function(t){return"%"+t.charCodeAt(0).toString(16)},d=/%2C/g,p=function(t){return encodeURIComponent(t).replace(l,f).replace(d,",")},h=decodeURIComponent;function v(t,e,n){void 0===e&&(e={});var r,o=n||g;try{r=o(t||"")}catch(a){r={}}for(var i in e)r[i]=e[i];return r}function g(t){var e={};return t=t.trim().replace(/^(\?|#|&)/,""),t?(t.split("&").forEach((function(t){var n=t.replace(/\+/g," ").split("="),r=h(n.shift()),o=n.length>0?h(n.join("=")):null;void 0===e[r]?e[r]=o:Array.isArray(e[r])?e[r].push(o):e[r]=[e[r],o]})),e):e}function m(t){var e=t?Object.keys(t).map((function(e){var n=t[e];if(void 0===n)return"";if(null===n)return p(e);if(Array.isArray(n)){var r=[];return n.forEach((function(t){void 0!==t&&(null===t?r.push(p(e)):r.push(p(e)+"="+p(t)))})),r.join("&")}return p(e)+"="+p(n)})).filter((function(t){return t.length>0})).join("&"):null;return e?"?"+e:""}var y=/\/?$/;function b(t,e,n,r){var o=r&&r.options.stringifyQuery,i=e.query||{};try{i=w(i)}catch(s){}var a={name:e.name||t&&t.name,meta:t&&t.meta||{},path:e.path||"/",hash:e.hash||"",query:i,params:e.params||{},fullPath:S(e,o),matched:t?x(t):[]};return n&&(a.redirectedFrom=S(n,o)),Object.freeze(a)}function w(t){if(Array.isArray(t))return t.map(w);if(t&&"object"===typeof t){var e={};for(var n in t)e[n]=w(t[n]);return e}return t}var _=b(null,{path:"/"});function x(t){var e=[];while(t)e.unshift(t),t=t.parent;return e}function S(t,e){var n=t.path,r=t.query;void 0===r&&(r={});var o=t.hash;void 0===o&&(o="");var i=e||m;return(n||"/")+i(r)+o}function O(t,e){return e===_?t===e:!!e&&(t.path&&e.path?t.path.replace(y,"")===e.path.replace(y,"")&&t.hash===e.hash&&E(t.query,e.query):!(!t.name||!e.name)&&(t.name===e.name&&t.hash===e.hash&&E(t.query,e.query)&&E(t.params,e.params)))}function E(t,e){if(void 0===t&&(t={}),void 0===e&&(e={}),!t||!e)return t===e;var n=Object.keys(t),r=Object.keys(e);return n.length===r.length&&n.every((function(n){var r=t[n],o=e[n];return"object"===typeof r&&"object"===typeof o?E(r,o):String(r)===String(o)}))}function C(t,e){return 0===t.path.replace(y,"/").indexOf(e.path.replace(y,"/"))&&(!e.hash||t.hash===e.hash)&&A(t.query,e.query)}function A(t,e){for(var n in e)if(!(n in t))return!1;return!0}function $(t,e,n){var r=t.charAt(0);if("/"===r)return t;if("?"===r||"#"===r)return e+t;var o=e.split("/");n&&o[o.length-1]||o.pop();for(var i=t.replace(/^\//,"").split("/"),a=0;a=0&&(e=t.slice(r),t=t.slice(0,r));var o=t.indexOf("?");return o>=0&&(n=t.slice(o+1),t=t.slice(0,o)),{path:t,query:n,hash:e}}function M(t){return t.replace(/\/\//g,"/")}var T=Array.isArray||function(t){return"[object Array]"==Object.prototype.toString.call(t)},j=J,P=R,D=F,I=H,L=X,N=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))"].join("|"),"g");function R(t,e){var n,r=[],o=0,i=0,a="",s=e&&e.delimiter||"/";while(null!=(n=N.exec(t))){var c=n[0],u=n[1],l=n.index;if(a+=t.slice(i,l),i=l+c.length,u)a+=u[1];else{var f=t[i],d=n[2],p=n[3],h=n[4],v=n[5],g=n[6],m=n[7];a&&(r.push(a),a="");var y=null!=d&&null!=f&&f!==d,b="+"===g||"*"===g,w="?"===g||"*"===g,_=n[2]||s,x=h||v;r.push({name:p||o++,prefix:d||"",delimiter:_,optional:w,repeat:b,partial:y,asterisk:!!m,pattern:x?V(x):m?".*":"[^"+B(_)+"]+?"})}}return i1||!_.length)return 0===_.length?t():t("span",{},_)}if("a"===this.tag)w.on=y,w.attrs={href:c};else{var x=st(this.$slots.default);if(x){x.isStatic=!1;var S=x.data=a({},x.data);for(var E in S.on=S.on||{},S.on){var A=S.on[E];E in y&&(S.on[E]=Array.isArray(A)?A:[A])}for(var $ in y)$ in S.on?S.on[$].push(y[$]):S.on[$]=m;var k=x.data.attrs=a({},x.data.attrs);k.href=c}else w.on=y}return t(this.tag,w,this.$slots.default)}};function at(t){if(!(t.metaKey||t.altKey||t.ctrlKey||t.shiftKey)&&!t.defaultPrevented&&(void 0===t.button||0===t.button)){if(t.currentTarget&&t.currentTarget.getAttribute){var e=t.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(e))return}return t.preventDefault&&t.preventDefault(),!0}}function st(t){if(t)for(var e,n=0;n-1&&(s.params[d]=n.params[d]);return s.path=Q(u.path,s.params,'named route "'+c+'"'),l(u,s,a)}if(s.path){s.params={};for(var p=0;p=t.length?n():t[o]?e(t[o],(function(){r(o+1)})):r(o+1)};r(0)}function Rt(t){return function(e,n,r){var i=!1,a=0,s=null;Ft(t,(function(t,e,n,c){if("function"===typeof t&&void 0===t.cid){i=!0,a++;var u,l=Bt((function(e){Ht(e)&&(e=e.default),t.resolved="function"===typeof e?e:et.extend(e),n.components[c]=e,a--,a<=0&&r()})),f=Bt((function(t){var e="Failed to resolve async component "+c+": "+t;s||(s=o(t)?t:new Error(e),r(s))}));try{u=t(l,f)}catch(p){f(p)}if(u)if("function"===typeof u.then)u.then(l,f);else{var d=u.component;d&&"function"===typeof d.then&&d.then(l,f)}}})),i||r()}}function Ft(t,e){return zt(t.map((function(t){return Object.keys(t.components).map((function(n){return e(t.components[n],t.instances[n],t,n)}))})))}function zt(t){return Array.prototype.concat.apply([],t)}var Ut="function"===typeof Symbol&&"symbol"===typeof Symbol.toStringTag;function Ht(t){return t.__esModule||Ut&&"Module"===t[Symbol.toStringTag]}function Bt(t){var e=!1;return function(){var n=[],r=arguments.length;while(r--)n[r]=arguments[r];if(!e)return e=!0,t.apply(this,n)}}var Vt=function(t){function e(e){t.call(this),this.name=this._name="NavigationDuplicated",this.message='Navigating to current location ("'+e.fullPath+'") is not allowed',Object.defineProperty(this,"stack",{value:(new t).stack,writable:!0,configurable:!0})}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e}(Error);Vt._name="NavigationDuplicated";var Yt=function(t,e){this.router=t,this.base=qt(e),this.current=_,this.pending=null,this.ready=!1,this.readyCbs=[],this.readyErrorCbs=[],this.errorCbs=[]};function qt(t){if(!t)if(ut){var e=document.querySelector("base");t=e&&e.getAttribute("href")||"/",t=t.replace(/^https?:\/\/[^\/]+/,"")}else t="/";return"/"!==t.charAt(0)&&(t="/"+t),t.replace(/\/$/,"")}function Gt(t,e){var n,r=Math.max(t.length,e.length);for(n=0;n-1?decodeURI(t.slice(0,r))+t.slice(r):decodeURI(t)}else t=decodeURI(t.slice(0,n))+t.slice(n);return t}function ce(t){var e=window.location.href,n=e.indexOf("#"),r=n>=0?e.slice(0,n):e;return r+"#"+t}function ue(t){Dt?It(ce(t)):window.location.hash=t}function le(t){Dt?Lt(ce(t)):window.location.replace(ce(t))}var fe=function(t){function e(e,n){t.call(this,e,n),this.stack=[],this.index=-1}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.push=function(t,e,n){var r=this;this.transitionTo(t,(function(t){r.stack=r.stack.slice(0,r.index+1).concat(t),r.index++,e&&e(t)}),n)},e.prototype.replace=function(t,e,n){var r=this;this.transitionTo(t,(function(t){r.stack=r.stack.slice(0,r.index).concat(t),e&&e(t)}),n)},e.prototype.go=function(t){var e=this,n=this.index+t;if(!(n<0||n>=this.stack.length)){var r=this.stack[n];this.confirmTransition(r,(function(){e.index=n,e.updateRoute(r)}),(function(t){i(Vt,t)&&(e.index=n)}))}},e.prototype.getCurrentLocation=function(){var t=this.stack[this.stack.length-1];return t?t.fullPath:"/"},e.prototype.ensureURL=function(){},e}(Yt),de=function(t){void 0===t&&(t={}),this.app=null,this.apps=[],this.options=t,this.beforeHooks=[],this.resolveHooks=[],this.afterHooks=[],this.matcher=ht(t.routes||[],this);var e=t.mode||"hash";switch(this.fallback="history"===e&&!Dt&&!1!==t.fallback,this.fallback&&(e="hash"),ut||(e="abstract"),this.mode=e,e){case"history":this.history=new ne(this,t.base);break;case"hash":this.history=new oe(this,t.base,this.fallback);break;case"abstract":this.history=new fe(this,t.base);break;default:0}},pe={currentRoute:{configurable:!0}};function he(t,e){return t.push(e),function(){var n=t.indexOf(e);n>-1&&t.splice(n,1)}}function ve(t,e,n){var r="hash"===n?"#"+e:e;return t?M(t+"/"+r):r}de.prototype.match=function(t,e,n){return this.matcher.match(t,e,n)},pe.currentRoute.get=function(){return this.history&&this.history.current},de.prototype.init=function(t){var e=this;if(this.apps.push(t),t.$once("hook:destroyed",(function(){var n=e.apps.indexOf(t);n>-1&&e.apps.splice(n,1),e.app===t&&(e.app=e.apps[0]||null)})),!this.app){this.app=t;var n=this.history;if(n instanceof ne)n.transitionTo(n.getCurrentLocation());else if(n instanceof oe){var r=function(){n.setupListeners()};n.transitionTo(n.getCurrentLocation(),r,r)}n.listen((function(t){e.apps.forEach((function(e){e._route=t}))}))}},de.prototype.beforeEach=function(t){return he(this.beforeHooks,t)},de.prototype.beforeResolve=function(t){return he(this.resolveHooks,t)},de.prototype.afterEach=function(t){return he(this.afterHooks,t)},de.prototype.onReady=function(t,e){this.history.onReady(t,e)},de.prototype.onError=function(t){this.history.onError(t)},de.prototype.push=function(t,e,n){var r=this;if(!e&&!n&&"undefined"!==typeof Promise)return new Promise((function(e,n){r.history.push(t,e,n)}));this.history.push(t,e,n)},de.prototype.replace=function(t,e,n){var r=this;if(!e&&!n&&"undefined"!==typeof Promise)return new Promise((function(e,n){r.history.replace(t,e,n)}));this.history.replace(t,e,n)},de.prototype.go=function(t){this.history.go(t)},de.prototype.back=function(){this.go(-1)},de.prototype.forward=function(){this.go(1)},de.prototype.getMatchedComponents=function(t){var e=t?t.matched?t:this.resolve(t).route:this.currentRoute;return e?[].concat.apply([],e.matched.map((function(t){return Object.keys(t.components).map((function(e){return t.components[e]}))}))):[]},de.prototype.resolve=function(t,e,n){e=e||this.history.current;var r=tt(t,e,n,this),o=this.match(r,e),i=o.redirectedFrom||o.fullPath,a=this.history.base,s=ve(a,i,this.mode);return{location:r,route:o,href:s,normalizedTo:r,resolved:o}},de.prototype.addRoutes=function(t){this.matcher.addRoutes(t),this.history.current!==_&&this.history.transitionTo(this.history.getCurrentLocation())},Object.defineProperties(de.prototype,pe),de.install=ct,de.version="3.1.6",ut&&window.Vue&&window.Vue.use(de),e["a"]=de},"90e3":function(t,e){var n=0,r=Math.random();t.exports=function(t){return"Symbol("+String(void 0===t?"":t)+")_"+(++n+r).toString(36)}},9112:function(t,e,n){var r=n("83ab"),o=n("9bf2"),i=n("5c6c");t.exports=r?function(t,e,n){return o.f(t,e,i(1,n))}:function(t,e,n){return t[e]=n,t}},"91d3":function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:":";return(0,r.withParams)({type:"macAddress"},(function(e){if(!(0,r.req)(e))return!0;if("string"!==typeof e)return!1;var n="string"===typeof t&&""!==t?e.split(t):12===e.length||16===e.length?e.match(/.{2}/g):null;return null!==n&&(6===n.length||8===n.length)&&n.every(o)}))};var o=function(t){return t.toLowerCase().match(/^[0-9a-f]{2}$/)}},9263:function(t,e,n){"use strict";var r=n("ad6d"),o=n("9f7f"),i=RegExp.prototype.exec,a=String.prototype.replace,s=i,c=function(){var t=/a/,e=/b*/g;return i.call(t,"a"),i.call(e,"a"),0!==t.lastIndex||0!==e.lastIndex}(),u=o.UNSUPPORTED_Y||o.BROKEN_CARET,l=void 0!==/()??/.exec("")[1],f=c||l||u;f&&(s=function(t){var e,n,o,s,f=this,d=u&&f.sticky,p=r.call(f),h=f.source,v=0,g=t;return d&&(p=p.replace("y",""),-1===p.indexOf("g")&&(p+="g"),g=String(t).slice(f.lastIndex),f.lastIndex>0&&(!f.multiline||f.multiline&&"\n"!==t[f.lastIndex-1])&&(h="(?: "+h+")",g=" "+g,v++),n=new RegExp("^(?:"+h+")",p)),l&&(n=new RegExp("^"+h+"$(?!\\s)",p)),c&&(e=f.lastIndex),o=i.call(d?n:f,g),d?o?(o.input=o.input.slice(v),o[0]=o[0].slice(v),o.index=f.lastIndex,f.lastIndex+=o[0].length):f.lastIndex=0:c&&o&&(f.lastIndex=f.global?o.index+o[0].length:e),l&&o&&o.length>1&&a.call(o[0],n,(function(){for(s=1;s=0;--i){var a=this.tryEntries[i],s=a.completion;if("root"===a.tryLoc)return o("end");if(a.tryLoc<=this.prev){var c=r.call(a,"catchLoc"),u=r.call(a,"finallyLoc");if(c&&u){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--e){var n=this.tryEntries[e];if(n.finallyLoc===t)return this.complete(n.completion,n.afterLoc),$(n),v}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var n=this.tryEntries[e];if(n.tryLoc===t){var r=n.completion;if("throw"===r.type){var o=r.arg;$(n)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,n,r){return this.delegate={iterator:M(t),resultName:n,nextLoc:r},"next"===this.method&&(this.arg=e),v}},t}(t.exports);try{regeneratorRuntime=r}catch(o){Function("r","regeneratorRuntime = r")(r)}},9911:function(t,e,n){"use strict";var r=n("23e7"),o=n("857a"),i=n("af03");r({target:"String",proto:!0,forced:i("link")},{link:function(t){return o(this,"a","href",t)}})},"99af":function(t,e,n){"use strict";var r=n("23e7"),o=n("d039"),i=n("e8b5"),a=n("861d"),s=n("7b0b"),c=n("50c4"),u=n("8418"),l=n("65f0"),f=n("1dde"),d=n("b622"),p=n("2d00"),h=d("isConcatSpreadable"),v=9007199254740991,g="Maximum allowed index exceeded",m=p>=51||!o((function(){var t=[];return t[h]=!1,t.concat()[0]!==t})),y=f("concat"),b=function(t){if(!a(t))return!1;var e=t[h];return void 0!==e?!!e:i(t)},w=!m||!y;r({target:"Array",proto:!0,forced:w},{concat:function(t){var e,n,r,o,i,a=s(this),f=l(a,0),d=0;for(e=-1,r=arguments.length;ev)throw TypeError(g);for(n=0;n=v)throw TypeError(g);u(f,d++,i)}return f.length=d,f}})},"9bdd":function(t,e,n){var r=n("825a");t.exports=function(t,e,n,o){try{return o?e(r(n)[0],n[1]):e(n)}catch(a){var i=t["return"];throw void 0!==i&&r(i.call(t)),a}}},"9bf2":function(t,e,n){var r=n("83ab"),o=n("0cfb"),i=n("825a"),a=n("c04e"),s=Object.defineProperty;e.f=r?s:function(t,e,n){if(i(t),e=a(e,!0),i(n),o)try{return s(t,e,n)}catch(r){}if("get"in n||"set"in n)throw TypeError("Accessors not supported");return"value"in n&&(t[e]=n.value),t}},"9ed3":function(t,e,n){"use strict";var r=n("ae93").IteratorPrototype,o=n("7c73"),i=n("5c6c"),a=n("d44e"),s=n("3f8c"),c=function(){return this};t.exports=function(t,e,n){var u=e+" Iterator";return t.prototype=o(r,{next:i(1,n)}),a(t,u,!1,!0),s[u]=c,t}},"9f7f":function(t,e,n){"use strict";var r=n("d039");function o(t,e){return RegExp(t,e)}e.UNSUPPORTED_Y=r((function(){var t=o("a","y");return t.lastIndex=2,null!=t.exec("abcd")})),e.BROKEN_CARET=r((function(){var t=o("^r","gy");return t.lastIndex=2,null!=t.exec("str")}))},a026:function(t,e,n){"use strict";(function(t){ +/*! + * Vue.js v2.6.11 + * (c) 2014-2019 Evan You + * Released under the MIT License. + */ +var n=Object.freeze({});function r(t){return void 0===t||null===t}function o(t){return void 0!==t&&null!==t}function i(t){return!0===t}function a(t){return!1===t}function s(t){return"string"===typeof t||"number"===typeof t||"symbol"===typeof t||"boolean"===typeof t}function c(t){return null!==t&&"object"===typeof t}var u=Object.prototype.toString;function l(t){return"[object Object]"===u.call(t)}function f(t){return"[object RegExp]"===u.call(t)}function d(t){var e=parseFloat(String(t));return e>=0&&Math.floor(e)===e&&isFinite(t)}function p(t){return o(t)&&"function"===typeof t.then&&"function"===typeof t.catch}function h(t){return null==t?"":Array.isArray(t)||l(t)&&t.toString===u?JSON.stringify(t,null,2):String(t)}function v(t){var e=parseFloat(t);return isNaN(e)?t:e}function g(t,e){for(var n=Object.create(null),r=t.split(","),o=0;o-1)return t.splice(n,1)}}var w=Object.prototype.hasOwnProperty;function _(t,e){return w.call(t,e)}function x(t){var e=Object.create(null);return function(n){var r=e[n];return r||(e[n]=t(n))}}var S=/-(\w)/g,O=x((function(t){return t.replace(S,(function(t,e){return e?e.toUpperCase():""}))})),E=x((function(t){return t.charAt(0).toUpperCase()+t.slice(1)})),C=/\B([A-Z])/g,A=x((function(t){return t.replace(C,"-$1").toLowerCase()}));function $(t,e){function n(n){var r=arguments.length;return r?r>1?t.apply(e,arguments):t.call(e,n):t.call(e)}return n._length=t.length,n}function k(t,e){return t.bind(e)}var M=Function.prototype.bind?k:$;function T(t,e){e=e||0;var n=t.length-e,r=new Array(n);while(n--)r[n]=t[n+e];return r}function j(t,e){for(var n in e)t[n]=e[n];return t}function P(t){for(var e={},n=0;n0,ot=et&&et.indexOf("edge/")>0,it=(et&&et.indexOf("android"),et&&/iphone|ipad|ipod|ios/.test(et)||"ios"===tt),at=(et&&/chrome\/\d+/.test(et),et&&/phantomjs/.test(et),et&&et.match(/firefox\/(\d+)/)),st={}.watch,ct=!1;if(Z)try{var ut={};Object.defineProperty(ut,"passive",{get:function(){ct=!0}}),window.addEventListener("test-passive",null,ut)}catch(Ju){}var lt=function(){return void 0===X&&(X=!Z&&!Q&&"undefined"!==typeof t&&(t["process"]&&"server"===t["process"].env.VUE_ENV)),X},ft=Z&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function dt(t){return"function"===typeof t&&/native code/.test(t.toString())}var pt,ht="undefined"!==typeof Symbol&&dt(Symbol)&&"undefined"!==typeof Reflect&&dt(Reflect.ownKeys);pt="undefined"!==typeof Set&&dt(Set)?Set:function(){function t(){this.set=Object.create(null)}return t.prototype.has=function(t){return!0===this.set[t]},t.prototype.add=function(t){this.set[t]=!0},t.prototype.clear=function(){this.set=Object.create(null)},t}();var vt=D,gt=0,mt=function(){this.id=gt++,this.subs=[]};mt.prototype.addSub=function(t){this.subs.push(t)},mt.prototype.removeSub=function(t){b(this.subs,t)},mt.prototype.depend=function(){mt.target&&mt.target.addDep(this)},mt.prototype.notify=function(){var t=this.subs.slice();for(var e=0,n=t.length;e-1)if(i&&!_(o,"default"))a=!1;else if(""===a||a===A(t)){var c=ne(String,o.type);(c<0||s0&&(a=ke(a,(e||"")+"_"+n),$e(a[0])&&$e(u)&&(l[c]=Ot(u.text+a[0].text),a.shift()),l.push.apply(l,a)):s(a)?$e(u)?l[c]=Ot(u.text+a):""!==a&&l.push(Ot(a)):$e(a)&&$e(u)?l[c]=Ot(u.text+a.text):(i(t._isVList)&&o(a.tag)&&r(a.key)&&o(e)&&(a.key="__vlist"+e+"_"+n+"__"),l.push(a)));return l}function Me(t){var e=t.$options.provide;e&&(t._provided="function"===typeof e?e.call(t):e)}function Te(t){var e=je(t.$options.inject,t);e&&(Tt(!1),Object.keys(e).forEach((function(n){Lt(t,n,e[n])})),Tt(!0))}function je(t,e){if(t){for(var n=Object.create(null),r=ht?Reflect.ownKeys(t):Object.keys(t),o=0;o0,a=t?!!t.$stable:!i,s=t&&t.$key;if(t){if(t._normalized)return t._normalized;if(a&&r&&r!==n&&s===r.$key&&!i&&!r.$hasNormal)return r;for(var c in o={},t)t[c]&&"$"!==c[0]&&(o[c]=Le(e,c,t[c]))}else o={};for(var u in e)u in o||(o[u]=Ne(e,u));return t&&Object.isExtensible(t)&&(t._normalized=o),G(o,"$stable",a),G(o,"$key",s),G(o,"$hasNormal",i),o}function Le(t,e,n){var r=function(){var t=arguments.length?n.apply(null,arguments):n({});return t=t&&"object"===typeof t&&!Array.isArray(t)?[t]:Ae(t),t&&(0===t.length||1===t.length&&t[0].isComment)?void 0:t};return n.proxy&&Object.defineProperty(t,e,{get:r,enumerable:!0,configurable:!0}),r}function Ne(t,e){return function(){return t[e]}}function Re(t,e){var n,r,i,a,s;if(Array.isArray(t)||"string"===typeof t)for(n=new Array(t.length),r=0,i=t.length;r1?T(n):n;for(var r=T(arguments,1),o='event handler for "'+t+'"',i=0,a=n.length;idocument.createEvent("Event").timeStamp&&(Xn=function(){return Jn.now()})}function Zn(){var t,e;for(Kn=Xn(),qn=!0,Hn.sort((function(t,e){return t.id-e.id})),Gn=0;GnGn&&Hn[n].id>t.id)n--;Hn.splice(n+1,0,t)}else Hn.push(t);Yn||(Yn=!0,ge(Zn))}}var rr=0,or=function(t,e,n,r,o){this.vm=t,o&&(t._watcher=this),t._watchers.push(this),r?(this.deep=!!r.deep,this.user=!!r.user,this.lazy=!!r.lazy,this.sync=!!r.sync,this.before=r.before):this.deep=this.user=this.lazy=this.sync=!1,this.cb=n,this.id=++rr,this.active=!0,this.dirty=this.lazy,this.deps=[],this.newDeps=[],this.depIds=new pt,this.newDepIds=new pt,this.expression="","function"===typeof e?this.getter=e:(this.getter=K(e),this.getter||(this.getter=D)),this.value=this.lazy?void 0:this.get()};or.prototype.get=function(){var t;bt(this);var e=this.vm;try{t=this.getter.call(e,e)}catch(Ju){if(!this.user)throw Ju;re(Ju,e,'getter for watcher "'+this.expression+'"')}finally{this.deep&&ye(t),wt(),this.cleanupDeps()}return t},or.prototype.addDep=function(t){var e=t.id;this.newDepIds.has(e)||(this.newDepIds.add(e),this.newDeps.push(t),this.depIds.has(e)||t.addSub(this))},or.prototype.cleanupDeps=function(){var t=this.deps.length;while(t--){var e=this.deps[t];this.newDepIds.has(e.id)||e.removeSub(this)}var n=this.depIds;this.depIds=this.newDepIds,this.newDepIds=n,this.newDepIds.clear(),n=this.deps,this.deps=this.newDeps,this.newDeps=n,this.newDeps.length=0},or.prototype.update=function(){this.lazy?this.dirty=!0:this.sync?this.run():nr(this)},or.prototype.run=function(){if(this.active){var t=this.get();if(t!==this.value||c(t)||this.deep){var e=this.value;if(this.value=t,this.user)try{this.cb.call(this.vm,t,e)}catch(Ju){re(Ju,this.vm,'callback for watcher "'+this.expression+'"')}else this.cb.call(this.vm,t,e)}}},or.prototype.evaluate=function(){this.value=this.get(),this.dirty=!1},or.prototype.depend=function(){var t=this.deps.length;while(t--)this.deps[t].depend()},or.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||b(this.vm._watchers,this);var t=this.deps.length;while(t--)this.deps[t].removeSub(this);this.active=!1}};var ir={enumerable:!0,configurable:!0,get:D,set:D};function ar(t,e,n){ir.get=function(){return this[e][n]},ir.set=function(t){this[e][n]=t},Object.defineProperty(t,n,ir)}function sr(t){t._watchers=[];var e=t.$options;e.props&&cr(t,e.props),e.methods&&gr(t,e.methods),e.data?ur(t):It(t._data={},!0),e.computed&&dr(t,e.computed),e.watch&&e.watch!==st&&mr(t,e.watch)}function cr(t,e){var n=t.$options.propsData||{},r=t._props={},o=t.$options._propKeys=[],i=!t.$parent;i||Tt(!1);var a=function(i){o.push(i);var a=Zt(i,e,n,t);Lt(r,i,a),i in t||ar(t,"_props",i)};for(var s in e)a(s);Tt(!0)}function ur(t){var e=t.$options.data;e=t._data="function"===typeof e?lr(e,t):e||{},l(e)||(e={});var n=Object.keys(e),r=t.$options.props,o=(t.$options.methods,n.length);while(o--){var i=n[o];0,r&&_(r,i)||q(i)||ar(t,"_data",i)}It(e,!0)}function lr(t,e){bt();try{return t.call(e,e)}catch(Ju){return re(Ju,e,"data()"),{}}finally{wt()}}var fr={lazy:!0};function dr(t,e){var n=t._computedWatchers=Object.create(null),r=lt();for(var o in e){var i=e[o],a="function"===typeof i?i:i.get;0,r||(n[o]=new or(t,a||D,D,fr)),o in t||pr(t,o,i)}}function pr(t,e,n){var r=!lt();"function"===typeof n?(ir.get=r?hr(e):vr(n),ir.set=D):(ir.get=n.get?r&&!1!==n.cache?hr(e):vr(n.get):D,ir.set=n.set||D),Object.defineProperty(t,e,ir)}function hr(t){return function(){var e=this._computedWatchers&&this._computedWatchers[t];if(e)return e.dirty&&e.evaluate(),mt.target&&e.depend(),e.value}}function vr(t){return function(){return t.call(this,this)}}function gr(t,e){t.$options.props;for(var n in e)t[n]="function"!==typeof e[n]?D:M(e[n],t)}function mr(t,e){for(var n in e){var r=e[n];if(Array.isArray(r))for(var o=0;o-1)return this;var n=T(arguments,1);return n.unshift(this),"function"===typeof t.install?t.install.apply(t,n):"function"===typeof t&&t.apply(null,n),e.push(t),this}}function Ar(t){t.mixin=function(t){return this.options=Xt(this.options,t),this}}function $r(t){t.cid=0;var e=1;t.extend=function(t){t=t||{};var n=this,r=n.cid,o=t._Ctor||(t._Ctor={});if(o[r])return o[r];var i=t.name||n.options.name;var a=function(t){this._init(t)};return a.prototype=Object.create(n.prototype),a.prototype.constructor=a,a.cid=e++,a.options=Xt(n.options,t),a["super"]=n,a.options.props&&kr(a),a.options.computed&&Mr(a),a.extend=n.extend,a.mixin=n.mixin,a.use=n.use,H.forEach((function(t){a[t]=n[t]})),i&&(a.options.components[i]=a),a.superOptions=n.options,a.extendOptions=t,a.sealedOptions=j({},a.options),o[r]=a,a}}function kr(t){var e=t.options.props;for(var n in e)ar(t.prototype,"_props",n)}function Mr(t){var e=t.options.computed;for(var n in e)pr(t.prototype,n,e[n])}function Tr(t){H.forEach((function(e){t[e]=function(t,n){return n?("component"===e&&l(n)&&(n.name=n.name||t,n=this.options._base.extend(n)),"directive"===e&&"function"===typeof n&&(n={bind:n,update:n}),this.options[e+"s"][t]=n,n):this.options[e+"s"][t]}}))}function jr(t){return t&&(t.Ctor.options.name||t.tag)}function Pr(t,e){return Array.isArray(t)?t.indexOf(e)>-1:"string"===typeof t?t.split(",").indexOf(e)>-1:!!f(t)&&t.test(e)}function Dr(t,e){var n=t.cache,r=t.keys,o=t._vnode;for(var i in n){var a=n[i];if(a){var s=jr(a.componentOptions);s&&!e(s)&&Ir(n,i,r,o)}}}function Ir(t,e,n,r){var o=t[e];!o||r&&o.tag===r.tag||o.componentInstance.$destroy(),t[e]=null,b(n,e)}_r(Er),br(Er),Tn(Er),In(Er),wn(Er);var Lr=[String,RegExp,Array],Nr={name:"keep-alive",abstract:!0,props:{include:Lr,exclude:Lr,max:[String,Number]},created:function(){this.cache=Object.create(null),this.keys=[]},destroyed:function(){for(var t in this.cache)Ir(this.cache,t,this.keys)},mounted:function(){var t=this;this.$watch("include",(function(e){Dr(t,(function(t){return Pr(e,t)}))})),this.$watch("exclude",(function(e){Dr(t,(function(t){return!Pr(e,t)}))}))},render:function(){var t=this.$slots.default,e=En(t),n=e&&e.componentOptions;if(n){var r=jr(n),o=this,i=o.include,a=o.exclude;if(i&&(!r||!Pr(i,r))||a&&r&&Pr(a,r))return e;var s=this,c=s.cache,u=s.keys,l=null==e.key?n.Ctor.cid+(n.tag?"::"+n.tag:""):e.key;c[l]?(e.componentInstance=c[l].componentInstance,b(u,l),u.push(l)):(c[l]=e,u.push(l),this.max&&u.length>parseInt(this.max)&&Ir(c,u[0],u,this._vnode)),e.data.keepAlive=!0}return e||t&&t[0]}},Rr={KeepAlive:Nr};function Fr(t){var e={get:function(){return V}};Object.defineProperty(t,"config",e),t.util={warn:vt,extend:j,mergeOptions:Xt,defineReactive:Lt},t.set=Nt,t.delete=Rt,t.nextTick=ge,t.observable=function(t){return It(t),t},t.options=Object.create(null),H.forEach((function(e){t.options[e+"s"]=Object.create(null)})),t.options._base=t,j(t.options.components,Rr),Cr(t),Ar(t),$r(t),Tr(t)}Fr(Er),Object.defineProperty(Er.prototype,"$isServer",{get:lt}),Object.defineProperty(Er.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(Er,"FunctionalRenderContext",{value:Qe}),Er.version="2.6.11";var zr=g("style,class"),Ur=g("input,textarea,option,select,progress"),Hr=function(t,e,n){return"value"===n&&Ur(t)&&"button"!==e||"selected"===n&&"option"===t||"checked"===n&&"input"===t||"muted"===n&&"video"===t},Br=g("contenteditable,draggable,spellcheck"),Vr=g("events,caret,typing,plaintext-only"),Yr=function(t,e){return Xr(e)||"false"===e?"false":"contenteditable"===t&&Vr(e)?e:"true"},qr=g("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,translate,truespeed,typemustmatch,visible"),Gr="http://www.w3.org/1999/xlink",Wr=function(t){return":"===t.charAt(5)&&"xlink"===t.slice(0,5)},Kr=function(t){return Wr(t)?t.slice(6,t.length):""},Xr=function(t){return null==t||!1===t};function Jr(t){var e=t.data,n=t,r=t;while(o(r.componentInstance))r=r.componentInstance._vnode,r&&r.data&&(e=Zr(r.data,e));while(o(n=n.parent))n&&n.data&&(e=Zr(e,n.data));return Qr(e.staticClass,e.class)}function Zr(t,e){return{staticClass:to(t.staticClass,e.staticClass),class:o(t.class)?[t.class,e.class]:e.class}}function Qr(t,e){return o(t)||o(e)?to(t,eo(e)):""}function to(t,e){return t?e?t+" "+e:t:e||""}function eo(t){return Array.isArray(t)?no(t):c(t)?ro(t):"string"===typeof t?t:""}function no(t){for(var e,n="",r=0,i=t.length;r-1?lo[t]=e.constructor===window.HTMLUnknownElement||e.constructor===window.HTMLElement:lo[t]=/HTMLUnknownElement/.test(e.toString())}var po=g("text,number,password,search,email,tel,url");function ho(t){if("string"===typeof t){var e=document.querySelector(t);return e||document.createElement("div")}return t}function vo(t,e){var n=document.createElement(t);return"select"!==t?n:(e.data&&e.data.attrs&&void 0!==e.data.attrs.multiple&&n.setAttribute("multiple","multiple"),n)}function go(t,e){return document.createElementNS(oo[t],e)}function mo(t){return document.createTextNode(t)}function yo(t){return document.createComment(t)}function bo(t,e,n){t.insertBefore(e,n)}function wo(t,e){t.removeChild(e)}function _o(t,e){t.appendChild(e)}function xo(t){return t.parentNode}function So(t){return t.nextSibling}function Oo(t){return t.tagName}function Eo(t,e){t.textContent=e}function Co(t,e){t.setAttribute(e,"")}var Ao=Object.freeze({createElement:vo,createElementNS:go,createTextNode:mo,createComment:yo,insertBefore:bo,removeChild:wo,appendChild:_o,parentNode:xo,nextSibling:So,tagName:Oo,setTextContent:Eo,setStyleScope:Co}),$o={create:function(t,e){ko(e)},update:function(t,e){t.data.ref!==e.data.ref&&(ko(t,!0),ko(e))},destroy:function(t){ko(t,!0)}};function ko(t,e){var n=t.data.ref;if(o(n)){var r=t.context,i=t.componentInstance||t.elm,a=r.$refs;e?Array.isArray(a[n])?b(a[n],i):a[n]===i&&(a[n]=void 0):t.data.refInFor?Array.isArray(a[n])?a[n].indexOf(i)<0&&a[n].push(i):a[n]=[i]:a[n]=i}}var Mo=new _t("",{},[]),To=["create","activate","update","remove","destroy"];function jo(t,e){return t.key===e.key&&(t.tag===e.tag&&t.isComment===e.isComment&&o(t.data)===o(e.data)&&Po(t,e)||i(t.isAsyncPlaceholder)&&t.asyncFactory===e.asyncFactory&&r(e.asyncFactory.error))}function Po(t,e){if("input"!==t.tag)return!0;var n,r=o(n=t.data)&&o(n=n.attrs)&&n.type,i=o(n=e.data)&&o(n=n.attrs)&&n.type;return r===i||po(r)&&po(i)}function Do(t,e,n){var r,i,a={};for(r=e;r<=n;++r)i=t[r].key,o(i)&&(a[i]=r);return a}function Io(t){var e,n,a={},c=t.modules,u=t.nodeOps;for(e=0;ev?(f=r(n[y+1])?null:n[y+1].elm,S(t,f,n,h,y,i)):h>y&&E(e,d,v)}function $(t,e,n,r){for(var i=n;i-1?qo(t,e,n):qr(e)?Xr(n)?t.removeAttribute(e):(n="allowfullscreen"===e&&"EMBED"===t.tagName?"true":e,t.setAttribute(e,n)):Br(e)?t.setAttribute(e,Yr(e,n)):Wr(e)?Xr(n)?t.removeAttributeNS(Gr,Kr(e)):t.setAttributeNS(Gr,e,n):qo(t,e,n)}function qo(t,e,n){if(Xr(n))t.removeAttribute(e);else{if(nt&&!rt&&"TEXTAREA"===t.tagName&&"placeholder"===e&&""!==n&&!t.__ieph){var r=function(e){e.stopImmediatePropagation(),t.removeEventListener("input",r)};t.addEventListener("input",r),t.__ieph=!0}t.setAttribute(e,n)}}var Go={create:Vo,update:Vo};function Wo(t,e){var n=e.elm,i=e.data,a=t.data;if(!(r(i.staticClass)&&r(i.class)&&(r(a)||r(a.staticClass)&&r(a.class)))){var s=Jr(e),c=n._transitionClasses;o(c)&&(s=to(s,eo(c))),s!==n._prevClass&&(n.setAttribute("class",s),n._prevClass=s)}}var Ko,Xo,Jo,Zo,Qo,ti,ei={create:Wo,update:Wo},ni=/[\w).+\-_$\]]/;function ri(t){var e,n,r,o,i,a=!1,s=!1,c=!1,u=!1,l=0,f=0,d=0,p=0;for(r=0;r=0;h--)if(v=t.charAt(h)," "!==v)break;v&&ni.test(v)||(u=!0)}}else void 0===o?(p=r+1,o=t.slice(0,r).trim()):g();function g(){(i||(i=[])).push(t.slice(p,r).trim()),p=r+1}if(void 0===o?o=t.slice(0,r).trim():0!==p&&g(),i)for(r=0;r-1?{exp:t.slice(0,Zo),key:'"'+t.slice(Zo+1)+'"'}:{exp:t,key:null};Xo=t,Zo=Qo=ti=0;while(!xi())Jo=_i(),Si(Jo)?Ei(Jo):91===Jo&&Oi(Jo);return{exp:t.slice(0,Qo),key:t.slice(Qo+1,ti)}}function _i(){return Xo.charCodeAt(++Zo)}function xi(){return Zo>=Ko}function Si(t){return 34===t||39===t}function Oi(t){var e=1;Qo=Zo;while(!xi())if(t=_i(),Si(t))Ei(t);else if(91===t&&e++,93===t&&e--,0===e){ti=Zo;break}}function Ei(t){var e=t;while(!xi())if(t=_i(),t===e)break}var Ci,Ai="__r",$i="__c";function ki(t,e,n){n;var r=e.value,o=e.modifiers,i=t.tag,a=t.attrsMap.type;if(t.component)return yi(t,r,o),!1;if("select"===i)ji(t,r,o);else if("input"===i&&"checkbox"===a)Mi(t,r,o);else if("input"===i&&"radio"===a)Ti(t,r,o);else if("input"===i||"textarea"===i)Pi(t,r,o);else{if(!V.isReservedTag(i))return yi(t,r,o),!1}return!0}function Mi(t,e,n){var r=n&&n.number,o=hi(t,"value")||"null",i=hi(t,"true-value")||"true",a=hi(t,"false-value")||"false";si(t,"checked","Array.isArray("+e+")?_i("+e+","+o+")>-1"+("true"===i?":("+e+")":":_q("+e+","+i+")")),di(t,"change","var $$a="+e+",$$el=$event.target,$$c=$$el.checked?("+i+"):("+a+");if(Array.isArray($$a)){var $$v="+(r?"_n("+o+")":o)+",$$i=_i($$a,$$v);if($$el.checked){$$i<0&&("+bi(e,"$$a.concat([$$v])")+")}else{$$i>-1&&("+bi(e,"$$a.slice(0,$$i).concat($$a.slice($$i+1))")+")}}else{"+bi(e,"$$c")+"}",null,!0)}function Ti(t,e,n){var r=n&&n.number,o=hi(t,"value")||"null";o=r?"_n("+o+")":o,si(t,"checked","_q("+e+","+o+")"),di(t,"change",bi(e,o),null,!0)}function ji(t,e,n){var r=n&&n.number,o='Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = "_value" in o ? o._value : o.value;return '+(r?"_n(val)":"val")+"})",i="$event.target.multiple ? $$selectedVal : $$selectedVal[0]",a="var $$selectedVal = "+o+";";a=a+" "+bi(e,i),di(t,"change",a,null,!0)}function Pi(t,e,n){var r=t.attrsMap.type,o=n||{},i=o.lazy,a=o.number,s=o.trim,c=!i&&"range"!==r,u=i?"change":"range"===r?Ai:"input",l="$event.target.value";s&&(l="$event.target.value.trim()"),a&&(l="_n("+l+")");var f=bi(e,l);c&&(f="if($event.target.composing)return;"+f),si(t,"value","("+e+")"),di(t,u,f,null,!0),(s||a)&&di(t,"blur","$forceUpdate()")}function Di(t){if(o(t[Ai])){var e=nt?"change":"input";t[e]=[].concat(t[Ai],t[e]||[]),delete t[Ai]}o(t[$i])&&(t.change=[].concat(t[$i],t.change||[]),delete t[$i])}function Ii(t,e,n){var r=Ci;return function o(){var i=e.apply(null,arguments);null!==i&&Ri(t,o,n,r)}}var Li=ce&&!(at&&Number(at[1])<=53);function Ni(t,e,n,r){if(Li){var o=Kn,i=e;e=i._wrapper=function(t){if(t.target===t.currentTarget||t.timeStamp>=o||t.timeStamp<=0||t.target.ownerDocument!==document)return i.apply(this,arguments)}}Ci.addEventListener(t,e,ct?{capture:n,passive:r}:n)}function Ri(t,e,n,r){(r||Ci).removeEventListener(t,e._wrapper||e,n)}function Fi(t,e){if(!r(t.data.on)||!r(e.data.on)){var n=e.data.on||{},o=t.data.on||{};Ci=e.elm,Di(n),xe(n,o,Ni,Ri,Ii,e.context),Ci=void 0}}var zi,Ui={create:Fi,update:Fi};function Hi(t,e){if(!r(t.data.domProps)||!r(e.data.domProps)){var n,i,a=e.elm,s=t.data.domProps||{},c=e.data.domProps||{};for(n in o(c.__ob__)&&(c=e.data.domProps=j({},c)),s)n in c||(a[n]="");for(n in c){if(i=c[n],"textContent"===n||"innerHTML"===n){if(e.children&&(e.children.length=0),i===s[n])continue;1===a.childNodes.length&&a.removeChild(a.childNodes[0])}if("value"===n&&"PROGRESS"!==a.tagName){a._value=i;var u=r(i)?"":String(i);Bi(a,u)&&(a.value=u)}else if("innerHTML"===n&&ao(a.tagName)&&r(a.innerHTML)){zi=zi||document.createElement("div"),zi.innerHTML=""+i+"";var l=zi.firstChild;while(a.firstChild)a.removeChild(a.firstChild);while(l.firstChild)a.appendChild(l.firstChild)}else if(i!==s[n])try{a[n]=i}catch(Ju){}}}}function Bi(t,e){return!t.composing&&("OPTION"===t.tagName||Vi(t,e)||Yi(t,e))}function Vi(t,e){var n=!0;try{n=document.activeElement!==t}catch(Ju){}return n&&t.value!==e}function Yi(t,e){var n=t.value,r=t._vModifiers;if(o(r)){if(r.number)return v(n)!==v(e);if(r.trim)return n.trim()!==e.trim()}return n!==e}var qi={create:Hi,update:Hi},Gi=x((function(t){var e={},n=/;(?![^(]*\))/g,r=/:(.+)/;return t.split(n).forEach((function(t){if(t){var n=t.split(r);n.length>1&&(e[n[0].trim()]=n[1].trim())}})),e}));function Wi(t){var e=Ki(t.style);return t.staticStyle?j(t.staticStyle,e):e}function Ki(t){return Array.isArray(t)?P(t):"string"===typeof t?Gi(t):t}function Xi(t,e){var n,r={};if(e){var o=t;while(o.componentInstance)o=o.componentInstance._vnode,o&&o.data&&(n=Wi(o.data))&&j(r,n)}(n=Wi(t.data))&&j(r,n);var i=t;while(i=i.parent)i.data&&(n=Wi(i.data))&&j(r,n);return r}var Ji,Zi=/^--/,Qi=/\s*!important$/,ta=function(t,e,n){if(Zi.test(e))t.style.setProperty(e,n);else if(Qi.test(n))t.style.setProperty(A(e),n.replace(Qi,""),"important");else{var r=na(e);if(Array.isArray(n))for(var o=0,i=n.length;o-1?e.split(ia).forEach((function(e){return t.classList.add(e)})):t.classList.add(e);else{var n=" "+(t.getAttribute("class")||"")+" ";n.indexOf(" "+e+" ")<0&&t.setAttribute("class",(n+e).trim())}}function sa(t,e){if(e&&(e=e.trim()))if(t.classList)e.indexOf(" ")>-1?e.split(ia).forEach((function(e){return t.classList.remove(e)})):t.classList.remove(e),t.classList.length||t.removeAttribute("class");else{var n=" "+(t.getAttribute("class")||"")+" ",r=" "+e+" ";while(n.indexOf(r)>=0)n=n.replace(r," ");n=n.trim(),n?t.setAttribute("class",n):t.removeAttribute("class")}}function ca(t){if(t){if("object"===typeof t){var e={};return!1!==t.css&&j(e,ua(t.name||"v")),j(e,t),e}return"string"===typeof t?ua(t):void 0}}var ua=x((function(t){return{enterClass:t+"-enter",enterToClass:t+"-enter-to",enterActiveClass:t+"-enter-active",leaveClass:t+"-leave",leaveToClass:t+"-leave-to",leaveActiveClass:t+"-leave-active"}})),la=Z&&!rt,fa="transition",da="animation",pa="transition",ha="transitionend",va="animation",ga="animationend";la&&(void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend&&(pa="WebkitTransition",ha="webkitTransitionEnd"),void 0===window.onanimationend&&void 0!==window.onwebkitanimationend&&(va="WebkitAnimation",ga="webkitAnimationEnd"));var ma=Z?window.requestAnimationFrame?window.requestAnimationFrame.bind(window):setTimeout:function(t){return t()};function ya(t){ma((function(){ma(t)}))}function ba(t,e){var n=t._transitionClasses||(t._transitionClasses=[]);n.indexOf(e)<0&&(n.push(e),aa(t,e))}function wa(t,e){t._transitionClasses&&b(t._transitionClasses,e),sa(t,e)}function _a(t,e,n){var r=Sa(t,e),o=r.type,i=r.timeout,a=r.propCount;if(!o)return n();var s=o===fa?ha:ga,c=0,u=function(){t.removeEventListener(s,l),n()},l=function(e){e.target===t&&++c>=a&&u()};setTimeout((function(){c0&&(n=fa,l=a,f=i.length):e===da?u>0&&(n=da,l=u,f=c.length):(l=Math.max(a,u),n=l>0?a>u?fa:da:null,f=n?n===fa?i.length:c.length:0);var d=n===fa&&xa.test(r[pa+"Property"]);return{type:n,timeout:l,propCount:f,hasTransform:d}}function Oa(t,e){while(t.length1}function Ma(t,e){!0!==e.data.show&&Ca(e)}var Ta=Z?{create:Ma,activate:Ma,remove:function(t,e){!0!==t.data.show?Aa(t,e):e()}}:{},ja=[Go,ei,Ui,qi,oa,Ta],Pa=ja.concat(Bo),Da=Io({nodeOps:Ao,modules:Pa});rt&&document.addEventListener("selectionchange",(function(){var t=document.activeElement;t&&t.vmodel&&Ha(t,"input")}));var Ia={inserted:function(t,e,n,r){"select"===n.tag?(r.elm&&!r.elm._vOptions?Se(n,"postpatch",(function(){Ia.componentUpdated(t,e,n)})):La(t,e,n.context),t._vOptions=[].map.call(t.options,Fa)):("textarea"===n.tag||po(t.type))&&(t._vModifiers=e.modifiers,e.modifiers.lazy||(t.addEventListener("compositionstart",za),t.addEventListener("compositionend",Ua),t.addEventListener("change",Ua),rt&&(t.vmodel=!0)))},componentUpdated:function(t,e,n){if("select"===n.tag){La(t,e,n.context);var r=t._vOptions,o=t._vOptions=[].map.call(t.options,Fa);if(o.some((function(t,e){return!R(t,r[e])}))){var i=t.multiple?e.value.some((function(t){return Ra(t,o)})):e.value!==e.oldValue&&Ra(e.value,o);i&&Ha(t,"change")}}}};function La(t,e,n){Na(t,e,n),(nt||ot)&&setTimeout((function(){Na(t,e,n)}),0)}function Na(t,e,n){var r=e.value,o=t.multiple;if(!o||Array.isArray(r)){for(var i,a,s=0,c=t.options.length;s-1,a.selected!==i&&(a.selected=i);else if(R(Fa(a),r))return void(t.selectedIndex!==s&&(t.selectedIndex=s));o||(t.selectedIndex=-1)}}function Ra(t,e){return e.every((function(e){return!R(e,t)}))}function Fa(t){return"_value"in t?t._value:t.value}function za(t){t.target.composing=!0}function Ua(t){t.target.composing&&(t.target.composing=!1,Ha(t.target,"input"))}function Ha(t,e){var n=document.createEvent("HTMLEvents");n.initEvent(e,!0,!0),t.dispatchEvent(n)}function Ba(t){return!t.componentInstance||t.data&&t.data.transition?t:Ba(t.componentInstance._vnode)}var Va={bind:function(t,e,n){var r=e.value;n=Ba(n);var o=n.data&&n.data.transition,i=t.__vOriginalDisplay="none"===t.style.display?"":t.style.display;r&&o?(n.data.show=!0,Ca(n,(function(){t.style.display=i}))):t.style.display=r?i:"none"},update:function(t,e,n){var r=e.value,o=e.oldValue;if(!r!==!o){n=Ba(n);var i=n.data&&n.data.transition;i?(n.data.show=!0,r?Ca(n,(function(){t.style.display=t.__vOriginalDisplay})):Aa(n,(function(){t.style.display="none"}))):t.style.display=r?t.__vOriginalDisplay:"none"}},unbind:function(t,e,n,r,o){o||(t.style.display=t.__vOriginalDisplay)}},Ya={model:Ia,show:Va},qa={name:String,appear:Boolean,css:Boolean,mode:String,type:String,enterClass:String,leaveClass:String,enterToClass:String,leaveToClass:String,enterActiveClass:String,leaveActiveClass:String,appearClass:String,appearActiveClass:String,appearToClass:String,duration:[Number,String,Object]};function Ga(t){var e=t&&t.componentOptions;return e&&e.Ctor.options.abstract?Ga(En(e.children)):t}function Wa(t){var e={},n=t.$options;for(var r in n.propsData)e[r]=t[r];var o=n._parentListeners;for(var i in o)e[O(i)]=o[i];return e}function Ka(t,e){if(/\d-keep-alive$/.test(e.tag))return t("keep-alive",{props:e.componentOptions.propsData})}function Xa(t){while(t=t.parent)if(t.data.transition)return!0}function Ja(t,e){return e.key===t.key&&e.tag===t.tag}var Za=function(t){return t.tag||On(t)},Qa=function(t){return"show"===t.name},ts={name:"transition",props:qa,abstract:!0,render:function(t){var e=this,n=this.$slots.default;if(n&&(n=n.filter(Za),n.length)){0;var r=this.mode;0;var o=n[0];if(Xa(this.$vnode))return o;var i=Ga(o);if(!i)return o;if(this._leaving)return Ka(t,o);var a="__transition-"+this._uid+"-";i.key=null==i.key?i.isComment?a+"comment":a+i.tag:s(i.key)?0===String(i.key).indexOf(a)?i.key:a+i.key:i.key;var c=(i.data||(i.data={})).transition=Wa(this),u=this._vnode,l=Ga(u);if(i.data.directives&&i.data.directives.some(Qa)&&(i.data.show=!0),l&&l.data&&!Ja(i,l)&&!On(l)&&(!l.componentInstance||!l.componentInstance._vnode.isComment)){var f=l.data.transition=j({},c);if("out-in"===r)return this._leaving=!0,Se(f,"afterLeave",(function(){e._leaving=!1,e.$forceUpdate()})),Ka(t,o);if("in-out"===r){if(On(i))return u;var d,p=function(){d()};Se(c,"afterEnter",p),Se(c,"enterCancelled",p),Se(f,"delayLeave",(function(t){d=t}))}}return o}}},es=j({tag:String,moveClass:String},qa);delete es.mode;var ns={props:es,beforeMount:function(){var t=this,e=this._update;this._update=function(n,r){var o=Pn(t);t.__patch__(t._vnode,t.kept,!1,!0),t._vnode=t.kept,o(),e.call(t,n,r)}},render:function(t){for(var e=this.tag||this.$vnode.data.tag||"span",n=Object.create(null),r=this.prevChildren=this.children,o=this.$slots.default||[],i=this.children=[],a=Wa(this),s=0;sc&&(s.push(i=t.slice(c,o)),a.push(JSON.stringify(i)));var u=ri(r[1].trim());a.push("_s("+u+")"),s.push({"@binding":u}),c=o+r[0].length}return c\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,Ss=/^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,Os="[a-zA-Z_][\\-\\.0-9_a-zA-Z"+Y.source+"]*",Es="((?:"+Os+"\\:)?"+Os+")",Cs=new RegExp("^<"+Es),As=/^\s*(\/?)>/,$s=new RegExp("^<\\/"+Es+"[^>]*>"),ks=/^]+>/i,Ms=/^",""":'"',"&":"&"," ":"\n"," ":"\t","'":"'"},Is=/&(?:lt|gt|quot|amp|#39);/g,Ls=/&(?:lt|gt|quot|amp|#39|#10|#9);/g,Ns=g("pre,textarea",!0),Rs=function(t,e){return t&&Ns(t)&&"\n"===e[0]};function Fs(t,e){var n=e?Ls:Is;return t.replace(n,(function(t){return Ds[t]}))}function zs(t,e){var n,r,o=[],i=e.expectHTML,a=e.isUnaryTag||I,s=e.canBeLeftOpenTag||I,c=0;while(t){if(n=t,r&&js(r)){var u=0,l=r.toLowerCase(),f=Ps[l]||(Ps[l]=new RegExp("([\\s\\S]*?)(]*>)","i")),d=t.replace(f,(function(t,n,r){return u=r.length,js(l)||"noscript"===l||(n=n.replace(//g,"$1").replace(//g,"$1")),Rs(l,n)&&(n=n.slice(1)),e.chars&&e.chars(n),""}));c+=t.length-d.length,t=d,C(l,c-u,c)}else{var p=t.indexOf("<");if(0===p){if(Ms.test(t)){var h=t.indexOf("--\x3e");if(h>=0){e.shouldKeepComment&&e.comment(t.substring(4,h),c,c+h+3),S(h+3);continue}}if(Ts.test(t)){var v=t.indexOf("]>");if(v>=0){S(v+2);continue}}var g=t.match(ks);if(g){S(g[0].length);continue}var m=t.match($s);if(m){var y=c;S(m[0].length),C(m[1],y,c);continue}var b=O();if(b){E(b),Rs(b.tagName,t)&&S(1);continue}}var w=void 0,_=void 0,x=void 0;if(p>=0){_=t.slice(p);while(!$s.test(_)&&!Cs.test(_)&&!Ms.test(_)&&!Ts.test(_)){if(x=_.indexOf("<",1),x<0)break;p+=x,_=t.slice(p)}w=t.substring(0,p)}p<0&&(w=t),w&&S(w.length),e.chars&&w&&e.chars(w,c-w.length,c)}if(t===n){e.chars&&e.chars(t);break}}function S(e){c+=e,t=t.substring(e)}function O(){var e=t.match(Cs);if(e){var n,r,o={tagName:e[1],attrs:[],start:c};S(e[0].length);while(!(n=t.match(As))&&(r=t.match(Ss)||t.match(xs)))r.start=c,S(r[0].length),r.end=c,o.attrs.push(r);if(n)return o.unarySlash=n[1],S(n[0].length),o.end=c,o}}function E(t){var n=t.tagName,c=t.unarySlash;i&&("p"===r&&_s(n)&&C(r),s(n)&&r===n&&C(n));for(var u=a(n)||!!c,l=t.attrs.length,f=new Array(l),d=0;d=0;a--)if(o[a].lowerCasedTag===s)break}else a=0;if(a>=0){for(var u=o.length-1;u>=a;u--)e.end&&e.end(o[u].tag,n,i);o.length=a,r=a&&o[a-1].tag}else"br"===s?e.start&&e.start(t,[],!0,n,i):"p"===s&&(e.start&&e.start(t,[],!1,n,i),e.end&&e.end(t,n,i))}C()}var Us,Hs,Bs,Vs,Ys,qs,Gs,Ws,Ks=/^@|^v-on:/,Xs=/^v-|^@|^:|^#/,Js=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,Zs=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,Qs=/^\(|\)$/g,tc=/^\[.*\]$/,ec=/:(.*)$/,nc=/^:|^\.|^v-bind:/,rc=/\.[^.\]]+(?=[^\]]*$)/g,oc=/^v-slot(:|$)|^#/,ic=/[\r\n]/,ac=/\s+/g,sc=x(ys.decode),cc="_empty_";function uc(t,e,n){return{type:1,tag:t,attrsList:e,attrsMap:Mc(e),rawAttrsMap:{},parent:n,children:[]}}function lc(t,e){Us=e.warn||ii,qs=e.isPreTag||I,Gs=e.mustUseProp||I,Ws=e.getTagNamespace||I;var n=e.isReservedTag||I;(function(t){return!!t.component||!n(t.tag)}),Bs=ai(e.modules,"transformNode"),Vs=ai(e.modules,"preTransformNode"),Ys=ai(e.modules,"postTransformNode"),Hs=e.delimiters;var r,o,i=[],a=!1!==e.preserveWhitespace,s=e.whitespace,c=!1,u=!1;function l(t){if(f(t),c||t.processed||(t=pc(t,e)),i.length||t===r||r.if&&(t.elseif||t.else)&&_c(r,{exp:t.elseif,block:t}),o&&!t.forbidden)if(t.elseif||t.else)bc(t,o);else{if(t.slotScope){var n=t.slotTarget||'"default"';(o.scopedSlots||(o.scopedSlots={}))[n]=t}o.children.push(t),t.parent=o}t.children=t.children.filter((function(t){return!t.slotScope})),f(t),t.pre&&(c=!1),qs(t.tag)&&(u=!1);for(var a=0;a|^function(?:\s+[\w$]+)?\s*\(/,tu=/\([^)]*?\);*$/,eu=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/,nu={esc:27,tab:9,enter:13,space:32,up:38,left:37,right:39,down:40,delete:[8,46]},ru={esc:["Esc","Escape"],tab:"Tab",enter:"Enter",space:[" ","Spacebar"],up:["Up","ArrowUp"],left:["Left","ArrowLeft"],right:["Right","ArrowRight"],down:["Down","ArrowDown"],delete:["Backspace","Delete","Del"]},ou=function(t){return"if("+t+")return null;"},iu={stop:"$event.stopPropagation();",prevent:"$event.preventDefault();",self:ou("$event.target !== $event.currentTarget"),ctrl:ou("!$event.ctrlKey"),shift:ou("!$event.shiftKey"),alt:ou("!$event.altKey"),meta:ou("!$event.metaKey"),left:ou("'button' in $event && $event.button !== 0"),middle:ou("'button' in $event && $event.button !== 1"),right:ou("'button' in $event && $event.button !== 2")};function au(t,e){var n=e?"nativeOn:":"on:",r="",o="";for(var i in t){var a=su(t[i]);t[i]&&t[i].dynamic?o+=i+","+a+",":r+='"'+i+'":'+a+","}return r="{"+r.slice(0,-1)+"}",o?n+"_d("+r+",["+o.slice(0,-1)+"])":n+r}function su(t){if(!t)return"function(){}";if(Array.isArray(t))return"["+t.map((function(t){return su(t)})).join(",")+"]";var e=eu.test(t.value),n=Qc.test(t.value),r=eu.test(t.value.replace(tu,""));if(t.modifiers){var o="",i="",a=[];for(var s in t.modifiers)if(iu[s])i+=iu[s],nu[s]&&a.push(s);else if("exact"===s){var c=t.modifiers;i+=ou(["ctrl","shift","alt","meta"].filter((function(t){return!c[t]})).map((function(t){return"$event."+t+"Key"})).join("||"))}else a.push(s);a.length&&(o+=cu(a)),i&&(o+=i);var u=e?"return "+t.value+"($event)":n?"return ("+t.value+")($event)":r?"return "+t.value:t.value;return"function($event){"+o+u+"}"}return e||n?t.value:"function($event){"+(r?"return "+t.value:t.value)+"}"}function cu(t){return"if(!$event.type.indexOf('key')&&"+t.map(uu).join("&&")+")return null;"}function uu(t){var e=parseInt(t,10);if(e)return"$event.keyCode!=="+e;var n=nu[t],r=ru[t];return"_k($event.keyCode,"+JSON.stringify(t)+","+JSON.stringify(n)+",$event.key,"+JSON.stringify(r)+")"}function lu(t,e){t.wrapListeners=function(t){return"_g("+t+","+e.value+")"}}function fu(t,e){t.wrapData=function(n){return"_b("+n+",'"+t.tag+"',"+e.value+","+(e.modifiers&&e.modifiers.prop?"true":"false")+(e.modifiers&&e.modifiers.sync?",true":"")+")"}}var du={on:lu,bind:fu,cloak:D},pu=function(t){this.options=t,this.warn=t.warn||ii,this.transforms=ai(t.modules,"transformCode"),this.dataGenFns=ai(t.modules,"genData"),this.directives=j(j({},du),t.directives);var e=t.isReservedTag||I;this.maybeComponent=function(t){return!!t.component||!e(t.tag)},this.onceId=0,this.staticRenderFns=[],this.pre=!1};function hu(t,e){var n=new pu(e),r=t?vu(t,n):'_c("div")';return{render:"with(this){return "+r+"}",staticRenderFns:n.staticRenderFns}}function vu(t,e){if(t.parent&&(t.pre=t.pre||t.parent.pre),t.staticRoot&&!t.staticProcessed)return gu(t,e);if(t.once&&!t.onceProcessed)return mu(t,e);if(t.for&&!t.forProcessed)return wu(t,e);if(t.if&&!t.ifProcessed)return yu(t,e);if("template"!==t.tag||t.slotTarget||e.pre){if("slot"===t.tag)return Du(t,e);var n;if(t.component)n=Iu(t.component,t,e);else{var r;(!t.plain||t.pre&&e.maybeComponent(t))&&(r=_u(t,e));var o=t.inlineTemplate?null:$u(t,e,!0);n="_c('"+t.tag+"'"+(r?","+r:"")+(o?","+o:"")+")"}for(var i=0;i>>0}function Cu(t){return 1===t.type&&("slot"===t.tag||t.children.some(Cu))}function Au(t,e){var n=t.attrsMap["slot-scope"];if(t.if&&!t.ifProcessed&&!n)return yu(t,e,Au,"null");if(t.for&&!t.forProcessed)return wu(t,e,Au);var r=t.slotScope===cc?"":String(t.slotScope),o="function("+r+"){return "+("template"===t.tag?t.if&&n?"("+t.if+")?"+($u(t,e)||"undefined")+":undefined":$u(t,e)||"undefined":vu(t,e))+"}",i=r?"":",proxy:true";return"{key:"+(t.slotTarget||'"default"')+",fn:"+o+i+"}"}function $u(t,e,n,r,o){var i=t.children;if(i.length){var a=i[0];if(1===i.length&&a.for&&"template"!==a.tag&&"slot"!==a.tag){var s=n?e.maybeComponent(a)?",1":",0":"";return""+(r||vu)(a,e)+s}var c=n?ku(i,e.maybeComponent):0,u=o||Tu;return"["+i.map((function(t){return u(t,e)})).join(",")+"]"+(c?","+c:"")}}function ku(t,e){for(var n=0,r=0;r':'
',Uu.innerHTML.indexOf(" ")>0}var qu=!!Z&&Yu(!1),Gu=!!Z&&Yu(!0),Wu=x((function(t){var e=ho(t);return e&&e.innerHTML})),Ku=Er.prototype.$mount;function Xu(t){if(t.outerHTML)return t.outerHTML;var e=document.createElement("div");return e.appendChild(t.cloneNode(!0)),e.innerHTML}Er.prototype.$mount=function(t,e){if(t=t&&ho(t),t===document.body||t===document.documentElement)return this;var n=this.$options;if(!n.render){var r=n.template;if(r)if("string"===typeof r)"#"===r.charAt(0)&&(r=Wu(r));else{if(!r.nodeType)return this;r=r.innerHTML}else t&&(r=Xu(t));if(r){0;var o=Vu(r,{outputSourceRange:!1,shouldDecodeNewlines:qu,shouldDecodeNewlinesForHref:Gu,delimiters:n.delimiters,comments:n.comments},this),i=o.render,a=o.staticRenderFns;n.render=i,n.staticRenderFns=a}}return Ku.call(this,t,e)},Er.compile=Vu,e["a"]=Er}).call(this,n("c8ba"))},a15b:function(t,e,n){"use strict";var r=n("23e7"),o=n("44ad"),i=n("fc6a"),a=n("a640"),s=[].join,c=o!=Object,u=a("join",",");r({target:"Array",proto:!0,forced:c||!u},{join:function(t){return s.call(i(this),void 0===t?",":t)}})},a434:function(t,e,n){"use strict";var r=n("23e7"),o=n("23cb"),i=n("a691"),a=n("50c4"),s=n("7b0b"),c=n("65f0"),u=n("8418"),l=n("1dde"),f=n("ae40"),d=l("splice"),p=f("splice",{ACCESSORS:!0,0:0,1:2}),h=Math.max,v=Math.min,g=9007199254740991,m="Maximum allowed length exceeded";r({target:"Array",proto:!0,forced:!d||!p},{splice:function(t,e){var n,r,l,f,d,p,y=s(this),b=a(y.length),w=o(t,b),_=arguments.length;if(0===_?n=r=0:1===_?(n=0,r=b-w):(n=_-2,r=v(h(i(e),0),b-w)),b+n-r>g)throw TypeError(m);for(l=c(y,r),f=0;fb-r+n;f--)delete y[f-1]}else if(n>r)for(f=b-r;f>w;f--)d=f+r-1,p=f+n-1,d in y?y[p]=y[d]:delete y[p];for(f=0;fi)o.push(arguments[i++]);if(r=e,(p(e)||void 0!==t)&&!st(t))return d(e)||(e=function(t,e){if("function"==typeof r&&(e=r.call(this,t,e)),!st(e))return e}),o[1]=e,G.apply(null,o)}})}q[U][H]||A(q[U],H,q[U].valueOf),L(q,z),T[F]=!0},a640:function(t,e,n){"use strict";var r=n("d039");t.exports=function(t,e){var n=[][t];return!!n&&r((function(){n.call(null,e||function(){throw 1},1)}))}},a691:function(t,e){var n=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:n)(t)}},a79d:function(t,e,n){"use strict";var r=n("23e7"),o=n("c430"),i=n("fea9"),a=n("d039"),s=n("d066"),c=n("4840"),u=n("cdf9"),l=n("6eeb"),f=!!i&&a((function(){i.prototype["finally"].call({then:function(){}},(function(){}))}));r({target:"Promise",proto:!0,real:!0,forced:f},{finally:function(t){var e=c(this,s("Promise")),n="function"==typeof t;return this.then(n?function(n){return u(e,t()).then((function(){return n}))}:t,n?function(n){return u(e,t()).then((function(){throw n}))}:t)}}),o||"function"!=typeof i||i.prototype["finally"]||l(i.prototype,"finally",s("Promise").prototype["finally"])},a9e3:function(t,e,n){"use strict";var r=n("83ab"),o=n("da84"),i=n("94ca"),a=n("6eeb"),s=n("5135"),c=n("c6b6"),u=n("7156"),l=n("c04e"),f=n("d039"),d=n("7c73"),p=n("241c").f,h=n("06cf").f,v=n("9bf2").f,g=n("58a8").trim,m="Number",y=o[m],b=y.prototype,w=c(d(b))==m,_=function(t){var e,n,r,o,i,a,s,c,u=l(t,!1);if("string"==typeof u&&u.length>2)if(u=g(u),e=u.charCodeAt(0),43===e||45===e){if(n=u.charCodeAt(2),88===n||120===n)return NaN}else if(48===e){switch(u.charCodeAt(1)){case 66:case 98:r=2,o=49;break;case 79:case 111:r=8,o=55;break;default:return+u}for(i=u.slice(2),a=i.length,s=0;so)return NaN;return parseInt(i,r)}return+u};if(i(m,!y(" 0o1")||!y("0b1")||y("+0x1"))){for(var x,S=function(t){var e=arguments.length<1?0:t,n=this;return n instanceof S&&(w?f((function(){b.valueOf.call(n)})):c(n)!=m)?u(new y(_(e)),n,S):_(e)},O=r?p(y):"MAX_VALUE,MIN_VALUE,NaN,NEGATIVE_INFINITY,POSITIVE_INFINITY,EPSILON,isFinite,isInteger,isNaN,isSafeInteger,MAX_SAFE_INTEGER,MIN_SAFE_INTEGER,parseFloat,parseInt,isInteger".split(","),E=0;O.length>E;E++)s(y,x=O[E])&&!s(S,x)&&v(S,x,h(y,x));S.prototype=b,b.constructor=S,a(o,m,S)}},aa47:function(t,e,n){"use strict"; +/**! + * Sortable 1.10.2 + * @author RubaXa + * @author owenm + * @license MIT + */ +function r(t){return r="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"===typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},r(t)}function o(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function i(){return i=Object.assign||function(t){for(var e=1;e=0||(o[n]=t[n]);return o}function c(t,e){if(null==t)return{};var n,r,o=s(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}function u(t){return l(t)||f(t)||d()}function l(t){if(Array.isArray(t)){for(var e=0,n=new Array(t.length);e"===e[0]&&(e=e.substring(1)),t)try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(n){return!1}return!1}}function E(t){return t.host&&t!==document&&t.host.nodeType?t.host:t.parentNode}function C(t,e,n,r){if(t){n=n||document;do{if(null!=e&&(">"===e[0]?t.parentNode===n&&O(t,e):O(t,e))||r&&t===n)return t;if(t===n)break}while(t=E(t))}return null}var A,$=/\s+/g;function k(t,e,n){if(t&&e)if(t.classList)t.classList[n?"add":"remove"](e);else{var r=(" "+t.className+" ").replace($," ").replace(" "+e+" "," ");t.className=(r+(n?" "+e:"")).replace($," ")}}function M(t,e,n){var r=t&&t.style;if(r){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];e in r||-1!==e.indexOf("webkit")||(e="-webkit-"+e),r[e]=n+("string"===typeof n?"":"px")}}function T(t,e){var n="";if("string"===typeof t)n=t;else do{var r=M(t,"transform");r&&"none"!==r&&(n=r+" "+n)}while(!e&&(t=t.parentNode));var o=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return o&&new o(n)}function j(t,e,n){if(t){var r=t.getElementsByTagName(e),o=0,i=r.length;if(n)for(;o=i:o<=i,!a)return r;if(r===P())break;r=U(r,!1)}return!1}function L(t,e,n){var r=0,o=0,i=t.children;while(o2&&void 0!==arguments[2]?arguments[2]:{},r=n.evt,o=c(n,["evt"]);nt.pluginEvent.bind(Zt)(t,e,a({dragEl:at,parentEl:st,ghostEl:ct,rootEl:ut,nextEl:lt,lastDownEl:ft,cloneEl:dt,cloneHidden:pt,dragStarted:Ct,putSortable:bt,activeSortable:Zt.active,originalEvent:r,oldIndex:ht,oldDraggableIndex:gt,newIndex:vt,newDraggableIndex:mt,hideGhostForTarget:Wt,unhideGhostForTarget:Kt,cloneNowHidden:function(){pt=!0},cloneNowShown:function(){pt=!1},dispatchSortableEvent:function(t){it({sortable:e,name:t,originalEvent:r})}},o))};function it(t){rt(a({putSortable:bt,cloneEl:dt,targetEl:at,rootEl:ut,oldIndex:ht,oldDraggableIndex:gt,newIndex:vt,newDraggableIndex:mt},t))}var at,st,ct,ut,lt,ft,dt,pt,ht,vt,gt,mt,yt,bt,wt,_t,xt,St,Ot,Et,Ct,At,$t,kt,Mt,Tt=!1,jt=!1,Pt=[],Dt=!1,It=!1,Lt=[],Nt=!1,Rt=[],Ft="undefined"!==typeof document,zt=b,Ut=g||v?"cssFloat":"float",Ht=Ft&&!w&&!b&&"draggable"in document.createElement("div"),Bt=function(){if(Ft){if(v)return!1;var t=document.createElement("x");return t.style.cssText="pointer-events:auto","auto"===t.style.pointerEvents}}(),Vt=function(t,e){var n=M(t),r=parseInt(n.width)-parseInt(n.paddingLeft)-parseInt(n.paddingRight)-parseInt(n.borderLeftWidth)-parseInt(n.borderRightWidth),o=L(t,0,e),i=L(t,1,e),a=o&&M(o),s=i&&M(i),c=a&&parseInt(a.marginLeft)+parseInt(a.marginRight)+D(o).width,u=s&&parseInt(s.marginLeft)+parseInt(s.marginRight)+D(i).width;if("flex"===n.display)return"column"===n.flexDirection||"column-reverse"===n.flexDirection?"vertical":"horizontal";if("grid"===n.display)return n.gridTemplateColumns.split(" ").length<=1?"vertical":"horizontal";if(o&&a["float"]&&"none"!==a["float"]){var l="left"===a["float"]?"left":"right";return!i||"both"!==s.clear&&s.clear!==l?"horizontal":"vertical"}return o&&("block"===a.display||"flex"===a.display||"table"===a.display||"grid"===a.display||c>=r&&"none"===n[Ut]||i&&"none"===n[Ut]&&c+u>r)?"vertical":"horizontal"},Yt=function(t,e,n){var r=n?t.left:t.top,o=n?t.right:t.bottom,i=n?t.width:t.height,a=n?e.left:e.top,s=n?e.right:e.bottom,c=n?e.width:e.height;return r===a||o===s||r+i/2===a+c/2},qt=function(t,e){var n;return Pt.some((function(r){if(!N(r)){var o=D(r),i=r[X].options.emptyInsertThreshold,a=t>=o.left-i&&t<=o.right+i,s=e>=o.top-i&&e<=o.bottom+i;return i&&a&&s?n=r:void 0}})),n},Gt=function(t){function e(t,n){return function(r,o,i,a){var s=r.options.group.name&&o.options.group.name&&r.options.group.name===o.options.group.name;if(null==t&&(n||s))return!0;if(null==t||!1===t)return!1;if(n&&"clone"===t)return t;if("function"===typeof t)return e(t(r,o,i,a),n)(r,o,i,a);var c=(n?r:o).options.group.name;return!0===t||"string"===typeof t&&t===c||t.join&&t.indexOf(c)>-1}}var n={},o=t.group;o&&"object"==r(o)||(o={name:o}),n.name=o.name,n.checkPull=e(o.pull,!0),n.checkPut=e(o.put),n.revertClone=o.revertClone,t.group=n},Wt=function(){!Bt&&ct&&M(ct,"display","none")},Kt=function(){!Bt&&ct&&M(ct,"display","")};Ft&&document.addEventListener("click",(function(t){if(jt)return t.preventDefault(),t.stopPropagation&&t.stopPropagation(),t.stopImmediatePropagation&&t.stopImmediatePropagation(),jt=!1,!1}),!0);var Xt=function(t){if(at){t=t.touches?t.touches[0]:t;var e=qt(t.clientX,t.clientY);if(e){var n={};for(var r in t)t.hasOwnProperty(r)&&(n[r]=t[r]);n.target=n.rootEl=e,n.preventDefault=void 0,n.stopPropagation=void 0,e[X]._onDragOver(n)}}},Jt=function(t){at&&at.parentNode[X]._isOutsideThisEl(t.target)};function Zt(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=i({},e),t[X]=this;var n={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return Vt(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==Zt.supportPointer&&"PointerEvent"in window,emptyInsertThreshold:5};for(var r in nt.initializePlugins(this,t,n),n)!(r in e)&&(e[r]=n[r]);for(var o in Gt(e),this)"_"===o.charAt(0)&&"function"===typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&Ht,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?x(t,"pointerdown",this._onTapStart):(x(t,"mousedown",this._onTapStart),x(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(x(t,"dragover",this),x(t,"dragenter",this)),Pt.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),i(this,J())}function Qt(t){t.dataTransfer&&(t.dataTransfer.dropEffect="move"),t.cancelable&&t.preventDefault()}function te(t,e,n,r,o,i,a,s){var c,u,l=t[X],f=l.options.onMove;return!window.CustomEvent||v||g?(c=document.createEvent("Event"),c.initEvent("move",!0,!0)):c=new CustomEvent("move",{bubbles:!0,cancelable:!0}),c.to=e,c.from=t,c.dragged=n,c.draggedRect=r,c.related=o||e,c.relatedRect=i||D(e),c.willInsertAfter=s,c.originalEvent=a,t.dispatchEvent(c),f&&(u=f.call(l,c,a)),u}function ee(t){t.draggable=!1}function ne(){Nt=!1}function re(t,e,n){var r=D(N(n.el,n.options.draggable)),o=10;return e?t.clientX>r.right+o||t.clientX<=r.right&&t.clientY>r.bottom&&t.clientX>=r.left:t.clientX>r.right&&t.clientY>r.top||t.clientX<=r.right&&t.clientY>r.bottom+o}function oe(t,e,n,r,o,i,a,s){var c=r?t.clientY:t.clientX,u=r?n.height:n.width,l=r?n.top:n.left,f=r?n.bottom:n.right,d=!1;if(!a)if(s&&ktl+u*i/2:cf-kt)return-$t}else if(c>l+u*(1-o)/2&&cf-u*i/2)?c>l+u/2?1:-1:0}function ie(t){return R(at)=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){at&&ee(at),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;S(t,"mouseup",this._disableDelayedDrag),S(t,"touchend",this._disableDelayedDrag),S(t,"touchcancel",this._disableDelayedDrag),S(t,"mousemove",this._delayedDragTouchMoveHandler),S(t,"touchmove",this._delayedDragTouchMoveHandler),S(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?x(document,"pointermove",this._onTouchMove):x(document,e?"touchmove":"mousemove",this._onTouchMove):(x(at,"dragend",this),x(ut,"dragstart",this._onDragStart));try{document.selection?ce((function(){document.selection.empty()})):window.getSelection().removeAllRanges()}catch(n){}},_dragStarted:function(t,e){if(Tt=!1,ut&&at){ot("dragStarted",this,{evt:e}),this.nativeDraggable&&x(document,"dragover",Jt);var n=this.options;!t&&k(at,n.dragClass,!1),k(at,n.ghostClass,!0),Zt.active=this,t&&this._appendGhost(),it({sortable:this,name:"start",originalEvent:e})}else this._nulling()},_emulateDragOver:function(){if(_t){this._lastX=_t.clientX,this._lastY=_t.clientY,Wt();var t=document.elementFromPoint(_t.clientX,_t.clientY),e=t;while(t&&t.shadowRoot){if(t=t.shadowRoot.elementFromPoint(_t.clientX,_t.clientY),t===e)break;e=t}if(at.parentNode[X]._isOutsideThisEl(t),e)do{if(e[X]){var n=void 0;if(n=e[X]._onDragOver({clientX:_t.clientX,clientY:_t.clientY,target:t,rootEl:e}),n&&!this.options.dragoverBubble)break}t=e}while(e=e.parentNode);Kt()}},_onTouchMove:function(t){if(wt){var e=this.options,n=e.fallbackTolerance,r=e.fallbackOffset,o=t.touches?t.touches[0]:t,i=ct&&T(ct,!0),a=ct&&i&&i.a,s=ct&&i&&i.d,c=zt&&Mt&&F(Mt),u=(o.clientX-wt.clientX+r.x)/(a||1)+(c?c[0]-Lt[0]:0)/(a||1),l=(o.clientY-wt.clientY+r.y)/(s||1)+(c?c[1]-Lt[1]:0)/(s||1);if(!Zt.active&&!Tt){if(n&&Math.max(Math.abs(o.clientX-this._lastX),Math.abs(o.clientY-this._lastY))=0&&(it({rootEl:st,name:"add",toEl:st,fromEl:ut,originalEvent:t}),it({sortable:this,name:"remove",toEl:st,originalEvent:t}),it({rootEl:st,name:"sort",toEl:st,fromEl:ut,originalEvent:t}),it({sortable:this,name:"sort",toEl:st,originalEvent:t})),bt&&bt.save()):vt!==ht&&vt>=0&&(it({sortable:this,name:"update",toEl:st,originalEvent:t}),it({sortable:this,name:"sort",toEl:st,originalEvent:t})),Zt.active&&(null!=vt&&-1!==vt||(vt=ht,mt=gt),it({sortable:this,name:"end",toEl:st,originalEvent:t}),this.save()))),this._nulling())},_nulling:function(){ot("nulling",this),ut=at=st=ct=lt=dt=ft=pt=wt=_t=Ct=vt=mt=ht=gt=At=$t=bt=yt=Zt.dragged=Zt.ghost=Zt.clone=Zt.active=null,Rt.forEach((function(t){t.checked=!0})),Rt.length=xt=St=0},handleEvent:function(t){switch(t.type){case"drop":case"dragend":this._onDrop(t);break;case"dragenter":case"dragover":at&&(this._onDragOver(t),Qt(t));break;case"selectstart":t.preventDefault();break}},toArray:function(){for(var t,e=[],n=this.el.children,r=0,o=n.length,i=this.options;r1&&(Pe.forEach((function(t){r.addAnimationState({target:t,rect:Le?D(t):o}),K(t),t.fromRect=o,e.removeAnimationState(t)})),Le=!1,Fe(!this.options.removeCloneOnHide,n))},dragOverCompleted:function(t){var e=t.sortable,n=t.isOwner,r=t.insertion,o=t.activeSortable,i=t.parentEl,a=t.putSortable,s=this.options;if(r){if(n&&o._hideClone(),Ie=!1,s.animation&&Pe.length>1&&(Le||!n&&!o.options.sort&&!a)){var c=D(Me,!1,!0,!0);Pe.forEach((function(t){t!==Me&&(W(t,c),i.appendChild(t))})),Le=!0}if(!n)if(Le||Ue(),Pe.length>1){var u=je;o._showClone(e),o.options.animation&&!je&&u&&De.forEach((function(t){o.addAnimationState({target:t,rect:Te}),t.fromRect=Te,t.thisAnimationDuration=null}))}else o._showClone(e)}},dragOverAnimationCapture:function(t){var e=t.dragRect,n=t.isOwner,r=t.activeSortable;if(Pe.forEach((function(t){t.thisAnimationDuration=null})),r.options.animation&&!n&&r.multiDrag.isMultiDrag){Te=i({},e);var o=T(Me,!0);Te.top-=o.f,Te.left-=o.e}},dragOverAnimationComplete:function(){Le&&(Le=!1,Ue())},drop:function(t){var e=t.originalEvent,n=t.rootEl,r=t.parentEl,o=t.sortable,i=t.dispatchSortableEvent,a=t.oldIndex,s=t.putSortable,c=s||this.sortable;if(e){var u=this.options,l=r.children;if(!Ne)if(u.multiDragKey&&!this.multiDragKeyDown&&this._deselectMultiDrag(),k(Me,u.selectedClass,!~Pe.indexOf(Me)),~Pe.indexOf(Me))Pe.splice(Pe.indexOf(Me),1),$e=null,rt({sortable:o,rootEl:n,name:"deselect",targetEl:Me,originalEvt:e});else{if(Pe.push(Me),rt({sortable:o,rootEl:n,name:"select",targetEl:Me,originalEvt:e}),e.shiftKey&&$e&&o.el.contains($e)){var f,d,p=R($e),h=R(Me);if(~p&&~h&&p!==h)for(h>p?(d=p,f=h):(d=h,f=p+1);d1){var v=D(Me),g=R(Me,":not(."+this.options.selectedClass+")");if(!Ie&&u.animation&&(Me.thisAnimationDuration=null),c.captureAnimationState(),!Ie&&(u.animation&&(Me.fromRect=v,Pe.forEach((function(t){if(t.thisAnimationDuration=null,t!==Me){var e=Le?D(t):v;t.fromRect=e,c.addAnimationState({target:t,rect:e})}}))),Ue(),Pe.forEach((function(t){l[g]?r.insertBefore(t,l[g]):r.appendChild(t),g++})),a===R(Me))){var m=!1;Pe.forEach((function(t){t.sortableIndex===R(t)||(m=!0)})),m&&i("update")}Pe.forEach((function(t){K(t)})),c.animateAll()}ke=c}(n===r||s&&"clone"!==s.lastPutMode)&&De.forEach((function(t){t.parentNode&&t.parentNode.removeChild(t)}))}},nullingGlobal:function(){this.isMultiDrag=Ne=!1,De.length=0},destroyGlobal:function(){this._deselectMultiDrag(),S(document,"pointerup",this._deselectMultiDrag),S(document,"mouseup",this._deselectMultiDrag),S(document,"touchend",this._deselectMultiDrag),S(document,"keydown",this._checkKeyDown),S(document,"keyup",this._checkKeyUp)},_deselectMultiDrag:function(t){if(("undefined"===typeof Ne||!Ne)&&ke===this.sortable&&(!t||!C(t.target,this.options.draggable,this.sortable.el,!1))&&(!t||0===t.button))while(Pe.length){var e=Pe[0];k(e,this.options.selectedClass,!1),Pe.shift(),rt({sortable:this.sortable,rootEl:this.sortable.el,name:"deselect",targetEl:e,originalEvt:t})}},_checkKeyDown:function(t){t.key===this.options.multiDragKey&&(this.multiDragKeyDown=!0)},_checkKeyUp:function(t){t.key===this.options.multiDragKey&&(this.multiDragKeyDown=!1)}},i(t,{pluginName:"multiDrag",utils:{select:function(t){var e=t.parentNode[X];e&&e.options.multiDrag&&!~Pe.indexOf(t)&&(ke&&ke!==e&&(ke.multiDrag._deselectMultiDrag(),ke=e),k(t,e.options.selectedClass,!0),Pe.push(t))},deselect:function(t){var e=t.parentNode[X],n=Pe.indexOf(t);e&&e.options.multiDrag&&~n&&(k(t,e.options.selectedClass,!1),Pe.splice(n,1))}},eventProperties:function(){var t=this,e=[],n=[];return Pe.forEach((function(r){var o;e.push({multiDragElement:r,index:r.sortableIndex}),o=Le&&r!==Me?-1:Le?R(r,":not(."+t.options.selectedClass+")"):R(r),n.push({multiDragElement:r,index:o})})),{items:u(Pe),clones:[].concat(De),oldIndicies:e,newIndicies:n}},optionListeners:{multiDragKey:function(t){return t=t.toLowerCase(),"ctrl"===t?t="Control":t.length>1&&(t=t.charAt(0).toUpperCase()+t.substr(1)),t}}})}function Fe(t,e){Pe.forEach((function(n,r){var o=e.children[n.sortableIndex+(t?Number(r):0)];o?e.insertBefore(n,o):e.appendChild(n)}))}function ze(t,e){De.forEach((function(n,r){var o=e.children[n.sortableIndex+(t?Number(r):0)];o?e.insertBefore(n,o):e.appendChild(n)}))}function Ue(){Pe.forEach((function(t){t!==Me&&t.parentNode&&t.parentNode.removeChild(t)}))}Zt.mount(new ye),Zt.mount(Ee,Oe),e["default"]=Zt},aa82:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=function(t){return(0,r.withParams)({type:"requiredIf",prop:t},(function(e,n){return!(0,r.ref)(t,this,n)||(0,r.req)(e)}))}},ab13:function(t,e,n){var r=n("b622"),o=r("match");t.exports=function(t){var e=/./;try{"/./"[t](e)}catch(n){try{return e[o]=!1,"/./"[t](e)}catch(r){}}return!1}},ac1f:function(t,e,n){"use strict";var r=n("23e7"),o=n("9263");r({target:"RegExp",proto:!0,forced:/./.exec!==o},{exec:o})},ad6d:function(t,e,n){"use strict";var r=n("825a");t.exports=function(){var t=r(this),e="";return t.global&&(e+="g"),t.ignoreCase&&(e+="i"),t.multiline&&(e+="m"),t.dotAll&&(e+="s"),t.unicode&&(e+="u"),t.sticky&&(e+="y"),e}},ae40:function(t,e,n){var r=n("83ab"),o=n("d039"),i=n("5135"),a=Object.defineProperty,s={},c=function(t){throw t};t.exports=function(t,e){if(i(s,t))return s[t];e||(e={});var n=[][t],u=!!i(e,"ACCESSORS")&&e.ACCESSORS,l=i(e,0)?e[0]:c,f=i(e,1)?e[1]:void 0;return s[t]=!!n&&!o((function(){if(u&&!r)return!0;var t={length:-1};u?a(t,1,{enumerable:!0,get:c}):t[1]=1,n.call(t,l,f)}))}},ae93:function(t,e,n){"use strict";var r,o,i,a=n("e163"),s=n("9112"),c=n("5135"),u=n("b622"),l=n("c430"),f=u("iterator"),d=!1,p=function(){return this};[].keys&&(i=[].keys(),"next"in i?(o=a(a(i)),o!==Object.prototype&&(r=o)):d=!0),void 0==r&&(r={}),l||c(r,f)||s(r,f,p),t.exports={IteratorPrototype:r,BUGGY_SAFARI_ITERATORS:d}},af03:function(t,e,n){var r=n("d039");t.exports=function(t){return r((function(){var e=""[t]('"');return e!==e.toLowerCase()||e.split('"').length>3}))}},b041:function(t,e,n){"use strict";var r=n("00ee"),o=n("f5df");t.exports=r?{}.toString:function(){return"[object "+o(this)+"]"}},b0c0:function(t,e,n){var r=n("83ab"),o=n("9bf2").f,i=Function.prototype,a=i.toString,s=/^\s*function ([^ (]*)/,c="name";!r||c in i||o(i,c,{configurable:!0,get:function(){try{return a.call(this).match(s)[1]}catch(t){return""}}})},b575:function(t,e,n){var r,o,i,a,s,c,u,l,f=n("da84"),d=n("06cf").f,p=n("c6b6"),h=n("2cf4").set,v=n("1cdc"),g=f.MutationObserver||f.WebKitMutationObserver,m=f.process,y=f.Promise,b="process"==p(m),w=d(f,"queueMicrotask"),_=w&&w.value;_||(r=function(){var t,e;b&&(t=m.domain)&&t.exit();while(o){e=o.fn,o=o.next;try{e()}catch(n){throw o?a():i=void 0,n}}i=void 0,t&&t.enter()},b?a=function(){m.nextTick(r)}:g&&!v?(s=!0,c=document.createTextNode(""),new g(r).observe(c,{characterData:!0}),a=function(){c.data=s=!s}):y&&y.resolve?(u=y.resolve(void 0),l=u.then,a=function(){l.call(u,r)}):a=function(){h.call(f,r)}),t.exports=_||function(t){var e={fn:t,next:void 0};i&&(i.next=e),o||(o=e,a()),i=e}},b5ae:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.maxValue=e.minValue=e.and=e.or=e.url=e.sameAs=e.requiredUnless=e.requiredIf=e.required=e.minLength=e.maxLength=e.macAddress=e.ipAddress=e.email=e.between=e.numeric=e.alphaNum=e.alpha=void 0;var r=n("6235"),o=F(r),i=n("3a54"),a=F(i),s=n("45b8"),c=F(s),u=n("ec11"),l=F(u),f=n("5d75"),d=F(f),p=n("c99d"),h=F(p),v=n("91d3"),g=F(v),m=n("2a12"),y=F(m),b=n("5db3"),w=F(b),_=n("d4f4"),x=F(_),S=n("aa82"),O=F(S),E=n("e652"),C=F(E),A=n("b6cb"),$=F(A),k=n("772d"),M=F(k),T=n("d294"),j=F(T),P=n("3360"),D=F(P),I=n("eb66"),L=F(I),N=n("46bc"),R=F(N);function F(t){return t&&t.__esModule?t:{default:t}}e.alpha=o.default,e.alphaNum=a.default,e.numeric=c.default,e.between=l.default,e.email=d.default,e.ipAddress=h.default,e.macAddress=g.default,e.maxLength=y.default,e.minLength=w.default,e.required=x.default,e.requiredIf=O.default,e.requiredUnless=C.default,e.sameAs=$.default,e.url=M.default,e.or=j.default,e.and=D.default,e.minValue=L.default,e.maxValue=R.default},b622:function(t,e,n){var r=n("da84"),o=n("5692"),i=n("5135"),a=n("90e3"),s=n("4930"),c=n("fdbf"),u=o("wks"),l=r.Symbol,f=c?l:l&&l.withoutSetter||a;t.exports=function(t){return i(u,t)||(s&&i(l,t)?u[t]=l[t]:u[t]=f("Symbol."+t)),u[t]}},b64b:function(t,e,n){var r=n("23e7"),o=n("7b0b"),i=n("df75"),a=n("d039"),s=a((function(){i(1)}));r({target:"Object",stat:!0,forced:s},{keys:function(t){return i(o(t))}})},b680:function(t,e,n){"use strict";var r=n("23e7"),o=n("a691"),i=n("408a"),a=n("1148"),s=n("d039"),c=1..toFixed,u=Math.floor,l=function(t,e,n){return 0===e?n:e%2===1?l(t,e-1,n*t):l(t*t,e/2,n)},f=function(t){var e=0,n=t;while(n>=4096)e+=12,n/=4096;while(n>=2)e+=1,n/=2;return e},d=c&&("0.000"!==8e-5.toFixed(3)||"1"!==.9.toFixed(0)||"1.25"!==1.255.toFixed(2)||"1000000000000000128"!==(0xde0b6b3a7640080).toFixed(0))||!s((function(){c.call({})}));r({target:"Number",proto:!0,forced:d},{toFixed:function(t){var e,n,r,s,c=i(this),d=o(t),p=[0,0,0,0,0,0],h="",v="0",g=function(t,e){var n=-1,r=e;while(++n<6)r+=t*p[n],p[n]=r%1e7,r=u(r/1e7)},m=function(t){var e=6,n=0;while(--e>=0)n+=p[e],p[e]=u(n/t),n=n%t*1e7},y=function(){var t=6,e="";while(--t>=0)if(""!==e||0===t||0!==p[t]){var n=String(p[t]);e=""===e?n:e+a.call("0",7-n.length)+n}return e};if(d<0||d>20)throw RangeError("Incorrect fraction digits");if(c!=c)return"NaN";if(c<=-1e21||c>=1e21)return String(c);if(c<0&&(h="-",c=-c),c>1e-21)if(e=f(c*l(2,69,1))-69,n=e<0?c*l(2,-e,1):c/l(2,e,1),n*=4503599627370496,e=52-e,e>0){g(0,n),r=d;while(r>=7)g(1e7,0),r-=7;g(l(10,r,1),0),r=e-1;while(r>=23)m(1<<23),r-=23;m(1<0?(s=v.length,v=h+(s<=d?"0."+a.call("0",d-s)+v:v.slice(0,s-d)+"."+v.slice(s-d))):v=h+v,v}})},b6cb:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=function(t){return(0,r.withParams)({type:"sameAs",eq:t},(function(e,n){return e===(0,r.ref)(t,this,n)}))}},b727:function(t,e,n){var r=n("0366"),o=n("44ad"),i=n("7b0b"),a=n("50c4"),s=n("65f0"),c=[].push,u=function(t){var e=1==t,n=2==t,u=3==t,l=4==t,f=6==t,d=5==t||f;return function(p,h,v,g){for(var m,y,b=i(p),w=o(b),_=r(h,v,3),x=a(w.length),S=0,O=g||s,E=e?O(p,x):n?O(p,0):void 0;x>S;S++)if((d||S in w)&&(m=w[S],y=_(m,S,b),t))if(e)E[S]=y;else if(y)switch(t){case 3:return!0;case 5:return m;case 6:return S;case 2:c.call(E,m)}else if(l)return!1;return f?-1:u||l?l:E}};t.exports={forEach:u(0),map:u(1),filter:u(2),some:u(3),every:u(4),find:u(5),findIndex:u(6)}},c04e:function(t,e,n){var r=n("861d");t.exports=function(t,e){if(!r(t))return t;var n,o;if(e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;if("function"==typeof(n=t.valueOf)&&!r(o=n.call(t)))return o;if(!e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;throw TypeError("Can't convert object to primitive value")}},c430:function(t,e){t.exports=!1},c6b6:function(t,e){var n={}.toString;t.exports=function(t){return n.call(t).slice(8,-1)}},c6cd:function(t,e,n){var r=n("da84"),o=n("ce4e"),i="__core-js_shared__",a=r[i]||o(i,{});t.exports=a},c740:function(t,e,n){"use strict";var r=n("23e7"),o=n("b727").findIndex,i=n("44d2"),a=n("ae40"),s="findIndex",c=!0,u=a(s);s in[]&&Array(1)[s]((function(){c=!1})),r({target:"Array",proto:!0,forced:c||!u},{findIndex:function(t){return o(this,t,arguments.length>1?arguments[1]:void 0)}}),i(s)},c8ba:function(t,e){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(r){"object"===typeof window&&(n=window)}t.exports=n},c8d2:function(t,e,n){var r=n("d039"),o=n("5899"),i="​…᠎";t.exports=function(t){return r((function(){return!!o[t]()||i[t]()!=i||o[t].name!==t}))}},c975:function(t,e,n){"use strict";var r=n("23e7"),o=n("4d64").indexOf,i=n("a640"),a=n("ae40"),s=[].indexOf,c=!!s&&1/[1].indexOf(1,-0)<0,u=i("indexOf"),l=a("indexOf",{ACCESSORS:!0,1:0});r({target:"Array",proto:!0,forced:c||!u||!l},{indexOf:function(t){return c?s.apply(this,arguments)||0:o(this,t,arguments.length>1?arguments[1]:void 0)}})},c99d:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=(0,r.withParams)({type:"ipAddress"},(function(t){if(!(0,r.req)(t))return!0;if("string"!==typeof t)return!1;var e=t.split(".");return 4===e.length&&e.every(o)}));var o=function(t){if(t.length>3||0===t.length)return!1;if("0"===t[0]&&"0"!==t)return!1;if(!t.match(/^\d+$/))return!1;var e=0|+t;return e>=0&&e<=255}},ca84:function(t,e,n){var r=n("5135"),o=n("fc6a"),i=n("4d64").indexOf,a=n("d012");t.exports=function(t,e){var n,s=o(t),c=0,u=[];for(n in s)!r(a,n)&&r(s,n)&&u.push(n);while(e.length>c)r(s,n=e[c++])&&(~i(u,n)||u.push(n));return u}},caad:function(t,e,n){"use strict";var r=n("23e7"),o=n("4d64").includes,i=n("44d2"),a=n("ae40"),s=a("indexOf",{ACCESSORS:!0,1:0});r({target:"Array",proto:!0,forced:!s},{includes:function(t){return o(this,t,arguments.length>1?arguments[1]:void 0)}}),i("includes")},cc12:function(t,e,n){var r=n("da84"),o=n("861d"),i=r.document,a=o(i)&&o(i.createElement);t.exports=function(t){return a?i.createElement(t):{}}},cca6:function(t,e,n){var r=n("23e7"),o=n("60da");r({target:"Object",stat:!0,forced:Object.assign!==o},{assign:o})},cdf9:function(t,e,n){var r=n("825a"),o=n("861d"),i=n("f069");t.exports=function(t,e){if(r(t),o(e)&&e.constructor===t)return e;var n=i.f(t),a=n.resolve;return a(e),n.promise}},ce4e:function(t,e,n){var r=n("da84"),o=n("9112");t.exports=function(t,e){try{o(r,t,e)}catch(n){r[t]=e}return e}},d012:function(t,e){t.exports={}},d039:function(t,e){t.exports=function(t){try{return!!t()}catch(e){return!0}}},d066:function(t,e,n){var r=n("428f"),o=n("da84"),i=function(t){return"function"==typeof t?t:void 0};t.exports=function(t,e){return arguments.length<2?i(r[t])||i(o[t]):r[t]&&r[t][e]||o[t]&&o[t][e]}},d1e7:function(t,e,n){"use strict";var r={}.propertyIsEnumerable,o=Object.getOwnPropertyDescriptor,i=o&&!r.call({1:2},1);e.f=i?function(t){var e=o(this,t);return!!e&&e.enumerable}:r},d294:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=function(){for(var t=arguments.length,e=Array(t),n=0;n0&&e.reduce((function(e,n){return e||n.apply(t,r)}),!1)}))}},d2bb:function(t,e,n){var r=n("825a"),o=n("3bbe");t.exports=Object.setPrototypeOf||("__proto__"in{}?function(){var t,e=!1,n={};try{t=Object.getOwnPropertyDescriptor(Object.prototype,"__proto__").set,t.call(n,[]),e=n instanceof Array}catch(i){}return function(n,i){return r(n),o(i),e?t.call(n,i):n.__proto__=i,n}}():void 0)},d3b7:function(t,e,n){var r=n("00ee"),o=n("6eeb"),i=n("b041");r||o(Object.prototype,"toString",i,{unsafe:!0})},d44e:function(t,e,n){var r=n("9bf2").f,o=n("5135"),i=n("b622"),a=i("toStringTag");t.exports=function(t,e,n){t&&!o(t=n?t:t.prototype,a)&&r(t,a,{configurable:!0,value:e})}},d4f4:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=(0,r.withParams)({type:"required"},r.req)},d784:function(t,e,n){"use strict";n("ac1f");var r=n("6eeb"),o=n("d039"),i=n("b622"),a=n("9263"),s=n("9112"),c=i("species"),u=!o((function(){var t=/./;return t.exec=function(){var t=[];return t.groups={a:"7"},t},"7"!=="".replace(t,"$")})),l=function(){return"$0"==="a".replace(/./,"$0")}(),f=i("replace"),d=function(){return!!/./[f]&&""===/./[f]("a","$0")}(),p=!o((function(){var t=/(?:)/,e=t.exec;t.exec=function(){return e.apply(this,arguments)};var n="ab".split(t);return 2!==n.length||"a"!==n[0]||"b"!==n[1]}));t.exports=function(t,e,n,f){var h=i(t),v=!o((function(){var e={};return e[h]=function(){return 7},7!=""[t](e)})),g=v&&!o((function(){var e=!1,n=/a/;return"split"===t&&(n={},n.constructor={},n.constructor[c]=function(){return n},n.flags="",n[h]=/./[h]),n.exec=function(){return e=!0,null},n[h](""),!e}));if(!v||!g||"replace"===t&&(!u||!l||d)||"split"===t&&!p){var m=/./[h],y=n(h,""[t],(function(t,e,n,r,o){return e.exec===a?v&&!o?{done:!0,value:m.call(e,n,r)}:{done:!0,value:t.call(n,e,r)}:{done:!1}}),{REPLACE_KEEPS_$0:l,REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE:d}),b=y[0],w=y[1];r(String.prototype,t,b),r(RegExp.prototype,h,2==e?function(t,e){return w.call(t,this,e)}:function(t){return w.call(t,this)})}f&&s(RegExp.prototype[h],"sham",!0)}},d81d:function(t,e,n){"use strict";var r=n("23e7"),o=n("b727").map,i=n("1dde"),a=n("ae40"),s=i("map"),c=a("map");r({target:"Array",proto:!0,forced:!s||!c},{map:function(t){return o(this,t,arguments.length>1?arguments[1]:void 0)}})},da84:function(t,e,n){(function(e){var n=function(t){return t&&t.Math==Math&&t};t.exports=n("object"==typeof globalThis&&globalThis)||n("object"==typeof window&&window)||n("object"==typeof self&&self)||n("object"==typeof e&&e)||Function("return this")()}).call(this,n("c8ba"))},ddb0:function(t,e,n){var r=n("da84"),o=n("fdbc"),i=n("e260"),a=n("9112"),s=n("b622"),c=s("iterator"),u=s("toStringTag"),l=i.values;for(var f in o){var d=r[f],p=d&&d.prototype;if(p){if(p[c]!==l)try{a(p,c,l)}catch(v){p[c]=l}if(p[u]||a(p,u,f),o[f])for(var h in i)if(p[h]!==i[h])try{a(p,h,i[h])}catch(v){p[h]=i[h]}}}},df75:function(t,e,n){var r=n("ca84"),o=n("7839");t.exports=Object.keys||function(t){return r(t,o)}},e01a:function(t,e,n){"use strict";var r=n("23e7"),o=n("83ab"),i=n("da84"),a=n("5135"),s=n("861d"),c=n("9bf2").f,u=n("e893"),l=i.Symbol;if(o&&"function"==typeof l&&(!("description"in l.prototype)||void 0!==l().description)){var f={},d=function(){var t=arguments.length<1||void 0===arguments[0]?void 0:String(arguments[0]),e=this instanceof d?new l(t):void 0===t?l():l(t);return""===t&&(f[e]=!0),e};u(d,l);var p=d.prototype=l.prototype;p.constructor=d;var h=p.toString,v="Symbol(test)"==String(l("test")),g=/^Symbol\((.*)\)[^)]+$/;c(p,"description",{configurable:!0,get:function(){var t=s(this)?this.valueOf():this,e=h.call(t);if(a(f,t))return"";var n=v?e.slice(7,-1):e.replace(g,"$1");return""===n?void 0:n}}),r({global:!0,forced:!0},{Symbol:d})}},e163:function(t,e,n){var r=n("5135"),o=n("7b0b"),i=n("f772"),a=n("e177"),s=i("IE_PROTO"),c=Object.prototype;t.exports=a?Object.getPrototypeOf:function(t){return t=o(t),r(t,s)?t[s]:"function"==typeof t.constructor&&t instanceof t.constructor?t.constructor.prototype:t instanceof Object?c:null}},e177:function(t,e,n){var r=n("d039");t.exports=!r((function(){function t(){}return t.prototype.constructor=null,Object.getPrototypeOf(new t)!==t.prototype}))},e260:function(t,e,n){"use strict";var r=n("fc6a"),o=n("44d2"),i=n("3f8c"),a=n("69f3"),s=n("7dd0"),c="Array Iterator",u=a.set,l=a.getterFor(c);t.exports=s(Array,"Array",(function(t,e){u(this,{type:c,target:r(t),index:0,kind:e})}),(function(){var t=l(this),e=t.target,n=t.kind,r=t.index++;return!e||r>=e.length?(t.target=void 0,{value:void 0,done:!0}):"keys"==n?{value:r,done:!1}:"values"==n?{value:e[r],done:!1}:{value:[r,e[r]],done:!1}}),"values"),i.Arguments=i.Array,o("keys"),o("values"),o("entries")},e2cc:function(t,e,n){var r=n("6eeb");t.exports=function(t,e,n){for(var o in e)r(t,o,e[o],n);return t}},e538:function(t,e,n){var r=n("b622");e.f=r},e652:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=function(t){return(0,r.withParams)({type:"requiredUnless",prop:t},(function(e,n){return!!(0,r.ref)(t,this,n)||(0,r.req)(e)}))}},e667:function(t,e){t.exports=function(t){try{return{error:!1,value:t()}}catch(e){return{error:!0,value:e}}}},e6cf:function(t,e,n){"use strict";var r,o,i,a,s=n("23e7"),c=n("c430"),u=n("da84"),l=n("d066"),f=n("fea9"),d=n("6eeb"),p=n("e2cc"),h=n("d44e"),v=n("2626"),g=n("861d"),m=n("1c0b"),y=n("19aa"),b=n("c6b6"),w=n("8925"),_=n("2266"),x=n("1c7e"),S=n("4840"),O=n("2cf4").set,E=n("b575"),C=n("cdf9"),A=n("44de"),$=n("f069"),k=n("e667"),M=n("69f3"),T=n("94ca"),j=n("b622"),P=n("2d00"),D=j("species"),I="Promise",L=M.get,N=M.set,R=M.getterFor(I),F=f,z=u.TypeError,U=u.document,H=u.process,B=l("fetch"),V=$.f,Y=V,q="process"==b(H),G=!!(U&&U.createEvent&&u.dispatchEvent),W="unhandledrejection",K="rejectionhandled",X=0,J=1,Z=2,Q=1,tt=2,et=T(I,(function(){var t=w(F)!==String(F);if(!t){if(66===P)return!0;if(!q&&"function"!=typeof PromiseRejectionEvent)return!0}if(c&&!F.prototype["finally"])return!0;if(P>=51&&/native code/.test(F))return!1;var e=F.resolve(1),n=function(t){t((function(){}),(function(){}))},r=e.constructor={};return r[D]=n,!(e.then((function(){}))instanceof n)})),nt=et||!x((function(t){F.all(t)["catch"]((function(){}))})),rt=function(t){var e;return!(!g(t)||"function"!=typeof(e=t.then))&&e},ot=function(t,e,n){if(!e.notified){e.notified=!0;var r=e.reactions;E((function(){var o=e.value,i=e.state==J,a=0;while(r.length>a){var s,c,u,l=r[a++],f=i?l.ok:l.fail,d=l.resolve,p=l.reject,h=l.domain;try{f?(i||(e.rejection===tt&&ct(t,e),e.rejection=Q),!0===f?s=o:(h&&h.enter(),s=f(o),h&&(h.exit(),u=!0)),s===l.promise?p(z("Promise-chain cycle")):(c=rt(s))?c.call(s,d,p):d(s)):p(o)}catch(v){h&&!u&&h.exit(),p(v)}}e.reactions=[],e.notified=!1,n&&!e.rejection&&at(t,e)}))}},it=function(t,e,n){var r,o;G?(r=U.createEvent("Event"),r.promise=e,r.reason=n,r.initEvent(t,!1,!0),u.dispatchEvent(r)):r={promise:e,reason:n},(o=u["on"+t])?o(r):t===W&&A("Unhandled promise rejection",n)},at=function(t,e){O.call(u,(function(){var n,r=e.value,o=st(e);if(o&&(n=k((function(){q?H.emit("unhandledRejection",r,t):it(W,t,r)})),e.rejection=q||st(e)?tt:Q,n.error))throw n.value}))},st=function(t){return t.rejection!==Q&&!t.parent},ct=function(t,e){O.call(u,(function(){q?H.emit("rejectionHandled",t):it(K,t,e.value)}))},ut=function(t,e,n,r){return function(o){t(e,n,o,r)}},lt=function(t,e,n,r){e.done||(e.done=!0,r&&(e=r),e.value=n,e.state=Z,ot(t,e,!0))},ft=function(t,e,n,r){if(!e.done){e.done=!0,r&&(e=r);try{if(t===n)throw z("Promise can't be resolved itself");var o=rt(n);o?E((function(){var r={done:!1};try{o.call(n,ut(ft,t,r,e),ut(lt,t,r,e))}catch(i){lt(t,r,i,e)}})):(e.value=n,e.state=J,ot(t,e,!1))}catch(i){lt(t,{done:!1},i,e)}}};et&&(F=function(t){y(this,F,I),m(t),r.call(this);var e=L(this);try{t(ut(ft,this,e),ut(lt,this,e))}catch(n){lt(this,e,n)}},r=function(t){N(this,{type:I,done:!1,notified:!1,parent:!1,reactions:[],rejection:!1,state:X,value:void 0})},r.prototype=p(F.prototype,{then:function(t,e){var n=R(this),r=V(S(this,F));return r.ok="function"!=typeof t||t,r.fail="function"==typeof e&&e,r.domain=q?H.domain:void 0,n.parent=!0,n.reactions.push(r),n.state!=X&&ot(this,n,!1),r.promise},catch:function(t){return this.then(void 0,t)}}),o=function(){var t=new r,e=L(t);this.promise=t,this.resolve=ut(ft,t,e),this.reject=ut(lt,t,e)},$.f=V=function(t){return t===F||t===i?new o(t):Y(t)},c||"function"!=typeof f||(a=f.prototype.then,d(f.prototype,"then",(function(t,e){var n=this;return new F((function(t,e){a.call(n,t,e)})).then(t,e)}),{unsafe:!0}),"function"==typeof B&&s({global:!0,enumerable:!0,forced:!0},{fetch:function(t){return C(F,B.apply(u,arguments))}}))),s({global:!0,wrap:!0,forced:et},{Promise:F}),h(F,I,!1,!0),v(I),i=l(I),s({target:I,stat:!0,forced:et},{reject:function(t){var e=V(this);return e.reject.call(void 0,t),e.promise}}),s({target:I,stat:!0,forced:c||et},{resolve:function(t){return C(c&&this===i?F:this,t)}}),s({target:I,stat:!0,forced:nt},{all:function(t){var e=this,n=V(e),r=n.resolve,o=n.reject,i=k((function(){var n=m(e.resolve),i=[],a=0,s=1;_(t,(function(t){var c=a++,u=!1;i.push(void 0),s++,n.call(e,t).then((function(t){u||(u=!0,i[c]=t,--s||r(i))}),o)})),--s||r(i)}));return i.error&&o(i.value),n.promise},race:function(t){var e=this,n=V(e),r=n.reject,o=k((function(){var o=m(e.resolve);_(t,(function(t){o.call(e,t).then(n.resolve,r)}))}));return o.error&&r(o.value),n.promise}})},e893:function(t,e,n){var r=n("5135"),o=n("56ef"),i=n("06cf"),a=n("9bf2");t.exports=function(t,e){for(var n=o(e),s=a.f,c=i.f,u=0;u=+t}))}},ec11:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("78ef");e.default=function(t,e){return(0,r.withParams)({type:"between",min:t,max:e},(function(n){return!(0,r.req)(n)||(!/\s/.test(n)||n instanceof Date)&&+t<=+n&&+e>=+n}))}},f069:function(t,e,n){"use strict";var r=n("1c0b"),o=function(t){var e,n;this.promise=new t((function(t,r){if(void 0!==e||void 0!==n)throw TypeError("Bad Promise constructor");e=t,n=r})),this.resolve=r(e),this.reject=r(n)};t.exports.f=function(t){return new o(t)}},f2f3:function(t,e,n){"use strict";function r(t){return r="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"===typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},r(t)}var o={namespaced:!0,state:{locale:null,fallback:null,translations:{}},mutations:{SET_LOCALE:function(t,e){t.locale=e.locale},ADD_LOCALE:function(t,e){var n=i(e.translations);if(t.translations.hasOwnProperty(e.locale)){var r=t.translations[e.locale];t.translations[e.locale]=Object.assign({},r,n)}else t.translations[e.locale]=n;try{t.translations.__ob__&&t.translations.__ob__.dep.notify()}catch(o){}},REPLACE_LOCALE:function(t,e){var n=i(e.translations);t.translations[e.locale]=n;try{t.translations.__ob__&&t.translations.__ob__.dep.notify()}catch(r){}},REMOVE_LOCALE:function(t,e){if(t.translations.hasOwnProperty(e.locale)){t.locale===e.locale&&(t.locale=null);var n=Object.assign({},t.translations);delete n[e.locale],t.translations=n}},SET_FALLBACK_LOCALE:function(t,e){t.fallback=e.locale}},actions:{setLocale:function(t,e){t.commit({type:"SET_LOCALE",locale:e.locale})},addLocale:function(t,e){t.commit({type:"ADD_LOCALE",locale:e.locale,translations:e.translations})},replaceLocale:function(t,e){t.commit({type:"REPLACE_LOCALE",locale:e.locale,translations:e.translations})},removeLocale:function(t,e){t.commit({type:"REMOVE_LOCALE",locale:e.locale,translations:e.translations})},setFallbackLocale:function(t,e){t.commit({type:"SET_FALLBACK_LOCALE",locale:e.locale})}}},i=function t(e){var n={};for(var o in e)if(e.hasOwnProperty(o)){var i=r(e[o]);if(a(e[o])){for(var s=e[o].length,c=0;c1?1:0;case"lv":return e%10===1&&e%100!==11?0:0!==e?1:2;case"lt":return e%10===1&&e%100!==11?0:e%10>=2&&(e%100<10||e%100>=20)?1:2;case"be":case"bs":case"hr":case"ru":case"sr":case"uk":return e%10===1&&e%100!==11?0:e%10>=2&&e%10<=4&&(e%100<10||e%100>=20)?1:2;case"mnk":return 0===e?0:1===e?1:2;case"ro":return 1===e?0:0===e||e%100>0&&e%100<20?1:2;case"pl":return 1===e?0:e%10>=2&&e%10<=4&&(e%100<10||e%100>=20)?1:2;case"cs":case"sk":return 1===e?0:e>=2&&e<=4?1:2;case"csb":return 1===e?0:e%10>=2&&e%10<=4&&(e%100<10||e%100>=20)?1:2;case"sl":return e%100===1?0:e%100===2?1:e%100===3||e%100===4?2:3;case"mt":return 1===e?0:0===e||e%100>1&&e%100<11?1:e%100>10&&e%100<20?2:3;case"gd":return 1===e||11===e?0:2===e||12===e?1:e>2&&e<20?2:3;case"cy":return 1===e?0:2===e?1:8!==e&&11!==e?2:3;case"kw":return 1===e?0:2===e?1:3===e?2:3;case"ga":return 1===e?0:2===e?1:e>2&&e<7?2:e>6&&e<11?3:4;case"ar":return 0===e?0:1===e?1:2===e?2:e%100>=3&&e%100<=10?3:e%100>=11?4:5;default:return 1!==e?1:0}}},c={install:function(t,e,n){"string"!==typeof arguments[2]&&"string"!==typeof arguments[3]||(console.warn("i18n: Registering the plugin vuex-i18n with a string for `moduleName` or `identifiers` is deprecated. Use a configuration object instead.","https://github.com/dkfbasel/vuex-i18n#setup"),n={moduleName:arguments[2],identifiers:arguments[3]}),n=Object.assign({warnings:!0,moduleName:"i18n",identifiers:["{","}"],preserveState:!1,translateFilterName:"translate",translateInFilterName:"translateIn",onTranslationNotFound:function(){}},n);var r=n.moduleName,i=n.identifiers,a=n.translateFilterName,s=n.translateInFilterName,c=n.onTranslationNotFound;if("function"!==typeof c&&(console.error("i18n: i18n config option onTranslationNotFound must be a function"),c=function(){}),e.registerModule(r,o,{preserveState:n.preserveState}),!1===e.state.hasOwnProperty(r))return console.error("i18n: i18n vuex module is not correctly initialized. Please check the module name:",r),t.prototype.$i18n=function(t){return t},t.prototype.$getLanguage=function(){return null},void(t.prototype.$setLanguage=function(){console.error("i18n: i18n vuex module is not correctly initialized")});var l=u(i,n.warnings),f=function(){var t=e.state[r].locale;return d.apply(void 0,[t].concat(Array.prototype.slice.call(arguments)))},d=function(t){var o=arguments,i="",a="",s={},u=null,f=o.length;if(f>=3&&"string"===typeof o[2]?(i=o[1],a=o[2],f>3&&(s=o[3]),f>4&&(u=o[4])):(i=o[1],a=i,f>2&&(s=o[2]),f>3&&(u=o[3])),!t)return n.warnings&&console.warn("i18n: i18n locale is not set when trying to access translations:",i),a;var d=e.state[r].translations,p=e.state[r].fallback,h=t.split("-"),v=!0;if(!1===d.hasOwnProperty(t)?v=!1:!1===d[t].hasOwnProperty(i)&&(v=!1),!0===v)return l(t,d[t][i],s,u);if(h.length>1&&!0===d.hasOwnProperty(h[0])&&!0===d[h[0]].hasOwnProperty(i))return l(h[0],d[h[0]][i],s,u);var g=c(t,i,a);return g&&Promise.resolve(g).then((function(e){var n={};n[i]=e,b(t,n)})),!1===d.hasOwnProperty(p)?l(t,a,s,u):!1===d[p].hasOwnProperty(i)?l(p,a,s,u):l(t,d[p][i],s,u)},p=function(t,e){for(var n=arguments.length,r=new Array(n>2?n-2:0),o=2;o1&&void 0!==arguments[1]?arguments[1]:"fallback",o=e.state[r].locale,i=e.state[r].fallback,a=e.state[r].translations;if(a.hasOwnProperty(o)&&a[o].hasOwnProperty(t))return!0;if("strict"==n)return!1;var s=o.split("-");return!!(s.length>1&&a.hasOwnProperty(s[0])&&a[s[0]].hasOwnProperty(t))||"locale"!=n&&!(!a.hasOwnProperty(i)||!a[i].hasOwnProperty(t))},v=function(t){e.dispatch({type:"".concat(r,"/setFallbackLocale"),locale:t})},g=function(t){e.dispatch({type:"".concat(r,"/setLocale"),locale:t})},m=function(){return e.state[r].locale},y=function(){return Object.keys(e.state[r].translations)},b=function(t,n){return e.dispatch({type:"".concat(r,"/addLocale"),locale:t,translations:n})},w=function(t,n){return e.dispatch({type:"".concat(r,"/replaceLocale"),locale:t,translations:n})},_=function(t){e.state[r].translations.hasOwnProperty(t)&&e.dispatch({type:"".concat(r,"/removeLocale"),locale:t})},x=function(t){return n.warnings&&console.warn("i18n: $i18n.exists is depreceated. Please use $i18n.localeExists instead. It provides exactly the same functionality."),S(t)},S=function(t){return e.state[r].translations.hasOwnProperty(t)};t.prototype.$i18n={locale:m,locales:y,set:g,add:b,replace:w,remove:_,fallback:v,localeExists:S,keyExists:h,translate:f,translateIn:d,exists:x},t.i18n={locale:m,locales:y,set:g,add:b,replace:w,remove:_,fallback:v,translate:f,translateIn:d,localeExists:S,keyExists:h,exists:x},t.prototype.$t=f,t.prototype.$tlang=d,t.filter(a,f),t.filter(s,p)}},u=function(t){var e=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];null!=t&&2==t.length||console.warn("i18n: You must specify the start and end character identifying variable substitutions");var n=new RegExp(t[0]+"{1}(\\w{1}|\\w.+?)"+t[1]+"{1}","g"),o=function(r,o){return r.replace?r.replace(n,(function(n){var i=n.replace(t[0],"").replace(t[1],"");return void 0!==o[i]?o[i]:(e&&(console.group?console.group("i18n: Not all placeholders found"):console.warn("i18n: Not all placeholders found"),console.warn("Text:",r),console.warn("Placeholder:",n),console.groupEnd&&console.groupEnd()),n)})):r},i=function(t,n){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},a=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,c=r(n),u=r(a),f=function(){return l(n)?n.map((function(t){return o(t,i,!1)})):"string"===c?o(n,i,!0):void 0};if(null===a)return f();if("number"!==u)return e&&console.warn("i18n: pluralization is not a number"),f();var d=f(),p=null;p=l(d)&&d.length>0?d:d.split(":::");var h=s.getTranslationIndex(t,a);return"undefined"===typeof p[h]?(e&&console.warn("i18n: pluralization not provided in locale",n,t,h),p[0].trim()):p[h].trim()};return i};function l(t){return!!t&&Array===t.constructor}var f={store:o,plugin:c};e["a"]=f},f5df:function(t,e,n){var r=n("00ee"),o=n("c6b6"),i=n("b622"),a=i("toStringTag"),s="Arguments"==o(function(){return arguments}()),c=function(t,e){try{return t[e]}catch(n){}};t.exports=r?o:function(t){var e,n,r;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(n=c(e=Object(t),a))?n:s?o(e):"Object"==(r=o(e))&&"function"==typeof e.callee?"Arguments":r}},f772:function(t,e,n){var r=n("5692"),o=n("90e3"),i=r("keys");t.exports=function(t){return i[t]||(i[t]=o(t))}},f906:function(t,e,n){!function(e,n){t.exports=n()}(0,(function(){"use strict";var t,e=/(\[[^[]*\])|([-:/.()\s]+)|(A|a|YYYY|YY?|MM?M?M?|Do|DD?|hh?|HH?|mm?|ss?|S{1,3}|z|ZZ?)/g,n=/\d\d/,r=/\d\d?/,o=/\d*[^\s\d-:/.()]+/,i=function(t){return function(e){this[t]=+e}},a=[/[+-]\d\d:?\d\d/,function(t){var e,n;(this.zone||(this.zone={})).offset=(e=t.match(/([+-]|\d\d)/g),0===(n=60*e[1]+ +e[2])?0:"+"===e[0]?-n:n)}],s={A:[/[AP]M/,function(t){this.afternoon="PM"===t}],a:[/[ap]m/,function(t){this.afternoon="pm"===t}],S:[/\d/,function(t){this.milliseconds=100*+t}],SS:[n,function(t){this.milliseconds=10*+t}],SSS:[/\d{3}/,function(t){this.milliseconds=+t}],s:[r,i("seconds")],ss:[r,i("seconds")],m:[r,i("minutes")],mm:[r,i("minutes")],H:[r,i("hours")],h:[r,i("hours")],HH:[r,i("hours")],hh:[r,i("hours")],D:[r,i("day")],DD:[n,i("day")],Do:[o,function(e){var n=t.ordinal,r=e.match(/\d+/);if(this.day=r[0],n)for(var o=1;o<=31;o+=1)n(o).replace(/\[|\]/g,"")===e&&(this.day=o)}],M:[r,i("month")],MM:[n,i("month")],MMM:[o,function(e){var n=t,r=n.months,o=n.monthsShort,i=o?o.findIndex((function(t){return t===e})):r.findIndex((function(t){return t.substr(0,3)===e}));if(i<0)throw new Error;this.month=i+1}],MMMM:[o,function(e){var n=t.months.indexOf(e);if(n<0)throw new Error;this.month=n+1}],Y:[/[+-]?\d+/,i("year")],YY:[n,function(t){t=+t,this.year=t+(t>68?1900:2e3)}],YYYY:[/\d{4}/,i("year")],Z:a,ZZ:a},c=function(t,n,r){try{var o=function(t){for(var n=t.match(e),r=n.length,o=0;o0?a-1:h.getMonth(),y=u||0,b=l||0,w=f||0,_=d||0;return r?new Date(Date.UTC(g,m,v,y,b,w,_)):new Date(g,m,v,y,b,w,_)}catch(t){return new Date("")}};return function(e,n,r){var o=n.prototype,i=o.parse;o.parse=function(e){var n=e.date,o=e.format,a=e.pl,s=e.utc;this.$u=s,o?(t=a?r.Ls[a]:this.$locale(),this.$d=c(n,o,s),this.init(e),a&&(this.$L=a)):i.call(this,e)}}}))},fb6a:function(t,e,n){"use strict";var r=n("23e7"),o=n("861d"),i=n("e8b5"),a=n("23cb"),s=n("50c4"),c=n("fc6a"),u=n("8418"),l=n("b622"),f=n("1dde"),d=n("ae40"),p=f("slice"),h=d("slice",{ACCESSORS:!0,0:0,1:2}),v=l("species"),g=[].slice,m=Math.max;r({target:"Array",proto:!0,forced:!p||!h},{slice:function(t,e){var n,r,l,f=c(this),d=s(f.length),p=a(t,d),h=a(void 0===e?d:e,d);if(i(f)&&(n=f.constructor,"function"!=typeof n||n!==Array&&!i(n.prototype)?o(n)&&(n=n[v],null===n&&(n=void 0)):n=void 0,n===Array||void 0===n))return g.call(f,p,h);for(r=new(void 0===n?Array:n)(m(h-p,0)),l=0;pu?l(e,s,v):s>v&&f(t,n,u)}function l(t,e,n){for(;e<=n;++e)a(t[e])}function f(t,e,n){for(;e<=n;++e){var r=t[e];o(r)&&(r.vm.$destroy(),r.vm=null)}}function d(t,e){t!==e&&(e.vm=t.vm,s(e))}function p(t,e){o(t)&&o(e)?t!==e&&u(t,e):o(e)?l(e,0,e.length-1):o(t)&&f(t,0,t.length-1)}function h(t,e,n){return{tag:t,key:e,args:n}}Object.defineProperty(e,"__esModule",{value:!0}),e.patchChildren=p,e.h=h},fc6a:function(t,e,n){var r=n("44ad"),o=n("1d80");t.exports=function(t){return r(o(t))}},fdbc:function(t,e){t.exports={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}},fdbf:function(t,e,n){var r=n("4930");t.exports=r&&!Symbol.sham&&"symbol"==typeof Symbol.iterator},fea9:function(t,e,n){var r=n("da84");t.exports=r.Promise}}]); \ No newline at end of file diff --git a/kirby/router.php b/kirby/router.php new file mode 100644 index 0000000..ba27ee0 --- /dev/null +++ b/kirby/router.php @@ -0,0 +1,12 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Api +{ + use Properties; + + /** + * Authentication callback + * + * @var \Closure + */ + protected $authentication; + + /** + * Debugging flag + * + * @var bool + */ + protected $debug = false; + + /** + * Collection definition + * + * @var array + */ + protected $collections = []; + + /** + * Injected data/dependencies + * + * @var array + */ + protected $data = []; + + /** + * Model definitions + * + * @var array + */ + protected $models = []; + + /** + * The current route + * + * @var \Kirby\Http\Route + */ + protected $route; + + /** + * The Router instance + * + * @var \Kirby\Http\Router + */ + protected $router; + + /** + * Route definition + * + * @var array + */ + protected $routes = []; + + /** + * Request data + * [query, body, files] + * + * @var array + */ + protected $requestData = []; + + /** + * The applied request method + * (GET, POST, PATCH, etc.) + * + * @var string + */ + protected $requestMethod; + + /** + * Magic accessor for any given data + * + * @param string $method + * @param array $args + * @return mixed + * @throws \Kirby\Exception\NotFoundException + */ + public function __call(string $method, array $args = []) + { + return $this->data($method, ...$args); + } + + /** + * Creates a new API instance + * + * @param array $props + */ + public function __construct(array $props) + { + $this->setProperties($props); + } + + /** + * Runs the authentication method + * if set + * + * @return mixed + */ + public function authenticate() + { + if ($auth = $this->authentication()) { + return $auth->call($this); + } + + return true; + } + + /** + * Returns the authentication callback + * + * @return \Closure|null + */ + public function authentication() + { + return $this->authentication; + } + + /** + * Execute an API call for the given path, + * request method and optional request data + * + * @param string|null $path + * @param string $method + * @param array $requestData + * @return mixed + * @throws \Kirby\Exception\NotFoundException + * @throws \Exception + */ + public function call(string $path = null, string $method = 'GET', array $requestData = []) + { + $path = rtrim($path, '/'); + + $this->setRequestMethod($method); + $this->setRequestData($requestData); + + $this->router = new Router($this->routes()); + $this->route = $this->router->find($path, $method); + $auth = $this->route->attributes()['auth'] ?? true; + + if ($auth !== false) { + $user = $this->authenticate(); + + // set PHP locales based on *user* language + // so that e.g. strftime() gets formatted correctly + if (is_a($user, 'Kirby\Cms\User') === true) { + $language = $user->language(); + + // get the locale from the translation + $translation = $user->kirby()->translation($language); + $locale = ($translation !== null)? $translation->locale() : $language; + + // provide some variants as fallbacks to be + // compatible with as many systems as possible + $locales = [ + $locale . '.UTF-8', + $locale . '.UTF8', + $locale . '.ISO8859-1', + $locale, + $language, + setlocale(LC_ALL, 0) // fall back to the previously defined locale + ]; + + // set the locales that are relevant for string formatting + // *don't* set LC_CTYPE to avoid breaking other parts of the system + setlocale(LC_MONETARY, $locales); + setlocale(LC_NUMERIC, $locales); + setlocale(LC_TIME, $locales); + } + } + + // don't throw pagination errors if pagination + // page is out of bounds + $validate = Pagination::$validate; + Pagination::$validate = false; + + $output = $this->route->action()->call($this, ...$this->route->arguments()); + + // restore old pagination validation mode + Pagination::$validate = $validate; + + if ( + is_object($output) === true && + is_a($output, 'Kirby\\Http\\Response') !== true + ) { + return $this->resolve($output)->toResponse(); + } + + return $output; + } + + /** + * Setter and getter for an API collection + * + * @param string $name + * @param array|null $collection + * @return \Kirby\Api\Collection + * @throws \Kirby\Exception\NotFoundException If no collection for `$name` exists + * @throws \Exception + */ + public function collection(string $name, $collection = null) + { + if (isset($this->collections[$name]) === false) { + throw new NotFoundException(sprintf('The collection "%s" does not exist', $name)); + } + + return new Collection($this, $collection, $this->collections[$name]); + } + + /** + * Returns the collections definition + * + * @return array + */ + public function collections(): array + { + return $this->collections; + } + + /** + * Returns the injected data array + * or certain parts of it by key + * + * @param string|null $key + * @param mixed ...$args + * @return mixed + * + * @throws \Kirby\Exception\NotFoundException If no data for `$key` exists + */ + public function data($key = null, ...$args) + { + if ($key === null) { + return $this->data; + } + + if ($this->hasData($key) === false) { + throw new NotFoundException(sprintf('Api data for "%s" does not exist', $key)); + } + + // lazy-load data wrapped in Closures + if (is_a($this->data[$key], 'Closure') === true) { + return $this->data[$key]->call($this, ...$args); + } + + return $this->data[$key]; + } + + /** + * Returns the debugging flag + * + * @return bool + */ + public function debug(): bool + { + return $this->debug; + } + + /** + * Checks if injected data exists for the given key + * + * @param string $key + * @return bool + */ + public function hasData(string $key): bool + { + return isset($this->data[$key]) === true; + } + + /** + * Matches an object with an array item + * based on the `type` field + * + * @param array models or collections + * @param mixed $object + * + * @return string key of match + */ + protected function match(array $array, $object = null) + { + foreach ($array as $definition => $model) { + if (is_a($object, $model['type']) === true) { + return $definition; + } + } + } + + /** + * Returns an API model instance by name + * + * @param string|null $name + * @param mixed $object + * @return \Kirby\Api\Model + * + * @throws \Kirby\Exception\NotFoundException If no model for `$name` exists + */ + public function model(string $name = null, $object = null) + { + // Try to auto-match object with API models + if ($name === null) { + if ($model = $this->match($this->models, $object)) { + $name = $model; + } + } + + if (isset($this->models[$name]) === false) { + throw new NotFoundException(sprintf('The model "%s" does not exist', $name)); + } + + return new Model($this, $object, $this->models[$name]); + } + + /** + * Returns all model definitions + * + * @return array + */ + public function models(): array + { + return $this->models; + } + + /** + * Getter for request data + * Can either get all the data + * or certain parts of it. + * + * @param string|null $type + * @param string|null $key + * @param mixed $default + * @return mixed + */ + public function requestData(string $type = null, string $key = null, $default = null) + { + if ($type === null) { + return $this->requestData; + } + + if ($key === null) { + return $this->requestData[$type] ?? []; + } + + $data = array_change_key_case($this->requestData($type)); + $key = strtolower($key); + + return $data[$key] ?? $default; + } + + /** + * Returns the request body if available + * + * @param string|null $key + * @param mixed $default + * @return mixed + */ + public function requestBody(string $key = null, $default = null) + { + return $this->requestData('body', $key, $default); + } + + /** + * Returns the files from the request if available + * + * @param string|null $key + * @param mixed $default + * @return mixed + */ + public function requestFiles(string $key = null, $default = null) + { + return $this->requestData('files', $key, $default); + } + + /** + * Returns all headers from the request if available + * + * @param string|null $key + * @param mixed $default + * @return mixed + */ + public function requestHeaders(string $key = null, $default = null) + { + return $this->requestData('headers', $key, $default); + } + + /** + * Returns the request method + * + * @return string + */ + public function requestMethod(): string + { + return $this->requestMethod; + } + + /** + * Returns the request query if available + * + * @param string|null $key + * @param mixed $default + * @return mixed + */ + public function requestQuery(string $key = null, $default = null) + { + return $this->requestData('query', $key, $default); + } + + /** + * Turns a Kirby object into an + * API model or collection representation + * + * @param mixed $object + * @return \Kirby\Api\Model|\Kirby\Api\Collection + * + * @throws \Kirby\Exception\NotFoundException If `$object` cannot be resolved + */ + public function resolve($object) + { + if (is_a($object, 'Kirby\Api\Model') === true || is_a($object, 'Kirby\Api\Collection') === true) { + return $object; + } + + if ($model = $this->match($this->models, $object)) { + return $this->model($model, $object); + } + + if ($collection = $this->match($this->collections, $object)) { + return $this->collection($collection, $object); + } + + throw new NotFoundException(sprintf('The object "%s" cannot be resolved', get_class($object))); + } + + /** + * Returns all defined routes + * + * @return array + */ + public function routes(): array + { + return $this->routes; + } + + /** + * Setter for the authentication callback + * + * @param \Closure|null $authentication + * @return self + */ + protected function setAuthentication(Closure $authentication = null) + { + $this->authentication = $authentication; + return $this; + } + + /** + * Setter for the collections definition + * + * @param array|null $collections + * @return self + */ + protected function setCollections(array $collections = null) + { + if ($collections !== null) { + $this->collections = array_change_key_case($collections); + } + return $this; + } + + /** + * Setter for the injected data + * + * @param array|null $data + * @return self + */ + protected function setData(array $data = null) + { + $this->data = $data ?? []; + return $this; + } + + /** + * Setter for the debug flag + * + * @param bool $debug + * @return self + */ + protected function setDebug(bool $debug = false) + { + $this->debug = $debug; + return $this; + } + + /** + * Setter for the model definitions + * + * @param array|null $models + * @return self + */ + protected function setModels(array $models = null) + { + if ($models !== null) { + $this->models = array_change_key_case($models); + } + + return $this; + } + + /** + * Setter for the request data + * + * @param array|null $requestData + * @return self + */ + protected function setRequestData(array $requestData = null) + { + $defaults = [ + 'query' => [], + 'body' => [], + 'files' => [] + ]; + + $this->requestData = array_merge($defaults, (array)$requestData); + return $this; + } + + /** + * Setter for the request method + * + * @param string|null $requestMethod + * @return self + */ + protected function setRequestMethod(string $requestMethod = null) + { + $this->requestMethod = $requestMethod ?? 'GET'; + return $this; + } + + /** + * Setter for the route definitions + * + * @param array|null $routes + * @return self + */ + protected function setRoutes(array $routes = null) + { + $this->routes = $routes ?? []; + return $this; + } + + /** + * Renders the API call + * + * @param string $path + * @param string $method + * @param array $requestData + * @return mixed + */ + public function render(string $path, $method = 'GET', array $requestData = []) + { + try { + $result = $this->call($path, $method, $requestData); + } catch (Throwable $e) { + $result = $this->responseForException($e); + } + + if ($result === null) { + $result = $this->responseFor404(); + } elseif ($result === false) { + $result = $this->responseFor400(); + } elseif ($result === true) { + $result = $this->responseFor200(); + } + + if (is_array($result) === false) { + return $result; + } + + // pretty print json data + $pretty = (bool)($requestData['query']['pretty'] ?? false) === true; + + if (($result['status'] ?? 'ok') === 'error') { + $code = $result['code'] ?? 400; + + // sanitize the error code + if ($code < 400 || $code > 599) { + $code = 500; + } + + return Response::json($result, $code, $pretty); + } + + return Response::json($result, 200, $pretty); + } + + /** + * Returns a 200 - ok + * response array. + * + * @return array + */ + public function responseFor200(): array + { + return [ + 'status' => 'ok', + 'message' => 'ok', + 'code' => 200 + ]; + } + + /** + * Returns a 400 - bad request + * response array. + * + * @return array + */ + public function responseFor400(): array + { + return [ + 'status' => 'error', + 'message' => 'bad request', + 'code' => 400, + ]; + } + + /** + * Returns a 404 - not found + * response array. + * + * @return array + */ + public function responseFor404(): array + { + return [ + 'status' => 'error', + 'message' => 'not found', + 'code' => 404, + ]; + } + + /** + * Creates the response array for + * an exception. Kirby exceptions will + * have more information + * + * @param \Throwable $e + * @return array + */ + public function responseForException(Throwable $e): array + { + // prepare the result array for all exception types + $result = [ + 'status' => 'error', + 'message' => $e->getMessage(), + 'code' => empty($e->getCode()) === true ? 500 : $e->getCode(), + 'exception' => get_class($e), + 'key' => null, + 'file' => F::relativepath($e->getFile(), $_SERVER['DOCUMENT_ROOT'] ?? null), + 'line' => $e->getLine(), + 'details' => [], + 'route' => $this->route ? $this->route->pattern() : null + ]; + + // extend the information for Kirby Exceptions + if (is_a($e, 'Kirby\Exception\Exception') === true) { + $result['key'] = $e->getKey(); + $result['details'] = $e->getDetails(); + $result['code'] = $e->getHttpCode(); + } + + // remove critical info from the result set if + // debug mode is switched off + if ($this->debug !== true) { + unset( + $result['file'], + $result['exception'], + $result['line'], + $result['route'] + ); + } + + return $result; + } + + /** + * Upload helper method + * + * move_uploaded_file() not working with unit test + * Added debug parameter for testing purposes as we did in the Email class + * + * @param \Closure $callback + * @param bool $single + * @param bool $debug + * @return array + * + * @throws \Exception If request has no files or there was an error with the upload + */ + public function upload(Closure $callback, $single = false, $debug = false): array + { + $trials = 0; + $uploads = []; + $errors = []; + $files = $this->requestFiles(); + + // get error messages from translation + $errorMessages = [ + UPLOAD_ERR_INI_SIZE => t('upload.error.iniSize'), + UPLOAD_ERR_FORM_SIZE => t('upload.error.formSize'), + UPLOAD_ERR_PARTIAL => t('upload.error.partial'), + UPLOAD_ERR_NO_FILE => t('upload.error.noFile'), + UPLOAD_ERR_NO_TMP_DIR => t('upload.error.tmpDir'), + UPLOAD_ERR_CANT_WRITE => t('upload.error.cantWrite'), + UPLOAD_ERR_EXTENSION => t('upload.error.extension') + ]; + + if (empty($files) === true) { + $postMaxSize = Str::toBytes(ini_get('post_max_size')); + $uploadMaxFileSize = Str::toBytes(ini_get('upload_max_filesize')); + + if ($postMaxSize < $uploadMaxFileSize) { + throw new Exception(t('upload.error.iniPostSize')); + } else { + throw new Exception(t('upload.error.noFiles')); + } + } + + foreach ($files as $upload) { + if (isset($upload['tmp_name']) === false && is_array($upload)) { + continue; + } + + $trials++; + + try { + if ($upload['error'] !== 0) { + $errorMessage = $errorMessages[$upload['error']] ?? t('upload.error.default'); + throw new Exception($errorMessage); + } + + // get the extension of the uploaded file + $extension = F::extension($upload['name']); + + // try to detect the correct mime and add the extension + // accordingly. This will avoid .tmp filenames + if (empty($extension) === true || in_array($extension, ['tmp', 'temp'])) { + $mime = F::mime($upload['tmp_name']); + $extension = F::mimeToExtension($mime); + $filename = F::name($upload['name']) . '.' . $extension; + } else { + $filename = basename($upload['name']); + } + + $source = dirname($upload['tmp_name']) . '/' . uniqid() . '.' . $filename; + + // move the file to a location including the extension, + // for better mime detection + if ($debug === false && move_uploaded_file($upload['tmp_name'], $source) === false) { + throw new Exception(t('upload.error.cantMove')); + } + + $data = $callback($source, $filename); + + if (is_object($data) === true) { + $data = $this->resolve($data)->toArray(); + } + + $uploads[$upload['name']] = $data; + } catch (Exception $e) { + $errors[$upload['name']] = $e->getMessage(); + } + + if ($single === true) { + break; + } + } + + // return a single upload response + if ($trials === 1) { + if (empty($errors) === false) { + return [ + 'status' => 'error', + 'message' => current($errors) + ]; + } + + return [ + 'status' => 'ok', + 'data' => current($uploads) + ]; + } + + if (empty($errors) === false) { + return [ + 'status' => 'error', + 'errors' => $errors + ]; + } + + return [ + 'status' => 'ok', + 'data' => $uploads + ]; + } +} diff --git a/kirby/src/Api/Collection.php b/kirby/src/Api/Collection.php new file mode 100644 index 0000000..ba5cb4a --- /dev/null +++ b/kirby/src/Api/Collection.php @@ -0,0 +1,178 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Collection +{ + /** + * @var \Kirby\Api\Api + */ + protected $api; + + /** + * @var mixed|null + */ + protected $data; + + /** + * @var mixed|null + */ + protected $model; + + /** + * @var mixed|null + */ + protected $select; + + /** + * @var mixed|null + */ + protected $view; + + /** + * Collection constructor + * + * @param \Kirby\Api\Api $api + * @param mixed|null $data + * @param array $schema + * @throws \Exception + */ + public function __construct(Api $api, $data = null, array $schema) + { + $this->api = $api; + $this->data = $data; + $this->model = $schema['model'] ?? null; + $this->view = $schema['view'] ?? null; + + if ($data === null) { + if (is_a($schema['default'] ?? null, 'Closure') === false) { + throw new Exception('Missing collection data'); + } + + $this->data = $schema['default']->call($this->api); + } + + if ( + isset($schema['type']) === true && + is_a($this->data, $schema['type']) === false + ) { + throw new Exception('Invalid collection type'); + } + } + + /** + * @param string|array|null $keys + * @return self + * @throws \Exception + */ + public function select($keys = null) + { + if ($keys === false) { + return $this; + } + + if (is_string($keys)) { + $keys = Str::split($keys); + } + + if ($keys !== null && is_array($keys) === false) { + throw new Exception('Invalid select keys'); + } + + $this->select = $keys; + return $this; + } + + /** + * @return array + * @throws \Kirby\Exception\NotFoundException + * @throws \Exception + */ + public function toArray(): array + { + $result = []; + + foreach ($this->data as $item) { + $model = $this->api->model($this->model, $item); + + if ($this->view !== null) { + $model = $model->view($this->view); + } + + if ($this->select !== null) { + $model = $model->select($this->select); + } + + $result[] = $model->toArray(); + } + + return $result; + } + + /** + * @return array + * @throws \Kirby\Exception\NotFoundException + * @throws \Exception + */ + public function toResponse(): array + { + if ($query = $this->api->requestQuery('query')) { + $this->data = $this->data->query($query); + } + + if (!$this->data->pagination()) { + $this->data = $this->data->paginate([ + 'page' => $this->api->requestQuery('page', 1), + 'limit' => $this->api->requestQuery('limit', 100) + ]); + } + + $pagination = $this->data->pagination(); + + if ($select = $this->api->requestQuery('select')) { + $this->select($select); + } + + if ($view = $this->api->requestQuery('view')) { + $this->view($view); + } + + return [ + 'code' => 200, + 'data' => $this->toArray(), + 'pagination' => [ + 'page' => $pagination->page(), + 'total' => $pagination->total(), + 'offset' => $pagination->offset(), + 'limit' => $pagination->limit(), + ], + 'status' => 'ok', + 'type' => 'collection' + ]; + } + + /** + * @param string $view + * @return self + */ + public function view(string $view) + { + $this->view = $view; + return $this; + } +} diff --git a/kirby/src/Api/Model.php b/kirby/src/Api/Model.php new file mode 100644 index 0000000..23012bb --- /dev/null +++ b/kirby/src/Api/Model.php @@ -0,0 +1,248 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Model +{ + /** + * @var \Kirby\Api\Api + */ + protected $api; + + /** + * @var mixed|null + */ + protected $data; + + /** + * @var array|mixed + */ + protected $fields; + + /** + * @var mixed|null + */ + protected $select; + + /** + * @var array|mixed + */ + protected $views; + + /** + * Model constructor + * + * @param \Kirby\Api\Api $api + * @param null $data + * @param array $schema + * @throws \Exception + */ + public function __construct(Api $api, $data = null, array $schema) + { + $this->api = $api; + $this->data = $data; + $this->fields = $schema['fields'] ?? []; + $this->select = $schema['select'] ?? null; + $this->views = $schema['views'] ?? []; + + if ($this->select === null && array_key_exists('default', $this->views)) { + $this->view('default'); + } + + if ($data === null) { + if (is_a($schema['default'] ?? null, 'Closure') === false) { + throw new Exception('Missing model data'); + } + + $this->data = $schema['default']->call($this->api); + } + + if ( + isset($schema['type']) === true && + is_a($this->data, $schema['type']) === false + ) { + throw new Exception(sprintf('Invalid model type "%s" expected: "%s"', get_class($this->data), $schema['type'])); + } + } + + /** + * @param null $keys + * @return self + * @throws \Exception + */ + public function select($keys = null) + { + if ($keys === false) { + return $this; + } + + if (is_string($keys)) { + $keys = Str::split($keys); + } + + if ($keys !== null && is_array($keys) === false) { + throw new Exception('Invalid select keys'); + } + + $this->select = $keys; + return $this; + } + + /** + * @return array + * @throws \Exception + */ + public function selection(): array + { + $select = $this->select; + + if ($select === null) { + $select = array_keys($this->fields); + } + + $selection = []; + + foreach ($select as $key => $value) { + if (is_int($key) === true) { + $selection[$value] = [ + 'view' => null, + 'select' => null + ]; + continue; + } + + if (is_string($value) === true) { + if ($value === 'any') { + throw new Exception('Invalid sub view: "any"'); + } + + $selection[$key] = [ + 'view' => $value, + 'select' => null + ]; + + continue; + } + + if (is_array($value) === true) { + $selection[$key] = [ + 'view' => null, + 'select' => $value + ]; + } + } + + return $selection; + } + + /** + * @return array + * @throws \Kirby\Exception\NotFoundException + * @throws \Exception + */ + public function toArray(): array + { + $select = $this->selection(); + $result = []; + + foreach ($this->fields as $key => $resolver) { + if (array_key_exists($key, $select) === false || is_a($resolver, 'Closure') === false) { + continue; + } + + $value = $resolver->call($this->api, $this->data); + + if (is_object($value)) { + $value = $this->api->resolve($value); + } + + if ( + is_a($value, 'Kirby\Api\Collection') === true || + is_a($value, 'Kirby\Api\Model') === true + ) { + $selection = $select[$key]; + + if ($subview = $selection['view']) { + $value->view($subview); + } + + if ($subselect = $selection['select']) { + $value->select($subselect); + } + + $value = $value->toArray(); + } + + $result[$key] = $value; + } + + ksort($result); + + return $result; + } + + /** + * @return array + * @throws \Kirby\Exception\NotFoundException + * @throws \Exception + */ + public function toResponse(): array + { + $model = $this; + + if ($select = $this->api->requestQuery('select')) { + $model = $model->select($select); + } + + if ($view = $this->api->requestQuery('view')) { + $model = $model->view($view); + } + + return [ + 'code' => 200, + 'data' => $model->toArray(), + 'status' => 'ok', + 'type' => 'model' + ]; + } + + /** + * @param string $name + * @return self + * @throws \Exception + */ + public function view(string $name) + { + if ($name === 'any') { + return $this->select(null); + } + + if (isset($this->views[$name]) === false) { + $name = 'default'; + + // try to fall back to the default view at least + if (isset($this->views[$name]) === false) { + throw new Exception(sprintf('The view "%s" does not exist', $name)); + } + } + + return $this->select($this->views[$name]); + } +} diff --git a/kirby/src/Cache/ApcuCache.php b/kirby/src/Cache/ApcuCache.php new file mode 100644 index 0000000..e750d20 --- /dev/null +++ b/kirby/src/Cache/ApcuCache.php @@ -0,0 +1,86 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class ApcuCache extends Cache +{ + /** + * Determines if an item exists in the cache + * + * @param string $key + * @return bool + */ + public function exists(string $key): bool + { + return apcu_exists($this->key($key)); + } + + /** + * Flushes the entire cache and returns + * whether the operation was successful + * + * @return bool + */ + public function flush(): bool + { + if (empty($this->options['prefix']) === false) { + return apcu_delete(new APCuIterator('!^' . preg_quote($this->options['prefix']) . '!')); + } else { + return apcu_clear_cache(); + } + } + + /** + * Removes an item from the cache and returns + * whether the operation was successful + * + * @param string $key + * @return bool + */ + public function remove(string $key): bool + { + return apcu_delete($this->key($key)); + } + + /** + * Internal method to retrieve the raw cache value; + * needs to return a Value object or null if not found + * + * @param string $key + * @return \Kirby\Cache\Value|null + */ + public function retrieve(string $key) + { + return Value::fromJson(apcu_fetch($this->key($key))); + } + + /** + * Writes an item to the cache for a given number of minutes and + * returns whether the operation was successful + * + * + * // put an item in the cache for 15 minutes + * $cache->set('value', 'my value', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return bool + */ + public function set(string $key, $value, int $minutes = 0): bool + { + return apcu_store($this->key($key), (new Value($value, $minutes))->toJson(), $this->expiration($minutes)); + } +} diff --git a/kirby/src/Cache/Cache.php b/kirby/src/Cache/Cache.php new file mode 100644 index 0000000..729d61b --- /dev/null +++ b/kirby/src/Cache/Cache.php @@ -0,0 +1,242 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +abstract class Cache +{ + /** + * Stores all options for the driver + * @var array + */ + protected $options = []; + + /** + * Sets all parameters which are needed to connect to the cache storage + * + * @param array $options + */ + public function __construct(array $options = []) + { + $this->options = $options; + } + + /** + * Writes an item to the cache for a given number of minutes and + * returns whether the operation was successful; + * this needs to be defined by the driver + * + * + * // put an item in the cache for 15 minutes + * $cache->set('value', 'my value', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return bool + */ + abstract public function set(string $key, $value, int $minutes = 0): bool; + + /** + * Adds the prefix to the key if given + * + * @param string $key + * @return string + */ + protected function key(string $key): string + { + if (empty($this->options['prefix']) === false) { + $key = $this->options['prefix'] . '/' . $key; + } + + return $key; + } + + /** + * Internal method to retrieve the raw cache value; + * needs to return a Value object or null if not found; + * this needs to be defined by the driver + * + * @param string $key + * @return \Kirby\Cache\Value|null + */ + abstract public function retrieve(string $key); + + /** + * Gets an item from the cache + * + * + * // get an item from the cache driver + * $value = $cache->get('value'); + * + * // return a default value if the requested item isn't cached + * $value = $cache->get('value', 'default value'); + * + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function get(string $key, $default = null) + { + // get the Value + $value = $this->retrieve($key); + + // check for a valid cache value + if (!is_a($value, 'Kirby\Cache\Value')) { + return $default; + } + + // remove the item if it is expired + if ($value->expires() > 0 && time() >= $value->expires()) { + $this->remove($key); + return $default; + } + + // return the pure value + return $value->value(); + } + + /** + * Calculates the expiration timestamp + * + * @param int $minutes + * @return int + */ + protected function expiration(int $minutes = 0): int + { + // 0 = keep forever + if ($minutes === 0) { + return 0; + } + + // calculate the time + return time() + ($minutes * 60); + } + + /** + * Checks when an item in the cache expires; + * returns the expiry timestamp on success, null if the + * item never expires and false if the item does not exist + * + * @param string $key + * @return int|null|false + */ + public function expires(string $key) + { + // get the Value object + $value = $this->retrieve($key); + + // check for a valid Value object + if (!is_a($value, 'Kirby\Cache\Value')) { + return false; + } + + // return the expires timestamp + return $value->expires(); + } + + /** + * Checks if an item in the cache is expired + * + * @param string $key + * @return bool + */ + public function expired(string $key): bool + { + $expires = $this->expires($key); + + if ($expires === null) { + return false; + } elseif (!is_int($expires)) { + return true; + } else { + return time() >= $expires; + } + } + + /** + * Checks when the cache has been created; + * returns the creation timestamp on success + * and false if the item does not exist + * + * @param string $key + * @return int|false + */ + public function created(string $key) + { + // get the Value object + $value = $this->retrieve($key); + + // check for a valid Value object + if (!is_a($value, 'Kirby\Cache\Value')) { + return false; + } + + // return the expires timestamp + return $value->created(); + } + + /** + * Alternate version for Cache::created($key) + * + * @param string $key + * @return int|false + */ + public function modified(string $key) + { + return static::created($key); + } + + /** + * Determines if an item exists in the cache + * + * @param string $key + * @return bool + */ + public function exists(string $key): bool + { + return $this->expired($key) === false; + } + + /** + * Removes an item from the cache and returns + * whether the operation was successful; + * this needs to be defined by the driver + * + * @param string $key + * @return bool + */ + abstract public function remove(string $key): bool; + + /** + * Flushes the entire cache and returns + * whether the operation was successful; + * this needs to be defined by the driver + * + * @return bool + */ + abstract public function flush(): bool; + + /** + * Returns all passed cache options + * + * @return array + */ + public function options(): array + { + return $this->options; + } +} diff --git a/kirby/src/Cache/FileCache.php b/kirby/src/Cache/FileCache.php new file mode 100644 index 0000000..783d31f --- /dev/null +++ b/kirby/src/Cache/FileCache.php @@ -0,0 +1,166 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class FileCache extends Cache +{ + /** + * Full root including prefix + * + * @var string + */ + protected $root; + + /** + * Sets all parameters which are needed for the file cache + * + * @param array $options 'root' (required) + * 'prefix' (default: none) + * 'extension' (file extension for cache files, default: none) + */ + public function __construct(array $options) + { + $defaults = [ + 'root' => null, + 'prefix' => null, + 'extension' => null + ]; + + parent::__construct(array_merge($defaults, $options)); + + // build the full root including prefix + $this->root = $this->options['root']; + if (empty($this->options['prefix']) === false) { + $this->root .= '/' . $this->options['prefix']; + } + + // try to create the directory + Dir::make($this->root, true); + } + + /** + * Returns the full root including prefix + * + * @return string + */ + public function root(): string + { + return $this->root; + } + + /** + * Returns the full path to a file for a given key + * + * @param string $key + * @return string + */ + protected function file(string $key): string + { + $file = $this->root . '/' . $key; + + if (isset($this->options['extension'])) { + return $file . '.' . $this->options['extension']; + } else { + return $file; + } + } + + /** + * Writes an item to the cache for a given number of minutes and + * returns whether the operation was successful + * + * + * // put an item in the cache for 15 minutes + * $cache->set('value', 'my value', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return bool + */ + public function set(string $key, $value, int $minutes = 0): bool + { + $file = $this->file($key); + + return F::write($file, (new Value($value, $minutes))->toJson()); + } + + /** + * Internal method to retrieve the raw cache value; + * needs to return a Value object or null if not found + * + * @param string $key + * @return \Kirby\Cache\Value|null + */ + public function retrieve(string $key) + { + $file = $this->file($key); + + return Value::fromJson(F::read($file)); + } + + /** + * Checks when the cache has been created; + * returns the creation timestamp on success + * and false if the item does not exist + * + * @param string $key + * @return mixed + */ + public function created(string $key) + { + // use the modification timestamp + // as indicator when the cache has been created/overwritten + clearstatcache(); + + // get the file for this cache key + $file = $this->file($key); + return file_exists($file) ? filemtime($this->file($key)) : false; + } + + /** + * Removes an item from the cache and returns + * whether the operation was successful + * + * @param string $key + * @return bool + */ + public function remove(string $key): bool + { + $file = $this->file($key); + + if (is_file($file) === true) { + return F::remove($file); + } else { + return false; + } + } + + /** + * Flushes the entire cache and returns + * whether the operation was successful + * + * @return bool + */ + public function flush(): bool + { + if (Dir::remove($this->root) === true && Dir::make($this->root) === true) { + return true; + } + + return false; // @codeCoverageIgnore + } +} diff --git a/kirby/src/Cache/MemCached.php b/kirby/src/Cache/MemCached.php new file mode 100644 index 0000000..82cff09 --- /dev/null +++ b/kirby/src/Cache/MemCached.php @@ -0,0 +1,97 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class MemCached extends Cache +{ + /** + * store for the memache connection + * @var Memcached + */ + protected $connection; + + /** + * Sets all parameters which are needed to connect to Memcached + * + * @param array $options 'host' (default: localhost) + * 'port' (default: 11211) + * 'prefix' (default: null) + */ + public function __construct(array $options = []) + { + $defaults = [ + 'host' => 'localhost', + 'port' => 11211, + 'prefix' => null, + ]; + + parent::__construct(array_merge($defaults, $options)); + + $this->connection = new \Memcached(); + $this->connection->addServer($this->options['host'], $this->options['port']); + } + + /** + * Writes an item to the cache for a given number of minutes and + * returns whether the operation was successful + * + * + * // put an item in the cache for 15 minutes + * $cache->set('value', 'my value', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return bool + */ + public function set(string $key, $value, int $minutes = 0): bool + { + return $this->connection->set($this->key($key), (new Value($value, $minutes))->toJson(), $this->expiration($minutes)); + } + + /** + * Internal method to retrieve the raw cache value; + * needs to return a Value object or null if not found + * + * @param string $key + * @return \Kirby\Cache\Value|null + */ + public function retrieve(string $key) + { + return Value::fromJson($this->connection->get($this->key($key))); + } + + /** + * Removes an item from the cache and returns + * whether the operation was successful + * + * @param string $key + * @return bool + */ + public function remove(string $key): bool + { + return $this->connection->delete($this->key($key)); + } + + /** + * Flushes the entire cache and returns + * whether the operation was successful; + * WARNING: Memcached only supports flushing the whole cache at once! + * + * @return bool + */ + public function flush(): bool + { + return $this->connection->flush(); + } +} diff --git a/kirby/src/Cache/MemoryCache.php b/kirby/src/Cache/MemoryCache.php new file mode 100644 index 0000000..7f2d098 --- /dev/null +++ b/kirby/src/Cache/MemoryCache.php @@ -0,0 +1,82 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class MemoryCache extends Cache +{ + /** + * Cache data + * @var array + */ + protected $store = []; + + /** + * Writes an item to the cache for a given number of minutes and + * returns whether the operation was successful + * + * + * // put an item in the cache for 15 minutes + * $cache->set('value', 'my value', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return bool + */ + public function set(string $key, $value, int $minutes = 0): bool + { + $this->store[$key] = new Value($value, $minutes); + return true; + } + + /** + * Internal method to retrieve the raw cache value; + * needs to return a Value object or null if not found + * + * @param string $key + * @return \Kirby\Cache\Value|null + */ + public function retrieve(string $key) + { + return $this->store[$key] ?? null; + } + + /** + * Removes an item from the cache and returns + * whether the operation was successful + * + * @param string $key + * @return bool + */ + public function remove(string $key): bool + { + if (isset($this->store[$key])) { + unset($this->store[$key]); + return true; + } else { + return false; + } + } + + /** + * Flushes the entire cache and returns + * whether the operation was successful + * + * @return bool + */ + public function flush(): bool + { + $this->store = []; + return true; + } +} diff --git a/kirby/src/Cache/NullCache.php b/kirby/src/Cache/NullCache.php new file mode 100644 index 0000000..a33fc9c --- /dev/null +++ b/kirby/src/Cache/NullCache.php @@ -0,0 +1,69 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class NullCache extends Cache +{ + /** + * Writes an item to the cache for a given number of minutes and + * returns whether the operation was successful + * + * + * // put an item in the cache for 15 minutes + * $cache->set('value', 'my value', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return bool + */ + public function set(string $key, $value, int $minutes = 0): bool + { + return true; + } + + /** + * Internal method to retrieve the raw cache value; + * needs to return a Value object or null if not found + * + * @param string $key + * @return \Kirby\Cache\Value|null + */ + public function retrieve(string $key) + { + return null; + } + + /** + * Removes an item from the cache and returns + * whether the operation was successful + * + * @param string $key + * @return bool + */ + public function remove(string $key): bool + { + return true; + } + + /** + * Flushes the entire cache and returns + * whether the operation was successful + * + * @return bool + */ + public function flush(): bool + { + return true; + } +} diff --git a/kirby/src/Cache/Value.php b/kirby/src/Cache/Value.php new file mode 100644 index 0000000..038965c --- /dev/null +++ b/kirby/src/Cache/Value.php @@ -0,0 +1,144 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Value +{ + /** + * Cached value + * @var mixed + */ + protected $value; + + /** + * the number of minutes until the value expires + * @var int + */ + protected $minutes; + + /** + * Creation timestamp + * @var int + */ + protected $created; + + /** + * Constructor + * + * @param mixed $value + * @param int $minutes the number of minutes until the value expires + * @param int $created the unix timestamp when the value has been created + */ + public function __construct($value, int $minutes = 0, int $created = null) + { + $this->value = $value; + $this->minutes = $minutes ?? 0; + $this->created = $created ?? time(); + } + + /** + * Returns the creation date as UNIX timestamp + * + * @return int + */ + public function created(): int + { + return $this->created; + } + + /** + * Returns the expiration date as UNIX timestamp or + * null if the value never expires + * + * @return int|null + */ + public function expires(): ?int + { + // 0 = keep forever + if ($this->minutes === 0) { + return null; + } + + return $this->created + ($this->minutes * 60); + } + + /** + * Creates a value object from an array + * + * @param array $array + * @return self + */ + public static function fromArray(array $array) + { + return new static($array['value'] ?? null, $array['minutes'] ?? 0, $array['created'] ?? null); + } + + /** + * Creates a value object from a JSON string; + * returns null on error + * + * @param string $json + * @return self|null + */ + public static function fromJson(string $json) + { + try { + $array = json_decode($json, true); + + if (is_array($array)) { + return static::fromArray($array); + } else { + return null; + } + } catch (Throwable $e) { + return null; + } + } + + /** + * Converts the object to a JSON string + * + * @return string + */ + public function toJson(): string + { + return json_encode($this->toArray()); + } + + /** + * Converts the object to an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'created' => $this->created, + 'minutes' => $this->minutes, + 'value' => $this->value, + ]; + } + + /** + * Returns the pure value + * + * @return mixed + */ + public function value() + { + return $this->value; + } +} diff --git a/kirby/src/Cms/Api.php b/kirby/src/Cms/Api.php new file mode 100644 index 0000000..314680f --- /dev/null +++ b/kirby/src/Cms/Api.php @@ -0,0 +1,335 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Api extends BaseApi +{ + /** + * @var App + */ + protected $kirby; + + /** + * Execute an API call for the given path, + * request method and optional request data + * + * @param string|null $path + * @param string $method + * @param array $requestData + * @return mixed + */ + public function call(string $path = null, string $method = 'GET', array $requestData = []) + { + $this->setRequestMethod($method); + $this->setRequestData($requestData); + + $this->kirby->setCurrentLanguage($this->language()); + + $allowImpersonation = $this->kirby()->option('api.allowImpersonation', false); + if ($user = $this->kirby->user(null, $allowImpersonation)) { + $this->kirby->setCurrentTranslation($user->language()); + } + + return parent::call($path, $method, $requestData); + } + + /** + * @param mixed $model + * @param string $name + * @param string|null $path + * @return mixed + * @throws \Kirby\Exception\NotFoundException if the field type cannot be found or the field cannot be loaded + */ + public function fieldApi($model, string $name, string $path = null) + { + $form = Form::for($model); + $fieldNames = Str::split($name, '+'); + $index = 0; + $count = count($fieldNames); + $field = null; + + foreach ($fieldNames as $fieldName) { + $index++; + + if ($field = $form->fields()->get($fieldName)) { + if ($count !== $index) { + $form = $field->form(); + } + } else { + throw new NotFoundException('The field "' . $fieldName . '" could not be found'); + } + } + + // it can get this error only if $name is an empty string as $name = '' + if ($field === null) { + throw new NotFoundException('No field could be loaded'); + } + + $fieldApi = $this->clone([ + 'routes' => $field->api(), + 'data' => array_merge($this->data(), ['field' => $field]) + ]); + + return $fieldApi->call($path, $this->requestMethod(), $this->requestData()); + } + + /** + * Returns the file object for the given + * parent path and filename + * + * @param string|null $path Path to file's parent model + * @param string $filename Filename + * @return \Kirby\Cms\File|null + * @throws \Kirby\Exception\NotFoundException if the file cannot be found + */ + public function file(string $path = null, string $filename) + { + $filename = urldecode($filename); + $file = $this->parent($path)->file($filename); + + if ($file && $file->isReadable() === true) { + return $file; + } + + throw new NotFoundException([ + 'key' => 'file.notFound', + 'data' => [ + 'filename' => $filename + ] + ]); + } + + /** + * Returns the model's object for the given path + * + * @param string $path Path to parent model + * @return \Kirby\Cms\Model|null + * @throws \Kirby\Exception\InvalidArgumentException if the model type is invalid + * @throws \Kirby\Exception\NotFoundException if the model cannot be found + */ + public function parent(string $path) + { + $modelType = in_array($path, ['site', 'account']) ? $path : trim(dirname($path), '/'); + $modelTypes = [ + 'site' => 'site', + 'users' => 'user', + 'pages' => 'page', + 'account' => 'account' + ]; + $modelName = $modelTypes[$modelType] ?? null; + + if (Str::endsWith($modelType, '/files') === true) { + $modelName = 'file'; + } + + $kirby = $this->kirby(); + + switch ($modelName) { + case 'site': + $model = $kirby->site(); + break; + case 'account': + $model = $kirby->user(null, $kirby->option('api.allowImpersonation', false)); + break; + case 'page': + $id = str_replace(['+', ' '], '/', basename($path)); + $model = $kirby->page($id); + break; + case 'file': + $model = $this->file(...explode('/files/', $path)); + break; + case 'user': + $model = $kirby->user(basename($path)); + break; + default: + throw new InvalidArgumentException('Invalid model type: ' . $modelType); + } + + if ($model) { + return $model; + } + + throw new NotFoundException([ + 'key' => $modelName . '.undefined' + ]); + } + + /** + * Returns the Kirby instance + * + * @return \Kirby\Cms\App + */ + public function kirby() + { + return $this->kirby; + } + + /** + * Returns the language request header + * + * @return string|null + */ + public function language(): ?string + { + return get('language') ?? $this->requestHeaders('x-language'); + } + + /** + * Returns the page object for the given id + * + * @param string $id Page's id + * @return \Kirby\Cms\Page|null + * @throws \Kirby\Exception\NotFoundException if the page cannot be found + */ + public function page(string $id) + { + $id = str_replace('+', '/', $id); + $page = $this->kirby->page($id); + + if ($page && $page->isReadable() === true) { + return $page; + } + + throw new NotFoundException([ + 'key' => 'page.notFound', + 'data' => [ + 'slug' => $id + ] + ]); + } + + /** + * Returns the subpages for the given + * parent. The subpages can be filtered + * by status (draft, listed, unlisted, published, all) + * + * @param string|null $parentId + * @param string|null $status + * @return \Kirby\Cms\Pages + */ + public function pages(string $parentId = null, string $status = null) + { + $parent = $parentId === null ? $this->site() : $this->page($parentId); + + switch ($status) { + case 'all': + return $parent->childrenAndDrafts(); + case 'draft': + case 'drafts': + return $parent->drafts(); + case 'listed': + return $parent->children()->listed(); + case 'unlisted': + return $parent->children()->unlisted(); + case 'published': + default: + return $parent->children(); + } + } + + /** + * Search for direct subpages of the + * given parent + * + * @param string|null $parent + * @return \Kirby\Cms\Pages + */ + public function searchPages(string $parent = null) + { + $pages = $this->pages($parent, $this->requestQuery('status')); + + if ($this->requestMethod() === 'GET') { + return $pages->search($this->requestQuery('q')); + } + + return $pages->query($this->requestBody()); + } + + /** + * Returns the current Session instance + * + * @param array $options Additional options, see the session component + * @return \Kirby\Session\Session + */ + public function session(array $options = []) + { + return $this->kirby->session(array_merge([ + 'detect' => true + ], $options)); + } + + /** + * Setter for the parent Kirby instance + * + * @param \Kirby\Cms\App $kirby + * @return self + */ + protected function setKirby(App $kirby) + { + $this->kirby = $kirby; + return $this; + } + + /** + * Returns the site object + * + * @return \Kirby\Cms\Site + */ + public function site() + { + return $this->kirby->site(); + } + + /** + * Returns the user object for the given id or + * returns the current authenticated user if no + * id is passed + * + * @param string|null $id User's id + * @return \Kirby\Cms\User|null + * @throws \Kirby\Exception\NotFoundException if the user for the given id cannot be found + */ + public function user(string $id = null) + { + // get the authenticated user + if ($id === null) { + return $this->kirby->auth()->user(null, $this->kirby()->option('api.allowImpersonation', false)); + } + + // get a specific user by id + if ($user = $this->kirby->users()->find($id)) { + return $user; + } + + throw new NotFoundException([ + 'key' => 'user.notFound', + 'data' => [ + 'name' => $id + ] + ]); + } + + /** + * Returns the users collection + * + * @return \Kirby\Cms\Users + */ + public function users() + { + return $this->kirby->users(); + } +} diff --git a/kirby/src/Cms/App.php b/kirby/src/Cms/App.php new file mode 100644 index 0000000..c3450fa --- /dev/null +++ b/kirby/src/Cms/App.php @@ -0,0 +1,1512 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class App +{ + const CLASS_ALIAS = 'kirby'; + + use AppCaches; + use AppErrors; + use AppPlugins; + use AppTranslations; + use AppUsers; + use Properties; + + protected static $instance; + protected static $version; + + public $data = []; + + protected $api; + protected $collections; + protected $defaultLanguage; + protected $language; + protected $languages; + protected $locks; + protected $multilang; + protected $nonce; + protected $options; + protected $path; + protected $request; + protected $response; + protected $roles; + protected $roots; + protected $routes; + protected $router; + protected $server; + protected $sessionHandler; + protected $site; + protected $system; + protected $urls; + protected $user; + protected $users; + protected $visitor; + + /** + * Creates a new App instance + * + * @param array $props + */ + public function __construct(array $props = []) + { + // register all roots to be able to load stuff afterwards + $this->bakeRoots($props['roots'] ?? []); + + // stuff from config and additional options + $this->optionsFromConfig(); + $this->optionsFromProps($props['options'] ?? []); + + // register the Whoops error handler + $this->handleErrors(); + + // set the path to make it available for the url bakery + $this->setPath($props['path'] ?? null); + + // create all urls after the config, so possible + // options can be taken into account + $this->bakeUrls($props['urls'] ?? []); + + // configurable properties + $this->setOptionalProperties($props, [ + 'languages', + 'request', + 'roles', + 'site', + 'user', + 'users' + ]); + + // set the singleton + Model::$kirby = static::$instance = $this; + + // setup the I18n class with the translation loader + $this->i18n(); + + // load all extensions + $this->extensionsFromSystem(); + $this->extensionsFromProps($props); + $this->extensionsFromPlugins(); + $this->extensionsFromOptions(); + $this->extensionsFromFolders(); + + // trigger hook for use in plugins + $this->trigger('system.loadPlugins:after'); + + // execute a ready callback from the config + $this->optionsFromReadyCallback(); + + // bake config + $this->bakeOptions(); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return [ + 'languages' => $this->languages(), + 'options' => $this->options(), + 'request' => $this->request(), + 'roots' => $this->roots(), + 'site' => $this->site(), + 'urls' => $this->urls(), + 'version' => $this->version(), + ]; + } + + /** + * Returns the Api instance + * + * @internal + * @return \Kirby\Cms\Api + */ + public function api() + { + if ($this->api !== null) { + return $this->api; + } + + $root = $this->root('kirby') . '/config/api'; + $extensions = $this->extensions['api'] ?? []; + $routes = (include $root . '/routes.php')($this); + + $api = [ + 'debug' => $this->option('debug', false), + 'authentication' => $extensions['authentication'] ?? include $root . '/authentication.php', + 'data' => $extensions['data'] ?? [], + 'collections' => array_merge($extensions['collections'] ?? [], include $root . '/collections.php'), + 'models' => array_merge($extensions['models'] ?? [], include $root . '/models.php'), + 'routes' => array_merge($routes, $extensions['routes'] ?? []), + 'kirby' => $this, + ]; + + return $this->api = new Api($api); + } + + /** + * Applies a hook to the given value + * + * @internal + * @param string $name Full event name + * @param array $args Associative array of named event arguments + * @param string $modify Key in $args that is modified by the hooks + * @param \Kirby\Cms\Event|null $originalEvent Event object (internal use) + * @return mixed Resulting value as modified by the hooks + */ + public function apply(string $name, array $args, string $modify, ?Event $originalEvent = null) + { + $event = $originalEvent ?? new Event($name, $args); + + if ($functions = $this->extension('hooks', $name)) { + foreach ($functions as $function) { + // bind the App object to the hook + $newValue = $event->call($this, $function); + + // update value if one was returned + if ($newValue !== null) { + $event->updateArgument($modify, $newValue); + } + } + } + + // apply wildcard hooks if available + $nameWildcards = $event->nameWildcards(); + if ($originalEvent === null && count($nameWildcards) > 0) { + foreach ($nameWildcards as $nameWildcard) { + // the $event object is passed by reference + // and will be modified down the chain + $this->apply($nameWildcard, $event->arguments(), $modify, $event); + } + } + + return $event->argument($modify); + } + + /** + * Normalizes and globally sets the configured options + * + * @return self + */ + protected function bakeOptions() + { + // convert the old plugin option syntax to the new one + foreach ($this->options as $key => $value) { + // detect option keys with the `vendor.plugin.option` format + if (preg_match('/^([a-z0-9-]+\.[a-z0-9-]+)\.(.*)$/i', $key, $matches) === 1) { + list(, $plugin, $option) = $matches; + + // verify that it's really a plugin option + if (isset(static::$plugins[str_replace('.', '/', $plugin)]) !== true) { + continue; + } + + // ensure that the target option array exists + // (which it will if the plugin has any options) + if (isset($this->options[$plugin]) !== true) { + $this->options[$plugin] = []; // @codeCoverageIgnore + } + + // move the option to the plugin option array + // don't overwrite nested arrays completely but merge them + $this->options[$plugin] = array_replace_recursive($this->options[$plugin], [$option => $value]); + unset($this->options[$key]); + } + } + + Config::$data = $this->options; + return $this; + } + + /** + * Sets the directory structure + * + * @param array|null $roots + * @return self + */ + protected function bakeRoots(array $roots = null) + { + $roots = array_merge(require dirname(__DIR__, 2) . '/config/roots.php', (array)$roots); + $this->roots = Ingredients::bake($roots); + return $this; + } + + /** + * Sets the Url structure + * + * @param array|null $urls + * @return self + */ + protected function bakeUrls(array $urls = null) + { + // inject the index URL from the config + if (isset($this->options['url']) === true) { + $urls['index'] = $this->options['url']; + } + + $urls = array_merge(require $this->root('kirby') . '/config/urls.php', (array)$urls); + $this->urls = Ingredients::bake($urls); + return $this; + } + + /** + * Returns all available blueprints for this installation + * + * @param string $type + * @return array + */ + public function blueprints(string $type = 'pages'): array + { + $blueprints = []; + + foreach ($this->extensions('blueprints') as $name => $blueprint) { + if (dirname($name) === $type) { + $name = basename($name); + $blueprints[$name] = $name; + } + } + + foreach (glob($this->root('blueprints') . '/' . $type . '/*.yml') as $blueprint) { + $name = F::name($blueprint); + $blueprints[$name] = $name; + } + + ksort($blueprints); + + return array_values($blueprints); + } + + /** + * Calls any Kirby route + * + * @param string|null $path + * @param string|null $method + * @return mixed + */ + public function call(string $path = null, string $method = null) + { + $router = $this->router(); + + $router::$beforeEach = function ($route, $path, $method) { + $this->trigger('route:before', compact('route', 'path', 'method')); + }; + + $router::$afterEach = function ($route, $path, $method, $result, $final) { + return $this->apply('route:after', compact('route', 'path', 'method', 'result', 'final'), 'result'); + }; + + return $router->call($path ?? $this->path(), $method ?? $this->request()->method()); + } + + /** + * Returns a specific user-defined collection + * by name. All relevant dependencies are + * automatically injected + * + * @param string $name + * @return \Kirby\Cms\Collection|null + */ + public function collection(string $name) + { + return $this->collections()->get($name, [ + 'kirby' => $this, + 'site' => $this->site(), + 'pages' => $this->site()->children(), + 'users' => $this->users() + ]); + } + + /** + * Returns all user-defined collections + * + * @return \Kirby\Cms\Collections + */ + public function collections() + { + return $this->collections = $this->collections ?? new Collections(); + } + + /** + * Returns a core component + * + * @internal + * @param string $name + * @return mixed + */ + public function component($name) + { + return $this->extensions['components'][$name] ?? null; + } + + /** + * Returns the content extension + * + * @internal + * @return string + */ + public function contentExtension(): string + { + return $this->options['content']['extension'] ?? 'txt'; + } + + /** + * Returns files that should be ignored when scanning folders + * + * @internal + * @return array + */ + public function contentIgnore(): array + { + return $this->options['content']['ignore'] ?? Dir::$ignore; + } + + /** + * Generates a non-guessable token based on model + * data and a configured salt + * + * @param mixed $model Object to pass to the salt callback if configured + * @param string $value Model data to include in the generated token + * @return string + */ + public function contentToken($model, string $value): string + { + if (method_exists($model, 'root') === true) { + $default = $model->root(); + } else { + $default = $this->root('content'); + } + + $salt = $this->option('content.salt', $default); + + if (is_a($salt, 'Closure') === true) { + $salt = $salt($model); + } + + return hash_hmac('sha1', $value, $salt); + } + + /** + * Calls a page controller by name + * and with the given arguments + * + * @internal + * @param string $name + * @param array $arguments + * @param string $contentType + * @return array + */ + public function controller(string $name, array $arguments = [], string $contentType = 'html'): array + { + $name = basename(strtolower($name)); + + if ($controller = $this->controllerLookup($name, $contentType)) { + return (array)$controller->call($this, $arguments); + } + + if ($contentType !== 'html') { + + // no luck for a specific representation controller? + // let's try the html controller instead + if ($controller = $this->controllerLookup($name)) { + return (array)$controller->call($this, $arguments); + } + } + + // still no luck? Let's take the site controller + if ($controller = $this->controllerLookup('site')) { + return (array)$controller->call($this, $arguments); + } + + return []; + } + + /** + * Try to find a controller by name + * + * @param string $name + * @param string $contentType + * @return \Kirby\Toolkit\Controller|null + */ + protected function controllerLookup(string $name, string $contentType = 'html') + { + if ($contentType !== null && $contentType !== 'html') { + $name .= '.' . $contentType; + } + + // controller on disk + if ($controller = Controller::load($this->root('controllers') . '/' . $name . '.php')) { + return $controller; + } + + // registry controller + if ($controller = $this->extension('controllers', $name)) { + return is_a($controller, 'Kirby\Toolkit\Controller') ? $controller : new Controller($controller); + } + + return null; + } + + /** + * Returns the default language object + * + * @return \Kirby\Cms\Language|null + */ + public function defaultLanguage() + { + return $this->defaultLanguage = $this->defaultLanguage ?? $this->languages()->default(); + } + + /** + * Destroy the instance singleton and + * purge other static props + * + * @internal + */ + public static function destroy(): void + { + static::$plugins = []; + static::$instance = null; + } + + /** + * Detect the preferred language from the visitor object + * + * @return \Kirby\Cms\Language + */ + public function detectedLanguage() + { + $languages = $this->languages(); + $visitor = $this->visitor(); + + foreach ($visitor->acceptedLanguages() as $lang) { + if ($language = $languages->findBy('locale', $lang->locale(LC_ALL))) { + return $language; + } + } + + foreach ($visitor->acceptedLanguages() as $lang) { + if ($language = $languages->findBy('code', $lang->code())) { + return $language; + } + } + + return $this->defaultLanguage(); + } + + /** + * Returns the Email singleton + * + * @param mixed $preset + * @param array $props + * @return \Kirby\Email\PHPMailer + */ + public function email($preset = [], array $props = []) + { + return new Emailer((new Email($preset, $props))->toArray(), $props['debug'] ?? false); + } + + /** + * Finds any file in the content directory + * + * @param string $path + * @param mixed $parent + * @param bool $drafts + * @return \Kirby\Cms\File|null + */ + public function file(string $path, $parent = null, bool $drafts = true) + { + $parent = $parent ?? $this->site(); + $id = dirname($path); + $filename = basename($path); + + if (is_a($parent, 'Kirby\Cms\User') === true) { + return $parent->file($filename); + } + + if (is_a($parent, 'Kirby\Cms\File') === true) { + $parent = $parent->parent(); + } + + if ($id === '.') { + if ($file = $parent->file($filename)) { + return $file; + } elseif ($file = $this->site()->file($filename)) { + return $file; + } else { + return null; + } + } + + if ($page = $this->page($id, $parent, $drafts)) { + return $page->file($filename); + } + + if ($page = $this->page($id, null, $drafts)) { + return $page->file($filename); + } + + return null; + } + + /** + * Returns the current App instance + * + * @param \Kirby\Cms\App|null $instance + * @param bool $lazy If `true`, the instance is only returned if already existing + * @return self|null + */ + public static function instance(self $instance = null, bool $lazy = false) + { + if ($instance === null) { + if ($lazy === true) { + return static::$instance; + } else { + return static::$instance ?? new static(); + } + } + + return static::$instance = $instance; + } + + /** + * Takes almost any kind of input and + * tries to convert it into a valid response + * + * @internal + * @param mixed $input + * @return \Kirby\Http\Response + */ + public function io($input) + { + // use the current response configuration + $response = $this->response(); + + // any direct exception will be turned into an error page + if (is_a($input, 'Throwable') === true) { + if (is_a($input, 'Kirby\Exception\Exception') === true) { + $code = $input->getHttpCode(); + $message = $input->getMessage(); + } else { + $code = $input->getCode(); + $message = $input->getMessage(); + } + + if ($code < 400 || $code > 599) { + $code = 500; + } + + if ($errorPage = $this->site()->errorPage()) { + return $response->code($code)->send($errorPage->render([ + 'errorCode' => $code, + 'errorMessage' => $message, + 'errorType' => get_class($input) + ])); + } + + return $response + ->code($code) + ->type('text/html') + ->send($message); + } + + // Empty input + if (empty($input) === true) { + return $this->io(new NotFoundException()); + } + + // Response Configuration + if (is_a($input, 'Kirby\Cms\Responder') === true) { + return $input->send(); + } + + // Responses + if (is_a($input, 'Kirby\Http\Response') === true) { + return $input; + } + + // Pages + if (is_a($input, 'Kirby\Cms\Page')) { + try { + $html = $input->render(); + } catch (ErrorPageException $e) { + return $this->io($e); + } + + if ($input->isErrorPage() === true) { + if ($response->code() === null) { + $response->code(404); + } + } + + return $response->send($html); + } + + // Files + if (is_a($input, 'Kirby\Cms\File')) { + return $response->redirect($input->mediaUrl(), 307)->send(); + } + + // Simple HTML response + if (is_string($input) === true) { + return $response->send($input); + } + + // array to json conversion + if (is_array($input) === true) { + return $response->json($input)->send(); + } + + throw new InvalidArgumentException('Unexpected input'); + } + + /** + * Renders a single KirbyTag with the given attributes + * + * @internal + * @param string $type + * @param string|null $value + * @param array $attr + * @param array $data + * @return string + */ + public function kirbytag(string $type, string $value = null, array $attr = [], array $data = []): string + { + $data['kirby'] = $data['kirby'] ?? $this; + $data['site'] = $data['site'] ?? $data['kirby']->site(); + $data['parent'] = $data['parent'] ?? $data['site']->page(); + + return (new KirbyTag($type, $value, $attr, $data, $this->options))->render(); + } + + /** + * KirbyTags Parser + * + * @internal + * @param string|null $text + * @param array $data + * @return string + */ + public function kirbytags(string $text = null, array $data = []): string + { + $data['kirby'] = $data['kirby'] ?? $this; + $data['site'] = $data['site'] ?? $data['kirby']->site(); + $data['parent'] = $data['parent'] ?? $data['site']->page(); + + return KirbyTags::parse($text, $data, $this->options, $this); + } + + /** + * Parses KirbyTags first and Markdown afterwards + * + * @internal + * @param string|null $text + * @param array $data + * @param bool $inline + * @return string + */ + public function kirbytext(string $text = null, array $data = [], bool $inline = false): string + { + $text = $this->apply('kirbytext:before', compact('text'), 'text'); + $text = $this->kirbytags($text, $data); + $text = $this->markdown($text, $inline); + + if ($this->option('smartypants', false) !== false) { + $text = $this->smartypants($text); + } + + $text = $this->apply('kirbytext:after', compact('text'), 'text'); + + return $text; + } + + /** + * Returns the current language + * + * @param string|null $code + * @return \Kirby\Cms\Language|null + */ + public function language(string $code = null) + { + if ($this->multilang() === false) { + return null; + } + + if ($code === 'default') { + return $this->languages()->default(); + } + + if ($code !== null) { + return $this->languages()->find($code); + } + + return $this->language = $this->language ?? $this->languages()->default(); + } + + /** + * Returns the current language code + * + * @internal + * @param string|null $languageCode + * @return string|null + */ + public function languageCode(string $languageCode = null): ?string + { + if ($language = $this->language($languageCode)) { + return $language->code(); + } + + return null; + } + + /** + * Returns all available site languages + * + * @return \Kirby\Cms\Languages + */ + public function languages() + { + if ($this->languages !== null) { + return clone $this->languages; + } + + return $this->languages = Languages::load(); + } + + /** + * Returns the app's locks object + * + * @return \Kirby\Cms\ContentLocks + */ + public function locks(): ContentLocks + { + if ($this->locks !== null) { + return $this->locks; + } + + return $this->locks = new ContentLocks(); + } + + /** + * Parses Markdown + * + * @internal + * @param string|null $text + * @param bool $inline + * @return string + */ + public function markdown(string $text = null, bool $inline = false): string + { + return $this->component('markdown')($this, $text, $this->options['markdown'] ?? [], $inline); + } + + /** + * Check for a multilang setup + * + * @return bool + */ + public function multilang(): bool + { + if ($this->multilang !== null) { + return $this->multilang; + } + + return $this->multilang = $this->languages()->count() !== 0; + } + + /** + * Returns the nonce, which is used + * in the panel for inline scripts + * @since 3.3.0 + * + * @return string + */ + public function nonce(): string + { + return $this->nonce = $this->nonce ?? base64_encode(random_bytes(20)); + } + + /** + * Load a specific configuration option + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function option(string $key, $default = null) + { + return A::get($this->options, $key, $default); + } + + /** + * Returns all configuration options + * + * @return array + */ + public function options(): array + { + return $this->options; + } + + /** + * Load all options from files in site/config + * + * @return array + */ + protected function optionsFromConfig(): array + { + $server = $this->server(); + $root = $this->root('config'); + + Config::$data = []; + + $main = F::load($root . '/config.php', []); + $host = F::load($root . '/config.' . basename($server->host()) . '.php', []); + $addr = F::load($root . '/config.' . basename($server->address()) . '.php', []); + + $config = Config::$data; + + return $this->options = array_replace_recursive($config, $main, $host, $addr); + } + + /** + * Inject options from Kirby instance props + * + * @param array $options + * @return array + */ + protected function optionsFromProps(array $options = []): array + { + return $this->options = array_replace_recursive($this->options, $options); + } + + /** + * Merge last-minute options from ready callback + * + * @return array + */ + protected function optionsFromReadyCallback(): array + { + if (isset($this->options['ready']) === true && is_callable($this->options['ready']) === true) { + // fetch last-minute options from the callback + $options = (array)$this->options['ready']($this); + + // inject all last-minute options recursively + $this->options = array_replace_recursive($this->options, $options); + + // update the system with changed options + if ( + isset($options['debug']) === true || + isset($options['whoops']) === true || + isset($options['editor']) === true + ) { + $this->handleErrors(); + } + + if (isset($options['debug']) === true) { + $this->api = null; + } + + if (isset($options['home']) === true || isset($options['error']) === true) { + $this->site = null; + } + + // checks custom language definition for slugs + if ($slugsOption = $this->option('slugs')) { + // slugs option must be set to string or "slugs" => ["language" => "de"] as array + if (is_string($slugsOption) === true || isset($slugsOption['language']) === true) { + $this->i18n(); + } + } + } + + return $this->options; + } + + /** + * Returns any page from the content folder + * + * @param string|null $id + * @param \Kirby\Cms\Page|\Kirby\Cms\Site|null $parent + * @param bool $drafts + * @return \Kirby\Cms\Page|null + */ + public function page(?string $id = null, $parent = null, bool $drafts = true) + { + if ($id === null) { + return null; + } + + $parent = $parent ?? $this->site(); + + if ($page = $parent->find($id)) { + return $page; + } + + if ($drafts === true && $draft = $parent->draft($id)) { + return $draft; + } + + return null; + } + + /** + * Returns the request path + * + * @return string + */ + public function path(): string + { + if (is_string($this->path) === true) { + return $this->path; + } + + $requestUri = '/' . $this->request()->url()->path(); + $scriptName = $_SERVER['SCRIPT_NAME']; + $scriptFile = basename($scriptName); + $scriptDir = dirname($scriptName); + $scriptPath = $scriptFile === 'index.php' ? $scriptDir : $scriptName; + $requestPath = preg_replace('!^' . preg_quote($scriptPath) . '!', '', $requestUri); + + return $this->setPath($requestPath)->path; + } + + /** + * Returns the Response object for the + * current request + * + * @param string|null $path + * @param string|null $method + * @return \Kirby\Http\Response + */ + public function render(string $path = null, string $method = null) + { + return $this->io($this->call($path, $method)); + } + + /** + * Returns the Request singleton + * + * @return \Kirby\Http\Request + */ + public function request() + { + return $this->request = $this->request ?? new Request(); + } + + /** + * Path resolver for the router + * + * @internal + * @param string|null $path + * @param string|null $language + * @return mixed + * @throws \Kirby\Exception\NotFoundException if the home page cannot be found + */ + public function resolve(string $path = null, string $language = null) + { + // set the current translation + $this->setCurrentTranslation($language); + + // set the current locale + $this->setCurrentLanguage($language); + + // the site is needed a couple times here + $site = $this->site(); + + // use the home page + if ($path === null) { + if ($homePage = $site->homePage()) { + return $homePage; + } + + throw new NotFoundException('The home page does not exist'); + } + + // search for the page by path + $page = $site->find($path); + + // search for a draft if the page cannot be found + if (!$page && $draft = $site->draft($path)) { + if ($this->user() || $draft->isVerified(get('token'))) { + $page = $draft; + } + } + + // try to resolve content representations if the path has an extension + $extension = F::extension($path); + + // no content representation? then return the page + if (empty($extension) === true) { + return $page; + } + + // only try to return a representation + // when the page has been found + if ($page) { + try { + $response = $this->response(); + + // attach a MIME type based on the representation + // only if no custom MIME type was set + if ($response->type() === null) { + $response->type($extension); + } + + return $response->body($page->render([], $extension)); + } catch (NotFoundException $e) { + return null; + } + } + + $id = dirname($path); + $filename = basename($path); + + // try to resolve image urls for pages and drafts + if ($page = $site->findPageOrDraft($id)) { + return $page->file($filename); + } + + // try to resolve site files at least + return $site->file($filename); + } + + /** + * Response configuration + * + * @return \Kirby\Cms\Responder + */ + public function response() + { + return $this->response = $this->response ?? new Responder(); + } + + /** + * Returns all user roles + * + * @return \Kirby\Cms\Roles + */ + public function roles() + { + return $this->roles = $this->roles ?? Roles::load($this->root('roles')); + } + + /** + * Returns a system root + * + * @param string $type + * @return string + */ + public function root(string $type = 'index'): string + { + return $this->roots->__get($type); + } + + /** + * Returns the directory structure + * + * @return \Kirby\Cms\Ingredients + */ + public function roots() + { + return $this->roots; + } + + /** + * Returns the currently active route + * + * @return \Kirby\Http\Route|null + */ + public function route() + { + return $this->router()->route(); + } + + /** + * Returns the Router singleton + * + * @internal + * @return \Kirby\Http\Router + */ + public function router() + { + $routes = $this->routes(); + + if ($this->multilang() === true) { + foreach ($routes as $index => $route) { + if (empty($route['language']) === false) { + unset($routes[$index]); + } + } + } + + return $this->router = $this->router ?? new Router($routes); + } + + /** + * Returns all defined routes + * + * @internal + * @return array + */ + public function routes(): array + { + if (is_array($this->routes) === true) { + return $this->routes; + } + + $registry = $this->extensions('routes'); + $system = (include $this->root('kirby') . '/config/routes.php')($this); + $routes = array_merge($system['before'], $registry, $system['after']); + + return $this->routes = $routes; + } + + /** + * Returns the current session object + * + * @param array $options Additional options, see the session component + * @return \Kirby\Session\Session + */ + public function session(array $options = []) + { + return $this->sessionHandler()->get($options); + } + + /** + * Returns the session handler + * + * @return \Kirby\Session\AutoSession + */ + public function sessionHandler() + { + $this->sessionHandler = $this->sessionHandler ?? new AutoSession($this->root('sessions'), $this->option('session', [])); + return $this->sessionHandler; + } + + /** + * Create your own set of languages + * + * @param array|null $languages + * @return self + */ + protected function setLanguages(array $languages = null) + { + if ($languages !== null) { + $objects = []; + + foreach ($languages as $props) { + $objects[] = new Language($props); + } + + $this->languages = new Languages($objects); + } + + return $this; + } + + /** + * Sets the request path that is + * used for the router + * + * @param string|null $path + * @return self + */ + protected function setPath(string $path = null) + { + $this->path = $path !== null ? trim($path, '/') : null; + return $this; + } + + /** + * Sets the request + * + * @param array|null $request + * @return self + */ + protected function setRequest(array $request = null) + { + if ($request !== null) { + $this->request = new Request($request); + } + + return $this; + } + + /** + * Create your own set of roles + * + * @param array|null $roles + * @return self + */ + protected function setRoles(array $roles = null) + { + if ($roles !== null) { + $this->roles = Roles::factory($roles, [ + 'kirby' => $this + ]); + } + + return $this; + } + + /** + * Sets a custom Site object + * + * @param \Kirby\Cms\Site|array|null $site + * @return self + */ + protected function setSite($site = null) + { + if (is_array($site) === true) { + $site = new Site($site + [ + 'kirby' => $this + ]); + } + + $this->site = $site; + return $this; + } + + /** + * Returns the Server object + * + * @return \Kirby\Http\Server + */ + public function server() + { + return $this->server = $this->server ?? new Server(); + } + + /** + * Initializes and returns the Site object + * + * @return \Kirby\Cms\Site + */ + public function site() + { + return $this->site = $this->site ?? new Site([ + 'errorPageId' => $this->options['error'] ?? 'error', + 'homePageId' => $this->options['home'] ?? 'home', + 'kirby' => $this, + 'url' => $this->url('index'), + ]); + } + + /** + * Applies the smartypants rule on the text + * + * @internal + * @param string|null $text + * @return string + */ + public function smartypants(string $text = null): string + { + $options = $this->option('smartypants', []); + + if ($options === false) { + return $text; + } elseif (is_array($options) === false) { + $options = []; + } + + if ($this->multilang() === true) { + $languageSmartypants = $this->language()->smartypants() ?? []; + + if (empty($languageSmartypants) === false) { + $options = array_merge($options, $languageSmartypants); + } + } + + return $this->component('smartypants')($this, $text, $options); + } + + /** + * Uses the snippet component to create + * and return a template snippet + * + * @internal + * @param mixed $name + * @param array $data + * @return string|null + */ + public function snippet($name, array $data = []): ?string + { + return $this->component('snippet')($this, $name, array_merge($this->data, $data)); + } + + /** + * System check class + * + * @return \Kirby\Cms\System + */ + public function system() + { + return $this->system = $this->system ?? new System($this); + } + + /** + * Uses the template component to initialize + * and return the Template object + * + * @internal + * @return \Kirby\Cms\Template + * @param string $name + * @param string $type + * @param string $defaultType + */ + public function template(string $name, string $type = 'html', string $defaultType = 'html') + { + return $this->component('template')($this, $name, $type, $defaultType); + } + + /** + * Thumbnail creator + * + * @param string $src + * @param string $dst + * @param array $options + * @return string + */ + public function thumb(string $src, string $dst, array $options = []): string + { + return $this->component('thumb')($this, $src, $dst, $options); + } + + /** + * Trigger a hook by name + * + * @internal + * @param string $name Full event name + * @param array $args Associative array of named event arguments + * @param \Kirby\Cms\Event|null $originalEvent Event object (internal use) + * @return void + */ + public function trigger(string $name, array $args = [], ?Event $originalEvent = null) + { + $event = $originalEvent ?? new Event($name, $args); + + if ($functions = $this->extension('hooks', $name)) { + static $level = 0; + static $triggered = []; + $level++; + + foreach ($functions as $index => $function) { + if (in_array($function, $triggered[$name] ?? []) === true) { + continue; + } + + // mark the hook as triggered, to avoid endless loops + $triggered[$name][] = $function; + + // bind the App object to the hook + $event->call($this, $function); + } + + $level--; + + if ($level === 0) { + $triggered = []; + } + } + + // trigger wildcard hooks if available + $nameWildcards = $event->nameWildcards(); + if ($originalEvent === null && count($nameWildcards) > 0) { + foreach ($nameWildcards as $nameWildcard) { + $this->trigger($nameWildcard, $args, $event); + } + } + } + + /** + * Returns a system url + * + * @param string $type + * @return string + */ + public function url(string $type = 'index'): string + { + return $this->urls->__get($type); + } + + /** + * Returns the url structure + * + * @return \Kirby\Cms\Ingredients + */ + public function urls() + { + return $this->urls; + } + + /** + * Returns the current version number from + * the composer.json (Keep that up to date! :)) + * + * @return string|null + * @throws \Kirby\Exception\LogicException if the Kirby version cannot be detected + */ + public static function version(): ?string + { + try { + return static::$version = static::$version ?? Data::read(dirname(__DIR__, 2) . '/composer.json')['version'] ?? null; + } catch (Throwable $e) { + throw new LogicException('The Kirby version cannot be detected. The composer.json is probably missing or not readable.'); + } + } + + /** + * Creates a hash of the version number + * + * @return string + */ + public static function versionHash(): string + { + return md5(static::version()); + } + + /** + * Returns the visitor object + * + * @return \Kirby\Http\Visitor + */ + public function visitor() + { + return $this->visitor = $this->visitor ?? new Visitor(); + } +} diff --git a/kirby/src/Cms/AppCaches.php b/kirby/src/Cms/AppCaches.php new file mode 100644 index 0000000..862bc38 --- /dev/null +++ b/kirby/src/Cms/AppCaches.php @@ -0,0 +1,138 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait AppCaches +{ + protected $caches = []; + + /** + * Returns a cache instance by key + * + * @param string $key + * @return \Kirby\Cache\Cache + */ + public function cache(string $key) + { + if (isset($this->caches[$key]) === true) { + return $this->caches[$key]; + } + + // get the options for this cache type + $options = $this->cacheOptions($key); + + if ($options['active'] === false) { + // use a dummy cache that does nothing + return $this->caches[$key] = new NullCache(); + } + + $type = strtolower($options['type']); + $types = $this->extensions['cacheTypes'] ?? []; + + if (array_key_exists($type, $types) === false) { + throw new InvalidArgumentException([ + 'key' => 'app.invalid.cacheType', + 'data' => ['type' => $type] + ]); + } + + $className = $types[$type]; + + // initialize the cache class + $cache = new $className($options); + + // check if it is a useable cache object + if (is_a($cache, 'Kirby\Cache\Cache') !== true) { + throw new InvalidArgumentException([ + 'key' => 'app.invalid.cacheType', + 'data' => ['type' => $type] + ]); + } + + return $this->caches[$key] = $cache; + } + + /** + * Returns the cache options by key + * + * @param string $key + * @return array + */ + protected function cacheOptions(string $key): array + { + $options = $this->option($cacheKey = $this->cacheOptionsKey($key), false); + + if ($options === false) { + return [ + 'active' => false + ]; + } + + $prefix = str_replace(['/', ':'], '_', $this->system()->indexUrl()) . + '/' . + str_replace('.', '/', $key); + + $defaults = [ + 'active' => true, + 'type' => 'file', + 'extension' => 'cache', + 'root' => $this->root('cache'), + 'prefix' => $prefix + ]; + + if ($options === true) { + return $defaults; + } else { + return array_merge($defaults, $options); + } + } + + /** + * Takes care of converting prefixed plugin cache setups + * to the right cache key, while leaving regular cache + * setups untouched. + * + * @param string $key + * @return string + */ + protected function cacheOptionsKey(string $key): string + { + $prefixedKey = 'cache.' . $key; + + if (isset($this->options[$prefixedKey])) { + return $prefixedKey; + } + + // plain keys without dots don't need further investigation + // since they can never be from a plugin. + if (strpos($key, '.') === false) { + return $prefixedKey; + } + + // try to extract the plugin name + $parts = explode('.', $key); + $pluginName = implode('/', array_slice($parts, 0, 2)); + $pluginPrefix = implode('.', array_slice($parts, 0, 2)); + $cacheName = implode('.', array_slice($parts, 2)); + + // check if such a plugin exists + if ($this->plugin($pluginName)) { + return empty($cacheName) === true ? $pluginPrefix . '.cache' : $pluginPrefix . '.cache.' . $cacheName; + } + + return $prefixedKey; + } +} diff --git a/kirby/src/Cms/AppErrors.php b/kirby/src/Cms/AppErrors.php new file mode 100644 index 0000000..36b823b --- /dev/null +++ b/kirby/src/Cms/AppErrors.php @@ -0,0 +1,188 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait AppErrors +{ + /** + * Whoops instance cache + * + * @var \Whoops\Run + */ + protected $whoops; + + /** + * Registers the PHP error handler for CLI usage + * + * @return void + */ + protected function handleCliErrors(): void + { + $this->setWhoopsHandler(new PlainTextHandler()); + } + + /** + * Registers the PHP error handler + * based on the environment + * + * @return void + */ + protected function handleErrors(): void + { + if ($this->request()->cli() === true) { + $this->handleCliErrors(); + return; + } + + if ($this->visitor()->prefersJson() === true) { + $this->handleJsonErrors(); + return; + } + + $this->handleHtmlErrors(); + } + + /** + * Registers the PHP error handler for HTML output + * + * @return void + */ + protected function handleHtmlErrors(): void + { + $handler = null; + + if ($this->option('debug') === true) { + if ($this->option('whoops', true) === true) { + $handler = new PrettyPageHandler(); + $handler->setPageTitle('Kirby CMS Debugger'); + $handler->setResourcesPath(dirname(__DIR__, 2) . '/assets'); + $handler->addCustomCss('whoops.css'); + + if ($editor = $this->option('editor')) { + $handler->setEditor($editor); + } + } + } else { + $handler = new CallbackHandler(function ($exception, $inspector, $run) { + $fatal = $this->option('fatal'); + + if (is_a($fatal, 'Closure') === true) { + echo $fatal($this); + } else { + include $this->root('kirby') . '/views/fatal.php'; + } + + return Handler::QUIT; + }); + } + + if ($handler !== null) { + $this->setWhoopsHandler($handler); + } else { + $this->unsetWhoopsHandler(); + } + } + + /** + * Registers the PHP error handler for JSON output + * + * @return void + */ + protected function handleJsonErrors(): void + { + $handler = new CallbackHandler(function ($exception, $inspector, $run) { + if (is_a($exception, 'Kirby\Exception\Exception') === true) { + $httpCode = $exception->getHttpCode(); + $code = $exception->getCode(); + $details = $exception->getDetails(); + } elseif (is_a($exception, '\Throwable') === true) { + $httpCode = 500; + $code = $exception->getCode(); + $details = null; + } else { + $httpCode = 500; + $code = 500; + $details = null; + } + + if ($this->option('debug') === true) { + echo Response::json([ + 'status' => 'error', + 'exception' => get_class($exception), + 'code' => $code, + 'message' => $exception->getMessage(), + 'details' => $details, + 'file' => ltrim($exception->getFile(), $_SERVER['DOCUMENT_ROOT'] ?? null), + 'line' => $exception->getLine(), + ], $httpCode); + } else { + echo Response::json([ + 'status' => 'error', + 'code' => $code, + 'details' => $details, + 'message' => 'An unexpected error occurred! Enable debug mode for more info: https://getkirby.com/docs/reference/system/options/debug', + ], $httpCode); + } + + return Handler::QUIT; + }); + + $this->setWhoopsHandler($handler); + } + + /** + * Enables Whoops with the specified handler + * + * @param Callable|\Whoops\Handler\HandlerInterface $handler + * @return void + */ + protected function setWhoopsHandler($handler): void + { + $whoops = $this->whoops(); + $whoops->clearHandlers(); + $whoops->pushHandler($handler); + $whoops->register(); // will only do something if not already registered + } + + /** + * Clears the Whoops handlers and disables Whoops + * + * @return void + */ + protected function unsetWhoopsHandler(): void + { + $whoops = $this->whoops(); + $whoops->clearHandlers(); + $whoops->unregister(); // will only do something if currently registered + } + + /** + * Returns the Whoops error handler instance + * + * @return \Whoops\Run + */ + protected function whoops() + { + if ($this->whoops !== null) { + return $this->whoops; + } + + return $this->whoops = new Whoops(); + } +} diff --git a/kirby/src/Cms/AppPlugins.php b/kirby/src/Cms/AppPlugins.php new file mode 100644 index 0000000..37c3628 --- /dev/null +++ b/kirby/src/Cms/AppPlugins.php @@ -0,0 +1,794 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait AppPlugins +{ + /** + * A list of all registered plugins + * + * @var array + */ + protected static $plugins = []; + + /** + * Cache for system extensions + * + * @var array + */ + protected static $systemExtensions = null; + + /** + * The extension registry + * + * @var array + */ + protected $extensions = [ + // load options first to make them available for the rest + 'options' => [], + + // other plugin types + 'api' => [], + 'blueprints' => [], + 'cacheTypes' => [], + 'collections' => [], + 'components' => [], + 'controllers' => [], + 'collectionFilters' => [], + 'collectionMethods' => [], + 'fieldMethods' => [], + 'fileMethods' => [], + 'filesMethods' => [], + 'fields' => [], + 'hooks' => [], + 'pages' => [], + 'pageMethods' => [], + 'pagesMethods' => [], + 'pageModels' => [], + 'permissions' => [], + 'routes' => [], + 'sections' => [], + 'siteMethods' => [], + 'snippets' => [], + 'tags' => [], + 'templates' => [], + 'thirdParty' => [], + 'translations' => [], + 'userMethods' => [], + 'userModels' => [], + 'usersMethods' => [], + 'validators' => [] + ]; + + /** + * Flag when plugins have been loaded + * to not load them again + * + * @var bool + */ + protected $pluginsAreLoaded = false; + + /** + * Register all given extensions + * + * @internal + * @param array $extensions + * @param \Kirby\Cms\Plugin $plugin|null The plugin which defined those extensions + * @return array + */ + public function extend(array $extensions, Plugin $plugin = null): array + { + foreach ($this->extensions as $type => $registered) { + if (isset($extensions[$type]) === true) { + $this->{'extend' . $type}($extensions[$type], $plugin); + } + } + + return $this->extensions; + } + + /** + * Registers API extensions + * + * @param array|bool $api + * @return array + */ + protected function extendApi($api): array + { + if (is_array($api) === true) { + if (is_a($api['routes'] ?? [], 'Closure') === true) { + $api['routes'] = $api['routes']($this); + } + + return $this->extensions['api'] = A::merge($this->extensions['api'], $api, A::MERGE_APPEND); + } else { + return $this->extensions['api']; + } + } + + /** + * Registers additional blueprints + * + * @param array $blueprints + * @return array + */ + protected function extendBlueprints(array $blueprints): array + { + return $this->extensions['blueprints'] = array_merge($this->extensions['blueprints'], $blueprints); + } + + /** + * Registers additional cache types + * + * @param array $cacheTypes + * @return array + */ + protected function extendCacheTypes(array $cacheTypes): array + { + return $this->extensions['cacheTypes'] = array_merge($this->extensions['cacheTypes'], $cacheTypes); + } + + /** + * Registers additional collection filters + * + * @param array $filters + * @return array + */ + protected function extendCollectionFilters(array $filters): array + { + return $this->extensions['collectionFilters'] = ToolkitCollection::$filters = array_merge(ToolkitCollection::$filters, $filters); + } + + /** + * Registers additional collection methods + * + * @param array $methods + * @return array + */ + protected function extendCollectionMethods(array $methods): array + { + return $this->extensions['collectionMethods'] = Collection::$methods = array_merge(Collection::$methods, $methods); + } + + /** + * Registers additional collections + * + * @param array $collections + * @return array + */ + protected function extendCollections(array $collections): array + { + return $this->extensions['collections'] = array_merge($this->extensions['collections'], $collections); + } + + /** + * Registers core components + * + * @param array $components + * @return array + */ + protected function extendComponents(array $components): array + { + return $this->extensions['components'] = array_merge($this->extensions['components'], $components); + } + + /** + * Registers additional controllers + * + * @param array $controllers + * @return array + */ + protected function extendControllers(array $controllers): array + { + return $this->extensions['controllers'] = array_merge($this->extensions['controllers'], $controllers); + } + + /** + * Registers additional file methods + * + * @param array $methods + * @return array + */ + protected function extendFileMethods(array $methods): array + { + return $this->extensions['fileMethods'] = File::$methods = array_merge(File::$methods, $methods); + } + + /** + * Registers additional files methods + * + * @param array $methods + * @return array + */ + protected function extendFilesMethods(array $methods): array + { + return $this->extensions['filesMethods'] = Files::$methods = array_merge(Files::$methods, $methods); + } + + /** + * Registers additional field methods + * + * @param array $methods + * @return array + */ + protected function extendFieldMethods(array $methods): array + { + return $this->extensions['fieldMethods'] = Field::$methods = array_merge(Field::$methods, array_change_key_case($methods)); + } + + /** + * Registers Panel fields + * + * @param array $fields + * @return array + */ + protected function extendFields(array $fields): array + { + return $this->extensions['fields'] = FormField::$types = array_merge(FormField::$types, $fields); + } + + /** + * Registers hooks + * + * @param array $hooks + * @return array + */ + protected function extendHooks(array $hooks): array + { + foreach ($hooks as $name => $callbacks) { + if (isset($this->extensions['hooks'][$name]) === false) { + $this->extensions['hooks'][$name] = []; + } + + if (is_array($callbacks) === false) { + $callbacks = [$callbacks]; + } + + foreach ($callbacks as $callback) { + $this->extensions['hooks'][$name][] = $callback; + } + } + + return $this->extensions['hooks']; + } + + /** + * Registers markdown component + * + * @param Closure $markdown + * @return Closure + */ + protected function extendMarkdown(Closure $markdown) + { + return $this->extensions['markdown'] = $markdown; + } + + /** + * Registers additional options + * + * @param array $options + * @param \Kirby\Cms\Plugin|null $plugin + * @return array + */ + protected function extendOptions(array $options, Plugin $plugin = null): array + { + if ($plugin !== null) { + $options = [$plugin->prefix() => $options]; + } + + return $this->extensions['options'] = $this->options = A::merge($options, $this->options, A::MERGE_REPLACE); + } + + /** + * Registers additional page methods + * + * @param array $methods + * @return array + */ + protected function extendPageMethods(array $methods): array + { + return $this->extensions['pageMethods'] = Page::$methods = array_merge(Page::$methods, $methods); + } + + /** + * Registers additional pages methods + * + * @param array $methods + * @return array + */ + protected function extendPagesMethods(array $methods): array + { + return $this->extensions['pagesMethods'] = Pages::$methods = array_merge(Pages::$methods, $methods); + } + + /** + * Registers additional page models + * + * @param array $models + * @return array + */ + protected function extendPageModels(array $models): array + { + return $this->extensions['pageModels'] = Page::$models = array_merge(Page::$models, $models); + } + + /** + * Registers pages + * + * @param array $pages + * @return array + */ + protected function extendPages(array $pages): array + { + return $this->extensions['pages'] = array_merge($this->extensions['pages'], $pages); + } + + /** + * Registers additional permissions + * + * @param array $permissions + * @param \Kirby\Cms\Plugin|null $plugin + * @return array + */ + protected function extendPermissions(array $permissions, Plugin $plugin = null): array + { + if ($plugin !== null) { + $permissions = [$plugin->prefix() => $permissions]; + } + + return $this->extensions['permissions'] = Permissions::$extendedActions = array_merge(Permissions::$extendedActions, $permissions); + } + + /** + * Registers additional routes + * + * @param array|\Closure $routes + * @return array + */ + protected function extendRoutes($routes): array + { + if (is_a($routes, 'Closure') === true) { + $routes = $routes($this); + } + + return $this->extensions['routes'] = array_merge($this->extensions['routes'], $routes); + } + + /** + * Registers Panel sections + * + * @param array $sections + * @return array + */ + protected function extendSections(array $sections): array + { + return $this->extensions['sections'] = Section::$types = array_merge(Section::$types, $sections); + } + + /** + * Registers additional site methods + * + * @param array $methods + * @return array + */ + protected function extendSiteMethods(array $methods): array + { + return $this->extensions['siteMethods'] = Site::$methods = array_merge(Site::$methods, $methods); + } + + /** + * Registers SmartyPants component + * + * @param \Closure $smartypants + * @return \Closure + */ + protected function extendSmartypants(Closure $smartypants) + { + return $this->extensions['smartypants'] = $smartypants; + } + + /** + * Registers additional snippets + * + * @param array $snippets + * @return array + */ + protected function extendSnippets(array $snippets): array + { + return $this->extensions['snippets'] = array_merge($this->extensions['snippets'], $snippets); + } + + /** + * Registers additional KirbyTags + * + * @param array $tags + * @return array + */ + protected function extendTags(array $tags): array + { + return $this->extensions['tags'] = KirbyTag::$types = array_merge(KirbyTag::$types, array_change_key_case($tags)); + } + + /** + * Registers additional templates + * + * @param array $templates + * @return array + */ + protected function extendTemplates(array $templates): array + { + return $this->extensions['templates'] = array_merge($this->extensions['templates'], $templates); + } + + /** + * Registers translations + * + * @param array $translations + * @return array + */ + protected function extendTranslations(array $translations): array + { + return $this->extensions['translations'] = array_replace_recursive($this->extensions['translations'], $translations); + } + + /** + * Add third party extensions to the registry + * so they can be used as plugins for plugins + * for example. + * + * @param array $extensions + * @return array + */ + protected function extendThirdParty(array $extensions): array + { + return $this->extensions['thirdParty'] = array_replace_recursive($this->extensions['thirdParty'], $extensions); + } + + /** + * Registers additional user methods + * + * @param array $methods + * @return array + */ + protected function extendUserMethods(array $methods): array + { + return $this->extensions['userMethods'] = User::$methods = array_merge(User::$methods, $methods); + } + + /** + * Registers additional user models + * + * @param array $models + * @return array + */ + protected function extendUserModels(array $models): array + { + return $this->extensions['userModels'] = User::$models = array_merge(User::$models, $models); + } + + /** + * Registers additional users methods + * + * @param array $methods + * @return array + */ + protected function extendUsersMethods(array $methods): array + { + return $this->extensions['usersMethods'] = Users::$methods = array_merge(Users::$methods, $methods); + } + + /** + * Registers additional custom validators + * + * @param array $validators + * @return array + */ + protected function extendValidators(array $validators): array + { + return $this->extensions['validators'] = V::$validators = array_merge(V::$validators, $validators); + } + + /** + * Returns a given extension by type and name + * + * @internal + * @param string $type i.e. `'hooks'` + * @param string $name i.e. `'page.delete:before'` + * @param mixed $fallback + * @return mixed + */ + public function extension(string $type, string $name, $fallback = null) + { + return $this->extensions($type)[$name] ?? $fallback; + } + + /** + * Returns the extensions registry + * + * @internal + * @param string|null $type + * @return array + */ + public function extensions(string $type = null) + { + if ($type === null) { + return $this->extensions; + } + + return $this->extensions[$type] ?? []; + } + + /** + * Load extensions from site folders. + * This is only used for models for now, but + * could be extended later + */ + protected function extensionsFromFolders() + { + $models = []; + + foreach (glob($this->root('models') . '/*.php') as $model) { + $name = F::name($model); + $class = str_replace(['.', '-', '_'], '', $name) . 'Page'; + + // load the model class + F::loadOnce($model); + + if (class_exists($class) === true) { + $models[$name] = $class; + } + } + + $this->extendPageModels($models); + } + + /** + * Register extensions that could be located in + * the options array. I.e. hooks and routes can be + * setup from the config. + * + * @return void + */ + protected function extensionsFromOptions() + { + // register routes and hooks from options + $this->extend([ + 'api' => $this->options['api'] ?? [], + 'routes' => $this->options['routes'] ?? [], + 'hooks' => $this->options['hooks'] ?? [] + ]); + } + + /** + * Apply all plugin extensions + * + * @return void + */ + protected function extensionsFromPlugins() + { + // register all their extensions + foreach ($this->plugins() as $plugin) { + $extends = $plugin->extends(); + + if (empty($extends) === false) { + $this->extend($extends, $plugin); + } + } + } + + /** + * Apply all passed extensions + * + * @param array $props + * @return void + */ + protected function extensionsFromProps(array $props) + { + $this->extend($props); + } + + /** + * Apply all default extensions + * + * @return void + */ + protected function extensionsFromSystem() + { + $root = $this->root('kirby'); + + // load static extensions only once + if (static::$systemExtensions === null) { + // Form Field Mixins + FormField::$mixins['filepicker'] = include $root . '/config/fields/mixins/filepicker.php'; + FormField::$mixins['min'] = include $root . '/config/fields/mixins/min.php'; + FormField::$mixins['options'] = include $root . '/config/fields/mixins/options.php'; + FormField::$mixins['pagepicker'] = include $root . '/config/fields/mixins/pagepicker.php'; + FormField::$mixins['picker'] = include $root . '/config/fields/mixins/picker.php'; + FormField::$mixins['upload'] = include $root . '/config/fields/mixins/upload.php'; + FormField::$mixins['userpicker'] = include $root . '/config/fields/mixins/userpicker.php'; + + // Tag Aliases + KirbyTag::$aliases = [ + 'youtube' => 'video', + 'vimeo' => 'video' + ]; + + // Field method aliases + Field::$aliases = [ + 'bool' => 'toBool', + 'esc' => 'escape', + 'excerpt' => 'toExcerpt', + 'float' => 'toFloat', + 'h' => 'html', + 'int' => 'toInt', + 'kt' => 'kirbytext', + 'kti' => 'kirbytextinline', + 'link' => 'toLink', + 'md' => 'markdown', + 'sp' => 'smartypants', + 'v' => 'isValid', + 'x' => 'xml' + ]; + + // blueprint presets + PageBlueprint::$presets['pages'] = include $root . '/config/presets/pages.php'; + PageBlueprint::$presets['page'] = include $root . '/config/presets/page.php'; + PageBlueprint::$presets['files'] = include $root . '/config/presets/files.php'; + + // section mixins + Section::$mixins['empty'] = include $root . '/config/sections/mixins/empty.php'; + Section::$mixins['headline'] = include $root . '/config/sections/mixins/headline.php'; + Section::$mixins['help'] = include $root . '/config/sections/mixins/help.php'; + Section::$mixins['layout'] = include $root . '/config/sections/mixins/layout.php'; + Section::$mixins['max'] = include $root . '/config/sections/mixins/max.php'; + Section::$mixins['min'] = include $root . '/config/sections/mixins/min.php'; + Section::$mixins['pagination'] = include $root . '/config/sections/mixins/pagination.php'; + Section::$mixins['parent'] = include $root . '/config/sections/mixins/parent.php'; + + // section types + Section::$types['info'] = include $root . '/config/sections/info.php'; + Section::$types['pages'] = include $root . '/config/sections/pages.php'; + Section::$types['files'] = include $root . '/config/sections/files.php'; + Section::$types['fields'] = include $root . '/config/sections/fields.php'; + + static::$systemExtensions = [ + 'components' => include $root . '/config/components.php', + 'blueprints' => include $root . '/config/blueprints.php', + 'fields' => include $root . '/config/fields.php', + 'fieldMethods' => include $root . '/config/methods.php', + 'tags' => include $root . '/config/tags.php' + ]; + } + + // default cache types + $this->extendCacheTypes([ + 'apcu' => 'Kirby\Cache\ApcuCache', + 'file' => 'Kirby\Cache\FileCache', + 'memcached' => 'Kirby\Cache\MemCached', + 'memory' => 'Kirby\Cache\MemoryCache', + ]); + + $this->extendComponents(static::$systemExtensions['components']); + $this->extendBlueprints(static::$systemExtensions['blueprints']); + $this->extendFields(static::$systemExtensions['fields']); + $this->extendFieldMethods((static::$systemExtensions['fieldMethods'])($this)); + $this->extendTags(static::$systemExtensions['tags']); + } + + /** + * Returns the native implementation + * of a core component + * + * @param string $component + * @return \Closure|false + */ + public function nativeComponent(string $component) + { + return static::$systemExtensions['components'][$component] ?? false; + } + + /** + * Kirby plugin factory and getter + * + * @param string $name + * @param array|null $extends If null is passed it will be used as getter. Otherwise as factory. + * @return \Kirby\Cms\Plugin|null + * @throws \Kirby\Exception\DuplicateException + */ + public static function plugin(string $name, array $extends = null) + { + if ($extends === null) { + return static::$plugins[$name] ?? null; + } + + // get the correct root for the plugin + $extends['root'] = $extends['root'] ?? dirname(debug_backtrace()[0]['file']); + + $plugin = new Plugin($name, $extends); + $name = $plugin->name(); + + if (isset(static::$plugins[$name]) === true) { + throw new DuplicateException('The plugin "' . $name . '" has already been registered'); + } + + return static::$plugins[$name] = $plugin; + } + + /** + * Loads and returns all plugins in the site/plugins directory + * Loading only happens on the first call. + * + * @internal + * @param array|null $plugins Can be used to overwrite the plugins registry + * @return array + */ + public function plugins(array $plugins = null): array + { + // overwrite the existing plugins registry + if ($plugins !== null) { + $this->pluginsAreLoaded = true; + return static::$plugins = $plugins; + } + + // don't load plugins twice + if ($this->pluginsAreLoaded === true) { + return static::$plugins; + } + + // load all plugins from site/plugins + $this->pluginsLoader(); + + // mark plugins as loaded to stop doing it twice + $this->pluginsAreLoaded = true; + return static::$plugins; + } + + /** + * Loads all plugins from site/plugins + * + * @return array Array of loaded directories + */ + protected function pluginsLoader(): array + { + $root = $this->root('plugins'); + $loaded = []; + + foreach (Dir::read($root) as $dirname) { + if (in_array(substr($dirname, 0, 1), ['.', '_']) === true) { + continue; + } + + $dir = $root . '/' . $dirname; + $entry = $dir . '/index.php'; + + if (is_dir($dir) !== true || is_file($entry) !== true) { + continue; + } + + F::loadOnce($entry); + + $loaded[] = $dir; + } + + return $loaded; + } +} diff --git a/kirby/src/Cms/AppTranslations.php b/kirby/src/Cms/AppTranslations.php new file mode 100644 index 0000000..8141b54 --- /dev/null +++ b/kirby/src/Cms/AppTranslations.php @@ -0,0 +1,186 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait AppTranslations +{ + protected $translations; + + /** + * Setup internationalization + * + * @return void + */ + protected function i18n(): void + { + I18n::$load = function ($locale): array { + $data = []; + + if ($translation = $this->translation($locale)) { + $data = $translation->data(); + } + + // inject translations from the current language + if ($this->multilang() === true && $language = $this->languages()->find($locale)) { + $data = array_merge($data, $language->translations()); + + // Add language slug rules to Str class + Str::$language = $language->rules(); + } + + + return $data; + }; + + I18n::$locale = function (): string { + if ($this->multilang() === true) { + return $this->defaultLanguage()->code(); + } else { + return 'en'; + } + }; + + I18n::$fallback = function (): string { + if ($this->multilang() === true) { + return $this->defaultLanguage()->code(); + } else { + return 'en'; + } + }; + + I18n::$translations = []; + + // checks custom language definition for slugs + if ($slugsOption = $this->option('slugs')) { + // checks setting in two different ways + // "slugs" => "de" or "slugs" => ["language" => "de"] + $slugsLanguage = is_string($slugsOption) === true ? $slugsOption : ($slugsOption['language'] ?? null); + + // load custom slugs language if it's defined + if ($slugsLanguage !== null) { + $file = $this->root('i18n:rules') . '/' . $slugsLanguage . '.json'; + + if (F::exists($file) === true) { + try { + $data = Data::read($file); + } catch (Exception $e) { + $data = []; + } + + Str::$language = $data; + } + } + } + } + + /** + * Load and set the current language if it exists + * Otherwise fall back to the default language + * + * @internal + * @param string|null $languageCode + * @return \Kirby\Cms\Language|null + */ + public function setCurrentLanguage(string $languageCode = null) + { + if ($this->multilang() === false) { + $this->setLocale($this->option('locale', 'en_US.utf-8')); + return $this->language = null; + } + + if ($language = $this->language($languageCode)) { + $this->language = $language; + } else { + $this->language = $this->defaultLanguage(); + } + + if ($this->language) { + $this->setLocale($this->language->locale()); + } + + return $this->language; + } + + /** + * Set the current translation + * + * @internal + * @param string|null $translationCode + * @return void + */ + public function setCurrentTranslation(string $translationCode = null): void + { + I18n::$locale = $translationCode ?? 'en'; + } + + /** + * Set locale settings + * + * @internal + * @param string|array $locale + */ + public function setLocale($locale): void + { + if (is_array($locale) === true) { + foreach ($locale as $key => $value) { + setlocale($key, $value); + } + } else { + setlocale(LC_ALL, $locale); + } + } + + /** + * Load a specific translation by locale + * + * @param string|null $locale + * @return \Kirby\Cms\Translation|null + */ + public function translation(string $locale = null) + { + $locale = $locale ?? I18n::locale(); + $locale = basename($locale); + + // prefer loading them from the translations collection + if (is_a($this->translations, 'Kirby\Cms\Translations') === true) { + if ($translation = $this->translations()->find($locale)) { + return $translation; + } + } + + // get injected translation data from plugins etc. + $inject = $this->extensions['translations'][$locale] ?? []; + + // load from disk instead + return Translation::load($locale, $this->root('i18n:translations') . '/' . $locale . '.json', $inject); + } + + /** + * Returns all available translations + * + * @return \Kirby\Cms\Translations + */ + public function translations() + { + if (is_a($this->translations, 'Kirby\Cms\Translations') === true) { + return $this->translations; + } + + return Translations::load($this->root('i18n:translations'), $this->extensions['translations'] ?? []); + } +} diff --git a/kirby/src/Cms/AppUsers.php b/kirby/src/Cms/AppUsers.php new file mode 100644 index 0000000..f5d235d --- /dev/null +++ b/kirby/src/Cms/AppUsers.php @@ -0,0 +1,140 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait AppUsers +{ + /** + * Cache for the auth auth layer + * + * @var Auth + */ + protected $auth; + + /** + * Returns the Authentication layer class + * + * @internal + * @return \Kirby\Cms\Auth + */ + public function auth() + { + return $this->auth = $this->auth ?? new Auth($this); + } + + /** + * Become any existing user + * + * @param string|null $who User ID or email address + * @param Closure|null $callback Optional action function that will be run with + * the permissions of the impersonated user; the + * impersonation will be reset afterwards + * @return mixed If called without callback: User that was impersonated; + * if called with callback: Return value from the callback + * @throws \Throwable + */ + public function impersonate(?string $who = null, ?Closure $callback = null) + { + $auth = $this->auth(); + + $userBefore = $auth->currentUserFromImpersonation(); + $userAfter = $auth->impersonate($who); + + if ($callback === null) { + return $userAfter; + } + + try { + // bind the App object to the callback + return $callback->call($this, $userAfter); + } catch (Throwable $e) { + throw $e; + } finally { + // ensure that the impersonation is *always* reset + // to the original value, even if an error occurred + $auth->impersonate($userBefore !== null ? $userBefore->id() : null); + } + } + + /** + * Set the currently active user id + * + * @param \Kirby\Cms\User|string $user + * @return \Kirby\Cms\App + */ + protected function setUser($user = null) + { + $this->user = $user; + return $this; + } + + /** + * Create your own set of app users + * + * @param array|null $users + * @return \Kirby\Cms\App + */ + protected function setUsers(array $users = null) + { + if ($users !== null) { + $this->users = Users::factory($users, [ + 'kirby' => $this + ]); + } + + return $this; + } + + /** + * Returns a specific user by id + * or the current user if no id is given + * + * @param string|null $id + * @param bool $allowImpersonation If set to false, only the actually + * logged in user will be returned + * (when `$id` is passed as `null`) + * @return \Kirby\Cms\User|null + */ + public function user(?string $id = null, bool $allowImpersonation = true) + { + if ($id !== null) { + return $this->users()->find($id); + } + + if ($allowImpersonation === true && is_string($this->user) === true) { + return $this->auth()->impersonate($this->user); + } else { + try { + return $this->auth()->user(null, $allowImpersonation); + } catch (Throwable $e) { + return null; + } + } + } + + /** + * Returns all users + * + * @return \Kirby\Cms\Users + */ + public function users() + { + if (is_a($this->users, 'Kirby\Cms\Users') === true) { + return $this->users; + } + + return $this->users = Users::load($this->root('accounts'), ['kirby' => $this]); + } +} diff --git a/kirby/src/Cms/Asset.php b/kirby/src/Cms/Asset.php new file mode 100644 index 0000000..a722238 --- /dev/null +++ b/kirby/src/Cms/Asset.php @@ -0,0 +1,126 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Asset +{ + use FileFoundation; + use FileModifications; + use Properties; + + /** + * @var string + */ + protected $path; + + /** + * Creates a new Asset object + * for the given path. + * + * @param string $path + */ + public function __construct(string $path) + { + $this->setPath(dirname($path)); + $this->setRoot($this->kirby()->root('index') . '/' . $path); + $this->setUrl($this->kirby()->url('index') . '/' . $path); + } + + /** + * Returns the alternative text for the asset + * + * @return null + */ + public function alt() + { + return null; + } + + /** + * Returns a unique id for the asset + * + * @return string + */ + public function id(): string + { + return $this->root(); + } + + /** + * Create a unique media hash + * + * @return string + */ + public function mediaHash(): string + { + return crc32($this->filename()) . '-' . $this->modified(); + } + + /** + * Returns the relative path starting at the media folder + * + * @return string + */ + public function mediaPath(): string + { + return 'assets/' . $this->path() . '/' . $this->mediaHash() . '/' . $this->filename(); + } + + /** + * Returns the absolute path to the file in the public media folder + * + * @return string + */ + public function mediaRoot(): string + { + return $this->kirby()->root('media') . '/' . $this->mediaPath(); + } + + /** + * Returns the absolute Url to the file in the public media folder + * + * @return string + */ + public function mediaUrl(): string + { + return $this->kirby()->url('media') . '/' . $this->mediaPath(); + } + + /** + * Returns the path of the file from the web root, + * excluding the filename + * + * @return string + */ + public function path(): string + { + return $this->path; + } + + /** + * Setter for the path + * + * @param string $path + * @return self + */ + protected function setPath(string $path) + { + $this->path = $path === '.' ? '' : $path; + return $this; + } +} diff --git a/kirby/src/Cms/Auth.php b/kirby/src/Cms/Auth.php new file mode 100644 index 0000000..a4c67f6 --- /dev/null +++ b/kirby/src/Cms/Auth.php @@ -0,0 +1,502 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Auth +{ + protected $impersonate; + protected $kirby; + protected $user = false; + protected $userException; + + /** + * @param \Kirby\Cms\App $kirby + * @codeCoverageIgnore + */ + public function __construct(App $kirby) + { + $this->kirby = $kirby; + } + + /** + * Returns the csrf token if it exists and if it is valid + * + * @return string|false + */ + public function csrf() + { + // get the csrf from the header + $fromHeader = $this->kirby->request()->csrf(); + + // check for a predefined csrf or use the one from session + $fromSession = $this->kirby->option('api.csrf', csrf()); + + // compare both tokens + if (hash_equals((string)$fromSession, (string)$fromHeader) !== true) { + return false; + } + + return $fromSession; + } + + /** + * Returns the logged in user by checking + * for a basic authentication header with + * valid credentials + * + * @param \Kirby\Http\Request\Auth\BasicAuth|null $auth + * @return \Kirby\Cms\User|null + * @throws \Kirby\Exception\InvalidArgumentException if the authorization header is invalid + * @throws \Kirby\Exception\PermissionException if basic authentication is not allowed + */ + public function currentUserFromBasicAuth(BasicAuth $auth = null) + { + if ($this->kirby->option('api.basicAuth', false) !== true) { + throw new PermissionException('Basic authentication is not activated'); + } + + $request = $this->kirby->request(); + $auth = $auth ?? $request->auth(); + + if (!$auth || $auth->type() !== 'basic') { + throw new InvalidArgumentException('Invalid authorization header'); + } + + // only allow basic auth when https is enabled or insecure requests permitted + if ($request->ssl() === false && $this->kirby->option('api.allowInsecure', false) !== true) { + throw new PermissionException('Basic authentication is only allowed over HTTPS'); + } + + return $this->validatePassword($auth->username(), $auth->password()); + } + + /** + * Returns the currently impersonated user + * + * @return \Kirby\Cms\User|null + */ + public function currentUserFromImpersonation() + { + return $this->impersonate; + } + + /** + * Returns the logged in user by checking + * the current session and finding a valid + * valid user id in there + * + * @param \Kirby\Session\Session|array|null $session + * @return \Kirby\Cms\User|null + */ + public function currentUserFromSession($session = null) + { + // use passed session options or session object if set + if (is_array($session) === true) { + $session = $this->kirby->session($session); + } + + // try session in header or cookie + if (is_a($session, 'Kirby\Session\Session') === false) { + $session = $this->kirby->session(['detect' => true]); + } + + $id = $session->data()->get('user.id'); + + if (is_string($id) !== true) { + return null; + } + + if ($user = $this->kirby->users()->find($id)) { + // in case the session needs to be updated, do it now + // for better performance + $session->commit(); + return $user; + } + + return null; + } + + /** + * Become any existing user + * + * @param string|null $who User ID or email address + * @return \Kirby\Cms\User|null + * @throws \Kirby\Exception\NotFoundException if the given user cannot be found + */ + public function impersonate(?string $who = null) + { + switch ($who) { + case null: + return $this->impersonate = null; + case 'kirby': + return $this->impersonate = new User([ + 'email' => 'kirby@getkirby.com', + 'id' => 'kirby', + 'role' => 'admin', + ]); + default: + if ($user = $this->kirby->users()->find($who)) { + return $this->impersonate = $user; + } + + throw new NotFoundException('The user "' . $who . '" cannot be found'); + } + } + + /** + * Returns the hashed ip of the visitor + * which is used to track invalid logins + * + * @return string + */ + public function ipHash(): string + { + $hash = hash('sha256', $this->kirby->visitor()->ip()); + + // only use the first 50 chars to ensure privacy + return substr($hash, 0, 50); + } + + /** + * Check if logins are blocked for the current ip or email + * + * @param string $email + * @return bool + */ + public function isBlocked(string $email): bool + { + $ip = $this->ipHash(); + $log = $this->log(); + $trials = $this->kirby->option('auth.trials', 10); + + if ($entry = ($log['by-ip'][$ip] ?? null)) { + if ($entry['trials'] >= $trials) { + return true; + } + } + + if ($this->kirby->users()->find($email)) { + if ($entry = ($log['by-email'][$email] ?? null)) { + if ($entry['trials'] >= $trials) { + return true; + } + } + } + + return false; + } + + /** + * Login a user by email and password + * + * @param string $email + * @param string $password + * @param bool $long + * @return \Kirby\Cms\User + * + * @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occured with debug mode off + * @throws \Kirby\Exception\NotFoundException If the email was invalid + * @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`) + */ + public function login(string $email, string $password, bool $long = false) + { + // session options + $options = [ + 'createMode' => 'cookie', + 'long' => $long === true + ]; + + // validate the user and log in to the session + $user = $this->validatePassword($email, $password); + $user->loginPasswordless($options); + + return $user; + } + + /** + * Sets a user object as the current user in the cache + * @internal + * + * @param \Kirby\Cms\User $user + * @return void + */ + public function setUser(User $user): void + { + // stop impersonating + $this->impersonate = null; + + $this->user = $user; + } + + /** + * Validates the user credentials and returns the user object on success; + * otherwise logs the failed attempt + * + * @param string $email + * @param string $password + * @return \Kirby\Cms\User + * + * @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occured with debug mode off + * @throws \Kirby\Exception\NotFoundException If the email was invalid + * @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`) + */ + public function validatePassword(string $email, string $password) + { + // ensure that email addresses with IDN domains are in Unicode format + $email = Idn::decodeEmail($email); + + // check for blocked ips + if ($this->isBlocked($email) === true) { + $this->kirby->trigger('user.login:failed', compact('email')); + + if ($this->kirby->option('debug') === true) { + $message = 'Rate limit exceeded'; + } else { + // avoid leaking security-relevant information + $message = 'Invalid email or password'; + } + + throw new PermissionException($message); + } + + // validate the user + try { + if ($user = $this->kirby->users()->find($email)) { + if ($user->validatePassword($password) === true) { + return $user; + } + } + + throw new NotFoundException([ + 'key' => 'user.notFound', + 'data' => [ + 'name' => $email + ] + ]); + } catch (Throwable $e) { + // log invalid login trial + $this->track($email); + + // sleep for a random amount of milliseconds + // to make automated attacks harder + usleep(random_int(1000, 2000000)); + + // keep throwing the original error in debug mode, + // otherwise hide it to avoid leaking security-relevant information + if ($this->kirby->option('debug') === true) { + throw $e; + } else { + throw new PermissionException('Invalid email or password'); + } + } + } + + /** + * Returns the absolute path to the logins log + * + * @return string + */ + public function logfile(): string + { + return $this->kirby->root('accounts') . '/.logins'; + } + + /** + * Read all tracked logins + * + * @return array + */ + public function log(): array + { + try { + $log = Data::read($this->logfile(), 'json'); + $read = true; + } catch (Throwable $e) { + $log = []; + $read = false; + } + + // ensure that the category arrays are defined + $log['by-ip'] = $log['by-ip'] ?? []; + $log['by-email'] = $log['by-email'] ?? []; + + // remove all elements on the top level with different keys (old structure) + $log = array_intersect_key($log, array_flip(['by-ip', 'by-email'])); + + // remove entries that are no longer needed + $originalLog = $log; + $time = time() - $this->kirby->option('auth.timeout', 3600); + foreach ($log as $category => $entries) { + $log[$category] = array_filter($entries, function ($entry) use ($time) { + return $entry['time'] > $time; + }); + } + + // write new log to the file system if it changed + if ($read === false || $log !== $originalLog) { + if (count($log['by-ip']) === 0 && count($log['by-email']) === 0) { + F::remove($this->logfile()); + } else { + Data::write($this->logfile(), $log, 'json'); + } + } + + return $log; + } + + /** + * Logout the current user + * + * @return void + */ + public function logout(): void + { + // stop impersonating; + // ensures that we log out the actually logged in user + $this->impersonate = null; + + // logout the current user if it exists + if ($user = $this->user()) { + $user->logout(); + } + } + + /** + * Clears the cached user data after logout + * @internal + * + * @return void + */ + public function flush(): void + { + $this->impersonate = null; + $this->user = null; + } + + /** + * Tracks a login + * + * @param string $email + * @return bool + */ + public function track(string $email): bool + { + $this->kirby->trigger('user.login:failed', compact('email')); + + $ip = $this->ipHash(); + $log = $this->log(); + $time = time(); + + if (isset($log['by-ip'][$ip]) === true) { + $log['by-ip'][$ip] = [ + 'time' => $time, + 'trials' => ($log['by-ip'][$ip]['trials'] ?? 0) + 1 + ]; + } else { + $log['by-ip'][$ip] = [ + 'time' => $time, + 'trials' => 1 + ]; + } + + if ($this->kirby->users()->find($email)) { + if (isset($log['by-email'][$email]) === true) { + $log['by-email'][$email] = [ + 'time' => $time, + 'trials' => ($log['by-email'][$email]['trials'] ?? 0) + 1 + ]; + } else { + $log['by-email'][$email] = [ + 'time' => $time, + 'trials' => 1 + ]; + } + } + + return Data::write($this->logfile(), $log, 'json'); + } + + /** + * Returns the current authentication type + * + * @param bool $allowImpersonation If set to false, 'impersonate' won't + * be returned as authentication type + * even if an impersonation is active + * @return string + */ + public function type(bool $allowImpersonation = true): string + { + $basicAuth = $this->kirby->option('api.basicAuth', false); + $auth = $this->kirby->request()->auth(); + + if ($basicAuth === true && $auth && $auth->type() === 'basic') { + return 'basic'; + } elseif ($allowImpersonation === true && $this->impersonate !== null) { + return 'impersonate'; + } else { + return 'session'; + } + } + + /** + * Validates the currently logged in user + * + * @param \Kirby\Session\Session|array|null $session + * @param bool $allowImpersonation If set to false, only the actually + * logged in user will be returned + * @return \Kirby\Cms\User + * + * @throws \Throwable If an authentication error occured + */ + public function user($session = null, bool $allowImpersonation = true) + { + if ($allowImpersonation === true && $this->impersonate !== null) { + return $this->impersonate; + } + + // return from cache + if ($this->user === null) { + // throw the same Exception again if one was captured before + if ($this->userException !== null) { + throw $this->userException; + } + + return null; + } elseif ($this->user !== false) { + return $this->user; + } + + try { + if ($this->type() === 'basic') { + return $this->user = $this->currentUserFromBasicAuth(); + } else { + return $this->user = $this->currentUserFromSession($session); + } + } catch (Throwable $e) { + $this->user = null; + + // capture the Exception for future calls + $this->userException = $e; + + throw $e; + } + } +} diff --git a/kirby/src/Cms/Blueprint.php b/kirby/src/Cms/Blueprint.php new file mode 100644 index 0000000..edfc9fe --- /dev/null +++ b/kirby/src/Cms/Blueprint.php @@ -0,0 +1,797 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Blueprint +{ + public static $presets = []; + public static $loaded = []; + + protected $fields = []; + protected $model; + protected $props; + protected $sections = []; + protected $tabs = []; + + /** + * Magic getter/caller for any blueprint prop + * + * @param string $key + * @param array|null $arguments + * @return mixed + */ + public function __call(string $key, array $arguments = null) + { + return $this->props[$key] ?? null; + } + + /** + * Creates a new blueprint object with the given props + * + * @param array $props + * @throws \Kirby\Exception\InvalidArgumentException If the blueprint model is missing + */ + public function __construct(array $props) + { + if (empty($props['model']) === true) { + throw new InvalidArgumentException('A blueprint model is required'); + } + + $this->model = $props['model']; + + // the model should not be included in the props array + unset($props['model']); + + // extend the blueprint in general + $props = $this->extend($props); + + // apply any blueprint preset + $props = $this->preset($props); + + // normalize the name + $props['name'] = $props['name'] ?? 'default'; + + // normalize and translate the title + $props['title'] = $this->i18n($props['title'] ?? ucfirst($props['name'])); + + // convert all shortcuts + $props = $this->convertFieldsToSections('main', $props); + $props = $this->convertSectionsToColumns('main', $props); + $props = $this->convertColumnsToTabs('main', $props); + + // normalize all tabs + $props['tabs'] = $this->normalizeTabs($props['tabs'] ?? []); + + $this->props = $props; + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->props ?? []; + } + + /** + * Converts all column definitions, that + * are not wrapped in a tab, into a generic tab + * + * @param string $tabName + * @param array $props + * @return array + */ + protected function convertColumnsToTabs(string $tabName, array $props): array + { + if (isset($props['columns']) === false) { + return $props; + } + + // wrap everything in a main tab + $props['tabs'] = [ + $tabName => [ + 'columns' => $props['columns'] + ] + ]; + + unset($props['columns']); + + return $props; + } + + /** + * Converts all field definitions, that are not + * wrapped in a fields section into a generic + * fields section. + * + * @param string $tabName + * @param array $props + * @return array + */ + protected function convertFieldsToSections(string $tabName, array $props): array + { + if (isset($props['fields']) === false) { + return $props; + } + + // wrap all fields in a section + $props['sections'] = [ + $tabName . '-fields' => [ + 'type' => 'fields', + 'fields' => $props['fields'] + ] + ]; + + unset($props['fields']); + + return $props; + } + + /** + * Converts all sections that are not wrapped in + * columns, into a single generic column. + * + * @param string $tabName + * @param array $props + * @return array + */ + protected function convertSectionsToColumns(string $tabName, array $props): array + { + if (isset($props['sections']) === false) { + return $props; + } + + // wrap everything in one big column + $props['columns'] = [ + [ + 'width' => '1/1', + 'sections' => $props['sections'] + ] + ]; + + unset($props['sections']); + + return $props; + } + + /** + * Extends the props with props from a given + * mixin, when an extends key is set or the + * props is just a string + * + * @param array|string $props + * @return array + */ + public static function extend($props): array + { + if (is_string($props) === true) { + $props = [ + 'extends' => $props + ]; + } + + $extends = $props['extends'] ?? null; + + if ($extends === null) { + return $props; + } + + try { + $mixin = static::find($extends); + $props = A::merge($mixin, $props, A::MERGE_REPLACE); + } catch (Exception $e) { + // keep the props unextended if the snippet wasn't found + } + + // remove the extends flag + unset($props['extends']); + + return $props; + } + + /** + * Create a new blueprint for a model + * + * @param string $name + * @param string|null $fallback + * @param \Kirby\Cms\Model $model + * @return self + */ + public static function factory(string $name, string $fallback = null, Model $model) + { + try { + $props = static::load($name); + } catch (Exception $e) { + $props = $fallback !== null ? static::load($fallback) : null; + } + + if ($props === null) { + return null; + } + + // inject the parent model + $props['model'] = $model; + + return new static($props); + } + + /** + * Returns a single field definition by name + * + * @param string $name + * @return array|null + */ + public function field(string $name): ?array + { + return $this->fields[$name] ?? null; + } + + /** + * Returns all field definitions + * + * @return array + */ + public function fields(): array + { + return $this->fields; + } + + /** + * Find a blueprint by name + * + * @param string $name + * @return array + * @throws \Kirby\Exception\NotFoundException If the blueprint cannot be found + */ + public static function find(string $name): array + { + if (isset(static::$loaded[$name]) === true) { + return static::$loaded[$name]; + } + + $kirby = App::instance(); + $root = $kirby->root('blueprints'); + $file = $root . '/' . $name . '.yml'; + + // first try to find a site blueprint, + // then check in the plugin extensions + if (F::exists($file, $root) !== true) { + $file = $kirby->extension('blueprints', $name); + } + + // now ensure that we always return the data array + if (is_string($file) === true && F::exists($file) === true) { + return static::$loaded[$name] = Data::read($file); + } elseif (is_array($file) === true) { + return static::$loaded[$name] = $file; + } + + // neither a valid file nor array data + throw new NotFoundException([ + 'key' => 'blueprint.notFound', + 'data' => ['name' => $name] + ]); + } + + /** + * Used to translate any label, heading, etc. + * + * @param mixed $value + * @param mixed $fallback + * @return mixed + */ + protected function i18n($value, $fallback = null) + { + return I18n::translate($value, $fallback ?? $value); + } + + /** + * Checks if this is the default blueprint + * + * @return bool + */ + public function isDefault(): bool + { + return $this->name() === 'default'; + } + + /** + * Loads a blueprint from file or array + * + * @param string $name + * @return array + */ + public static function load(string $name): array + { + $props = static::find($name); + + $normalize = function ($props) use ($name) { + // inject the filename as name if no name is set + $props['name'] = $props['name'] ?? $name; + + // normalize the title + $title = $props['title'] ?? ucfirst($props['name']); + + // translate the title + $props['title'] = I18n::translate($title, $title); + + return $props; + }; + + return $normalize($props); + } + + /** + * Returns the parent model + * + * @return \Kirby\Cms\Model + */ + public function model() + { + return $this->model; + } + + /** + * Returns the blueprint name + * + * @return string + */ + public function name(): string + { + return $this->props['name']; + } + + /** + * Normalizes all required props in a column setup + * + * @param string $tabName + * @param array $columns + * @return array + */ + protected function normalizeColumns(string $tabName, array $columns): array + { + foreach ($columns as $columnKey => $columnProps) { + if (is_array($columnProps) === false) { + continue; + } + + $columnProps = $this->convertFieldsToSections($tabName . '-col-' . $columnKey, $columnProps); + + // inject getting started info, if the sections are empty + if (empty($columnProps['sections']) === true) { + $columnProps['sections'] = [ + $tabName . '-info-' . $columnKey => [ + 'headline' => 'Column (' . ($columnProps['width'] ?? '1/1') . ')', + 'type' => 'info', + 'text' => 'No sections yet' + ] + ]; + } + + $columns[$columnKey] = array_merge($columnProps, [ + 'width' => $columnProps['width'] ?? '1/1', + 'sections' => $this->normalizeSections($tabName, $columnProps['sections'] ?? []) + ]); + } + + return $columns; + } + + /** + * @param array $items + * @return string + */ + public static function helpList(array $items): string + { + $md = []; + + foreach ($items as $item) { + $md[] = '- *' . $item . '*'; + } + + return PHP_EOL . implode(PHP_EOL, $md); + } + + /** + * Normalize field props for a single field + * + * @param array|string $props + * @return array + * @throws \Kirby\Exception\InvalidArgumentException If the filed name is missing or the field type is invalid + */ + public static function fieldProps($props): array + { + $props = static::extend($props); + + if (isset($props['name']) === false) { + throw new InvalidArgumentException('The field name is missing'); + } + + $name = $props['name']; + $type = $props['type'] ?? $name; + + if ($type !== 'group' && isset(Field::$types[$type]) === false) { + throw new InvalidArgumentException('Invalid field type ("' . $type . '")'); + } + + // support for nested fields + if (isset($props['fields']) === true) { + $props['fields'] = static::fieldsProps($props['fields']); + } + + // groups don't need all the crap + if ($type === 'group') { + return [ + 'fields' => $props['fields'], + 'name' => $name, + 'type' => $type, + ]; + } + + // add some useful defaults + return array_merge($props, [ + 'label' => $props['label'] ?? ucfirst($name), + 'name' => $name, + 'type' => $type, + 'width' => $props['width'] ?? '1/1', + ]); + } + + /** + * Creates an error field with the given error message + * + * @param string $name + * @param string $message + * @return array + */ + public static function fieldError(string $name, string $message): array + { + return [ + 'label' => 'Error', + 'name' => $name, + 'text' => strip_tags($message), + 'theme' => 'negative', + 'type' => 'info', + ]; + } + + /** + * Normalizes all fields and adds automatic labels, + * types and widths. + * + * @param array $fields + * @return array + */ + public static function fieldsProps($fields): array + { + if (is_array($fields) === false) { + $fields = []; + } + + foreach ($fields as $fieldName => $fieldProps) { + + // extend field from string + if (is_string($fieldProps) === true) { + $fieldProps = [ + 'extends' => $fieldProps, + 'name' => $fieldName + ]; + } + + // use the name as type definition + if ($fieldProps === true) { + $fieldProps = []; + } + + // unset / remove field if its property is false + if ($fieldProps === false) { + unset($fields[$fieldName]); + continue; + } + + // inject the name + $fieldProps['name'] = $fieldName; + + // create all props + try { + $fieldProps = static::fieldProps($fieldProps); + } catch (Throwable $e) { + $fieldProps = static::fieldError($fieldName, $e->getMessage()); + } + + // resolve field groups + if ($fieldProps['type'] === 'group') { + if (empty($fieldProps['fields']) === false && is_array($fieldProps['fields']) === true) { + $index = array_search($fieldName, array_keys($fields)); + $before = array_slice($fields, 0, $index); + $after = array_slice($fields, $index + 1); + $fields = array_merge($before, $fieldProps['fields'] ?? [], $after); + } else { + unset($fields[$fieldName]); + } + } else { + $fields[$fieldName] = $fieldProps; + } + } + + return $fields; + } + + /** + * Normalizes blueprint options. This must be used in the + * constructor of an extended class, if you want to make use of it. + * + * @param array|true|false|null|string $options + * @param array $defaults + * @param array $aliases + * @return array + */ + protected function normalizeOptions($options, array $defaults, array $aliases = []): array + { + // return defaults when options are not defined or set to true + if ($options === true) { + return $defaults; + } + + // set all options to false + if ($options === false) { + return array_map(function () { + return false; + }, $defaults); + } + + // extend options if possible + $options = $this->extend($options); + + foreach ($options as $key => $value) { + $alias = $aliases[$key] ?? null; + + if ($alias !== null) { + $options[$alias] = $options[$alias] ?? $value; + unset($options[$key]); + } + } + + return array_merge($defaults, $options); + } + + /** + * Normalizes all required keys in sections + * + * @param string $tabName + * @param array $sections + * @return array + */ + protected function normalizeSections(string $tabName, array $sections): array + { + foreach ($sections as $sectionName => $sectionProps) { + + // unset / remove section if its property is false + if ($sectionProps === false) { + unset($sections[$sectionName]); + continue; + } + + // fallback to default props when true is passed + if ($sectionProps === true) { + $sectionProps = []; + } + + // inject all section extensions + $sectionProps = $this->extend($sectionProps); + + $sections[$sectionName] = $sectionProps = array_merge($sectionProps, [ + 'name' => $sectionName, + 'type' => $type = $sectionProps['type'] ?? $sectionName + ]); + + if (empty($type) === true || is_string($type) === false) { + $sections[$sectionName] = [ + 'name' => $sectionName, + 'headline' => 'Invalid section type for section "' . $sectionName . '"', + 'type' => 'info', + 'text' => 'The following section types are available: ' . $this->helpList(array_keys(Section::$types)) + ]; + } elseif (isset(Section::$types[$type]) === false) { + $sections[$sectionName] = [ + 'name' => $sectionName, + 'headline' => 'Invalid section type ("' . $type . '")', + 'type' => 'info', + 'text' => 'The following section types are available: ' . $this->helpList(array_keys(Section::$types)) + ]; + } + + if ($sectionProps['type'] === 'fields') { + $fields = Blueprint::fieldsProps($sectionProps['fields'] ?? []); + + // inject guide fields guide + if (empty($fields) === true) { + $fields = [ + $tabName . '-info' => [ + 'label' => 'Fields', + 'text' => 'No fields yet', + 'type' => 'info' + ] + ]; + } else { + foreach ($fields as $fieldName => $fieldProps) { + if (isset($this->fields[$fieldName]) === true) { + $this->fields[$fieldName] = $fields[$fieldName] = [ + 'type' => 'info', + 'label' => $fieldProps['label'] ?? 'Error', + 'text' => 'The field name "' . $fieldName . '" already exists in your blueprint.', + 'theme' => 'negative' + ]; + } else { + $this->fields[$fieldName] = $fieldProps; + } + } + } + + $sections[$sectionName]['fields'] = $fields; + } + } + + // store all normalized sections + $this->sections = array_merge($this->sections, $sections); + + return $sections; + } + + /** + * Normalizes all required keys in tabs + * + * @param array $tabs + * @return array + */ + protected function normalizeTabs($tabs): array + { + if (is_array($tabs) === false) { + $tabs = []; + } + + foreach ($tabs as $tabName => $tabProps) { + + // unset / remove tab if its property is false + if ($tabProps === false) { + unset($tabs[$tabName]); + continue; + } + + // inject all tab extensions + $tabProps = $this->extend($tabProps); + + // inject a preset if available + $tabProps = $this->preset($tabProps); + + $tabProps = $this->convertFieldsToSections($tabName, $tabProps); + $tabProps = $this->convertSectionsToColumns($tabName, $tabProps); + + $tabs[$tabName] = array_merge($tabProps, [ + 'columns' => $this->normalizeColumns($tabName, $tabProps['columns'] ?? []), + 'icon' => $tabProps['icon'] ?? null, + 'label' => $this->i18n($tabProps['label'] ?? ucfirst($tabName)), + 'name' => $tabName, + ]); + } + + return $this->tabs = $tabs; + } + + /** + * Injects a blueprint preset + * + * @param array $props + * @return array + */ + protected function preset(array $props): array + { + if (isset($props['preset']) === false) { + return $props; + } + + if (isset(static::$presets[$props['preset']]) === false) { + return $props; + } + + return static::$presets[$props['preset']]($props); + } + + /** + * Returns a single section by name + * + * @param string $name + * @return \Kirby\Cms\Section|null + */ + public function section(string $name) + { + if (empty($this->sections[$name]) === true) { + return null; + } + + // get all props + $props = $this->sections[$name]; + + // inject the blueprint model + $props['model'] = $this->model(); + + // create a new section object + return new Section($props['type'], $props); + } + + /** + * Returns all sections + * + * @return array + */ + public function sections(): array + { + return array_map(function ($section) { + return $this->section($section['name']); + }, $this->sections); + } + + /** + * Returns a single tab by name + * + * @param string $name + * @return array|null + */ + public function tab(string $name): ?array + { + return $this->tabs[$name] ?? null; + } + + /** + * Returns all tabs + * + * @return array + */ + public function tabs(): array + { + return array_values($this->tabs); + } + + /** + * Returns the blueprint title + * + * @return string + */ + public function title(): string + { + return $this->props['title']; + } + + /** + * Converts the blueprint object to a plain array + * + * @return array + */ + public function toArray(): array + { + return $this->props; + } +} diff --git a/kirby/src/Cms/Collection.php b/kirby/src/Cms/Collection.php new file mode 100644 index 0000000..4bb9431 --- /dev/null +++ b/kirby/src/Cms/Collection.php @@ -0,0 +1,338 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Collection extends BaseCollection +{ + use HasMethods; + + /** + * Stores the parent object, which is needed + * in some collections to get the finder methods right. + * + * @var object + */ + protected $parent; + + /** + * Magic getter function + * + * @param string $key + * @param mixed $arguments + * @return mixed + */ + public function __call(string $key, $arguments) + { + // collection methods + if ($this->hasMethod($key) === true) { + return $this->callMethod($key, $arguments); + } + } + + /** + * Creates a new Collection with the given objects + * + * @param array $objects + * @param object|null $parent + */ + public function __construct($objects = [], $parent = null) + { + $this->parent = $parent; + + foreach ($objects as $object) { + $this->add($object); + } + } + + /** + * Internal setter for each object in the Collection. + * This takes care of Component validation and of setting + * the collection prop on each object correctly. + * + * @param string $id + * @param object $object + */ + public function __set(string $id, $object) + { + $this->data[$id] = $object; + } + + /** + * Adds a single object or + * an entire second collection to the + * current collection + * + * @param mixed $object + */ + public function add($object) + { + if (is_a($object, static::class) === true) { + $this->data = array_merge($this->data, $object->data); + } elseif (method_exists($object, 'id') === true) { + $this->__set($object->id(), $object); + } else { + $this->append($object); + } + + return $this; + } + + /** + * Appends an element to the data array + * + * @param mixed ...$args + * @param mixed $key Optional collection key, will be determined from the item if not given + * @param mixed $item + * @return \Kirby\Cms\Collection + */ + public function append(...$args) + { + if (count($args) === 1) { + // try to determine the key from the provided item + if (is_object($args[0]) === true && is_callable([$args[0], 'id']) === true) { + return parent::append($args[0]->id(), $args[0]); + } else { + return parent::append($args[0]); + } + } + + return parent::append(...$args); + } + + /** + * Groups the items by a given field. Returns a collection + * with an item for each group and a collection for each group. + * + * @param string $field + * @param bool $i Ignore upper/lowercase for group names + * @return \Kirby\Cms\Collection + * @throws \Kirby\Exception\Exception + */ + public function groupBy($field, bool $i = true) + { + if (is_string($field) === false) { + throw new Exception('Cannot group by non-string values. Did you mean to call group()?'); + } + + $groups = new Collection([], $this->parent()); + + foreach ($this->data as $key => $item) { + $value = $this->getAttribute($item, $field); + + // make sure that there's always a proper value to group by + if (!$value) { + throw new InvalidArgumentException('Invalid grouping value for key: ' . $key); + } + + // ignore upper/lowercase for group names + if ($i) { + $value = Str::lower($value); + } + + if (isset($groups->data[$value]) === false) { + // create a new entry for the group if it does not exist yet + $groups->data[$value] = new static([$key => $item]); + } else { + // add the item to an existing group + $groups->data[$value]->set($key, $item); + } + } + + return $groups; + } + + /** + * Checks if the given object or id + * is in the collection + * + * @param string|object $id + * @return bool + */ + public function has($id): bool + { + if (is_object($id) === true) { + $id = $id->id(); + } + + return parent::has($id); + } + + /** + * Correct position detection for objects. + * The method will automatically detect objects + * or ids and then search accordingly. + * + * @param string|object $object + * @return int + */ + public function indexOf($object): int + { + if (is_string($object) === true) { + return array_search($object, $this->keys()); + } + + return array_search($object->id(), $this->keys()); + } + + /** + * Returns a Collection without the given element(s) + * + * @param mixed ...$keys any number of keys, passed as individual arguments + * @return \Kirby\Cms\Collection + */ + public function not(...$keys) + { + $collection = $this->clone(); + foreach ($keys as $key) { + if (is_array($key) === true) { + return $this->not(...$key); + } elseif (is_a($key, 'Kirby\Toolkit\Collection') === true) { + $collection = $collection->not(...$key->keys()); + } elseif (is_object($key) === true) { + $key = $key->id(); + } + unset($collection->$key); + } + return $collection; + } + + /** + * Add pagination and return a sliced set of data. + * + * @param mixed ...$arguments + * @return \Kirby\Cms\Collection + */ + public function paginate(...$arguments) + { + $this->pagination = Pagination::for($this, ...$arguments); + + // slice and clone the collection according to the pagination + return $this->slice($this->pagination->offset(), $this->pagination->limit()); + } + + /** + * Returns the parent model + * + * @return \Kirby\Cms\Model + */ + public function parent() + { + return $this->parent; + } + + /** + * Prepends an element to the data array + * + * @param mixed ...$args + * @param mixed $key Optional collection key, will be determined from the item if not given + * @param mixed $item + * @return \Kirby\Cms\Collection + */ + public function prepend(...$args) + { + if (count($args) === 1) { + // try to determine the key from the provided item + if (is_object($args[0]) === true && is_callable([$args[0], 'id']) === true) { + return parent::prepend($args[0]->id(), $args[0]); + } else { + return parent::prepend($args[0]); + } + } + + return parent::prepend(...$args); + } + + /** + * Runs a combination of filterBy, sortBy, not + * offset, limit, search and paginate on the collection. + * Any part of the query is optional. + * + * @param array $query + * @return self + */ + public function query(array $query = []) + { + $paginate = $query['paginate'] ?? null; + $search = $query['search'] ?? null; + + unset($query['paginate']); + + $result = parent::query($query); + + if (empty($search) === false) { + if (is_array($search) === true) { + $result = $result->search($search['query'] ?? null, $search['options'] ?? []); + } else { + $result = $result->search($search); + } + } + + if (empty($paginate) === false) { + $result = $result->paginate($paginate); + } + + return $result; + } + + /** + * Removes an object + * + * @param mixed $key the name of the key + */ + public function remove($key) + { + if (is_object($key) === true) { + $key = $key->id(); + } + + return parent::remove($key); + } + + /** + * Searches the collection + * + * @param string|null $query + * @param array $params + * @return self + */ + public function search(string $query = null, $params = []) + { + return Search::collection($this, $query, $params); + } + + /** + * Converts all objects in the collection + * to an array. This can also take a callback + * function to further modify the array result. + * + * @param \Closure|null $map + * @return array + */ + public function toArray(Closure $map = null): array + { + return parent::toArray($map ?? function ($object) { + return $object->toArray(); + }); + } +} diff --git a/kirby/src/Cms/Collections.php b/kirby/src/Cms/Collections.php new file mode 100644 index 0000000..2b49e6f --- /dev/null +++ b/kirby/src/Cms/Collections.php @@ -0,0 +1,141 @@ +collection()` + * method to provide easy access to registered collections + * + * @package Kirby Cms + * @author Bastian Allgeier + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Collections +{ + /** + * Each collection is cached once it + * has been called, to avoid further + * processing on sequential calls to + * the same collection. + * + * @var array + */ + protected $cache = []; + + /** + * Store of all collections + * + * @var array + */ + protected $collections = []; + + /** + * Magic caller to enable something like + * `$collections->myCollection()` + * + * @param string $name + * @param array $arguments + * @return \Kirby\Cms\Collection|null + */ + public function __call(string $name, array $arguments = []) + { + return $this->get($name, ...$arguments); + } + + /** + * Loads a collection by name if registered + * + * @param string $name + * @param array $data + * @return \Kirby\Cms\Collection|null + */ + public function get(string $name, array $data = []) + { + // if not yet loaded + if (isset($this->collections[$name]) === false) { + $this->collections[$name] = $this->load($name); + } + + // if not yet cached + if ( + isset($this->cache[$name]) === false || + $this->cache[$name]['data'] !== $data + ) { + $controller = new Controller($this->collections[$name]); + $this->cache[$name] = [ + 'result' => $controller->call(null, $data), + 'data' => $data + ]; + } + + // return cloned object + if (is_object($this->cache[$name]['result']) === true) { + return clone $this->cache[$name]['result']; + } + + return $this->cache[$name]['result']; + } + + /** + * Checks if a collection exists + * + * @param string $name + * @return bool + */ + public function has(string $name): bool + { + if (isset($this->collections[$name]) === true) { + return true; + } + + try { + $this->load($name); + return true; + } catch (NotFoundException $e) { + return false; + } + } + + /** + * Loads collection from php file in a + * given directory or from plugin extension. + * + * @param string $name + * @return mixed + * @throws \Kirby\Exception\NotFoundException + */ + public function load(string $name) + { + $kirby = App::instance(); + + // first check for collection file + $file = $kirby->root('collections') . '/' . $name . '.php'; + + if (is_file($file) === true) { + $collection = F::load($file); + + if (is_a($collection, 'Closure')) { + return $collection; + } + } + + // fallback to collections from plugins + $collections = $kirby->extensions('collections'); + + if (isset($collections[$name]) === true) { + return $collections[$name]; + } + + throw new NotFoundException('The collection cannot be found'); + } +} diff --git a/kirby/src/Cms/Content.php b/kirby/src/Cms/Content.php new file mode 100644 index 0000000..a9cbf86 --- /dev/null +++ b/kirby/src/Cms/Content.php @@ -0,0 +1,266 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Content +{ + /** + * The raw data array + * + * @var array + */ + protected $data = []; + + /** + * Cached field objects + * Once a field is being fetched + * it is added to this array for + * later reuse + * + * @var array + */ + protected $fields = []; + + /** + * A potential parent object. + * Not necessarily needed. Especially + * for testing, but field methods might + * need it. + * + * @var Model + */ + protected $parent; + + /** + * Magic getter for content fields + * + * @param string $name + * @param array $arguments + * @return \Kirby\Cms\Field + */ + public function __call(string $name, array $arguments = []) + { + return $this->get($name); + } + + /** + * Creates a new Content object + * + * @param array|null $data + * @param object|null $parent + */ + public function __construct(array $data = [], $parent = null) + { + $this->data = $data; + $this->parent = $parent; + } + + /** + * Same as `self::data()` to improve + * `var_dump` output + * + * @see self::data() + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + /** + * Converts the content to a new blueprint + * + * @param string $to + * @return array + */ + public function convertTo(string $to): array + { + // prepare data + $data = []; + $content = $this; + + // blueprints + $old = $this->parent->blueprint(); + $subfolder = dirname($old->name()); + $new = Blueprint::factory($subfolder . '/' . $to, $subfolder . '/default', $this->parent); + + // forms + $oldForm = new Form(['fields' => $old->fields(), 'model' => $this->parent]); + $newForm = new Form(['fields' => $new->fields(), 'model' => $this->parent]); + + // fields + $oldFields = $oldForm->fields(); + $newFields = $newForm->fields(); + + // go through all fields of new template + foreach ($newFields as $newField) { + $name = $newField->name(); + $oldField = $oldFields->get($name); + + // field name and type matches with old template + if ($oldField && $oldField->type() === $newField->type()) { + $data[$name] = $content->get($name)->value(); + } else { + $data[$name] = $newField->default(); + } + } + + // preserve existing fields + return array_merge($this->data, $data); + } + + /** + * Returns the raw data array + * + * @return array + */ + public function data(): array + { + return $this->data; + } + + /** + * Returns all registered field objects + * + * @return array + */ + public function fields(): array + { + foreach ($this->data as $key => $value) { + $this->get($key); + } + return $this->fields; + } + + /** + * Returns either a single field object + * or all registered fields + * + * @param string|null $key + * @return \Kirby\Cms\Field|array + */ + public function get(string $key = null) + { + if ($key === null) { + return $this->fields(); + } + + $key = strtolower($key); + + if (isset($this->fields[$key])) { + return $this->fields[$key]; + } + + // fetch the value no matter the case + $data = $this->data(); + $value = $data[$key] ?? array_change_key_case($data)[$key] ?? null; + + return $this->fields[$key] = new Field($this->parent, $key, $value); + } + + /** + * Checks if a content field is set + * + * @param string $key + * @return bool + */ + public function has(string $key): bool + { + $key = strtolower($key); + $data = array_change_key_case($this->data); + + return isset($data[$key]) === true; + } + + /** + * Returns all field keys + * + * @return array + */ + public function keys(): array + { + return array_keys($this->data()); + } + + /** + * Returns a clone of the content object + * without the fields, specified by the + * passed key(s) + * + * @param string ...$keys + * @return self + */ + public function not(...$keys) + { + $copy = clone $this; + $copy->fields = null; + + foreach ($keys as $key) { + unset($copy->data[$key]); + } + + return $copy; + } + + /** + * Returns the parent + * Site, Page, File or User object + * + * @return \Kirby\Cms\Model + */ + public function parent() + { + return $this->parent; + } + + /** + * Set the parent model + * + * @param \Kirby\Cms\Model $parent + * @return self + */ + public function setParent(Model $parent) + { + $this->parent = $parent; + return $this; + } + + /** + * Returns the raw data array + * + * @see self::data() + * @return array + */ + public function toArray(): array + { + return $this->data(); + } + + /** + * Updates the content and returns + * a cloned object + * + * @param array|null $content + * @param bool $overwrite + * @return self + */ + public function update(array $content = null, bool $overwrite = false) + { + $this->data = $overwrite === true ? (array)$content : array_merge($this->data, (array)$content); + + // clear cache of Field objects + $this->fields = []; + + return $this; + } +} diff --git a/kirby/src/Cms/ContentLock.php b/kirby/src/Cms/ContentLock.php new file mode 100644 index 0000000..7769630 --- /dev/null +++ b/kirby/src/Cms/ContentLock.php @@ -0,0 +1,232 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class ContentLock +{ + /** + * Lock data + * + * @var array + */ + protected $data; + + /** + * The model to manage locking/unlocking for + * + * @var ModelWithContent + */ + protected $model; + + /** + * @param \Kirby\Cms\ModelWithContent $model + */ + public function __construct(ModelWithContent $model) + { + $this->model = $model; + $this->data = $this->kirby()->locks()->get($model); + } + + /** + * Clears the lock unconditionally + * + * @return bool + */ + protected function clearLock(): bool + { + // if no lock exists, skip + if (isset($this->data['lock']) === false) { + return true; + } + + // remove lock + unset($this->data['lock']); + + return $this->kirby()->locks()->set($this->model, $this->data); + } + + /** + * Sets lock with the current user + * + * @return bool + * @throws \Kirby\Exception\DuplicateException + */ + public function create(): bool + { + // check if model is already locked by another user + if ( + isset($this->data['lock']) === true && + $this->data['lock']['user'] !== $this->user()->id() + ) { + $id = ContentLocks::id($this->model); + throw new DuplicateException($id . ' is already locked'); + } + + $this->data['lock'] = [ + 'user' => $this->user()->id(), + 'time' => time() + ]; + + return $this->kirby()->locks()->set($this->model, $this->data); + } + + /** + * Returns either `false` or array with `user`, `email`, + * `time` and `unlockable` keys + * + * @return array|bool + */ + public function get() + { + $data = $this->data['lock'] ?? []; + + if (empty($data) === false && $data['user'] !== $this->user()->id()) { + if ($user = $this->kirby()->user($data['user'])) { + $time = (int)($data['time']); + + return [ + 'user' => $user->id(), + 'email' => $user->email(), + 'time' => $time, + 'unlockable' => ($time + 60) <= time() + ]; + } + + // clear lock if user not found + $this->clearLock(); + } + + return false; + } + + /** + * Returns if the model is locked by another user + * + * @return bool + */ + public function isLocked(): bool + { + $lock = $this->get(); + + if ($lock !== false && $lock['user'] !== $this->user()->id()) { + return true; + } + + return false; + } + + /** + * Returns if the current user's lock has been removed by another user + * + * @return bool + */ + public function isUnlocked(): bool + { + $data = $this->data['unlock'] ?? []; + + return in_array($this->user()->id(), $data) === true; + } + + /** + * Returns the app instance + * + * @return \Kirby\Cms\App + */ + protected function kirby(): App + { + return $this->model->kirby(); + } + + /** + * Removes lock of current user + * + * @return bool + * @throws \Kirby\Exception\LogicException + */ + public function remove(): bool + { + // if no lock exists, skip + if (isset($this->data['lock']) === false) { + return true; + } + + // check if lock was set by another user + if ($this->data['lock']['user'] !== $this->user()->id()) { + throw new LogicException([ + 'fallback' => 'The content lock can only be removed by the user who created it. Use unlock instead.', + 'httpCode' => 409 + ]); + } + + return $this->clearLock(); + } + + /** + * Removes unlock information for current user + * + * @return bool + */ + public function resolve(): bool + { + // if no unlocks exist, skip + if (isset($this->data['unlock']) === false) { + return true; + } + + // remove user from unlock array + $this->data['unlock'] = array_diff( + $this->data['unlock'], + [$this->user()->id()] + ); + + return $this->kirby()->locks()->set($this->model, $this->data); + } + + /** + * Removes current lock and adds lock user to unlock data + * + * @return bool + */ + public function unlock(): bool + { + // if no lock exists, skip + if (isset($this->data['lock']) === false) { + return true; + } + + // add lock user to unlocked data + $this->data['unlock'] = $this->data['unlock'] ?? []; + $this->data['unlock'][] = $this->data['lock']['user']; + + return $this->clearLock(); + } + + /** + * Returns currently authenticated user; + * throws exception if none is authenticated + * + * @return \Kirby\Cms\User + * @throws \Kirby\Exception\PermissionException + */ + protected function user(): User + { + if ($user = $this->kirby()->user()) { + return $user; + } + + throw new PermissionException('No user authenticated.'); + } +} diff --git a/kirby/src/Cms/ContentLocks.php b/kirby/src/Cms/ContentLocks.php new file mode 100644 index 0000000..bd67918 --- /dev/null +++ b/kirby/src/Cms/ContentLocks.php @@ -0,0 +1,228 @@ +, + * Lukas Bestle + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class ContentLocks +{ + /** + * Data from the `.lock` files + * that have been read so far + * cached by `.lock` file path + * + * @var array + */ + protected $data = []; + + /** + * PHP file handles for all currently + * open `.lock` files + * + * @var array + */ + protected $handles = []; + + /** + * Closes the open file handles + * + * @codeCoverageIgnore + */ + public function __destruct() + { + foreach ($this->handles as $file => $handle) { + $this->closeHandle($file); + } + } + + /** + * Removes the file lock and closes the file handle + * + * @param string $file + * @return void + * @throws \Kirby\Exception\Exception + */ + protected function closeHandle(string $file) + { + if (isset($this->handles[$file]) === false) { + return; + } + + $handle = $this->handles[$file]; + $result = flock($handle, LOCK_UN) && fclose($handle); + + if ($result !== true) { + throw new Exception('Unexpected file system error.'); // @codeCoverageIgnore + } + + unset($this->handles[$file]); + } + + /** + * Returns the path to a model's lock file + * + * @param \Kirby\Cms\ModelWithContent $model + * @return string + */ + public static function file(ModelWithContent $model): string + { + return $model->contentFileDirectory() . '/.lock'; + } + + /** + * Returns the lock/unlock data for the specified model + * + * @param \Kirby\Cms\ModelWithContent $model + * @return array + */ + public function get(ModelWithContent $model): array + { + $file = static::file($model); + $id = static::id($model); + + // return from cache if file was already loaded + if (isset($this->data[$file]) === true) { + return $this->data[$file][$id] ?? []; + } + + // first get a handle to ensure a file system lock + $handle = $this->handle($file); + + if (is_resource($handle) === true) { + // read data from file + clearstatcache(); + $filesize = filesize($file); + + if ($filesize > 0) { + // always read the whole file + rewind($handle); + $string = fread($handle, $filesize); + $data = Data::decode($string, 'yaml'); + } + } + + $this->data[$file] = $data ?? []; + + return $this->data[$file][$id] ?? []; + } + + /** + * Returns the file handle to a `.lock` file + * + * @param string $file + * @param bool $create Whether to create the file if it does not exist + * @return resource|null File handle + * @throws \Kirby\Exception\Exception + */ + protected function handle(string $file, bool $create = false) + { + // check for an already open handle + if (isset($this->handles[$file]) === true) { + return $this->handles[$file]; + } + + // don't create a file if not requested + if (is_file($file) !== true && $create !== true) { + return null; + } + + $handle = @fopen($file, 'c+b'); + if (is_resource($handle) === false) { + throw new Exception('Lock file ' . $file . ' could not be opened.'); // @codeCoverageIgnore + } + + // lock the lock file exclusively to prevent changes by other threads + $result = flock($handle, LOCK_EX); + if ($result !== true) { + throw new Exception('Unexpected file system error.'); // @codeCoverageIgnore + } + + return $this->handles[$file] = $handle; + } + + /** + * Returns model ID used as the key for the data array; + * prepended with a slash because the $site otherwise won't have an ID + * + * @param \Kirby\Cms\ModelWithContent $model + * @return string + */ + public static function id(ModelWithContent $model): string + { + return '/' . $model->id(); + } + + /** + * Sets and writes the lock/unlock data for the specified model + * + * @param \Kirby\Cms\ModelWithContent $model + * @param array $data + * @return bool + * @throws \Kirby\Exception\Exception + */ + public function set(ModelWithContent $model, array $data): bool + { + $file = static::file($model); + $id = static::id($model); + $handle = $this->handle($file, true); + + $this->data[$file][$id] = $data; + + // make sure to unset model id entries, + // if no lock data for the model exists + foreach ($this->data[$file] as $id => $data) { + // there is no data for that model whatsoever + if ( + isset($data['lock']) === false && + (isset($data['unlock']) === false || + count($data['unlock']) === 0) + ) { + unset($this->data[$file][$id]); + + // there is empty unlock data, but still lock data + } elseif ( + isset($data['unlock']) === true && + count($data['unlock']) === 0 + ) { + unset($this->data[$file][$id]['unlock']); + } + } + + // there is no data left in the file whatsoever, delete the file + if (count($this->data[$file]) === 0) { + unset($this->data[$file]); + + // close the file handle, otherwise we can't delete it on Windows + $this->closeHandle($file); + + return F::remove($file); + } + + $yaml = Data::encode($this->data[$file], 'yaml'); + + // delete all file contents first + if (rewind($handle) !== true || ftruncate($handle, 0) !== true) { + throw new Exception('Could not write lock file ' . $file . '.'); // @codeCoverageIgnore + } + + // write the new contents + $result = fwrite($handle, $yaml); + if (is_int($result) === false || $result === 0) { + throw new Exception('Could not write lock file ' . $file . '.'); // @codeCoverageIgnore + } + + return true; + } +} diff --git a/kirby/src/Cms/ContentTranslation.php b/kirby/src/Cms/ContentTranslation.php new file mode 100644 index 0000000..cf25974 --- /dev/null +++ b/kirby/src/Cms/ContentTranslation.php @@ -0,0 +1,242 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class ContentTranslation +{ + use Properties; + + /** + * @var string + */ + protected $code; + + /** + * @var array + */ + protected $content; + + /** + * @var string + */ + protected $contentFile; + + /** + * @var Model + */ + protected $parent; + + /** + * @var string + */ + protected $slug; + + /** + * Creates a new translation object + * + * @param array $props + */ + public function __construct(array $props) + { + $this->setRequiredProperties($props, ['parent', 'code']); + $this->setOptionalProperties($props, ['slug', 'content']); + } + + /** + * Improve `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + /** + * Returns the language code of the + * translation + * + * @return string + */ + public function code(): string + { + return $this->code; + } + + /** + * Returns the translation content + * as plain array + * + * @return array + */ + public function content(): array + { + $parent = $this->parent(); + + if ($this->content === null) { + $this->content = $parent->readContent($this->code()); + } + + $content = $this->content; + + // merge with the default content + if ($this->isDefault() === false && $defaultLanguage = $parent->kirby()->defaultLanguage()) { + $default = []; + + if ($defaultTranslation = $parent->translation($defaultLanguage->code())) { + $default = $defaultTranslation->content(); + } + + $content = array_merge($default, $content); + } + + return $content; + } + + /** + * Absolute path to the translation content file + * + * @return string + */ + public function contentFile(): string + { + return $this->contentFile = $this->parent->contentFile($this->code, true); + } + + /** + * Checks if the translation file exists + * + * @return bool + */ + public function exists(): bool + { + return file_exists($this->contentFile()) === true; + } + + /** + * Returns the translation code as id + * + * @return string + */ + public function id(): string + { + return $this->code(); + } + + /** + * Checks if the this is the default translation + * of the model + * + * @return bool + */ + public function isDefault(): bool + { + if ($defaultLanguage = $this->parent->kirby()->defaultLanguage()) { + return $this->code() === $defaultLanguage->code(); + } + + return false; + } + + /** + * Returns the parent page, file or site object + * + * @return \Kirby\Cms\Model + */ + public function parent() + { + return $this->parent; + } + + /** + * @param string $code + * @return self + */ + protected function setCode(string $code) + { + $this->code = $code; + return $this; + } + + /** + * @param array|null $content + * @return self + */ + protected function setContent(array $content = null) + { + $this->content = $content; + return $this; + } + + /** + * @param \Kirby\Cms\Model $parent + * @return self + */ + protected function setParent(Model $parent) + { + $this->parent = $parent; + return $this; + } + + /** + * @param string|null $slug + * @return self + */ + protected function setSlug(string $slug = null) + { + $this->slug = $slug; + return $this; + } + + /** + * Returns the custom translation slug + * + * @return string|null + */ + public function slug(): ?string + { + return $this->slug = $this->slug ?? ($this->content()['slug'] ?? null); + } + + /** + * Merge the old and new data + * + * @param array|null $data + * @param bool $overwrite + * @return self + */ + public function update(array $data = null, bool $overwrite = false) + { + $this->content = $overwrite === true ? (array)$data : array_merge($this->content(), (array)$data); + return $this; + } + + /** + * Converts the most important translation + * props to an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'code' => $this->code(), + 'content' => $this->content(), + 'exists' => $this->exists(), + 'slug' => $this->slug(), + ]; + } +} diff --git a/kirby/src/Cms/Dir.php b/kirby/src/Cms/Dir.php new file mode 100644 index 0000000..7c42c62 --- /dev/null +++ b/kirby/src/Cms/Dir.php @@ -0,0 +1,180 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Dir extends \Kirby\Toolkit\Dir +{ + public static $numSeparator = '_'; + + /** + * Scans the directory and analyzes files, + * content, meta info and children. This is used + * in Page, Site and User objects to fetch all + * relevant information. + * + * @param string $dir + * @param string $contentExtension + * @param array|null $contentIgnore + * @param bool $multilang + * @return array + */ + public static function inventory(string $dir, string $contentExtension = 'txt', array $contentIgnore = null, bool $multilang = false): array + { + $dir = realpath($dir); + + $inventory = [ + 'children' => [], + 'files' => [], + 'template' => 'default', + ]; + + if ($dir === false) { + return $inventory; + } + + $items = Dir::read($dir, $contentIgnore); + + // a temporary store for all content files + $content = []; + + // sort all items naturally to avoid sorting issues later + natsort($items); + + foreach ($items as $item) { + + // ignore all items with a leading dot + if (in_array(substr($item, 0, 1), ['.', '_']) === true) { + continue; + } + + $root = $dir . '/' . $item; + + if (is_dir($root) === true) { + + // extract the slug and num of the directory + if (preg_match('/^([0-9]+)' . static::$numSeparator . '(.*)$/', $item, $match)) { + $num = $match[1]; + $slug = $match[2]; + } else { + $num = null; + $slug = $item; + } + + $inventory['children'][] = [ + 'dirname' => $item, + 'model' => null, + 'num' => $num, + 'root' => $root, + 'slug' => $slug, + ]; + } else { + $extension = pathinfo($item, PATHINFO_EXTENSION); + + switch ($extension) { + case 'htm': + case 'html': + case 'php': + // don't track those files + break; + case $contentExtension: + $content[] = pathinfo($item, PATHINFO_FILENAME); + break; + default: + $inventory['files'][$item] = [ + 'filename' => $item, + 'extension' => $extension, + 'root' => $root, + ]; + } + } + } + + // remove the language codes from all content filenames + if ($multilang === true) { + foreach ($content as $key => $filename) { + $content[$key] = pathinfo($filename, PATHINFO_FILENAME); + } + + $content = array_unique($content); + } + + $inventory = static::inventoryContent($inventory, $content); + $inventory = static::inventoryModels($inventory, $contentExtension, $multilang); + + return $inventory; + } + + /** + * Take all content files, + * remove those who are meta files and + * detect the main content file + * + * @param array $inventory + * @param array $content + * @return array + */ + protected static function inventoryContent(array $inventory, array $content): array + { + + // filter meta files from the content file + if (empty($content) === true) { + $inventory['template'] = 'default'; + return $inventory; + } + + foreach ($content as $contentName) { + + // could be a meta file. i.e. cover.jpg + if (isset($inventory['files'][$contentName]) === true) { + continue; + } + + // it's most likely the template + $inventory['template'] = $contentName; + } + + return $inventory; + } + + /** + * Go through all inventory children + * and inject a model for each + * + * @param array $inventory + * @param string $contentExtension + * @param bool $multilang + * @return array + */ + protected static function inventoryModels(array $inventory, string $contentExtension, bool $multilang = false): array + { + // inject models + if (empty($inventory['children']) === false && empty(Page::$models) === false) { + if ($multilang === true) { + $contentExtension = App::instance()->defaultLanguage()->code() . '.' . $contentExtension; + } + + foreach ($inventory['children'] as $key => $child) { + foreach (Page::$models as $modelName => $modelClass) { + if (file_exists($child['root'] . '/' . $modelName . '.' . $contentExtension) === true) { + $inventory['children'][$key]['model'] = $modelName; + break; + } + } + } + } + + return $inventory; + } +} diff --git a/kirby/src/Cms/Email.php b/kirby/src/Cms/Email.php new file mode 100644 index 0000000..4b78858 --- /dev/null +++ b/kirby/src/Cms/Email.php @@ -0,0 +1,255 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Email +{ + /** + * Options configured through the `email` CMS option + * + * @var array + */ + protected $options; + + /** + * Props for the email object; will be passed to the + * Kirby\Email\Email class + * + * @var array + */ + protected $props; + + /** + * Class constructor + * + * @param string|array $preset Preset name from the config or a simple props array + * @param array $props Props array to override the $preset + */ + public function __construct($preset = [], array $props = []) + { + $this->options = App::instance()->option('email'); + + // build a prop array based on preset and props + $preset = $this->preset($preset); + $this->props = array_merge($preset, $props); + + // add transport settings + if (isset($this->props['transport']) === false) { + $this->props['transport'] = $this->options['transport'] ?? []; + } + + // add predefined beforeSend option + if (isset($this->props['beforeSend']) === false) { + $this->props['beforeSend'] = $this->options['beforeSend'] ?? null; + } + + // transform model objects to values + $this->transformUserSingle('from', 'fromName'); + $this->transformUserSingle('replyTo', 'replyToName'); + $this->transformUserMultiple('to'); + $this->transformUserMultiple('cc'); + $this->transformUserMultiple('bcc'); + $this->transformFile('attachments'); + + // load template for body text + $this->template(); + } + + /** + * Grabs a preset from the options; supports fixed + * prop arrays in case a preset is not needed + * + * @param string|array $preset Preset name or simple prop array + * @return array + * @throws \Kirby\Exception\NotFoundException + */ + protected function preset($preset): array + { + // only passed props, not preset name + if (is_array($preset) === true) { + return $preset; + } + + // preset does not exist + if (isset($this->options['presets'][$preset]) !== true) { + throw new NotFoundException([ + 'key' => 'email.preset.notFound', + 'data' => ['name' => $preset] + ]); + } + + return $this->options['presets'][$preset]; + } + + /** + * Renders the email template(s) and sets the body props + * to the result + * + * @return void + * @throws \Kirby\Exception\NotFoundException + */ + protected function template(): void + { + if (isset($this->props['template']) === true) { + + // prepare data to be passed to template + $data = $this->props['data'] ?? []; + + // check if html/text templates exist + $html = $this->getTemplate($this->props['template'], 'html'); + $text = $this->getTemplate($this->props['template'], 'text'); + + if ($html->exists()) { + $this->props['body'] = [ + 'html' => $html->render($data) + ]; + + if ($text->exists()) { + $this->props['body']['text'] = $text->render($data); + } + + // fallback to single email text template + } elseif ($text->exists()) { + $this->props['body'] = $text->render($data); + } else { + throw new NotFoundException('The email template "' . $this->props['template'] . '" cannot be found'); + } + } + } + + /** + * Returns an email template by name and type + * + * @param string $name Template name + * @param string|null $type `html` or `text` + * @return \Kirby\Cms\Template + */ + protected function getTemplate(string $name, string $type = null) + { + return App::instance()->template('emails/' . $name, $type, 'text'); + } + + /** + * Returns the prop array + * + * @return array + */ + public function toArray(): array + { + return $this->props; + } + + /** + * Transforms file object(s) to an array of file roots; + * supports simple strings, file objects or collections/arrays of either + * + * @param string $prop Prop to transform + * @return void + */ + protected function transformFile(string $prop): void + { + $this->props[$prop] = $this->transformModel($prop, 'Kirby\Cms\File', 'root'); + } + + /** + * Transforms Kirby models to a simplified collection + * + * @param string $prop Prop to transform + * @param string $class Fully qualified class name of the supported model + * @param string $contentValue Model method that returns the array value + * @param string|null $contentKey Optional model method that returns the array key; + * returns a simple value-only array if not given + * @return array Simple key-value or just value array with the transformed prop data + */ + protected function transformModel(string $prop, string $class, string $contentValue, string $contentKey = null): array + { + $value = $this->props[$prop] ?? []; + + // ensure consistent input by making everything an iterable value + if (is_iterable($value) !== true) { + $value = [$value]; + } + + $result = []; + foreach ($value as $key => $item) { + if (is_string($item) === true) { + // value is already a string + if ($contentKey !== null && is_string($key) === true) { + $result[$key] = $item; + } else { + $result[] = $item; + } + } elseif (is_a($item, $class) === true) { + // value is a model object, get value through content method(s) + if ($contentKey !== null) { + $result[(string)$item->$contentKey()] = (string)$item->$contentValue(); + } else { + $result[] = (string)$item->$contentValue(); + } + } else { + // invalid input + throw new InvalidArgumentException('Invalid input for prop "' . $prop . '", expected string or "' . $class . '" object or collection'); + } + } + + return $result; + } + + /** + * Transforms an user object to the email address and name; + * supports simple strings, user objects or collections/arrays of either + * (note: only the first item in a collection/array will be used) + * + * @param string $addressProp Prop with the email address + * @param string $nameProp Prop with the name corresponding to the $addressProp + * @return void + */ + protected function transformUserSingle(string $addressProp, string $nameProp): void + { + $result = $this->transformModel($addressProp, 'Kirby\Cms\User', 'name', 'email'); + + $address = array_keys($result)[0] ?? null; + $name = $result[$address] ?? null; + + // if the array is non-associative, the value is the address + if (is_int($address) === true) { + $address = $name; + $name = null; + } + + // always use the address as we have transformed that prop above + $this->props[$addressProp] = $address; + + // only use the name from the user if no custom name was set + if (isset($this->props[$nameProp]) === false || $this->props[$nameProp] === null) { + $this->props[$nameProp] = $name; + } + } + + /** + * Transforms user object(s) to the email address(es) and name(s); + * supports simple strings, user objects or collections/arrays of either + * + * @param string $prop Prop to transform + * @return void + */ + protected function transformUserMultiple(string $prop): void + { + $this->props[$prop] = $this->transformModel($prop, 'Kirby\Cms\User', 'name', 'email'); + } +} diff --git a/kirby/src/Cms/Event.php b/kirby/src/Cms/Event.php new file mode 100644 index 0000000..deaccdc --- /dev/null +++ b/kirby/src/Cms/Event.php @@ -0,0 +1,289 @@ +trigger()` + * or `$kirby->apply()` methods are called. It collects all + * event information and handles calling the individual hooks. + * + * @package Kirby Cms + * @author Lukas Bestle , + * Ahmet Bora + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Event +{ + /** + * The full event name + * (e.g. `page.create:after`) + * + * @var string + */ + protected $name; + + /** + * The event type + * (e.g. `page` in `page.create:after`) + * + * @var string + */ + protected $type; + + /** + * The event action + * (e.g. `create` in `page.create:after`) + * + * @var string|null + */ + protected $action; + + /** + * The event state + * (e.g. `after` in `page.create:after`) + * + * @var string|null + */ + protected $state; + + /** + * The event arguments + * + * @var array + */ + protected $arguments = []; + + /** + * Class constructor + * + * @param string $name Full event name + * @param array $arguments Associative array of named event arguments + */ + public function __construct(string $name, array $arguments = []) + { + // split the event name into `$type.$action:$state` + // $action and $state are optional; + // if there is more than one dot, $type will be greedy + $regex = '/^(?.+?)(?:\.(?[^.]*?))?(?:\:(?.*))?$/'; + preg_match($regex, $name, $matches, PREG_UNMATCHED_AS_NULL); + + $this->name = $name; + $this->type = $matches['type']; + $this->action = $matches['action'] ?? null; + $this->state = $matches['state'] ?? null; + $this->arguments = $arguments; + } + + /** + * Magic caller for event arguments + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + return $this->argument($method); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + /** + * Makes it possible to simply echo + * or stringify the entire object + * + * @return string + */ + public function __toString(): string + { + return $this->toString(); + } + + /** + * Returns the action of the event (e.g. `create`) + * or `null` if the event name does not include an action + * + * @return string|null + */ + public function action(): ?string + { + return $this->action; + } + + /** + * Returns a specific event argument + * + * @param string $name + * @return mixed + */ + public function argument(string $name) + { + if (isset($this->arguments[$name]) === true) { + return $this->arguments[$name]; + } + + return null; + } + + /** + * Returns the arguments of the event + * + * @return array + */ + public function arguments(): array + { + return $this->arguments; + } + + /** + * Calls a hook with the event data and returns + * the hook's return value + * + * @param object|null $bind Optional object to bind to the hook function + * @param \Closure $hook + * @return mixed + */ + public function call($bind = null, Closure $hook) + { + // collect the list of possible hook arguments + $data = $this->arguments(); + $data['event'] = $this; + + // magically call the hook with the arguments it requested + $hook = new Controller($hook); + return $hook->call($bind, $data); + } + + /** + * Returns the full name of the event + * + * @return string + */ + public function name(): string + { + return $this->name; + } + + /** + * Returns the full list of possible wildcard + * event names based on the current event name + * + * @return array + */ + public function nameWildcards(): array + { + // if the event is already a wildcard event, no further variation is possible + if ($this->type === '*' || $this->action === '*' || $this->state === '*') { + return []; + } + + if ($this->action !== null && $this->state !== null) { + // full $type.$action:$state event + + return [ + $this->type . '.*:' . $this->state, + $this->type . '.' . $this->action . ':*', + $this->type . '.*:*', + '*.' . $this->action . ':' . $this->state, + '*.' . $this->action . ':*', + '*:' . $this->state, + '*' + ]; + } elseif ($this->state !== null) { + // event without action: $type:$state + + return [ + $this->type . ':*', + '*:' . $this->state, + '*' + ]; + } elseif ($this->action !== null) { + // event without state: $type.$action + + return [ + $this->type . '.*', + '*.' . $this->action, + '*' + ]; + } else { + // event with a simple name + + return ['*']; + } + } + + /** + * Returns the state of the event (e.g. `after`) + * + * @return string|null + */ + public function state(): ?string + { + return $this->state; + } + + /** + * Returns the event data as array + * + * @return array + */ + public function toArray(): array + { + return [ + 'name' => $this->name, + 'arguments' => $this->arguments + ]; + } + + /** + * Returns the event name as string + * + * @return string + */ + public function toString(): string + { + return $this->name; + } + + /** + * Returns the type of the event (e.g. `page`) + * + * @return string + */ + public function type(): string + { + return $this->type; + } + + /** + * Updates a given argument with a new value + * + * @internal + * @param string $name + * @param mixed $value + * @return void + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function updateArgument(string $name, $value): void + { + if (array_key_exists($name, $this->arguments) !== true) { + throw new InvalidArgumentException('The argument ' . $name . ' does not exist'); + } + + $this->arguments[$name] = $value; + } +} diff --git a/kirby/src/Cms/Field.php b/kirby/src/Cms/Field.php new file mode 100644 index 0000000..e6632e1 --- /dev/null +++ b/kirby/src/Cms/Field.php @@ -0,0 +1,257 @@ +myField()->lower(); + * ``` + * + * @package Kirby Cms + * @author Bastian Allgeier + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Field +{ + /** + * Field method aliases + * + * @var array + */ + public static $aliases = []; + + /** + * The field name + * + * @var string + */ + protected $key; + + /** + * Registered field methods + * + * @var array + */ + public static $methods = []; + + /** + * The parent object if available. + * This will be the page, site, user or file + * to which the content belongs + * + * @var Model + */ + protected $parent; + + /** + * The value of the field + * + * @var mixed + */ + public $value; + + /** + * Magic caller for field methods + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + $method = strtolower($method); + + if (isset(static::$methods[$method]) === true) { + return static::$methods[$method](clone $this, ...$arguments); + } + + if (isset(static::$aliases[$method]) === true) { + $method = strtolower(static::$aliases[$method]); + + if (isset(static::$methods[$method]) === true) { + return static::$methods[$method](clone $this, ...$arguments); + } + } + + return $this; + } + + /** + * Creates a new field object + * + * @param object|null $parent + * @param string $key + * @param mixed $value + */ + public function __construct($parent = null, string $key, $value) + { + $this->key = $key; + $this->value = $value; + $this->parent = $parent; + } + + /** + * Simplifies the var_dump result + * + * @see Field::toArray + * @return array + */ + public function __debugInfo() + { + return $this->toArray(); + } + + /** + * Makes it possible to simply echo + * or stringify the entire object + * + * @see Field::toString + * @return string + */ + public function __toString(): string + { + return $this->toString(); + } + + /** + * Checks if the field exists in the content data array + * + * @return bool + */ + public function exists(): bool + { + return $this->parent->content()->has($this->key); + } + + /** + * Checks if the field content is empty + * + * @return bool + */ + public function isEmpty(): bool + { + return empty($this->value) === true && in_array($this->value, [0, '0', false], true) === false; + } + + /** + * Checks if the field content is not empty + * + * @return bool + */ + public function isNotEmpty(): bool + { + return $this->isEmpty() === false; + } + + /** + * Returns the name of the field + * + * @return string + */ + public function key(): string + { + return $this->key; + } + + /** + * @see Field::parent() + * @return \Kirby\Cms\Model|null + */ + public function model() + { + return $this->parent; + } + + /** + * Provides a fallback if the field value is empty + * + * @param mixed $fallback + * @return self + */ + public function or($fallback = null) + { + if ($this->isNotEmpty()) { + return $this; + } + + if (is_a($fallback, 'Kirby\Cms\Field') === true) { + return $fallback; + } + + $field = clone $this; + $field->value = $fallback; + return $field; + } + + /** + * Returns the parent object of the field + * + * @return \Kirby\Cms\Model|null + */ + public function parent() + { + return $this->parent; + } + + /** + * Converts the Field object to an array + * + * @return array + */ + public function toArray(): array + { + return [$this->key => $this->value]; + } + + /** + * Returns the field value as string + * + * @return string + */ + public function toString(): string + { + return (string)$this->value; + } + + /** + * Returns the field content. If a new value is passed, + * the modified field will be returned. Otherwise it + * will return the field value. + * + * @param string|\Closure $value + * @return mixed + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function value($value = null) + { + if ($value === null) { + return $this->value; + } + + if (is_scalar($value)) { + $value = (string)$value; + } elseif (is_callable($value)) { + $value = (string)$value->call($this, $this->value); + } else { + throw new InvalidArgumentException('Invalid field value type: ' . gettype($value)); + } + + $clone = clone $this; + $clone->value = $value; + + return $clone; + } +} diff --git a/kirby/src/Cms/File.php b/kirby/src/Cms/File.php new file mode 100644 index 0000000..45dda5e --- /dev/null +++ b/kirby/src/Cms/File.php @@ -0,0 +1,785 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class File extends ModelWithContent +{ + const CLASS_ALIAS = 'file'; + + use FileActions; + use FileFoundation; + use FileModifications; + use HasMethods; + use HasSiblings; + + /** + * The parent asset object + * This is used to do actual file + * method calls, like size, mime, etc. + * + * @var \Kirby\Image\Image + */ + protected $asset; + + /** + * Cache for the initialized blueprint object + * + * @var \Kirby\Cms\FileBlueprint + */ + protected $blueprint; + + /** + * @var string + */ + protected $id; + + /** + * @var string + */ + protected $filename; + + /** + * All registered file methods + * + * @var array + */ + public static $methods = []; + + /** + * The parent object + * + * @var \Kirby\Cms\Model + */ + protected $parent; + + /** + * The absolute path to the file + * + * @var string|null + */ + protected $root; + + /** + * @var string + */ + protected $template; + + /** + * The public file Url + * + * @var string + */ + protected $url; + + /** + * Magic caller for file methods + * and content fields. (in this order) + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + // public property access + if (isset($this->$method) === true) { + return $this->$method; + } + + // asset method proxy + if (method_exists($this->asset(), $method)) { + return $this->asset()->$method(...$arguments); + } + + // file methods + if ($this->hasMethod($method)) { + return $this->callMethod($method, $arguments); + } + + // content fields + return $this->content()->get($method, $arguments); + } + + /** + * Creates a new File object + * + * @param array $props + */ + public function __construct(array $props) + { + // properties + $this->setProperties($props); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return array_merge($this->toArray(), [ + 'content' => $this->content(), + 'siblings' => $this->siblings(), + ]); + } + + /** + * Returns the url to api endpoint + * + * @internal + * @param bool $relative + * @return string + */ + public function apiUrl(bool $relative = false): string + { + return $this->parent()->apiUrl($relative) . '/files/' . $this->filename(); + } + + /** + * Returns the Image object + * + * @internal + * @return \Kirby\Image\Image + */ + public function asset() + { + return $this->asset = $this->asset ?? new Image($this->root()); + } + + /** + * Returns the FileBlueprint object for the file + * + * @return \Kirby\Cms\FileBlueprint + */ + public function blueprint() + { + if (is_a($this->blueprint, 'Kirby\Cms\FileBlueprint') === true) { + return $this->blueprint; + } + + return $this->blueprint = FileBlueprint::factory('files/' . $this->template(), 'files/default', $this); + } + + /** + * Store the template in addition to the + * other content. + * + * @internal + * @param array $data + * @param string|null $languageCode + * @return array + */ + public function contentFileData(array $data, string $languageCode = null): array + { + return A::append($data, [ + 'template' => $this->template(), + ]); + } + + /** + * Returns the directory in which + * the content file is located + * + * @internal + * @return string + */ + public function contentFileDirectory(): string + { + return dirname($this->root()); + } + + /** + * Filename for the content file + * + * @internal + * @return string + */ + public function contentFileName(): string + { + return $this->filename(); + } + + /** + * Provides a kirbytag or markdown + * tag for the file, which will be + * used in the panel, when the file + * gets dragged onto a textarea + * + * @internal + * @param string|null $type (null|auto|kirbytext|markdown) + * @param bool $absolute + * @return string + */ + public function dragText(string $type = null, bool $absolute = false): string + { + $type = $this->dragTextType($type); + $url = $absolute ? $this->id() : $this->filename(); + + if ($dragTextFromCallback = $this->dragTextFromCallback($type, $url)) { + return $dragTextFromCallback; + } + + if ($type === 'markdown') { + if ($this->type() === 'image') { + return '![' . $this->alt() . '](' . $url . ')'; + } else { + return '[' . $this->filename() . '](' . $url . ')'; + } + } else { + if ($this->type() === 'image') { + return '(image: ' . $url . ')'; + } else { + return '(file: ' . $url . ')'; + } + } + } + + /** + * Constructs a File object + * + * @internal + * @param mixed $props + * @return self + */ + public static function factory($props) + { + return new static($props); + } + + /** + * Returns the filename with extension + * + * @return string + */ + public function filename(): string + { + return $this->filename; + } + + /** + * Returns the parent Files collection + * + * @return \Kirby\Cms\Files + */ + public function files() + { + return $this->siblingsCollection(); + } + + /** + * Returns the id + * + * @return string + */ + public function id(): string + { + if ($this->id !== null) { + return $this->id; + } + + if (is_a($this->parent(), 'Kirby\Cms\Page') === true) { + return $this->id = $this->parent()->id() . '/' . $this->filename(); + } elseif (is_a($this->parent(), 'Kirby\Cms\User') === true) { + return $this->id = $this->parent()->id() . '/' . $this->filename(); + } + + return $this->id = $this->filename(); + } + + /** + * Compares the current object with the given file object + * + * @param \Kirby\Cms\File $file + * @return bool + */ + public function is(File $file): bool + { + return $this->id() === $file->id(); + } + + /** + * Check if the file can be read by the current user + * + * @return bool + */ + public function isReadable(): bool + { + static $readable = []; + + $template = $this->template(); + + if (isset($readable[$template]) === true) { + return $readable[$template]; + } + + return $readable[$template] = $this->permissions()->can('read'); + } + + /** + * Creates a unique media hash + * + * @internal + * @return string + */ + public function mediaHash(): string + { + return $this->mediaToken() . '-' . $this->modifiedFile(); + } + + /** + * Returns the absolute path to the file in the public media folder + * + * @internal + * @return string + */ + public function mediaRoot(): string + { + return $this->parent()->mediaRoot() . '/' . $this->mediaHash() . '/' . $this->filename(); + } + + /** + * Creates a non-guessable token string for this file + * + * @internal + * @return string + */ + public function mediaToken(): string + { + $token = $this->kirby()->contentToken($this, $this->id()); + return substr($token, 0, 10); + } + + /** + * Returns the absolute Url to the file in the public media folder + * + * @internal + * @return string + */ + public function mediaUrl(): string + { + return $this->parent()->mediaUrl() . '/' . $this->mediaHash() . '/' . $this->filename(); + } + + /** + * @deprecated 3.0.0 Use `File::content()` instead + * + * @return \Kirby\Cms\Content + * @codeCoverageIgnore + */ + public function meta() + { + deprecated('$file->meta() is deprecated, use $file->content() instead. $file->meta() will be removed in Kirby 3.5.0.'); + + return $this->content(); + } + + /** + * Get the file's last modification time. + * + * @param string|null $format + * @param string|null $handler date or strftime + * @param string|null $languageCode + * @return mixed + */ + public function modified(string $format = null, string $handler = null, string $languageCode = null) + { + $file = $this->modifiedFile(); + $content = $this->modifiedContent($languageCode); + $modified = max($file, $content); + + if (is_null($format) === true) { + return $modified; + } + + $handler = $handler ?? $this->kirby()->option('date.handler', 'date'); + + return $handler($format, $modified); + } + + /** + * Timestamp of the last modification + * of the content file + * + * @param string|null $languageCode + * @return int + */ + protected function modifiedContent(string $languageCode = null): int + { + return F::modified($this->contentFile($languageCode)); + } + + /** + * Timestamp of the last modification + * of the source file + * + * @return int + */ + protected function modifiedFile(): int + { + return F::modified($this->root()); + } + + /** + * Returns the parent Page object + * + * @return \Kirby\Cms\Page|null + */ + public function page() + { + return is_a($this->parent(), 'Kirby\Cms\Page') === true ? $this->parent() : null; + } + + /** + * Panel icon definition + * + * @internal + * @param array|null $params + * @return array + */ + public function panelIcon(array $params = null): array + { + $colorBlue = '#81a2be'; + $colorPurple = '#b294bb'; + $colorOrange = '#de935f'; + $colorGreen = '#a7bd68'; + $colorAqua = '#8abeb7'; + $colorYellow = '#f0c674'; + $colorRed = '#d16464'; + $colorWhite = '#c5c9c6'; + + $types = [ + 'image' => ['color' => $colorOrange, 'type' => 'file-image'], + 'video' => ['color' => $colorYellow, 'type' => 'file-video'], + 'document' => ['color' => $colorRed, 'type' => 'file-document'], + 'audio' => ['color' => $colorAqua, 'type' => 'file-audio'], + 'code' => ['color' => $colorBlue, 'type' => 'file-code'], + 'archive' => ['color' => $colorWhite, 'type' => 'file-zip'], + ]; + + $extensions = [ + 'indd' => ['color' => $colorPurple], + 'xls' => ['color' => $colorGreen, 'type' => 'file-spreadsheet'], + 'xlsx' => ['color' => $colorGreen, 'type' => 'file-spreadsheet'], + 'csv' => ['color' => $colorGreen, 'type' => 'file-spreadsheet'], + 'docx' => ['color' => $colorBlue, 'type' => 'file-word'], + 'doc' => ['color' => $colorBlue, 'type' => 'file-word'], + 'rtf' => ['color' => $colorBlue, 'type' => 'file-word'], + 'mdown' => ['type' => 'file-text'], + 'md' => ['type' => 'file-text'] + ]; + + $definition = array_merge($types[$this->type()] ?? [], $extensions[$this->extension()] ?? []); + + $params['type'] = $definition['type'] ?? 'file'; + $params['color'] = $definition['color'] ?? $colorWhite; + + return parent::panelIcon($params); + } + + /** + * Returns the image file object based on provided query + * + * @internal + * @param string|null $query + * @return \Kirby\Cms\File|\Kirby\Cms\Asset|null + */ + protected function panelImageSource(string $query = null) + { + if ($query === null && $this->isViewable()) { + return $this; + } + + return parent::panelImageSource($query); + } + + /** + * Returns the full path without leading slash + * + * @internal + * @return string + */ + public function panelPath(): string + { + return 'files/' . $this->filename(); + } + + /** + * Prepares the response data for file pickers + * and file fields + * + * @param array|null $params + * @return array + */ + public function panelPickerData(array $params = []): array + { + $image = $this->panelImage($params['image'] ?? []); + $icon = $this->panelIcon($image); + $uuid = $this->id(); + + if (empty($params['model']) === false) { + $uuid = $this->parent() === $params['model'] ? $this->filename() : $this->id(); + $absolute = $this->parent() !== $params['model']; + } + + return [ + 'filename' => $this->filename(), + 'dragText' => $this->dragText('auto', $absolute ?? false), + 'icon' => $icon, + 'id' => $this->id(), + 'image' => $image, + 'info' => $this->toString($params['info'] ?? false), + 'link' => $this->panelUrl(true), + 'text' => $this->toString($params['text'] ?? '{{ file.filename }}'), + 'type' => $this->type(), + 'url' => $this->url(), + 'uuid' => $uuid, + ]; + } + + /** + * Returns the url to the editing view + * in the panel + * + * @internal + * @param bool $relative + * @return string + */ + public function panelUrl(bool $relative = false): string + { + return $this->parent()->panelUrl($relative) . '/' . $this->panelPath(); + } + + /** + * Returns the parent Model object + * + * @return \Kirby\Cms\Model + */ + public function parent() + { + return $this->parent = $this->parent ?? $this->kirby()->site(); + } + + /** + * Returns the parent id if a parent exists + * + * @internal + * @return string|null + */ + public function parentId(): ?string + { + if ($parent = $this->parent()) { + return $parent->id(); + } + + return null; + } + + /** + * Returns a collection of all parent pages + * + * @return \Kirby\Cms\Pages + */ + public function parents() + { + if (is_a($this->parent(), 'Kirby\Cms\Page') === true) { + return $this->parent()->parents()->prepend($this->parent()->id(), $this->parent()); + } + + return new Pages(); + } + + /** + * Returns the permissions object for this file + * + * @return \Kirby\Cms\FilePermissions + */ + public function permissions() + { + return new FilePermissions($this); + } + + /** + * Returns the absolute root to the file + * + * @return string|null + */ + public function root(): ?string + { + return $this->root = $this->root ?? $this->parent()->root() . '/' . $this->filename(); + } + + /** + * Returns the FileRules class to + * validate any important action. + * + * @return \Kirby\Cms\FileRules + */ + protected function rules() + { + return new FileRules(); + } + + /** + * Sets the Blueprint object + * + * @param array|null $blueprint + * @return self + */ + protected function setBlueprint(array $blueprint = null) + { + if ($blueprint !== null) { + $blueprint['model'] = $this; + $this->blueprint = new FileBlueprint($blueprint); + } + + return $this; + } + + /** + * Sets the filename + * + * @param string $filename + * @return self + */ + protected function setFilename(string $filename) + { + $this->filename = $filename; + return $this; + } + + /** + * Sets the parent model object + * + * @param \Kirby\Cms\Model|null $parent + * @return self + */ + protected function setParent(Model $parent = null) + { + $this->parent = $parent; + return $this; + } + + /** + * Always set the root to null, to invoke + * auto root detection + * + * @param string|null $root + * @return self + */ + protected function setRoot(string $root = null) + { + $this->root = null; + return $this; + } + + /** + * @param string|null $template + * @return self + */ + protected function setTemplate(string $template = null) + { + $this->template = $template; + return $this; + } + + /** + * Sets the url + * + * @param string|null $url + * @return self + */ + protected function setUrl(string $url = null) + { + $this->url = $url; + return $this; + } + + /** + * Returns the parent Files collection + * @internal + * + * @return \Kirby\Cms\Files + */ + protected function siblingsCollection() + { + return $this->parent()->files(); + } + + /** + * Returns the parent Site object + * + * @return \Kirby\Cms\Site + */ + public function site() + { + return is_a($this->parent(), 'Kirby\Cms\Site') === true ? $this->parent() : $this->kirby()->site(); + } + + /** + * Returns the final template + * + * @return string|null + */ + public function template(): ?string + { + return $this->template = $this->template ?? $this->content()->get('template')->value(); + } + + /** + * Returns siblings with the same template + * + * @param bool $self + * @return \Kirby\Cms\Files + */ + public function templateSiblings(bool $self = true) + { + return $this->siblings($self)->filterBy('template', $this->template()); + } + + /** + * Extended info for the array export + * by injecting the information from + * the asset. + * + * @return array + */ + public function toArray(): array + { + return array_merge($this->asset()->toArray(), parent::toArray()); + } + + /** + * Returns the Url + * + * @return string + */ + public function url(): string + { + return $this->url ?? $this->url = $this->kirby()->component('file::url')($this->kirby(), $this); + } +} diff --git a/kirby/src/Cms/FileActions.php b/kirby/src/Cms/FileActions.php new file mode 100644 index 0000000..6b188be --- /dev/null +++ b/kirby/src/Cms/FileActions.php @@ -0,0 +1,320 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait FileActions +{ + /** + * Renames the file without touching the extension + * The store is used to actually execute this. + * + * @param string $name + * @param bool $sanitize + * @return self + * @throws \Kirby\Exception\LogicException + */ + public function changeName(string $name, bool $sanitize = true) + { + if ($sanitize === true) { + $name = F::safeName($name); + } + + // don't rename if not necessary + if ($name === $this->name()) { + return $this; + } + + return $this->commit('changeName', ['file' => $this, 'name' => $name], function ($oldFile, $name) { + $newFile = $oldFile->clone([ + 'filename' => $name . '.' . $oldFile->extension(), + ]); + + if ($oldFile->exists() === false) { + return $newFile; + } + + if ($newFile->exists() === true) { + throw new LogicException('The new file exists and cannot be overwritten'); + } + + // remove the lock of the old file + if ($lock = $oldFile->lock()) { + $lock->remove(); + } + + // remove all public versions + $oldFile->unpublish(); + + // rename the main file + F::move($oldFile->root(), $newFile->root()); + + if ($newFile->kirby()->multilang() === true) { + foreach ($newFile->translations() as $translation) { + $translationCode = $translation->code(); + + // rename the content file + F::move($oldFile->contentFile($translationCode), $newFile->contentFile($translationCode)); + } + } else { + // rename the content file + F::move($oldFile->contentFile(), $newFile->contentFile()); + } + + + return $newFile; + }); + } + + /** + * Changes the file's sorting number in the meta file + * + * @param int $sort + * @return self + */ + public function changeSort(int $sort) + { + return $this->commit('changeSort', ['file' => $this, 'position' => $sort], function ($file, $sort) { + return $file->save(['sort' => $sort]); + }); + } + + /** + * Commits a file action, by following these steps + * + * 1. checks the action rules + * 2. sends the before hook + * 3. commits the store action + * 4. sends the after hook + * 5. returns the result + * + * @param string $action + * @param array $arguments + * @param Closure $callback + * @return mixed + */ + protected function commit(string $action, array $arguments, Closure $callback) + { + $old = $this->hardcopy(); + $kirby = $this->kirby(); + $argumentValues = array_values($arguments); + + $this->rules()->$action(...$argumentValues); + $kirby->trigger('file.' . $action . ':before', $arguments); + + $result = $callback(...$argumentValues); + + if ($action === 'create') { + $argumentsAfter = ['file' => $result]; + } elseif ($action === 'delete') { + $argumentsAfter = ['status' => $result, 'file' => $old]; + } else { + $argumentsAfter = ['newFile' => $result, 'oldFile' => $old]; + } + $kirby->trigger('file.' . $action . ':after', $argumentsAfter); + + $kirby->cache('pages')->flush(); + return $result; + } + + /** + * Copy the file to the given page + * + * @param \Kirby\Cms\Page $page + * @return \Kirby\Cms\File + */ + public function copy(Page $page) + { + F::copy($this->root(), $page->root() . '/' . $this->filename()); + + if ($this->kirby()->multilang() === true) { + foreach ($this->kirby()->languages() as $language) { + $contentFile = $this->contentFile($language->code()); + F::copy($contentFile, $page->root() . '/' . basename($contentFile)); + } + } else { + $contentFile = $this->contentFile(); + F::copy($contentFile, $page->root() . '/' . basename($contentFile)); + } + + return $page->clone()->file($this->filename()); + } + + /** + * Creates a new file on disk and returns the + * File object. The store is used to handle file + * writing, so it can be replaced by any other + * way of generating files. + * + * @param array $props + * @return self + * @throws \Kirby\Exception\InvalidArgumentException + * @throws \Kirby\Exception\LogicException + */ + public static function create(array $props) + { + if (isset($props['source'], $props['parent']) === false) { + throw new InvalidArgumentException('Please provide the "source" and "parent" props for the File'); + } + + // prefer the filename from the props + $props['filename'] = F::safeName($props['filename'] ?? basename($props['source'])); + + $props['model'] = strtolower($props['template'] ?? 'default'); + + // create the basic file and a test upload object + $file = static::factory($props); + $upload = new Image($props['source']); + + // create a form for the file + $form = Form::for($file, [ + 'values' => $props['content'] ?? [] + ]); + + // inject the content + $file = $file->clone(['content' => $form->strings(true)]); + + // run the hook + return $file->commit('create', compact('file', 'upload'), function ($file, $upload) { + + // delete all public versions + $file->unpublish(); + + // overwrite the original + if (F::copy($upload->root(), $file->root(), true) !== true) { + throw new LogicException('The file could not be created'); + } + + // always create pages in the default language + if ($file->kirby()->multilang() === true) { + $languageCode = $file->kirby()->defaultLanguage()->code(); + } else { + $languageCode = null; + } + + // store the content if necessary + $file->save($file->content()->toArray(), $languageCode); + + // add the file to the list of siblings + $file->siblings()->append($file->id(), $file); + + // return a fresh clone + return $file->clone(); + }); + } + + /** + * Deletes the file. The store is used to + * manipulate the filesystem or whatever you prefer. + * + * @return bool + */ + public function delete(): bool + { + return $this->commit('delete', ['file' => $this], function ($file) { + + // remove all versions in the media folder + $file->unpublish(); + + // remove the lock of the old file + if ($lock = $file->lock()) { + $lock->remove(); + } + + if ($file->kirby()->multilang() === true) { + foreach ($file->translations() as $translation) { + F::remove($file->contentFile($translation->code())); + } + } else { + F::remove($file->contentFile()); + } + + F::remove($file->root()); + + // remove the file from the sibling collection + $file->parent()->files()->remove($file); + + return true; + }); + } + + /** + * Move the file to the public media folder + * if it's not already there. + * + * @return self + */ + public function publish() + { + Media::publish($this, $this->mediaRoot()); + return $this; + } + + /** + * @deprecated 3.0.0 Use `File::changeName()` instead + * + * @param string $name + * @param bool $sanitize + * @return self + * @codeCoverageIgnore + */ + public function rename(string $name, bool $sanitize = true) + { + deprecated('$file->rename() is deprecated, use $file->changeName() instead. $file->rename() will be removed in Kirby 3.5.0.'); + + return $this->changeName($name, $sanitize); + } + + /** + * Replaces the file. The source must + * be an absolute path to a file or a Url. + * The store handles the replacement so it + * finally decides what it will support as + * source. + * + * @param string $source + * @return self + * @throws \Kirby\Exception\LogicException + */ + public function replace(string $source) + { + return $this->commit('replace', ['file' => $this, 'upload' => new Image($source)], function ($file, $upload) { + + // delete all public versions + $file->unpublish(); + + // overwrite the original + if (F::copy($upload->root(), $file->root(), true) !== true) { + throw new LogicException('The file could not be created'); + } + + // return a fresh clone + return $file->clone(); + }); + } + + /** + * Remove all public versions of this file + * + * @return self + */ + public function unpublish() + { + Media::unpublish($this->parent()->mediaRoot(), $this); + return $this; + } +} diff --git a/kirby/src/Cms/FileBlueprint.php b/kirby/src/Cms/FileBlueprint.php new file mode 100644 index 0000000..9323e67 --- /dev/null +++ b/kirby/src/Cms/FileBlueprint.php @@ -0,0 +1,79 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class FileBlueprint extends Blueprint +{ + public function __construct(array $props) + { + parent::__construct($props); + + // normalize all available page options + $this->props['options'] = $this->normalizeOptions( + $this->props['options'] ?? true, + // defaults + [ + 'changeName' => null, + 'create' => null, + 'delete' => null, + 'read' => null, + 'replace' => null, + 'update' => null, + ] + ); + + // normalize the accept settings + $this->props['accept'] = $this->normalizeAccept($this->props['accept'] ?? []); + } + + /** + * @return array + */ + public function accept(): array + { + return $this->props['accept']; + } + + /** + * @param mixed $accept + * @return array + */ + protected function normalizeAccept($accept = null): array + { + if (is_string($accept) === true) { + $accept = [ + 'mime' => $accept + ]; + } + + // accept anything + if (empty($accept) === true) { + return []; + } + + $accept = array_change_key_case($accept); + + $defaults = [ + 'mime' => null, + 'maxheight' => null, + 'maxsize' => null, + 'maxwidth' => null, + 'minheight' => null, + 'minsize' => null, + 'minwidth' => null, + 'orientation' => null + ]; + + return array_merge($defaults, $accept); + } +} diff --git a/kirby/src/Cms/FileFoundation.php b/kirby/src/Cms/FileFoundation.php new file mode 100644 index 0000000..058a455 --- /dev/null +++ b/kirby/src/Cms/FileFoundation.php @@ -0,0 +1,248 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait FileFoundation +{ + protected $asset; + protected $root; + protected $url; + + /** + * Magic caller for asset methods + * + * @param string $method + * @param array $arguments + * @return mixed + * @throws \Kirby\Exception\BadMethodCallException + */ + public function __call(string $method, array $arguments = []) + { + // public property access + if (isset($this->$method) === true) { + return $this->$method; + } + + // asset method proxy + if (method_exists($this->asset(), $method)) { + return $this->asset()->$method(...$arguments); + } + + throw new BadMethodCallException('The method: "' . $method . '" does not exist'); + } + + /** + * Constructor sets all file properties + * + * @param array $props + */ + public function __construct(array $props) + { + $this->setProperties($props); + } + + /** + * Converts the file object to a string + * In case of an image, it will create an image tag + * Otherwise it will return the url + * + * @return string + */ + public function __toString(): string + { + if ($this->type() === 'image') { + return $this->html(); + } + + return $this->url(); + } + + /** + * Returns the Image object + * + * @return \Kirby\Image\Image + */ + public function asset() + { + return $this->asset = $this->asset ?? new Image($this->root()); + } + + /** + * Checks if the file exists on disk + * + * @return bool + */ + public function exists(): bool + { + return file_exists($this->root()) === true; + } + + /** + * Returns the file extension + * + * @return string + */ + public function extension(): string + { + return F::extension($this->root()); + } + + /** + * Converts the file to html + * + * @param array $attr + * @return string + */ + public function html(array $attr = []): string + { + if ($this->type() === 'image') { + return Html::img($this->url(), array_merge(['alt' => $this->alt()], $attr)); + } else { + return Html::a($this->url(), $attr); + } + } + + /** + * Checks if the file is a resizable image + * + * @return bool + */ + public function isResizable(): bool + { + $resizable = [ + 'jpg', + 'jpeg', + 'gif', + 'png', + 'webp' + ]; + + return in_array($this->extension(), $resizable) === true; + } + + /** + * Checks if a preview can be displayed for the file + * in the panel or in the frontend + * + * @return bool + */ + public function isViewable(): bool + { + $viewable = [ + 'jpg', + 'jpeg', + 'gif', + 'png', + 'svg', + 'webp' + ]; + + return in_array($this->extension(), $viewable) === true; + } + + /** + * Returns the app instance + * + * @return \Kirby\Cms\App + */ + public function kirby() + { + return App::instance(); + } + + /** + * Get the file's last modification time. + * + * @param string $format + * @param string|null $handler date or strftime + * @return mixed + */ + public function modified(string $format = null, string $handler = null) + { + return F::modified($this->root(), $format, $handler ?? $this->kirby()->option('date.handler', 'date')); + } + + /** + * Returns the absolute path to the file root + * + * @return string|null + */ + public function root(): ?string + { + return $this->root; + } + + /** + * Setter for the root + * + * @param string|null $root + * @return self + */ + protected function setRoot(string $root = null) + { + $this->root = $root; + return $this; + } + + /** + * Setter for the file url + * + * @param string $url + * @return self + */ + protected function setUrl(string $url) + { + $this->url = $url; + return $this; + } + + /** + * Convert the object to an array + * + * @return array + */ + public function toArray(): array + { + $array = array_merge($this->asset()->toArray(), [ + 'isResizable' => $this->isResizable(), + 'url' => $this->url(), + ]); + + ksort($array); + + return $array; + } + + /** + * Returns the file type + * + * @return string|null + */ + public function type(): ?string + { + return F::type($this->root()); + } + + /** + * Returns the absolute url for the file + * + * @return string + */ + public function url(): string + { + return $this->url; + } +} diff --git a/kirby/src/Cms/FileModifications.php b/kirby/src/Cms/FileModifications.php new file mode 100644 index 0000000..e628ab9 --- /dev/null +++ b/kirby/src/Cms/FileModifications.php @@ -0,0 +1,202 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait FileModifications +{ + /** + * Blurs the image by the given amount of pixels + * + * @param bool $pixels + * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File + */ + public function blur($pixels = true) + { + return $this->thumb(['blur' => $pixels]); + } + + /** + * Converts the image to black and white + * + * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File + */ + public function bw() + { + return $this->thumb(['grayscale' => true]); + } + + /** + * Crops the image by the given width and height + * + * @param int $width + * @param int|null $height + * @param string|array $options + * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File + */ + public function crop(int $width, int $height = null, $options = null) + { + $quality = null; + $crop = 'center'; + + if (is_int($options) === true) { + $quality = $options; + } elseif (is_string($options)) { + $crop = $options; + } elseif (is_a($options, 'Kirby\Cms\Field') === true) { + $crop = $options->value(); + } elseif (is_array($options)) { + $quality = $options['quality'] ?? $quality; + $crop = $options['crop'] ?? $crop; + } + + return $this->thumb([ + 'width' => $width, + 'height' => $height, + 'quality' => $quality, + 'crop' => $crop + ]); + } + + /** + * Alias for File::bw() + * + * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File + */ + public function grayscale() + { + return $this->thumb(['grayscale' => true]); + } + + /** + * Alias for File::bw() + * + * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File + */ + public function greyscale() + { + return $this->thumb(['grayscale' => true]); + } + + /** + * Sets the JPEG compression quality + * + * @param int $quality + * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File + */ + public function quality(int $quality) + { + return $this->thumb(['quality' => $quality]); + } + + /** + * Resizes the file with the given width and height + * while keeping the aspect ratio. + * + * @param int|null $width + * @param int|null $height + * @param int|null $quality + * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function resize(int $width = null, int $height = null, int $quality = null) + { + return $this->thumb([ + 'width' => $width, + 'height' => $height, + 'quality' => $quality + ]); + } + + /** + * Create a srcset definition for the given sizes + * Sizes can be defined as a simple array. They can + * also be set up in the config with the thumbs.srcsets option. + * @since 3.1.0 + * + * @param array|string|null $sizes + * @return string|null + */ + public function srcset($sizes = null): ?string + { + if (empty($sizes) === true) { + $sizes = $this->kirby()->option('thumbs.srcsets.default', []); + } + + if (is_string($sizes) === true) { + $sizes = $this->kirby()->option('thumbs.srcsets.' . $sizes, []); + } + + if (is_array($sizes) === false || empty($sizes) === true) { + return null; + } + + $set = []; + + foreach ($sizes as $key => $value) { + if (is_array($value)) { + $options = $value; + $condition = $key; + } elseif (is_string($value) === true) { + $options = [ + 'width' => $key + ]; + $condition = $value; + } else { + $options = [ + 'width' => $value + ]; + $condition = $value . 'w'; + } + + $set[] = $this->thumb($options)->url() . ' ' . $condition; + } + + return implode(', ', $set); + } + + /** + * Creates a modified version of images + * The media manager takes care of generating + * those modified versions and putting them + * in the right place. This is normally the + * `/media` folder of your installation, but + * could potentially also be a CDN or any other + * place. + * + * @param array|null|string $options + * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function thumb($options = null) + { + // thumb presets + if (empty($options) === true) { + $options = $this->kirby()->option('thumbs.presets.default'); + } elseif (is_string($options) === true) { + $options = $this->kirby()->option('thumbs.presets.' . $options); + } + + if (empty($options) === true || is_array($options) === false) { + return $this; + } + + $result = $this->kirby()->component('file::version')($this->kirby(), $this, $options); + + if (is_a($result, 'Kirby\Cms\FileVersion') === false && is_a($result, 'Kirby\Cms\File') === false) { + throw new InvalidArgumentException('The file::version component must return a File or FileVersion object'); + } + + return $result; + } +} diff --git a/kirby/src/Cms/FilePermissions.php b/kirby/src/Cms/FilePermissions.php new file mode 100644 index 0000000..91fca71 --- /dev/null +++ b/kirby/src/Cms/FilePermissions.php @@ -0,0 +1,17 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class FilePermissions extends ModelPermissions +{ + protected $category = 'files'; +} diff --git a/kirby/src/Cms/FilePicker.php b/kirby/src/Cms/FilePicker.php new file mode 100644 index 0000000..4385679 --- /dev/null +++ b/kirby/src/Cms/FilePicker.php @@ -0,0 +1,74 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class FilePicker extends Picker +{ + /** + * Extends the basic defaults + * + * @return array + */ + public function defaults(): array + { + $defaults = parent::defaults(); + $defaults['text'] = '{{ file.filename }}'; + + return $defaults; + } + + /** + * Search all files for the picker + * + * @return \Kirby\Cms\Files|null + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function items() + { + $model = $this->options['model']; + + // find the right default query + if (empty($this->options['query']) === false) { + $query = $this->options['query']; + } elseif (is_a($model, 'Kirby\Cms\File') === true) { + $query = 'file.siblings'; + } else { + $query = $model::CLASS_ALIAS . '.files'; + } + + // fetch all files for the picker + $files = $model->query($query); + + // help mitigate some typical query usage issues + // by converting site and page objects to proper + // pages by returning their children + if (is_a($files, 'Kirby\Cms\Site') === true) { + $files = $files->files(); + } elseif (is_a($files, 'Kirby\Cms\Page') === true) { + $files = $files->files(); + } elseif (is_a($files, 'Kirby\Cms\User') === true) { + $files = $files->files(); + } elseif (is_a($files, 'Kirby\Cms\Files') === false) { + throw new InvalidArgumentException('Your query must return a set of files'); + } + + // search + $files = $this->search($files); + + // paginate + return $this->paginate($files); + } +} diff --git a/kirby/src/Cms/FileRules.php b/kirby/src/Cms/FileRules.php new file mode 100644 index 0000000..889dda1 --- /dev/null +++ b/kirby/src/Cms/FileRules.php @@ -0,0 +1,278 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class FileRules +{ + /** + * Validates if the filename can be changed + * + * @param \Kirby\Cms\File $file + * @param string $name + * @return bool + * @throws \Kirby\Exception\DuplicateException If a file with this name exists + * @throws \Kirby\Exception\PermissionException If the user is not allowed to rename the file + */ + public static function changeName(File $file, string $name): bool + { + if ($file->permissions()->changeName() !== true) { + throw new PermissionException([ + 'key' => 'file.changeName.permission', + 'data' => ['filename' => $file->filename()] + ]); + } + + $parent = $file->parent(); + $duplicate = $parent->files()->not($file)->findBy('filename', $name . '.' . $file->extension()); + + if ($duplicate) { + throw new DuplicateException([ + 'key' => 'file.duplicate', + 'data' => ['filename' => $duplicate->filename()] + ]); + } + + return true; + } + + /** + * Validates if the file can be sorted + * + * @param \Kirby\Cms\File $file + * @param int $sort + * @return bool + */ + public static function changeSort(File $file, int $sort): bool + { + return true; + } + + /** + * Validates if the file can be created + * + * @param \Kirby\Cms\File $file + * @param \Kirby\Image\Image $upload + * @return bool + * @throws \Kirby\Exception\DuplicateException If a file with the same name exists + * @throws \Kirby\Exception\PermissionException If the user is not allowed to create the file + */ + public static function create(File $file, Image $upload): bool + { + if ($file->exists() === true) { + throw new DuplicateException('The file exists and cannot be overwritten'); + } + + if ($file->permissions()->create() !== true) { + throw new PermissionException('The file cannot be created'); + } + + static::validExtension($file, $file->extension()); + static::validMime($file, $upload->mime()); + static::validFilename($file, $file->filename()); + + $upload->match($file->blueprint()->accept()); + + return true; + } + + /** + * Validates if the file can be deleted + * + * @param \Kirby\Cms\File $file + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to delete the file + */ + public static function delete(File $file): bool + { + if ($file->permissions()->delete() !== true) { + throw new PermissionException('The file cannot be deleted'); + } + + return true; + } + + /** + * Validates if the file can be replaced + * + * @param \Kirby\Cms\File $file + * @param \Kirby\Image\Image $upload + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to replace the file + * @throws \Kirby\Exception\InvalidArgumentException If the file type of the new file is different + */ + public static function replace(File $file, Image $upload): bool + { + if ($file->permissions()->replace() !== true) { + throw new PermissionException('The file cannot be replaced'); + } + + static::validMime($file, $upload->mime()); + + if ( + (string)$upload->mime() !== (string)$file->mime() && + (string)$upload->extension() !== (string)$file->extension() + ) { + throw new InvalidArgumentException([ + 'key' => 'file.mime.differs', + 'data' => ['mime' => $file->mime()] + ]); + } + + $upload->match($file->blueprint()->accept()); + + return true; + } + + /** + * Validates if the file can be updated + * + * @param \Kirby\Cms\File $file + * @param array $content + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to update the file + */ + public static function update(File $file, array $content = []): bool + { + if ($file->permissions()->update() !== true) { + throw new PermissionException('The file cannot be updated'); + } + + return true; + } + + /** + * Validates the file extension + * + * @param \Kirby\Cms\File $file + * @param string $extension + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the extension is missing or forbidden + */ + public static function validExtension(File $file, string $extension): bool + { + // make it easier to compare the extension + $extension = strtolower($extension); + + if (empty($extension)) { + throw new InvalidArgumentException([ + 'key' => 'file.extension.missing', + 'data' => ['filename' => $file->filename()] + ]); + } + + if (V::in($extension, ['php', 'html', 'htm', 'exe', App::instance()->contentExtension()])) { + throw new InvalidArgumentException([ + 'key' => 'file.extension.forbidden', + 'data' => ['extension' => $extension] + ]); + } + + if (Str::contains($extension, 'php')) { + throw new InvalidArgumentException([ + 'key' => 'file.type.forbidden', + 'data' => ['type' => 'PHP'] + ]); + } + + if (Str::contains($extension, 'htm')) { + throw new InvalidArgumentException([ + 'key' => 'file.type.forbidden', + 'data' => ['type' => 'HTML'] + ]); + } + + return true; + } + + /** + * Validates the filename + * + * @param \Kirby\Cms\File $file + * @param string $filename + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the filename is missing or forbidden + */ + public static function validFilename(File $file, string $filename) + { + // make it easier to compare the filename + $filename = strtolower($filename); + + // check for missing filenames + if (empty($filename)) { + throw new InvalidArgumentException([ + 'key' => 'file.name.missing' + ]); + } + + // Block htaccess files + if (Str::startsWith($filename, '.ht')) { + throw new InvalidArgumentException([ + 'key' => 'file.type.forbidden', + 'data' => ['type' => 'Apache config'] + ]); + } + + // Block invisible files + if (Str::startsWith($filename, '.')) { + throw new InvalidArgumentException([ + 'key' => 'file.type.forbidden', + 'data' => ['type' => 'invisible'] + ]); + } + + return true; + } + + /** + * Validates the MIME type + * + * @param \Kirby\Cms\File $file + * @param string|null $mime + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the MIME type is missing or forbidden + */ + public static function validMime(File $file, string $mime = null) + { + // make it easier to compare the mime + $mime = strtolower($mime); + + if (empty($mime)) { + throw new InvalidArgumentException([ + 'key' => 'file.mime.missing', + 'data' => ['filename' => $file->filename()] + ]); + } + + if (Str::contains($mime, 'php')) { + throw new InvalidArgumentException([ + 'key' => 'file.type.forbidden', + 'data' => ['type' => 'PHP'] + ]); + } + + if (V::in($mime, ['text/html', 'application/x-msdownload'])) { + throw new InvalidArgumentException([ + 'key' => 'file.mime.forbidden', + 'data' => ['mime' => $mime] + ]); + } + + return true; + } +} diff --git a/kirby/src/Cms/FileVersion.php b/kirby/src/Cms/FileVersion.php new file mode 100644 index 0000000..b9913a5 --- /dev/null +++ b/kirby/src/Cms/FileVersion.php @@ -0,0 +1,143 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class FileVersion +{ + use FileFoundation { + toArray as parentToArray; + } + use Properties; + + protected $modifications; + protected $original; + + /** + * Proxy for public properties, asset methods + * and content field getters + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + // public property access + if (isset($this->$method) === true) { + return $this->$method; + } + + // asset method proxy + if (method_exists($this->asset(), $method)) { + if ($this->exists() === false) { + $this->save(); + } + + return $this->asset()->$method(...$arguments); + } + + if (is_a($this->original(), 'Kirby\Cms\File') === true) { + // content fields + return $this->original()->content()->get($method, $arguments); + } + } + + /** + * Returns the unique ID + * + * @return string + */ + public function id(): string + { + return dirname($this->original()->id()) . '/' . $this->filename(); + } + + /** + * Returns the parent Kirby App instance + * + * @return \Kirby\Cms\App + */ + public function kirby() + { + return $this->original()->kirby(); + } + + /** + * Returns an array with all applied modifications + * + * @return array + */ + public function modifications(): array + { + return $this->modifications ?? []; + } + + /** + * Returns the instance of the original File object + * + * @return mixed + */ + public function original() + { + return $this->original; + } + + /** + * Applies the stored modifications and + * saves the file on disk + * + * @return self + */ + public function save() + { + $this->kirby()->thumb($this->original()->root(), $this->root(), $this->modifications()); + return $this; + } + + /** + * Setter for modifications + * + * @param array|null $modifications + */ + protected function setModifications(array $modifications = null) + { + $this->modifications = $modifications; + } + + /** + * Setter for the original File object + * + * @param $original + */ + protected function setOriginal($original) + { + $this->original = $original; + } + + /** + * Converts the object to an array + * + * @return array + */ + public function toArray(): array + { + $array = array_merge($this->parentToArray(), [ + 'modifications' => $this->modifications(), + ]); + + ksort($array); + + return $array; + } +} diff --git a/kirby/src/Cms/Filename.php b/kirby/src/Cms/Filename.php new file mode 100644 index 0000000..b5e1867 --- /dev/null +++ b/kirby/src/Cms/Filename.php @@ -0,0 +1,303 @@ + 'top left', + * 'width' => 300, + * 'height' => 200 + * 'quality' => 80 + * ]); + * + * echo $filename->toString(); + * // result: some-file-300x200-crop-top-left-q80.jpg + * + * @package Kirby Cms + * @author Bastian Allgeier + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Filename +{ + /** + * List of all applicable attributes + * + * @var array + */ + protected $attributes; + + /** + * The sanitized file extension + * + * @var string + */ + protected $extension; + + /** + * The source original filename + * + * @var string + */ + protected $filename; + + /** + * The sanitized file name + * + * @var string + */ + protected $name; + + /** + * The template for the final name + * + * @var string + */ + protected $template; + + /** + * Creates a new Filename object + * + * @param string $filename + * @param string $template + * @param array $attributes + */ + public function __construct(string $filename, string $template, array $attributes = []) + { + $this->filename = $filename; + $this->template = $template; + $this->attributes = $attributes; + $this->extension = $this->sanitizeExtension(pathinfo($filename, PATHINFO_EXTENSION)); + $this->name = $this->sanitizeName(pathinfo($filename, PATHINFO_FILENAME)); + } + + /** + * Converts the entire object to a string + * + * @return string + */ + public function __toString(): string + { + return $this->toString(); + } + + /** + * Converts all processed attributes + * to an array. The array keys are already + * the shortened versions for the filename + * + * @return array + */ + public function attributesToArray(): array + { + $array = [ + 'dimensions' => implode('x', $this->dimensions()), + 'crop' => $this->crop(), + 'blur' => $this->blur(), + 'bw' => $this->grayscale(), + 'q' => $this->quality(), + ]; + + $array = array_filter($array, function ($item) { + return $item !== null && $item !== false && $item !== ''; + }); + + return $array; + } + + /** + * Converts all processed attributes + * to a string, that can be used in the + * new filename + * + * @param string|null $prefix The prefix will be used in the filename creation + * @return string + */ + public function attributesToString(string $prefix = null): string + { + $array = $this->attributesToArray(); + $result = []; + + foreach ($array as $key => $value) { + if ($value === true) { + $value = ''; + } + + switch ($key) { + case 'dimensions': + $result[] = $value; + break; + case 'crop': + $result[] = ($value === 'center') ? null : $key . '-' . $value; + break; + default: + $result[] = $key . $value; + } + } + + $result = array_filter($result); + $attributes = implode('-', $result); + + if (empty($attributes) === true) { + return ''; + } + + return $prefix . $attributes; + } + + /** + * Normalizes the blur option value + * + * @return false|int + */ + public function blur() + { + $value = $this->attributes['blur'] ?? false; + + if ($value === false) { + return false; + } + + return (int)$value; + } + + /** + * Normalizes the crop option value + * + * @return false|string + */ + public function crop() + { + // get the crop value + $crop = $this->attributes['crop'] ?? false; + + if ($crop === false) { + return false; + } + + return Str::slug($crop); + } + + /** + * Returns a normalized array + * with width and height values + * if available + * + * @return array + */ + public function dimensions() + { + if (empty($this->attributes['width']) === true && empty($this->attributes['height']) === true) { + return []; + } + + return [ + 'width' => $this->attributes['width'] ?? null, + 'height' => $this->attributes['height'] ?? null + ]; + } + + /** + * Returns the sanitized extension + * + * @return string + */ + public function extension(): string + { + return $this->extension; + } + + /** + * Normalizes the grayscale option value + * and also the available ways to write + * the option. You can use `grayscale`, + * `greyscale` or simply `bw`. The function + * will always return `grayscale` + * + * @return bool + */ + public function grayscale(): bool + { + // normalize options + $value = $this->attributes['grayscale'] ?? $this->attributes['greyscale'] ?? $this->attributes['bw'] ?? false; + + // turn anything into boolean + return filter_var($value, FILTER_VALIDATE_BOOLEAN); + } + + /** + * Returns the filename without extension + * + * @return string + */ + public function name(): string + { + return $this->name; + } + + /** + * Normalizes the quality option value + * + * @return false|int + */ + public function quality() + { + $value = $this->attributes['quality'] ?? false; + + if ($value === false || $value === true) { + return false; + } + + return (int)$value; + } + + /** + * Sanitizes the file extension. + * The extension will be converted + * to lowercase and `jpeg` will be + * replaced with `jpg` + * + * @param string $extension + * @return string + */ + protected function sanitizeExtension(string $extension): string + { + $extension = strtolower($extension); + $extension = str_replace('jpeg', 'jpg', $extension); + return $extension; + } + + /** + * Sanitizes the name with Kirby's + * Str::slug function + * + * @param string $name + * @return string + */ + protected function sanitizeName(string $name): string + { + return Str::slug($name); + } + + /** + * Returns the converted filename as string + * + * @return string + */ + public function toString(): string + { + return Str::template($this->template, [ + 'name' => $this->name(), + 'attributes' => $this->attributesToString('-'), + 'extension' => $this->extension() + ], ''); + } +} diff --git a/kirby/src/Cms/Files.php b/kirby/src/Cms/Files.php new file mode 100644 index 0000000..25e28dd --- /dev/null +++ b/kirby/src/Cms/Files.php @@ -0,0 +1,137 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Files extends Collection +{ + /** + * All registered files methods + * + * @var array + */ + public static $methods = []; + + /** + * Adds a single file or + * an entire second collection to the + * current collection + * + * @param mixed $object + * @return self + */ + public function add($object) + { + // add a page collection + if (is_a($object, static::class) === true) { + $this->data = array_merge($this->data, $object->data); + + // add a file by id + } elseif (is_string($object) === true && $file = App::instance()->file($object)) { + $this->__set($file->id(), $file); + + // add a file object + } elseif (is_a($object, 'Kirby\Cms\File') === true) { + $this->__set($object->id(), $object); + } + + return $this; + } + + /** + * Sort all given files by the + * order in the array + * + * @param array $files List of file ids + * @param int $offset Sorting offset + * @return self + */ + public function changeSort(array $files, int $offset = 0) + { + foreach ($files as $filename) { + if ($file = $this->get($filename)) { + $offset++; + $file->changeSort($offset); + } + } + + return $this; + } + + /** + * Creates a files collection from an array of props + * + * @param array $files + * @param \Kirby\Cms\Model $parent + * @return self + */ + public static function factory(array $files, Model $parent) + { + $collection = new static([], $parent); + $kirby = $parent->kirby(); + + foreach ($files as $props) { + $props['collection'] = $collection; + $props['kirby'] = $kirby; + $props['parent'] = $parent; + + $file = File::factory($props); + + $collection->data[$file->id()] = $file; + } + + return $collection; + } + + /** + * Tries to find a file by id/filename + * + * @param string $id + * @return \Kirby\Cms\File|null + */ + public function findById(string $id) + { + return $this->get(ltrim($this->parent->id() . '/' . $id, '/')); + } + + /** + * Alias for FilesFinder::findById() which is + * used internally in the Files collection to + * map the get method correctly. + * + * @param string $key + * @return \Kirby\Cms\File|null + */ + public function findByKey(string $key) + { + return $this->findById($key); + } + + /** + * Filter all files by the given template + * + * @param null|string|array $template + * @return self + */ + public function template($template) + { + if (empty($template) === true) { + return $this; + } + + return $this->filterBy('template', is_array($template) ? 'in' : '==', $template); + } +} diff --git a/kirby/src/Cms/Form.php b/kirby/src/Cms/Form.php new file mode 100644 index 0000000..0bf6e4b --- /dev/null +++ b/kirby/src/Cms/Form.php @@ -0,0 +1,92 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Form extends BaseForm +{ + protected $errors; + protected $fields; + protected $values = []; + + /** + * Form constructor. + * + * @param array $props + */ + public function __construct(array $props) + { + $kirby = App::instance(); + + if ($kirby->multilang() === true) { + $fields = $props['fields'] ?? []; + $languageCode = $props['language'] ?? $kirby->language()->code(); + $isDefaultLanguage = $languageCode === $kirby->defaultLanguage()->code(); + + foreach ($fields as $fieldName => $fieldProps) { + // switch untranslatable fields to readonly + if (($fieldProps['translate'] ?? true) === false && $isDefaultLanguage === false) { + $fields[$fieldName]['unset'] = true; + $fields[$fieldName]['disabled'] = true; + } + } + + $props['fields'] = $fields; + } + + parent::__construct($props); + } + + /** + * @param \Kirby\Cms\Model $model + * @param array $props + * @return self + */ + public static function for(Model $model, array $props = []) + { + // get the original model data + $original = $model->content($props['language'] ?? null)->toArray(); + $values = $props['values'] ?? []; + + // convert closures to values + foreach ($values as $key => $value) { + if (is_a($value, 'Closure') === true) { + $values[$key] = $value($original[$key] ?? null); + } + } + + // set a few defaults + $props['values'] = array_merge($original, $values); + $props['fields'] = $props['fields'] ?? []; + $props['model'] = $model; + + // search for the blueprint + if (method_exists($model, 'blueprint') === true && $blueprint = $model->blueprint()) { + $props['fields'] = $blueprint->fields(); + } + + $ignoreDisabled = $props['ignoreDisabled'] ?? false; + + // REFACTOR: this could be more elegant + if ($ignoreDisabled === true) { + $props['fields'] = array_map(function ($field) { + $field['disabled'] = false; + return $field; + }, $props['fields']); + } + + return new static($props); + } +} diff --git a/kirby/src/Cms/HasChildren.php b/kirby/src/Cms/HasChildren.php new file mode 100644 index 0000000..644b718 --- /dev/null +++ b/kirby/src/Cms/HasChildren.php @@ -0,0 +1,265 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait HasChildren +{ + /** + * The Pages collection + * + * @var \Kirby\Cms\Pages + */ + public $children; + + /** + * The list of available drafts + * + * @var \Kirby\Cms\Pages + */ + public $drafts; + + /** + * Returns the Pages collection + * + * @return \Kirby\Cms\Pages + */ + public function children() + { + if (is_a($this->children, 'Kirby\Cms\Pages') === true) { + return $this->children; + } + + return $this->children = Pages::factory($this->inventory()['children'], $this); + } + + /** + * Returns all children and drafts at the same time + * + * @return \Kirby\Cms\Pages + */ + public function childrenAndDrafts() + { + return $this->children()->merge($this->drafts()); + } + + /** + * Return a list of ids for the model's + * toArray method + * + * @return array + */ + protected function convertChildrenToArray(): array + { + return $this->children()->keys(); + } + + /** + * Searches for a child draft by id + * + * @param string $path + * @return \Kirby\Cms\Page|null + */ + public function draft(string $path) + { + $path = str_replace('_drafts/', '', $path); + + if (Str::contains($path, '/') === false) { + return $this->drafts()->find($path); + } + + $parts = explode('/', $path); + $parent = $this; + + foreach ($parts as $slug) { + if ($page = $parent->find($slug)) { + $parent = $page; + continue; + } + + if ($draft = $parent->drafts()->find($slug)) { + $parent = $draft; + continue; + } + + return null; + } + + return $parent; + } + + /** + * Return all drafts of the model + * + * @return \Kirby\Cms\Pages + */ + public function drafts() + { + if (is_a($this->drafts, 'Kirby\Cms\Pages') === true) { + return $this->drafts; + } + + $kirby = $this->kirby(); + + // create the inventory for all drafts + $inventory = Dir::inventory( + $this->root() . '/_drafts', + $kirby->contentExtension(), + $kirby->contentIgnore(), + $kirby->multilang() + ); + + return $this->drafts = Pages::factory($inventory['children'], $this, true); + } + + /** + * Finds one or multiple children by id + * + * @param string ...$arguments + * @return \Kirby\Cms\Page|\Kirby\Cms\Pages|null + */ + public function find(...$arguments) + { + return $this->children()->find(...$arguments); + } + + /** + * Finds a single page or draft + * + * @param string $path + * @return \Kirby\Cms\Page|null + */ + public function findPageOrDraft(string $path) + { + return $this->children()->find($path) ?? $this->drafts()->find($path); + } + + /** + * Returns a collection of all children of children + * + * @return \Kirby\Cms\Pages + */ + public function grandChildren() + { + return $this->children()->children(); + } + + /** + * Checks if the model has any children + * + * @return bool + */ + public function hasChildren(): bool + { + return $this->children()->count() > 0; + } + + /** + * Checks if the model has any drafts + * + * @return bool + */ + public function hasDrafts(): bool + { + return $this->drafts()->count() > 0; + } + + /** + * @deprecated 3.0.0 Use `Page::hasUnlistedChildren()` instead + * @return bool + * @codeCoverageIgnore + */ + public function hasInvisibleChildren(): bool + { + deprecated('$page->hasInvisibleChildren() is deprecated, use $page->hasUnlistedChildren() instead. $page->hasInvisibleChildren() will be removed in Kirby 3.5.0.'); + + return $this->hasUnlistedChildren(); + } + + /** + * Checks if the page has any listed children + * + * @return bool + */ + public function hasListedChildren(): bool + { + return $this->children()->listed()->count() > 0; + } + + /** + * Checks if the page has any unlisted children + * + * @return bool + */ + public function hasUnlistedChildren(): bool + { + return $this->children()->unlisted()->count() > 0; + } + + /** + * @deprecated 3.0.0 Use `Page::hasListedChildren()` instead + * @return bool + * @codeCoverageIgnore + */ + public function hasVisibleChildren(): bool + { + deprecated('$page->hasVisibleChildren() is deprecated, use $page->hasListedChildren() instead. $page->hasVisibleChildren() will be removed in Kirby 3.5.0.'); + + return $this->hasListedChildren(); + } + + /** + * Creates a flat child index + * + * @param bool $drafts + * @return \Kirby\Cms\Pages + */ + public function index(bool $drafts = false) + { + if ($drafts === true) { + return $this->childrenAndDrafts()->index($drafts); + } else { + return $this->children()->index(); + } + } + + /** + * Sets the Children collection + * + * @param array|null $children + * @return self + */ + protected function setChildren(array $children = null) + { + if ($children !== null) { + $this->children = Pages::factory($children, $this); + } + + return $this; + } + + /** + * Sets the Drafts collection + * + * @param array|null $drafts + * @return self + */ + protected function setDrafts(array $drafts = null) + { + if ($drafts !== null) { + $this->drafts = Pages::factory($drafts, $this, true); + } + + return $this; + } +} diff --git a/kirby/src/Cms/HasFiles.php b/kirby/src/Cms/HasFiles.php new file mode 100644 index 0000000..41048a3 --- /dev/null +++ b/kirby/src/Cms/HasFiles.php @@ -0,0 +1,226 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait HasFiles +{ + /** + * The Files collection + * + * @var \Kirby\Cms\Files + */ + protected $files; + + /** + * Filters the Files collection by type audio + * + * @return \Kirby\Cms\Files + */ + public function audio() + { + return $this->files()->filterBy('type', '==', 'audio'); + } + + /** + * Filters the Files collection by type code + * + * @return \Kirby\Cms\Files + */ + public function code() + { + return $this->files()->filterBy('type', '==', 'code'); + } + + /** + * Returns a list of file ids + * for the toArray method of the model + * + * @return array + */ + protected function convertFilesToArray(): array + { + return $this->files()->keys(); + } + + /** + * Creates a new file + * + * @param array $props + * @return \Kirby\Cms\File + */ + public function createFile(array $props) + { + $props = array_merge($props, [ + 'parent' => $this, + 'url' => null + ]); + + return File::create($props); + } + + /** + * Filters the Files collection by type documents + * + * @return \Kirby\Cms\Files + */ + public function documents() + { + return $this->files()->filterBy('type', '==', 'document'); + } + + /** + * Returns a specific file by filename or the first one + * + * @param string|null $filename + * @param string $in + * @return \Kirby\Cms\File|null + */ + public function file(string $filename = null, string $in = 'files') + { + if ($filename === null) { + return $this->$in()->first(); + } + + if (strpos($filename, '/') !== false) { + $path = dirname($filename); + $filename = basename($filename); + + if ($page = $this->find($path)) { + return $page->$in()->find($filename); + } + + return null; + } + + return $this->$in()->find($filename); + } + + /** + * Returns the Files collection + * + * @return \Kirby\Cms\Files + */ + public function files() + { + if (is_a($this->files, 'Kirby\Cms\Files') === true) { + return $this->files; + } + + return $this->files = Files::factory($this->inventory()['files'], $this); + } + + /** + * Checks if the Files collection has any audio files + * + * @return bool + */ + public function hasAudio(): bool + { + return $this->audio()->count() > 0; + } + + /** + * Checks if the Files collection has any code files + * + * @return bool + */ + public function hasCode(): bool + { + return $this->code()->count() > 0; + } + + /** + * Checks if the Files collection has any document files + * + * @return bool + */ + public function hasDocuments(): bool + { + return $this->documents()->count() > 0; + } + + /** + * Checks if the Files collection has any files + * + * @return bool + */ + public function hasFiles(): bool + { + return $this->files()->count() > 0; + } + + /** + * Checks if the Files collection has any images + * + * @return bool + */ + public function hasImages(): bool + { + return $this->images()->count() > 0; + } + + /** + * Checks if the Files collection has any videos + * + * @return bool + */ + public function hasVideos(): bool + { + return $this->videos()->count() > 0; + } + + /** + * Returns a specific image by filename or the first one + * + * @param string|null $filename + * @return \Kirby\Cms\File|null + */ + public function image(string $filename = null) + { + return $this->file($filename, 'images'); + } + + /** + * Filters the Files collection by type image + * + * @return \Kirby\Cms\Files + */ + public function images() + { + return $this->files()->filterBy('type', '==', 'image'); + } + + /** + * Sets the Files collection + * + * @param \Kirby\Cms\Files|null $files + * @return self + */ + protected function setFiles(array $files = null) + { + if ($files !== null) { + $this->files = Files::factory($files, $this); + } + + return $this; + } + + /** + * Filters the Files collection by type videos + * + * @return \Kirby\Cms\Files + */ + public function videos() + { + return $this->files()->filterBy('type', '==', 'video'); + } +} diff --git a/kirby/src/Cms/HasMethods.php b/kirby/src/Cms/HasMethods.php new file mode 100644 index 0000000..9abe109 --- /dev/null +++ b/kirby/src/Cms/HasMethods.php @@ -0,0 +1,80 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait HasMethods +{ + /** + * All registered methods + * + * @var array + */ + public static $methods = []; + + /** + * Calls a registered method class with the + * passed arguments + * + * @internal + * @param string $method + * @param array $args + * @return mixed + * @throws \Kirby\Exception\BadMethodCallException + */ + public function callMethod(string $method, array $args = []) + { + $closure = $this->getMethod($method); + + if ($closure === null) { + throw new BadMethodCallException('The method ' . $method . ' does not exist'); + } + + return $closure->call($this, ...$args); + } + + /** + * Checks if the object has a registered method + * + * @internal + * @param string $method + * @return bool + */ + public function hasMethod(string $method): bool + { + return $this->getMethod($method) !== null; + } + + /** + * Returns a registered method by name, either from + * the current class or from a parent class ordered by + * inheritance order (top to bottom) + * + * @param string $method + * @return \Closure|null + */ + protected function getMethod(string $method) + { + if (isset(static::$methods[$method]) === true) { + return static::$methods[$method]; + } + + foreach (class_parents($this) as $parent) { + if (isset($parent::$methods[$method]) === true) { + return $parent::$methods[$method]; + } + } + + return null; + } +} diff --git a/kirby/src/Cms/HasSiblings.php b/kirby/src/Cms/HasSiblings.php new file mode 100644 index 0000000..3bc162f --- /dev/null +++ b/kirby/src/Cms/HasSiblings.php @@ -0,0 +1,182 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait HasSiblings +{ + /** + * Returns the position / index in the collection + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return int + */ + public function indexOf($collection = null): int + { + if ($collection === null) { + $collection = $this->siblingsCollection(); + } + + return $collection->indexOf($this); + } + + /** + * Returns the next item in the collection if available + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return \Kirby\Cms\Model|null + */ + public function next($collection = null) + { + if ($collection === null) { + $collection = $this->siblingsCollection(); + } + + return $collection->nth($this->indexOf($collection) + 1); + } + + /** + * Returns the end of the collection starting after the current item + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return \Kirby\Cms\Collection + */ + public function nextAll($collection = null) + { + if ($collection === null) { + $collection = $this->siblingsCollection(); + } + + return $collection->slice($this->indexOf($collection) + 1); + } + + /** + * Returns the previous item in the collection if available + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return \Kirby\Cms\Model|null + */ + public function prev($collection = null) + { + if ($collection === null) { + $collection = $this->siblingsCollection(); + } + + return $collection->nth($this->indexOf($collection) - 1); + } + + /** + * Returns the beginning of the collection before the current item + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return \Kirby\Cms\Collection + */ + public function prevAll($collection = null) + { + if ($collection === null) { + $collection = $this->siblingsCollection(); + } + + return $collection->slice(0, $this->indexOf($collection)); + } + + /** + * Returns all sibling elements + * + * @param bool $self + * @return \Kirby\Cms\Collection + */ + public function siblings(bool $self = true) + { + $siblings = $this->siblingsCollection(); + + if ($self === false) { + return $siblings->not($this); + } + + return $siblings; + } + + /** + * Checks if there's a next item in the collection + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return bool + */ + public function hasNext($collection = null): bool + { + return $this->next($collection) !== null; + } + + /** + * Checks if there's a previous item in the collection + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return bool + */ + public function hasPrev($collection = null): bool + { + return $this->prev($collection) !== null; + } + + /** + * Checks if the item is the first in the collection + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return bool + */ + public function isFirst($collection = null): bool + { + if ($collection === null) { + $collection = $this->siblingsCollection(); + } + + return $collection->first()->is($this); + } + + /** + * Checks if the item is the last in the collection + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return bool + */ + public function isLast($collection = null): bool + { + if ($collection === null) { + $collection = $this->siblingsCollection(); + } + + return $collection->last()->is($this); + } + + /** + * Checks if the item is at a certain position + * + * @param \Kirby\Cms\Collection|null $collection + * @param int $n + * + * @return bool + */ + public function isNth(int $n, $collection = null): bool + { + return $this->indexOf($collection) === $n; + } +} diff --git a/kirby/src/Cms/Html.php b/kirby/src/Cms/Html.php new file mode 100644 index 0000000..634f8de --- /dev/null +++ b/kirby/src/Cms/Html.php @@ -0,0 +1,30 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Html extends \Kirby\Toolkit\Html +{ + /** + * Generates an `a` tag with an absolute Url + * + * @param string|null $href Relative or absolute Url + * @param string|array|null $text If `null`, the link will be used as link text. If an array is passed, each element will be added unencoded + * @param array $attr Additional attributes for the a tag. + * @return string + */ + public static function link(string $href = null, $text = null, array $attr = []): string + { + return parent::link(Url::to($href), $text, $attr); + } +} diff --git a/kirby/src/Cms/Ingredients.php b/kirby/src/Cms/Ingredients.php new file mode 100644 index 0000000..618ed4d --- /dev/null +++ b/kirby/src/Cms/Ingredients.php @@ -0,0 +1,95 @@ +urls()` and `$kirby->roots()` objects. + * Those are configured in `kirby/config/urls.php` + * and `kirby/config/roots.php` + * + * @package Kirby Cms + * @author Bastian Allgeier + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Ingredients +{ + /** + * @var array + */ + protected $ingredients = []; + + /** + * Creates a new ingredient collection + * + * @param array $ingredients + */ + public function __construct(array $ingredients) + { + $this->ingredients = $ingredients; + } + + /** + * Magic getter for single ingredients + * + * @param string $method + * @param array|null $args + * @return mixed + */ + public function __call(string $method, array $args = null) + { + return $this->ingredients[$method] ?? null; + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->ingredients; + } + + /** + * Get a single ingredient by key + * + * @param string $key + * @return mixed + */ + public function __get(string $key) + { + return $this->ingredients[$key] ?? null; + } + + /** + * Resolves all ingredient callbacks + * and creates a plain array + * + * @internal + * @param array $ingredients + * @return self + */ + public static function bake(array $ingredients) + { + foreach ($ingredients as $name => $ingredient) { + if (is_a($ingredient, 'Closure') === true) { + $ingredients[$name] = $ingredient($ingredients); + } + } + + return new static($ingredients); + } + + /** + * Returns all ingredients as plain array + * + * @return array + */ + public function toArray(): array + { + return $this->ingredients; + } +} diff --git a/kirby/src/Cms/KirbyTag.php b/kirby/src/Cms/KirbyTag.php new file mode 100644 index 0000000..86fade3 --- /dev/null +++ b/kirby/src/Cms/KirbyTag.php @@ -0,0 +1,60 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class KirbyTag extends \Kirby\Text\KirbyTag +{ + /** + * Finds a file for the given path. + * The method first searches the file + * in the current parent, if it's a page. + * Afterwards it uses Kirby's global file finder. + * + * @param string $path + * @return \Kirby\Cms\File|null + */ + public function file(string $path) + { + $parent = $this->parent(); + + if (method_exists($parent, 'file') === true && $file = $parent->file($path)) { + return $file; + } + + if (is_a($parent, 'Kirby\Cms\File') === true && $file = $parent->page()->file($path)) { + return $file; + } + + return $this->kirby()->file($path, null, true); + } + + /** + * Returns the current Kirby instance + * + * @return \Kirby\Cms\App + */ + public function kirby() + { + return $this->data['kirby'] ?? App::instance(); + } + + /** + * Returns the parent model + * + * @return \Kirby\Cms\Model|null + */ + public function parent() + { + return $this->data['parent']; + } +} diff --git a/kirby/src/Cms/KirbyTags.php b/kirby/src/Cms/KirbyTags.php new file mode 100644 index 0000000..d92d719 --- /dev/null +++ b/kirby/src/Cms/KirbyTags.php @@ -0,0 +1,45 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class KirbyTags extends \Kirby\Text\KirbyTags +{ + /** + * The KirbyTag rendering class + * + * @var string + */ + protected static $tagClass = 'Kirby\Cms\KirbyTag'; + + /** + * @param string|null $text + * @param array $data + * @param array $options + * @param \Kirby\Cms\App|null $app + * @return string + */ + public static function parse(string $text = null, array $data = [], array $options = [], ?App $app = null): string + { + if ($app !== null) { + $text = $app->apply('kirbytags:before', compact('text', 'data', 'options'), 'text'); + } + + $text = parent::parse($text, $data, $options); + + if ($app !== null) { + $text = $app->apply('kirbytags:after', compact('text', 'data', 'options'), 'text'); + } + + return $text; + } +} diff --git a/kirby/src/Cms/Language.php b/kirby/src/Cms/Language.php new file mode 100644 index 0000000..c597208 --- /dev/null +++ b/kirby/src/Cms/Language.php @@ -0,0 +1,727 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Language extends Model +{ + /** + * @var string + */ + protected $code; + + /** + * @var bool + */ + protected $default; + + /** + * @var string + */ + protected $direction; + + /** + * @var array + */ + protected $locale; + + /** + * @var string + */ + protected $name; + + /** + * @var array|null + */ + protected $slugs; + + /** + * @var array|null + */ + protected $smartypants; + + /** + * @var array|null + */ + protected $translations; + + /** + * @var string + */ + protected $url; + + /** + * Creates a new language object + * + * @param array $props + */ + public function __construct(array $props) + { + $this->setRequiredProperties($props, [ + 'code' + ]); + + $this->setOptionalProperties($props, [ + 'default', + 'direction', + 'locale', + 'name', + 'slugs', + 'smartypants', + 'translations', + 'url', + ]); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + /** + * Returns the language code + * when the language is converted to a string + * + * @return string + */ + public function __toString(): string + { + return $this->code(); + } + + /** + * Returns the base Url for the language + * without the path or other cruft + * + * @return string + */ + public function baseUrl(): string + { + $kirbyUrl = $this->kirby()->url(); + $languageUrl = $this->url(); + + if (empty($this->url)) { + return $kirbyUrl; + } + + if (Str::startsWith($languageUrl, $kirbyUrl) === true) { + return $kirbyUrl; + } + + return Url::base($languageUrl) ?? $kirbyUrl; + } + + /** + * Returns the language code/id. + * The language code is used in + * text file names as appendix. + * + * @return string + */ + public function code(): string + { + return $this->code; + } + + /** + * Internal converter to create or remove + * translation files. + * + * @param string $from + * @param string $to + * @return bool + */ + protected static function converter(string $from, string $to): bool + { + $kirby = App::instance(); + $site = $kirby->site(); + + // convert site + foreach ($site->files() as $file) { + F::move($file->contentFile($from, true), $file->contentFile($to, true)); + } + + F::move($site->contentFile($from, true), $site->contentFile($to, true)); + + // convert all pages + foreach ($kirby->site()->index(true) as $page) { + foreach ($page->files() as $file) { + F::move($file->contentFile($from, true), $file->contentFile($to, true)); + } + + F::move($page->contentFile($from, true), $page->contentFile($to, true)); + } + + // convert all users + foreach ($kirby->users() as $user) { + foreach ($user->files() as $file) { + F::move($file->contentFile($from, true), $file->contentFile($to, true)); + } + + F::move($user->contentFile($from, true), $user->contentFile($to, true)); + } + + return true; + } + + /** + * Creates a new language object + * + * @internal + * @param array $props + * @return self + */ + public static function create(array $props) + { + $props['code'] = Str::slug($props['code'] ?? null); + $kirby = App::instance(); + $languages = $kirby->languages(); + + // make the first language the default language + if ($languages->count() === 0) { + $props['default'] = true; + } + + $language = new static($props); + + // validate the new language + LanguageRules::create($language); + + $language->save(); + + if ($languages->count() === 0) { + static::converter('', $language->code()); + } + + return $language; + } + + /** + * Delete the current language and + * all its translation files + * + * @internal + * @return bool + * @throws \Kirby\Exception\Exception + */ + public function delete(): bool + { + if ($this->exists() === false) { + return true; + } + + $kirby = App::instance(); + $languages = $kirby->languages(); + $code = $this->code(); + + if (F::remove($this->root()) !== true) { + throw new Exception('The language could not be deleted'); + } + + if ($languages->count() === 1) { + return $this->converter($code, ''); + } else { + return $this->deleteContentFiles($code); + } + } + + /** + * When the language is deleted, all content files with + * the language code must be removed as well. + * + * @param mixed $code + * @return bool + */ + protected function deleteContentFiles($code): bool + { + $kirby = App::instance(); + $site = $kirby->site(); + + F::remove($site->contentFile($code, true)); + + foreach ($kirby->site()->index(true) as $page) { + foreach ($page->files() as $file) { + F::remove($file->contentFile($code, true)); + } + + F::remove($page->contentFile($code, true)); + } + + foreach ($kirby->users() as $user) { + foreach ($user->files() as $file) { + F::remove($file->contentFile($code, true)); + } + + F::remove($user->contentFile($code, true)); + } + + return true; + } + + /** + * Reading direction of this language + * + * @return string + */ + public function direction(): string + { + return $this->direction; + } + + /** + * Check if the language file exists + * + * @return bool + */ + public function exists(): bool + { + return file_exists($this->root()); + } + + /** + * Checks if this is the default language + * for the site. + * + * @return bool + */ + public function isDefault(): bool + { + return $this->default; + } + + /** + * The id is required for collections + * to work properly. The code is used as id + * + * @return string + */ + public function id(): string + { + return $this->code; + } + + /** + * Returns the PHP locale setting array + * + * @param int $category If passed, returns the locale for the specified category (e.g. LC_ALL) as string + * @return array|string + */ + public function locale(int $category = null) + { + if ($category !== null) { + return $this->locale[$category] ?? $this->locale[LC_ALL] ?? null; + } else { + return $this->locale; + } + } + + /** + * Returns the locale array but with the locale + * constants replaced with their string representations + * + * @return array + */ + protected function localeExport(): array + { + // list of all possible constant names + $constantNames = [ + 'LC_ALL', 'LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY', + 'LC_NUMERIC', 'LC_TIME', 'LC_MESSAGES' + ]; + + // build an associative array with the locales + // that are actually supported on this system + $constants = []; + foreach ($constantNames as $name) { + if (defined($name) === true) { + $constants[constant($name)] = $name; + } + } + + // replace the keys in the locale data array with the locale names + $return = []; + foreach ($this->locale() as $key => $value) { + if (isset($constants[$key]) === true) { + // the key is a valid constant, + // replace it with its string representation + $return[$constants[$key]] = $value; + } else { + // not found, keep it as-is + $return[$key] = $value; + } + } + + return $return; + } + + /** + * Returns the human-readable name + * of the language + * + * @return string + */ + public function name(): string + { + return $this->name; + } + + /** + * Returns the URL path for the language + * + * @return string + */ + public function path(): string + { + if ($this->url === null) { + return $this->code; + } + + return Url::path($this->url); + } + + /** + * Returns the routing pattern for the language + * + * @return string + */ + public function pattern(): string + { + $path = $this->path(); + + if (empty($path) === true) { + return '(:all)'; + } + + return $path . '/(:all?)'; + } + + /** + * Returns the absolute path to the language file + * + * @return string + */ + public function root(): string + { + return App::instance()->root('languages') . '/' . $this->code() . '.php'; + } + + /** + * Returns the LanguageRouter instance + * which is used to handle language specific + * routes. + * + * @return \Kirby\Cms\LanguageRouter + */ + public function router() + { + return new LanguageRouter($this); + } + + /** + * Get slug rules for language + * + * @internal + * @return array + */ + public function rules(): array + { + $code = $this->locale(LC_CTYPE); + $code = Str::contains($code, '.') ? Str::before($code, '.') : $code; + $file = $this->kirby()->root('i18n:rules') . '/' . $code . '.json'; + + if (F::exists($file) === false) { + $file = $this->kirby()->root('i18n:rules') . '/' . Str::before($code, '_') . '.json'; + } + + try { + $data = Data::read($file); + } catch (\Exception $e) { + $data = []; + } + + return array_merge($data, $this->slugs()); + } + + /** + * Saves the language settings in the languages folder + * + * @internal + * @return self + */ + public function save() + { + try { + $existingData = Data::read($this->root()); + } catch (Throwable $e) { + $existingData = []; + } + + $props = [ + 'code' => $this->code(), + 'default' => $this->isDefault(), + 'direction' => $this->direction(), + 'locale' => $this->localeExport(), + 'name' => $this->name(), + 'translations' => $this->translations(), + 'url' => $this->url, + ]; + + $data = array_merge($existingData, $props); + + ksort($data); + + Data::write($this->root(), $data); + + return $this; + } + + /** + * @param string $code + * @return self + */ + protected function setCode(string $code) + { + $this->code = trim($code); + return $this; + } + + /** + * @param bool $default + * @return self + */ + protected function setDefault(bool $default = false) + { + $this->default = $default; + return $this; + } + + /** + * @param string $direction + * @return self + */ + protected function setDirection(string $direction = 'ltr') + { + $this->direction = $direction === 'rtl' ? 'rtl' : 'ltr'; + return $this; + } + + /** + * @param string|array $locale + * @return self + */ + protected function setLocale($locale = null) + { + if (is_array($locale)) { + // replace string constant keys with the constant values + $convertedLocale = []; + foreach ($locale as $key => $value) { + if (is_string($key) === true && Str::startsWith($key, 'LC_') === true) { + $key = constant($key); + } + + $convertedLocale[$key] = $value; + } + + $this->locale = $convertedLocale; + } elseif (is_string($locale)) { + $this->locale = [LC_ALL => $locale]; + } elseif ($locale === null) { + $this->locale = [LC_ALL => $this->code]; + } else { + throw new InvalidArgumentException('Locale must be string or array'); + } + + return $this; + } + + /** + * @param string $name + * @return self + */ + protected function setName(string $name = null) + { + $this->name = trim($name ?? $this->code); + return $this; + } + + /** + * @param array $slugs + * @return self + */ + protected function setSlugs(array $slugs = null) + { + $this->slugs = $slugs ?? []; + return $this; + } + + /** + * @param array $smartypants + * @return self + */ + protected function setSmartypants(array $smartypants = null) + { + $this->smartypants = $smartypants ?? []; + return $this; + } + + /** + * @param array $translations + * @return self + */ + protected function setTranslations(array $translations = null) + { + $this->translations = $translations ?? []; + return $this; + } + + /** + * @param string $url + * @return self + */ + protected function setUrl(string $url = null) + { + $this->url = $url; + return $this; + } + + /** + * Returns the custom slug rules for this language + * + * @return array + */ + public function slugs(): array + { + return $this->slugs; + } + + /** + * Returns the custom SmartyPants options for this language + * + * @return array + */ + public function smartypants(): array + { + return $this->smartypants; + } + + /** + * Returns the most important + * properties as array + * + * @return array + */ + public function toArray(): array + { + return [ + 'code' => $this->code(), + 'default' => $this->isDefault(), + 'direction' => $this->direction(), + 'locale' => $this->locale(), + 'name' => $this->name(), + 'rules' => $this->rules(), + 'url' => $this->url() + ]; + } + + /** + * Returns the translation strings for this language + * + * @return array + */ + public function translations(): array + { + return $this->translations; + } + + /** + * Returns the absolute Url for the language + * + * @return string + */ + public function url(): string + { + $url = $this->url; + + if ($url === null) { + $url = '/' . $this->code; + } + + return Url::makeAbsolute($url, $this->kirby()->url()); + } + + /** + * Update language properties and save them + * + * @internal + * @param array $props + * @return self + */ + public function update(array $props = null) + { + // don't change the language code + unset($props['code']); + + // make sure the slug is nice and clean + $props['slug'] = Str::slug($props['slug'] ?? null); + + $kirby = App::instance(); + $updated = $this->clone($props); + + // validate the updated language + LanguageRules::update($updated); + + // convert the current default to a non-default language + if ($updated->isDefault() === true) { + if ($oldDefault = $kirby->defaultLanguage()) { + $oldDefault->clone(['default' => false])->save(); + } + + $code = $this->code(); + $site = $kirby->site(); + + touch($site->contentFile($code)); + + foreach ($kirby->site()->index(true) as $page) { + $files = $page->files(); + + foreach ($files as $file) { + touch($file->contentFile($code)); + } + + touch($page->contentFile($code)); + } + } elseif ($this->isDefault() === true) { + throw new PermissionException('Please select another language to be the primary language'); + } + + return $updated->save(); + } +} diff --git a/kirby/src/Cms/LanguageRouter.php b/kirby/src/Cms/LanguageRouter.php new file mode 100644 index 0000000..a770c35 --- /dev/null +++ b/kirby/src/Cms/LanguageRouter.php @@ -0,0 +1,135 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class LanguageRouter +{ + /** + * The parent language + * + * @var Language + */ + protected $language; + + /** + * The router instance + * + * @var Router + */ + protected $router; + + /** + * Creates a new language router instance + * for the given language + * + * @param \Kirby\Cms\Language $language + */ + public function __construct(Language $language) + { + $this->language = $language; + } + + /** + * Fetches all scoped routes for the + * current language from the Kirby instance + * + * @return array + * @throws \Kirby\Exception\NotFoundException + */ + public function routes(): array + { + $language = $this->language; + $kirby = $language->kirby(); + $routes = $kirby->routes(); + + // only keep the scoped language routes + $routes = array_values(array_filter($routes, function ($route) use ($language) { + + // no language scope + if (empty($route['language']) === true) { + return false; + } + + // wildcard + if ($route['language'] === '*') { + return true; + } + + // get all applicable languages + $languages = Str::split(strtolower($route['language']), '|'); + + // validate the language + return in_array($language->code(), $languages) === true; + })); + + // add the page-scope if necessary + foreach ($routes as $index => $route) { + if ($pageId = ($route['page'] ?? null)) { + if ($page = $kirby->page($pageId)) { + + // convert string patterns to arrays + $patterns = A::wrap($route['pattern']); + + // prefix all patterns with the page slug + $patterns = array_map(function ($pattern) use ($page, $language) { + return $page->uri($language) . '/' . $pattern; + }, $patterns); + + // reinject the pattern and the full page object + $routes[$index]['pattern'] = $patterns; + $routes[$index]['page'] = $page; + } else { + throw new NotFoundException('The page "' . $pageId . '" does not exist'); + } + } + } + + return $routes; + } + + /** + * Wrapper around the Router::call method + * that injects the Language instance and + * if needed also the Page as arguments. + * + * @param string|null $path + * @return mixed + */ + public function call(string $path = null) + { + $language = $this->language; + $kirby = $language->kirby(); + $router = new Router($this->routes()); + + try { + return $router->call($path, $kirby->request()->method(), function ($route) use ($kirby, $language) { + $kirby->setCurrentTranslation($language); + $kirby->setCurrentLanguage($language); + + if ($page = $route->page()) { + return $route->action()->call($route, $language, $page, ...$route->arguments()); + } else { + return $route->action()->call($route, $language, ...$route->arguments()); + } + }); + } catch (Exception $e) { + return $kirby->resolve($path, $language->code()); + } + } +} diff --git a/kirby/src/Cms/LanguageRoutes.php b/kirby/src/Cms/LanguageRoutes.php new file mode 100644 index 0000000..c4d418c --- /dev/null +++ b/kirby/src/Cms/LanguageRoutes.php @@ -0,0 +1,154 @@ +url(); + + foreach ($kirby->languages() as $language) { + + // ignore languages with a different base url + if ($language->baseurl() !== $baseurl) { + continue; + } + + $routes[] = [ + 'pattern' => $language->pattern(), + 'method' => 'ALL', + 'env' => 'site', + 'action' => function ($path = null) use ($language) { + if ($result = $language->router()->call($path)) { + return $result; + } + + // jump through to the fallback if nothing + // can be found for this language + $this->next(); + } + ]; + } + + $routes[] = static::fallback($kirby); + + return $routes; + } + + + /** + * Create the fallback route + * for unprefixed default language URLs. + * + * @param \Kirby\Cms\App $kirby + * @return array + */ + public static function fallback(App $kirby): array + { + return [ + 'pattern' => '(:all)', + 'method' => 'ALL', + 'env' => 'site', + 'action' => function (string $path) use ($kirby) { + + // check for content representations or files + $extension = F::extension($path); + + // try to redirect prefixed pages + if (empty($extension) === true && $page = $kirby->page($path)) { + $url = $kirby->request()->url([ + 'query' => null, + 'params' => null, + 'fragment' => null + ]); + + if ($url->toString() !== $page->url()) { + // redirect to translated page directly + // if translation is exists and languages detect is enabled + if ( + $kirby->option('languages.detect') === true && + $page->translation($kirby->detectedLanguage()->code())->exists() === true + ) { + return $kirby + ->response() + ->redirect($page->url($kirby->detectedLanguage()->code())); + } + + return $kirby + ->response() + ->redirect($page->url()); + } + } + + return $kirby->language()->router()->call($path); + } + ]; + } + + /** + * Create the multi-language home page route + * + * @param \Kirby\Cms\App $kirby + * @return array + */ + public static function home(App $kirby): array + { + // Multi-language home + return [ + 'pattern' => '', + 'method' => 'ALL', + 'env' => 'site', + 'action' => function () use ($kirby) { + + // find all languages with the same base url as the current installation + $languages = $kirby->languages()->filterBy('baseurl', $kirby->url()); + + // if there's no language with a matching base url, + // redirect to the default language + if ($languages->count() === 0) { + return $kirby + ->response() + ->redirect($kirby->defaultLanguage()->url()); + } + + // if there's just one language, we take that to render the home page + if ($languages->count() === 1) { + $currentLanguage = $languages->first(); + } else { + $currentLanguage = $kirby->defaultLanguage(); + } + + // language detection on the home page with / as URL + if ($kirby->url() !== $currentLanguage->url()) { + if ($kirby->option('languages.detect') === true) { + return $kirby + ->response() + ->redirect($kirby->detectedLanguage()->url()); + } + + return $kirby + ->response() + ->redirect($currentLanguage->url()); + } + + // render the home page of the current language + return $currentLanguage->router()->call(); + } + ]; + } +} diff --git a/kirby/src/Cms/LanguageRules.php b/kirby/src/Cms/LanguageRules.php new file mode 100644 index 0000000..4588f1b --- /dev/null +++ b/kirby/src/Cms/LanguageRules.php @@ -0,0 +1,98 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class LanguageRules +{ + /** + * Validates if the language can be created + * + * @param \Kirby\Cms\Language $language + * @return bool + * @throws \Kirby\Exception\DuplicateException If the language already exists + */ + public static function create(Language $language): bool + { + static::validLanguageCode($language); + static::validLanguageName($language); + + if ($language->exists() === true) { + throw new DuplicateException([ + 'key' => 'language.duplicate', + 'data' => [ + 'code' => $language->code() + ] + ]); + } + + return true; + } + + /** + * Validates if the language can be updated + * + * @param \Kirby\Cms\Language $language + */ + public static function update(Language $language) + { + static::validLanguageCode($language); + static::validLanguageName($language); + } + + /** + * Validates if the language code is formatted correctly + * + * @param \Kirby\Cms\Language $language + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the language code is not valid + */ + public static function validLanguageCode(Language $language): bool + { + if (Str::length($language->code()) < 2) { + throw new InvalidArgumentException([ + 'key' => 'language.code', + 'data' => [ + 'code' => $language->code(), + 'name' => $language->name() + ] + ]); + } + + return true; + } + + /** + * Validates if the language name is formatted correctly + * + * @param \Kirby\Cms\Language $language + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the language name is invalid + */ + public static function validLanguageName(Language $language): bool + { + if (Str::length($language->name()) < 1) { + throw new InvalidArgumentException([ + 'key' => 'language.name', + 'data' => [ + 'code' => $language->code(), + 'name' => $language->name() + ] + ]); + } + + return true; + } +} diff --git a/kirby/src/Cms/Languages.php b/kirby/src/Cms/Languages.php new file mode 100644 index 0000000..f27349b --- /dev/null +++ b/kirby/src/Cms/Languages.php @@ -0,0 +1,111 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Languages extends Collection +{ + /** + * Creates a new collection with the given language objects + * + * @param array $objects + * @param null $parent + * @throws \Kirby\Exception\DuplicateException + */ + public function __construct($objects = [], $parent = null) + { + $defaults = array_filter($objects, function ($language) { + return $language->isDefault() === true; + }); + + if (count($defaults) > 1) { + throw new DuplicateException('You cannot have multiple default languages. Please check your language config files.'); + } + + parent::__construct($objects, $parent); + } + + /** + * Returns all language codes as array + * + * @return array + */ + public function codes(): array + { + return $this->keys(); + } + + /** + * Creates a new language with the given props + * + * @internal + * @param array $props + * @return \Kirby\Cms\Language + */ + public function create(array $props) + { + return Language::create($props); + } + + /** + * Returns the default language + * + * @return \Kirby\Cms\Language|null + */ + public function default() + { + if ($language = $this->findBy('isDefault', true)) { + return $language; + } else { + return $this->first(); + } + } + + /** + * @deprecated 3.0.0 Use `Languages::default()` instead + * @return \Kirby\Cms\Language|null + * @codeCoverageIgnore + */ + public function findDefault() + { + deprecated('$languages->findDefault() is deprecated, use $languages->default() instead. $languages->findDefault() will be removed in Kirby 3.5.0.'); + + return $this->default(); + } + + /** + * Convert all defined languages to a collection + * + * @internal + * @return self + */ + public static function load() + { + $languages = []; + $files = glob(App::instance()->root('languages') . '/*.php'); + + foreach ($files as $file) { + $props = F::load($file); + + if (is_array($props) === true) { + // inject the language code from the filename if it does not exist + $props['code'] = $props['code'] ?? F::name($file); + + $languages[] = new Language($props); + } + } + + return new static($languages); + } +} diff --git a/kirby/src/Cms/Media.php b/kirby/src/Cms/Media.php new file mode 100644 index 0000000..179bcad --- /dev/null +++ b/kirby/src/Cms/Media.php @@ -0,0 +1,169 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Media +{ + /** + * Tries to find a file by model and filename + * and to copy it to the media folder. + * + * @param \Kirby\Cms\Model|null $model + * @param string $hash + * @param string $filename + * @return \Kirby\Cms\Response|false + */ + public static function link(Model $model = null, string $hash, string $filename) + { + if ($model === null) { + return false; + } + + // fix issues with spaces in filenames + $filename = urldecode($filename); + + // try to find a file by model and filename + // this should work for all original files + if ($file = $model->file($filename)) { + + // check if the request contained an outdated media hash + if ($file->mediaHash() !== $hash) { + // if at least the token was correct, redirect + if (Str::startsWith($hash, $file->mediaToken() . '-') === true) { + return Response::redirect($file->mediaUrl(), 307); + } else { + // don't leak the correct token + return new Response('Not Found', 'text/plain', 404); + } + } + + // send the file to the browser + return Response::file($file->publish()->mediaRoot()); + } + + // try to generate a thumb for the file + return static::thumb($model, $hash, $filename); + } + + /** + * Copy the file to the final media folder location + * + * @param \Kirby\Cms\File $file + * @param string $dest + * @return bool + */ + public static function publish(File $file, string $dest): bool + { + $src = $file->root(); + $version = dirname($dest); + $directory = dirname($version); + + // unpublish all files except stuff in the version folder + Media::unpublish($directory, $file, $version); + + // copy/overwrite the file to the dest folder + return F::copy($src, $dest, true); + } + + /** + * Tries to find a job file for the + * given filename and then calls the thumb + * component to create a thumbnail accordingly + * + * @param \Kirby\Cms\Model|string $model + * @param string $hash + * @param string $filename + * @return \Kirby\Cms\Response|false + */ + public static function thumb($model, string $hash, string $filename) + { + $kirby = App::instance(); + + // assets + if (is_string($model) === true) { + $root = $kirby->root('media') . '/assets/' . $model . '/' . $hash; + // parent files for file model that already included hash + } elseif (is_a($model, '\Kirby\Cms\File')) { + $root = dirname($model->mediaRoot()); + // model files + } else { + $root = $model->mediaRoot() . '/' . $hash; + } + + try { + $thumb = $root . '/' . $filename; + $job = $root . '/.jobs/' . $filename . '.json'; + $options = Data::read($job); + + if (empty($options) === true) { + return false; + } + + if (is_string($model) === true) { + $source = $kirby->root('index') . '/' . $model . '/' . $options['filename']; + } else { + $source = $model->file($options['filename'])->root(); + } + + try { + $kirby->thumb($source, $thumb, $options); + F::remove($job); + return Response::file($thumb); + } catch (Throwable $e) { + F::remove($thumb); + return Response::file($source); + } + } catch (Throwable $e) { + return false; + } + } + + /** + * Deletes all versions of the given file + * within the parent directory + * + * @param string $directory + * @param \Kirby\Cms\File $file + * @param string|null $ignore + * @return bool + */ + public static function unpublish(string $directory, File $file, string $ignore = null): bool + { + if (is_dir($directory) === false) { + return true; + } + + // get both old and new versions (pre and post Kirby 3.4.0) + $versions = array_merge( + glob($directory . '/' . crc32($file->filename()) . '-*', GLOB_ONLYDIR), + glob($directory . '/' . $file->mediaToken() . '-*', GLOB_ONLYDIR) + ); + + // delete all versions of the file + foreach ($versions as $version) { + if ($version === $ignore) { + continue; + } + + Dir::remove($version); + } + + return true; + } +} diff --git a/kirby/src/Cms/Model.php b/kirby/src/Cms/Model.php new file mode 100644 index 0000000..5788640 --- /dev/null +++ b/kirby/src/Cms/Model.php @@ -0,0 +1,109 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +abstract class Model +{ + use Properties; + + /** + * The parent Kirby instance + * + * @var \Kirby\Cms\App + */ + public static $kirby; + + /** + * The parent site instance + * + * @var \Kirby\Cms\Site + */ + protected $site; + + /** + * Makes it possible to convert the entire model + * to a string. Mostly useful for debugging + * + * @return string + */ + public function __toString(): string + { + return $this->id(); + } + + /** + * Each model must return a unique id + * + * @return string|int + */ + public function id() + { + return null; + } + + /** + * Returns the parent Kirby instance + * + * @return \Kirby\Cms\App + */ + public function kirby() + { + return static::$kirby = static::$kirby ?? App::instance(); + } + + /** + * Returns the parent Site instance + * + * @return \Kirby\Cms\Site + */ + public function site() + { + return $this->site = $this->site ?? $this->kirby()->site(); + } + + /** + * Setter for the parent Kirby object + * + * @param \Kirby\Cms\App|null $kirby + * @return self + */ + protected function setKirby(App $kirby = null) + { + static::$kirby = $kirby; + return $this; + } + + /** + * Setter for the parent site object + * + * @internal + * @param \Kirby\Cms\Site|null $site + * @return self + */ + public function setSite(Site $site = null) + { + $this->site = $site; + return $this; + } + + /** + * Convert the model to a simple array + * + * @return array + */ + public function toArray(): array + { + return $this->propertiesToArray(); + } +} diff --git a/kirby/src/Cms/ModelPermissions.php b/kirby/src/Cms/ModelPermissions.php new file mode 100644 index 0000000..faba61e --- /dev/null +++ b/kirby/src/Cms/ModelPermissions.php @@ -0,0 +1,116 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +abstract class ModelPermissions +{ + protected $category; + protected $model; + protected $options; + protected $permissions; + protected $user; + + /** + * @param string $method + * @param array $arguments + * @return bool + */ + public function __call(string $method, array $arguments = []): bool + { + return $this->can($method); + } + + /** + * ModelPermissions constructor + * + * @param \Kirby\Cms\Model $model + */ + public function __construct(Model $model) + { + $this->model = $model; + $this->options = $model->blueprint()->options(); + $this->user = $model->kirby()->user() ?? User::nobody(); + $this->permissions = $this->user->role()->permissions(); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + /** + * @param string $action + * @return bool + */ + public function can(string $action): bool + { + $role = $this->user->role()->id(); + + if ($role === 'nobody') { + return false; + } + + // check for a custom overall can method + if (method_exists($this, 'can' . $action) === true && $this->{'can' . $action}() === false) { + return false; + } + + // evaluate the blueprint options block + if (isset($this->options[$action]) === true) { + $options = $this->options[$action]; + + if ($options === false) { + return false; + } + + if ($options === true) { + return true; + } + + if (is_array($options) === true && A::isAssociative($options) === true) { + return $options[$role] ?? $options['*'] ?? false; + } + } + + return $this->permissions->for($this->category, $action); + } + + /** + * @param string $action + * @return bool + */ + public function cannot(string $action): bool + { + return $this->can($action) === false; + } + + /** + * @return array + */ + public function toArray(): array + { + $array = []; + + foreach ($this->options as $key => $value) { + $array[$key] = $this->can($key); + } + + return $array; + } +} diff --git a/kirby/src/Cms/ModelWithContent.php b/kirby/src/Cms/ModelWithContent.php new file mode 100644 index 0000000..3b338aa --- /dev/null +++ b/kirby/src/Cms/ModelWithContent.php @@ -0,0 +1,806 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +abstract class ModelWithContent extends Model +{ + /** + * Each model must define a CLASS_ALIAS + * which will be used in template queries. + * The CLASS_ALIAS is a short human-readable + * version of the class name. I.e. page. + */ + const CLASS_ALIAS = null; + + /** + * The content + * + * @var \Kirby\Cms\Content + */ + public $content; + + /** + * @var \Kirby\Cms\Translations + */ + public $translations; + + /** + * Returns the blueprint of the model + * + * @return \Kirby\Cms\Blueprint + */ + abstract public function blueprint(); + + /** + * Returns an array with all blueprints that are available + * + * @param string|null $inSection + * @return array + */ + public function blueprints(string $inSection = null): array + { + $blueprints = []; + $blueprint = $this->blueprint(); + $sections = $inSection !== null ? [$blueprint->section($inSection)] : $blueprint->sections(); + + foreach ($sections as $section) { + if ($section === null) { + continue; + } + + foreach ((array)$section->blueprints() as $blueprint) { + $blueprints[$blueprint['name']] = $blueprint; + } + } + + return array_values($blueprints); + } + + /** + * Executes any given model action + * + * @param string $action + * @param array $arguments + * @param \Closure $callback + * @return mixed + */ + abstract protected function commit(string $action, array $arguments, Closure $callback); + + /** + * Returns the content + * + * @param string|null $languageCode + * @return \Kirby\Cms\Content + * @throws \Kirby\Exception\InvalidArgumentException If the language for the given code does not exist + */ + public function content(string $languageCode = null) + { + + // single language support + if ($this->kirby()->multilang() === false) { + if (is_a($this->content, 'Kirby\Cms\Content') === true) { + return $this->content; + } + + return $this->setContent($this->readContent())->content; + + // multi language support + } else { + + // only fetch from cache for the default language + if ($languageCode === null && is_a($this->content, 'Kirby\Cms\Content') === true) { + return $this->content; + } + + // get the translation by code + if ($translation = $this->translation($languageCode)) { + $content = new Content($translation->content(), $this); + } else { + throw new InvalidArgumentException('Invalid language: ' . $languageCode); + } + + // only store the content for the current language + if ($languageCode === null) { + $this->content = $content; + } + + return $content; + } + } + + /** + * Returns the absolute path to the content file + * + * @internal + * @param string|null $languageCode + * @param bool $force + * @return string + * @throws \Kirby\Exception\InvalidArgumentException If the language for the given code does not exist + */ + public function contentFile(string $languageCode = null, bool $force = false): string + { + $extension = $this->contentFileExtension(); + $directory = $this->contentFileDirectory(); + $filename = $this->contentFileName(); + + // overwrite the language code + if ($force === true) { + if (empty($languageCode) === false) { + return $directory . '/' . $filename . '.' . $languageCode . '.' . $extension; + } else { + return $directory . '/' . $filename . '.' . $extension; + } + } + + // add and validate the language code in multi language mode + if ($this->kirby()->multilang() === true) { + if ($language = $this->kirby()->languageCode($languageCode)) { + return $directory . '/' . $filename . '.' . $language . '.' . $extension; + } else { + throw new InvalidArgumentException('Invalid language: ' . $languageCode); + } + } else { + return $directory . '/' . $filename . '.' . $extension; + } + } + + /** + * Returns an array with all content files + * + * @return array + */ + public function contentFiles(): array + { + if ($this->kirby()->multilang() === true) { + $files = []; + foreach ($this->kirby()->languages()->codes() as $code) { + $files[] = $this->contentFile($code); + } + return $files; + } else { + return [ + $this->contentFile() + ]; + } + } + + /** + * Prepares the content that should be written + * to the text file + * + * @internal + * @param array $data + * @param string|null $languageCode + * @return array + */ + public function contentFileData(array $data, string $languageCode = null): array + { + return $data; + } + + /** + * Returns the absolute path to the + * folder in which the content file is + * located + * + * @internal + * @return string|null + */ + public function contentFileDirectory(): ?string + { + return $this->root(); + } + + /** + * Returns the extension of the content file + * + * @internal + * @return string + */ + public function contentFileExtension(): string + { + return $this->kirby()->contentExtension(); + } + + /** + * Needs to be declared by the final model + * + * @internal + * @return string + */ + abstract public function contentFileName(): string; + + /** + * Decrement a given field value + * + * @param string $field + * @param int $by + * @param int $min + * @return self + */ + public function decrement(string $field, int $by = 1, int $min = 0) + { + $value = (int)$this->content()->get($field)->value() - $by; + + if ($value < $min) { + $value = $min; + } + + return $this->update([$field => $value]); + } + + /** + * Returns the drag text from a custom callback + * if the callback is defined in the config + * + * @internal + * @param string $type markdown or kirbytext + * @param mixed ...$args + * @return string|null + */ + public function dragTextFromCallback(string $type, ...$args): ?string + { + $dragTextCallback = option('panel.' . $type . '.' . static::CLASS_ALIAS . 'DragText'); + + if (empty($dragTextCallback) === false && is_a($dragTextCallback, 'Closure') === true && ($dragText = $dragTextCallback($this, ...$args)) !== null) { + return $dragText; + } + + return null; + } + + /** + * Returns the correct drag text type + * depending on the given type or the + * configuration + * + * @internal + * @param string $type (null|auto|kirbytext|markdown) + * @return string + */ + public function dragTextType(string $type = null): string + { + $type = $type ?? 'auto'; + + if ($type === 'auto') { + $type = option('panel.kirbytext', true) ? 'kirbytext' : 'markdown'; + } + + return $type === 'markdown' ? 'markdown' : 'kirbytext'; + } + + /** + * Returns all content validation errors + * + * @return array + */ + public function errors(): array + { + $errors = []; + + foreach ($this->blueprint()->sections() as $section) { + if (method_exists($section, 'errors') === true || isset($section->errors)) { + $errors = array_merge($errors, $section->errors()); + } + } + + return $errors; + } + + /** + * Increment a given field value + * + * @param string $field + * @param int $by + * @param int|null $max + * @return self + */ + public function increment(string $field, int $by = 1, int $max = null) + { + $value = (int)$this->content()->get($field)->value() + $by; + + if ($max && $value > $max) { + $value = $max; + } + + return $this->update([$field => $value]); + } + + /** + * Checks if the model is locked for the current user + * + * @return bool + */ + public function isLocked(): bool + { + $lock = $this->lock(); + return $lock && $lock->isLocked() === true; + } + + /** + * Checks if the data has any errors + * + * @return bool + */ + public function isValid(): bool + { + return Form::for($this)->hasErrors() === false; + } + + /** + * Returns the lock object for this model + * + * Only if a content directory exists, + * virtual pages will need to overwrite this method + * + * @return \Kirby\Cms\ContentLock|null + */ + public function lock() + { + $dir = $this->contentFileDirectory(); + + if ( + $this->kirby()->option('content.locking', true) && + is_string($dir) === true && + file_exists($dir) === true + ) { + return new ContentLock($this); + } + } + + /** + * Returns the panel icon definition + * + * @internal + * @param array|null $params + * @return array + */ + public function panelIcon(array $params = null): array + { + $defaults = [ + 'type' => 'page', + 'ratio' => null, + 'back' => 'pattern', + 'color' => '#c5c9c6', + ]; + + return array_merge($defaults, $params ?? []); + } + + /** + * @internal + * @param string|array|false|null $settings + * @return array|null + */ + public function panelImage($settings = null): ?array + { + $defaults = [ + 'ratio' => '3/2', + 'back' => 'pattern', + 'cover' => false + ]; + + // switch the image off + if ($settings === false) { + return null; + } + + if (is_string($settings) === true) { + // use defined icon in blueprint + if ($settings === 'icon') { + return []; + } + + $settings = [ + 'query' => $settings + ]; + } + + if ($image = $this->panelImageSource($settings['query'] ?? null)) { + + // main url + $settings['url'] = $image->url(); + + // only create srcsets for actual File objects + if (is_a($image, 'Kirby\Cms\File') === true) { + + // for cards + $settings['cards'] = [ + 'url' => 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw', + 'srcset' => $image->srcset([ + 352, + 864, + 1408, + ]) + ]; + + // for lists + $settings['list'] = [ + 'url' => 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw', + 'srcset' => $image->srcset([ + '1x' => [ + 'width' => 38, + 'height' => 38, + 'crop' => 'center' + ], + '2x' => [ + 'width' => 76, + 'height' => 76, + 'crop' => 'center' + ], + ]) + ]; + } + + unset($settings['query']); + } + + return array_merge($defaults, (array)$settings); + } + + /** + * Returns the image file object based on provided query + * + * @internal + * @param string|null $query + * @return \Kirby\Cms\File|\Kirby\Cms\Asset|null + */ + protected function panelImageSource(string $query = null) + { + $image = $this->query($query ?? null); + + // validate the query result + if (is_a($image, 'Kirby\Cms\File') === false && is_a($image, 'Kirby\Cms\Asset') === false) { + $image = null; + } + + // fallback for files + if ($image === null && is_a($this, 'Kirby\Cms\File') === true && $this->isViewable() === true) { + $image = $this; + } + + return $image; + } + + /** + * Returns an array of all actions + * that can be performed in the Panel + * This also checks for the lock status + * @since 3.3.0 + * + * @param array $unlock An array of options that will be force-unlocked + * @return array + */ + public function panelOptions(array $unlock = []): array + { + $options = $this->permissions()->toArray(); + + if ($this->isLocked()) { + foreach ($options as $key => $value) { + if (in_array($key, $unlock)) { + continue; + } + + $options[$key] = false; + } + } + + return $options; + } + + /** + * Must return the permissions object for the model + * + * @return \Kirby\Cms\ModelPermissions + */ + abstract public function permissions(); + + /** + * Creates a string query, starting from the model + * + * @internal + * @param string|null $query + * @param string|null $expect + * @return mixed + */ + public function query(string $query = null, string $expect = null) + { + if ($query === null) { + return null; + } + + try { + $result = Str::query($query, [ + 'kirby' => $this->kirby(), + 'site' => is_a($this, 'Kirby\Cms\Site') ? $this : $this->site(), + static::CLASS_ALIAS => $this + ]); + } catch (Throwable $e) { + return null; + } + + if ($expect !== null && is_a($result, $expect) !== true) { + return null; + } + + return $result; + } + + /** + * Read the content from the content file + * + * @internal + * @param string|null $languageCode + * @return array + */ + public function readContent(string $languageCode = null): array + { + try { + return Data::read($this->contentFile($languageCode)); + } catch (Throwable $e) { + return []; + } + } + + /** + * Returns the absolute path to the model + * + * @return string|null + */ + abstract public function root(): ?string; + + /** + * Stores the content on disk + * + * @internal + * @param array|null $data + * @param string|null $languageCode + * @param bool $overwrite + * @return self + */ + public function save(array $data = null, string $languageCode = null, bool $overwrite = false) + { + if ($this->kirby()->multilang() === true) { + return $this->saveTranslation($data, $languageCode, $overwrite); + } else { + return $this->saveContent($data, $overwrite); + } + } + + /** + * Save the single language content + * + * @param array|null $data + * @param bool $overwrite + * @return self + */ + protected function saveContent(array $data = null, bool $overwrite = false) + { + // create a clone to avoid modifying the original + $clone = $this->clone(); + + // merge the new data with the existing content + $clone->content()->update($data, $overwrite); + + // send the full content array to the writer + $clone->writeContent($clone->content()->toArray()); + + return $clone; + } + + /** + * Save a translation + * + * @param array|null $data + * @param string|null $languageCode + * @param bool $overwrite + * @return self + * @throws \Kirby\Exception\InvalidArgumentException If the language for the given code does not exist + */ + protected function saveTranslation(array $data = null, string $languageCode = null, bool $overwrite = false) + { + // create a clone to not touch the original + $clone = $this->clone(); + + // fetch the matching translation and update all the strings + $translation = $clone->translation($languageCode); + + if ($translation === null) { + throw new InvalidArgumentException('Invalid language: ' . $languageCode); + } + + // get the content to store + $content = $translation->update($data, $overwrite)->content(); + $kirby = $this->kirby(); + $languageCode = $kirby->languageCode($languageCode); + + // remove all untranslatable fields + if ($languageCode !== $kirby->defaultLanguage()->code()) { + foreach ($this->blueprint()->fields() as $field) { + if (($field['translate'] ?? true) === false) { + $content[$field['name']] = null; + } + } + + // merge the translation with the new data + $translation->update($content, true); + } + + // send the full translation array to the writer + $clone->writeContent($translation->content(), $languageCode); + + // reset the content object + $clone->content = null; + + // return the updated model + return $clone; + } + + /** + * Sets the Content object + * + * @param array|null $content + * @return self + */ + protected function setContent(array $content = null) + { + if ($content !== null) { + $content = new Content($content, $this); + } + + $this->content = $content; + return $this; + } + + /** + * Create the translations collection from an array + * + * @param array|null $translations + * @return self + */ + protected function setTranslations(array $translations = null) + { + if ($translations !== null) { + $this->translations = new Collection(); + + foreach ($translations as $props) { + $props['parent'] = $this; + $translation = new ContentTranslation($props); + $this->translations->data[$translation->code()] = $translation; + } + } + + return $this; + } + + /** + * String template builder + * + * @param string|null $template + * @param array $data + * @param string $fallback Fallback for tokens in the template that cannot be replaced + * @return string + */ + public function toString(string $template = null, array $data = [], string $fallback = ''): string + { + if ($template === null) { + return $this->id(); + } + + $result = Str::template($template, array_replace([ + 'kirby' => $this->kirby(), + 'site' => is_a($this, 'Kirby\Cms\Site') ? $this : $this->site(), + static::CLASS_ALIAS => $this + ], $data), $fallback); + + return $result; + } + + /** + * Returns a single translation by language code + * If no code is specified the current translation is returned + * + * @param string|null $languageCode + * @return \Kirby\Cms\ContentTranslation|null + */ + public function translation(string $languageCode = null) + { + return $this->translations()->find($languageCode ?? $this->kirby()->language()->code()); + } + + /** + * Returns the translations collection + * + * @return \Kirby\Cms\Collection + */ + public function translations() + { + if ($this->translations !== null) { + return $this->translations; + } + + $this->translations = new Collection(); + + foreach ($this->kirby()->languages() as $language) { + $translation = new ContentTranslation([ + 'parent' => $this, + 'code' => $language->code(), + ]); + + $this->translations->data[$translation->code()] = $translation; + } + + return $this->translations; + } + + /** + * Updates the model data + * + * @param array|null $input + * @param string|null $languageCode + * @param bool $validate + * @return self + * @throws \Kirby\Exception\InvalidArgumentException If the input array contains invalid values + */ + public function update(array $input = null, string $languageCode = null, bool $validate = false) + { + $form = Form::for($this, [ + 'ignoreDisabled' => $validate === false, + 'input' => $input, + 'language' => $languageCode, + ]); + + // validate the input + if ($validate === true) { + if ($form->isInvalid() === true) { + throw new InvalidArgumentException([ + 'fallback' => 'Invalid form with errors', + 'details' => $form->errors() + ]); + } + } + + $arguments = [static::CLASS_ALIAS => $this, 'values' => $form->data(), 'strings' => $form->strings(), 'languageCode' => $languageCode]; + return $this->commit('update', $arguments, function ($model, $values, $strings, $languageCode) { + // save updated values + $model = $model->save($strings, $languageCode, true); + + // update model in siblings collection + $model->siblings()->add($model); + + return $model; + }); + } + + /** + * Low level data writer method + * to store the given data on disk or anywhere else + * + * @internal + * @param array $data + * @param string|null $languageCode + * @return bool + */ + public function writeContent(array $data, string $languageCode = null): bool + { + return Data::write( + $this->contentFile($languageCode), + $this->contentFileData($data, $languageCode) + ); + } +} diff --git a/kirby/src/Cms/Nest.php b/kirby/src/Cms/Nest.php new file mode 100644 index 0000000..0fa2318 --- /dev/null +++ b/kirby/src/Cms/Nest.php @@ -0,0 +1,48 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Nest +{ + /** + * @param $data + * @param null $parent + * @return mixed + */ + public static function create($data, $parent = null) + { + if (is_scalar($data) === true) { + return new Field($parent, $data, $data); + } + + $result = []; + + foreach ($data as $key => $value) { + if (is_array($value) === true) { + $result[$key] = static::create($value, $parent); + } elseif (is_scalar($value) === true) { + $result[$key] = new Field($parent, $key, $value); + } + } + + if (is_int(key($data))) { + return new NestCollection($result); + } else { + return new NestObject($result); + } + } +} diff --git a/kirby/src/Cms/NestCollection.php b/kirby/src/Cms/NestCollection.php new file mode 100644 index 0000000..4dbe31b --- /dev/null +++ b/kirby/src/Cms/NestCollection.php @@ -0,0 +1,33 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class NestCollection extends BaseCollection +{ + /** + * Converts all objects in the collection + * to an array. This can also take a callback + * function to further modify the array result. + * + * @param \Closure|null $map + * @return array + */ + public function toArray(Closure $map = null): array + { + return parent::toArray($map ?? function ($object) { + return $object->toArray(); + }); + } +} diff --git a/kirby/src/Cms/NestObject.php b/kirby/src/Cms/NestObject.php new file mode 100644 index 0000000..f225376 --- /dev/null +++ b/kirby/src/Cms/NestObject.php @@ -0,0 +1,43 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class NestObject extends Obj +{ + /** + * Converts the object to an array + * + * @return array + */ + public function toArray(): array + { + $result = []; + + foreach ((array)$this as $key => $value) { + if (is_a($value, 'Kirby\Cms\Field') === true) { + $result[$key] = $value->value(); + continue; + } + + if (is_object($value) === true && method_exists($value, 'toArray')) { + $result[$key] = $value->toArray(); + continue; + } + + $result[$key] = $value; + } + + return $result; + } +} diff --git a/kirby/src/Cms/Page.php b/kirby/src/Cms/Page.php new file mode 100644 index 0000000..bfc1888 --- /dev/null +++ b/kirby/src/Cms/Page.php @@ -0,0 +1,1594 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Page extends ModelWithContent +{ + const CLASS_ALIAS = 'page'; + + use PageActions; + use PageSiblings; + use HasChildren; + use HasFiles; + use HasMethods; + use HasSiblings; + + /** + * All registered page methods + * + * @var array + */ + public static $methods = []; + + /** + * Registry with all Page models + * + * @var array + */ + public static $models = []; + + /** + * The PageBlueprint object + * + * @var \Kirby\Cms\PageBlueprint + */ + protected $blueprint; + + /** + * Nesting level + * + * @var int + */ + protected $depth; + + /** + * Sorting number + slug + * + * @var string + */ + protected $dirname; + + /** + * Path of dirnames + * + * @var string + */ + protected $diruri; + + /** + * Draft status flag + * + * @var bool + */ + protected $isDraft; + + /** + * The Page id + * + * @var string + */ + protected $id; + + /** + * The template, that should be loaded + * if it exists + * + * @var \Kirby\Cms\Template + */ + protected $intendedTemplate; + + /** + * @var array + */ + protected $inventory; + + /** + * The sorting number + * + * @var int|null + */ + protected $num; + + /** + * The parent page + * + * @var \Kirby\Cms\Page|null + */ + protected $parent; + + /** + * Absolute path to the page directory + * + * @var string + */ + protected $root; + + /** + * The parent Site object + * + * @var \Kirby\Cms\Site|null + */ + protected $site; + + /** + * The URL-appendix aka slug + * + * @var string + */ + protected $slug; + + /** + * The intended page template + * + * @var string + */ + protected $template; + + /** + * The page url + * + * @var string|null + */ + protected $url; + + /** + * Magic caller + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + // public property access + if (isset($this->$method) === true) { + return $this->$method; + } + + // page methods + if ($this->hasMethod($method)) { + return $this->callMethod($method, $arguments); + } + + // return page content otherwise + return $this->content()->get($method, $arguments); + } + + /** + * Creates a new page object + * + * @param array $props + */ + public function __construct(array $props) + { + // set the slug as the first property + $this->slug = $props['slug'] ?? null; + + // add all other properties + $this->setProperties($props); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return array_merge($this->toArray(), [ + 'content' => $this->content(), + 'children' => $this->children(), + 'siblings' => $this->siblings(), + 'translations' => $this->translations(), + 'files' => $this->files(), + ]); + } + + /** + * Returns the url to the api endpoint + * + * @internal + * @param bool $relative + * @return string + */ + public function apiUrl(bool $relative = false): string + { + if ($relative === true) { + return 'pages/' . $this->panelId(); + } else { + return $this->kirby()->url('api') . '/pages/' . $this->panelId(); + } + } + + /** + * Returns the blueprint object + * + * @return \Kirby\Cms\PageBlueprint + */ + public function blueprint() + { + if (is_a($this->blueprint, 'Kirby\Cms\PageBlueprint') === true) { + return $this->blueprint; + } + + return $this->blueprint = PageBlueprint::factory('pages/' . $this->intendedTemplate(), 'pages/default', $this); + } + + /** + * Returns an array with all blueprints that are available for the page + * + * @param string|null $inSection + * @return array + */ + public function blueprints(string $inSection = null): array + { + if ($inSection !== null) { + return $this->blueprint()->section($inSection)->blueprints(); + } + + $blueprints = []; + $templates = $this->blueprint()->changeTemplate() ?? $this->blueprint()->options()['changeTemplate'] ?? []; + $currentTemplate = $this->intendedTemplate()->name(); + + if (is_array($templates) === false) { + $templates = []; + } + + // add the current template to the array if it's not already there + if (in_array($currentTemplate, $templates) === false) { + array_unshift($templates, $currentTemplate); + } + + // make sure every template is only included once + $templates = array_unique($templates); + + foreach ($templates as $template) { + try { + $props = Blueprint::load('pages/' . $template); + + $blueprints[] = [ + 'name' => basename($props['name']), + 'title' => $props['title'], + ]; + } catch (Exception $e) { + // skip invalid blueprints + } + } + + return array_values($blueprints); + } + + /** + * Builds the cache id for the page + * + * @param string $contentType + * @return string + */ + protected function cacheId(string $contentType): string + { + $cacheId = [$this->id()]; + + if ($this->kirby()->multilang() === true) { + $cacheId[] = $this->kirby()->language()->code(); + } + + $cacheId[] = $contentType; + + return implode('.', $cacheId); + } + + /** + * Prepares the content for the write method + * + * @internal + * @param array $data + * @param string|null $languageCode + * @return array + */ + public function contentFileData(array $data, string $languageCode = null): array + { + return A::prepend($data, [ + 'title' => $data['title'] ?? null, + 'slug' => $data['slug'] ?? null + ]); + } + + /** + * Returns the content text file + * which is found by the inventory method + * + * @internal + * @param string|null $languageCode + * @return string + */ + public function contentFileName(string $languageCode = null): string + { + return $this->intendedTemplate()->name(); + } + + /** + * Call the page controller + * + * @internal + * @param array $data + * @param string $contentType + * @return array + * @throws \Kirby\Exception\InvalidArgumentException If the controller returns invalid objects for `kirby`, `site`, `pages` or `page` + */ + public function controller($data = [], $contentType = 'html'): array + { + // create the template data + $data = array_merge($data, [ + 'kirby' => $kirby = $this->kirby(), + 'site' => $site = $this->site(), + 'pages' => $site->children(), + 'page' => $site->visit($this) + ]); + + // call the template controller if there's one. + $controllerData = $kirby->controller($this->template()->name(), $data, $contentType); + + // merge controller data with original data safely + if (empty($controllerData) === false) { + $classes = [ + 'kirby' => 'Kirby\Cms\App', + 'site' => 'Kirby\Cms\Site', + 'pages' => 'Kirby\Cms\Pages', + 'page' => 'Kirby\Cms\Page' + ]; + + foreach ($controllerData as $key => $value) { + if (array_key_exists($key, $classes) === true) { + if (is_a($value, $classes[$key]) === true) { + $data[$key] = $value; + } else { + throw new InvalidArgumentException('The returned variable "' . $key . '" from the controller "' . $this->template()->name() . '" is not of the required type "' . $classes[$key] . '"'); + } + } else { + $data[$key] = $value; + } + } + } + + return $data; + } + + /** + * Returns a number indicating how deep the page + * is nested within the content folder + * + * @return int + */ + public function depth(): int + { + return $this->depth = $this->depth ?? (substr_count($this->id(), '/') + 1); + } + + /** + * Sorting number + Slug + * + * @return string + */ + public function dirname(): string + { + if ($this->dirname !== null) { + return $this->dirname; + } + + if ($this->num() !== null) { + return $this->dirname = $this->num() . Dir::$numSeparator . $this->uid(); + } else { + return $this->dirname = $this->uid(); + } + } + + /** + * Sorting number + Slug + * + * @return string + */ + public function diruri(): string + { + if (is_string($this->diruri) === true) { + return $this->diruri; + } + + if ($this->isDraft() === true) { + $dirname = '_drafts/' . $this->dirname(); + } else { + $dirname = $this->dirname(); + } + + if ($parent = $this->parent()) { + return $this->diruri = $parent->diruri() . '/' . $dirname; + } else { + return $this->diruri = $dirname; + } + } + + /** + * Provides a kirbytag or markdown + * tag for the page, which will be + * used in the panel, when the page + * gets dragged onto a textarea + * + * @internal + * @param string|null $type (null|auto|kirbytext|markdown) + * @return string + */ + public function dragText(string $type = null): string + { + $type = $this->dragTextType($type); + + if ($dragTextFromCallback = $this->dragTextFromCallback($type)) { + return $dragTextFromCallback; + } + + if ($type === 'markdown') { + return '[' . $this->title() . '](' . $this->url() . ')'; + } else { + return '(link: ' . $this->id() . ' text: ' . $this->title() . ')'; + } + } + + /** + * Checks if the page exists on disk + * + * @return bool + */ + public function exists(): bool + { + return is_dir($this->root()) === true; + } + + /** + * Constructs a Page object and also + * takes page models into account. + * + * @internal + * @param mixed $props + * @return self + */ + public static function factory($props) + { + if (empty($props['model']) === false) { + return static::model($props['model'], $props); + } + + return new static($props); + } + + /** + * Redirects to this page, + * wrapper for the `go()` helper + * + * @since 3.4.0 + * + * @param array $options Options for `Kirby\Http\Uri` to create URL parts + * @param int $code HTTP status code + */ + public function go(array $options = [], int $code = 302) + { + go($this->url($options), $code); + } + + /** + * Checks if the intended template + * for the page exists. + * + * @return bool + */ + public function hasTemplate(): bool + { + return $this->intendedTemplate() === $this->template(); + } + + /** + * Returns the Page Id + * + * @return string + */ + public function id(): string + { + if ($this->id !== null) { + return $this->id; + } + + // set the id, depending on the parent + if ($parent = $this->parent()) { + return $this->id = $parent->id() . '/' . $this->uid(); + } + + return $this->id = $this->uid(); + } + + /** + * Returns the template that should be + * loaded if it exists. + * + * @return \Kirby\Cms\Template + */ + public function intendedTemplate() + { + if ($this->intendedTemplate !== null) { + return $this->intendedTemplate; + } + + return $this->setTemplate($this->inventory()['template'])->intendedTemplate(); + } + + /** + * Returns the inventory of files + * children and content files + * + * @internal + * @return array + */ + public function inventory(): array + { + if ($this->inventory !== null) { + return $this->inventory; + } + + $kirby = $this->kirby(); + + return $this->inventory = Dir::inventory( + $this->root(), + $kirby->contentExtension(), + $kirby->contentIgnore(), + $kirby->multilang() + ); + } + + /** + * Compares the current object with the given page object + * + * @param \Kirby\Cms\Page|string $page + * @return bool + */ + public function is($page): bool + { + if (is_a($page, 'Kirby\Cms\Page') === false) { + if (is_string($page) === false) { + return false; + } + + $page = $this->kirby()->page($page); + } + + if (is_a($page, 'Kirby\Cms\Page') === false) { + return false; + } + + return $this->id() === $page->id(); + } + + /** + * Checks if the page is the current page + * + * @return bool + */ + public function isActive(): bool + { + if ($page = $this->site()->page()) { + if ($page->is($this) === true) { + return true; + } + } + + return false; + } + + /** + * Checks if the page is a direct or indirect ancestor of the given $page object + * + * @param Page $child + * @return bool + */ + public function isAncestorOf(Page $child): bool + { + return $child->parents()->has($this->id()) === true; + } + + /** + * Checks if the page can be cached in the + * pages cache. This will also check if one + * of the ignore rules from the config kick in. + * + * @return bool + */ + public function isCacheable(): bool + { + $kirby = $this->kirby(); + $cache = $kirby->cache('pages'); + $options = $cache->options(); + $ignore = $options['ignore'] ?? null; + + // the pages cache is switched off + if (($options['active'] ?? false) === false) { + return false; + } + + // inspect the current request + $request = $kirby->request(); + + // disable the pages cache for any request types but GET or HEAD + if (in_array($request->method(), ['GET', 'HEAD']) === false) { + return false; + } + + // disable the pages cache when there's request data + if (empty($request->data()) === false) { + return false; + } + + // disable the pages cache when there are any params + if ($request->params()->isNotEmpty()) { + return false; + } + + // check for a custom ignore rule + if (is_a($ignore, 'Closure') === true) { + if ($ignore($this) === true) { + return false; + } + } + + // ignore pages by id + if (is_array($ignore) === true) { + if (in_array($this->id(), $ignore) === true) { + return false; + } + } + + return true; + } + + /** + * Checks if the page is a child of the given page + * + * @param \Kirby\Cms\Page|string $parent + * @return bool + */ + public function isChildOf($parent): bool + { + if ($parentObj = $this->parent()) { + return $parentObj->is($parent); + } + + return false; + } + + /** + * Checks if the page is a descendant of the given page + * + * @param \Kirby\Cms\Page|string $parent + * @return bool + */ + public function isDescendantOf($parent): bool + { + if (is_string($parent) === true) { + $parent = $this->site()->find($parent); + } + + if (!$parent) { + return false; + } + + return $this->parents()->has($parent->id()) === true; + } + + /** + * Checks if the page is a descendant of the currently active page + * + * @return bool + */ + public function isDescendantOfActive(): bool + { + if ($active = $this->site()->page()) { + return $this->isDescendantOf($active); + } + + return false; + } + + /** + * Checks if the current page is a draft + * + * @return bool + */ + public function isDraft(): bool + { + return $this->isDraft; + } + + /** + * Checks if the page is the error page + * + * @return bool + */ + public function isErrorPage(): bool + { + return $this->id() === $this->site()->errorPageId(); + } + + /** + * Checks if the page is the home page + * + * @return bool + */ + public function isHomePage(): bool + { + return $this->id() === $this->site()->homePageId(); + } + + /** + * It's often required to check for the + * home and error page to stop certain + * actions. That's why there's a shortcut. + * + * @return bool + */ + public function isHomeOrErrorPage(): bool + { + return $this->isHomePage() === true || $this->isErrorPage() === true; + } + + /** + * @deprecated 3.0.0 Use `Page::isUnlisted()` instead + * @return bool + * @codeCoverageIgnore + */ + public function isInvisible(): bool + { + deprecated('$page->isInvisible() is deprecated, use $page->isUnlisted() instead. $page->isInvisible() will be removed in Kirby 3.5.0.'); + + return $this->isUnlisted(); + } + + /** + * Checks if the page has a sorting number + * + * @return bool + */ + public function isListed(): bool + { + return $this->num() !== null; + } + + /** + * Checks if the page is open. + * Open pages are either the current one + * or descendants of the current one. + * + * @return bool + */ + public function isOpen(): bool + { + if ($this->isActive() === true) { + return true; + } + + if ($page = $this->site()->page()) { + if ($page->parents()->has($this->id()) === true) { + return true; + } + } + + return false; + } + + /** + * Checks if the page is not a draft. + * + * @return bool + */ + public function isPublished(): bool + { + return $this->isDraft() === false; + } + + /** + * Check if the page can be read by the current user + * + * @return bool + */ + public function isReadable(): bool + { + static $readable = []; + + $template = $this->intendedTemplate()->name(); + + if (isset($readable[$template]) === true) { + return $readable[$template]; + } + + return $readable[$template] = $this->permissions()->can('read'); + } + + /** + * Checks if the page is sortable + * + * @return bool + */ + public function isSortable(): bool + { + return $this->permissions()->can('sort'); + } + + /** + * Checks if the page has no sorting number + * + * @return bool + */ + public function isUnlisted(): bool + { + return $this->isListed() === false; + } + + /** + * @deprecated 3.0.0 Use `Page::isListed()` instead + * @return bool + * @codeCoverageIgnore + */ + public function isVisible(): bool + { + deprecated('$page->isVisible() is deprecated, use $page->isListed() instead. $page->isVisible() will be removed in Kirby 3.5.0.'); + + return $this->isListed(); + } + + /** + * Checks if the page access is verified. + * This is only used for drafts so far. + * + * @internal + * @param string|null $token + * @return bool + */ + public function isVerified(string $token = null) + { + if ( + $this->isDraft() === false && + $this->parents()->findBy('status', 'draft') === null + ) { + return true; + } + + if ($token === null) { + return false; + } + + return $this->token() === $token; + } + + /** + * Returns the root to the media folder for the page + * + * @internal + * @return string + */ + public function mediaRoot(): string + { + return $this->kirby()->root('media') . '/pages/' . $this->id(); + } + + /** + * The page's base URL for any files + * + * @internal + * @return string + */ + public function mediaUrl(): string + { + return $this->kirby()->url('media') . '/pages/' . $this->id(); + } + + /** + * Creates a page model if it has been registered + * + * @internal + * @param string $name + * @param array $props + * @return self + */ + public static function model(string $name, array $props = []) + { + if ($class = (static::$models[$name] ?? null)) { + $object = new $class($props); + + if (is_a($object, 'Kirby\Cms\Page') === true) { + return $object; + } + } + + return new static($props); + } + + /** + * Returns the last modification date of the page + * + * @param string|null $format + * @param string|null $handler + * @param string|null $languageCode + * @return int|string + */ + public function modified(string $format = null, string $handler = null, string $languageCode = null) + { + return F::modified( + $this->contentFile($languageCode), + $format, + $handler ?? $this->kirby()->option('date.handler', 'date') + ); + } + + /** + * Returns the sorting number + * + * @return int|null + */ + public function num(): ?int + { + return $this->num; + } + + /** + * Returns the panel icon definition + * according to the blueprint settings + * + * @internal + * @param array|null $params + * @return array + */ + public function panelIcon(array $params = null): array + { + if ($icon = $this->blueprint()->icon()) { + $params['type'] = $icon; + } + + return parent::panelIcon($params); + } + + /** + * Returns the escaped Id, which is + * used in the panel to make routing work properly + * + * @internal + * @return string + */ + public function panelId(): string + { + return str_replace('/', '+', $this->id()); + } + + /** + * Returns the image file object based on provided query + * + * @internal + * @param string|null $query + * @return \Kirby\Cms\File|\Kirby\Cms\Asset|null + */ + protected function panelImageSource(string $query = null) + { + if ($query === null) { + $query = 'page.image'; + } + + return parent::panelImageSource($query); + } + + /** + * Returns the full path without leading slash + * + * @internal + * @return string + */ + public function panelPath(): string + { + return 'pages/' . $this->panelId(); + } + + /** + * Prepares the response data for page pickers + * and page fields + * + * @param array|null $params + * @return array + */ + public function panelPickerData(array $params = []): array + { + $image = $this->panelImage($params['image'] ?? []); + $icon = $this->panelIcon($image); + + return [ + 'dragText' => $this->dragText(), + 'hasChildren' => $this->hasChildren(), + 'icon' => $icon, + 'id' => $this->id(), + 'image' => $image, + 'info' => $this->toString($params['info'] ?? false), + 'link' => $this->panelUrl(true), + 'text' => $this->toString($params['text'] ?? '{{ page.title }}'), + 'url' => $this->url(), + ]; + } + + /** + * Returns the url to the editing view + * in the panel + * + * @internal + * @param bool $relative + * @return string + */ + public function panelUrl(bool $relative = false): string + { + if ($relative === true) { + return '/' . $this->panelPath(); + } else { + return $this->kirby()->url('panel') . '/' . $this->panelPath(); + } + } + + /** + * Returns the parent Page object + * + * @return \Kirby\Cms\Page|null + */ + public function parent() + { + return $this->parent; + } + + /** + * Returns the parent id, if a parent exists + * + * @internal + * @return string|null + */ + public function parentId(): ?string + { + if ($parent = $this->parent()) { + return $parent->id(); + } + + return null; + } + + /** + * Returns the parent model, + * which can either be another Page + * or the Site + * + * @internal + * @return \Kirby\Cms\Page|\Kirby\Cms\Site + */ + public function parentModel() + { + return $this->parent() ?? $this->site(); + } + + /** + * Returns a list of all parents and their parents recursively + * + * @return \Kirby\Cms\Pages + */ + public function parents() + { + $parents = new Pages(); + $page = $this->parent(); + + while ($page !== null) { + $parents->append($page->id(), $page); + $page = $page->parent(); + } + + return $parents; + } + + /** + * Returns the permissions object for this page + * + * @return \Kirby\Cms\PagePermissions + */ + public function permissions() + { + return new PagePermissions($this); + } + + /** + * Draft preview Url + * + * @internal + * @return string|null + */ + public function previewUrl(): ?string + { + $preview = $this->blueprint()->preview(); + + if ($preview === false) { + return null; + } + + if ($preview === true) { + $url = $this->url(); + } else { + $url = $preview; + } + + if ($this->isDraft() === true) { + $uri = new Uri($url); + $uri->query->token = $this->token(); + + $url = $uri->toString(); + } + + return $url; + } + + /** + * Renders the page with the given data. + * + * An optional content type can be passed to + * render a content representation instead of + * the default template. + * + * @param array $data + * @param string $contentType + * @return string + * @throws \Kirby\Exception\NotFoundException If the default template cannot be found + */ + public function render(array $data = [], $contentType = 'html'): string + { + $kirby = $this->kirby(); + $cache = $cacheId = $html = null; + + // try to get the page from cache + if (empty($data) === true && $this->isCacheable() === true) { + $cache = $kirby->cache('pages'); + $cacheId = $this->cacheId($contentType); + $result = $cache->get($cacheId); + $html = $result['html'] ?? null; + $response = $result['response'] ?? []; + + // reconstruct the response configuration + if (empty($html) === false && empty($response) === false) { + $kirby->response()->fromArray($response); + } + } + + // fetch the page regularly + if ($html === null) { + if ($contentType === 'html') { + $template = $this->template(); + } else { + $template = $this->representation($contentType); + } + + if ($template->exists() === false) { + throw new NotFoundException([ + 'key' => 'template.default.notFound' + ]); + } + + $kirby->data = $this->controller($data, $contentType); + + // render the page + $html = $template->render($kirby->data); + + // convert the response configuration to an array + $response = $kirby->response()->toArray(); + + // cache the result + if ($cache !== null) { + $cache->set($cacheId, [ + 'html' => $html, + 'response' => $response + ]); + } + } + + return $html; + } + + /** + * @internal + * @param mixed $type + * @return \Kirby\Cms\Template + * @throws \Kirby\Exception\NotFoundException If the content representation cannot be found + */ + public function representation($type) + { + $kirby = $this->kirby(); + $template = $this->template(); + $representation = $kirby->template($template->name(), $type); + + if ($representation->exists() === true) { + return $representation; + } + + throw new NotFoundException('The content representation cannot be found'); + } + + /** + * Returns the absolute root to the page directory + * No matter if it exists or not. + * + * @return string + */ + public function root(): string + { + return $this->root = $this->root ?? $this->kirby()->root('content') . '/' . $this->diruri(); + } + + /** + * Returns the PageRules class instance + * which is being used in various methods + * to check for valid actions and input. + * + * @return \Kirby\Cms\PageRules + */ + protected function rules() + { + return new PageRules(); + } + + /** + * Search all pages within the current page + * + * @param string|null $query + * @param array $params + * @return \Kirby\Cms\Pages + */ + public function search(string $query = null, $params = []) + { + return $this->index()->search($query, $params); + } + + /** + * Sets the Blueprint object + * + * @param array|null $blueprint + * @return self + */ + protected function setBlueprint(array $blueprint = null) + { + if ($blueprint !== null) { + $blueprint['model'] = $this; + $this->blueprint = new PageBlueprint($blueprint); + } + + return $this; + } + + /** + * Sets the dirname manually, which works + * more reliable in connection with the inventory + * than computing the dirname afterwards + * + * @param string|null $dirname + * @return self + */ + protected function setDirname(string $dirname = null) + { + $this->dirname = $dirname; + return $this; + } + + /** + * Sets the draft flag + * + * @param bool $isDraft + * @return self + */ + protected function setIsDraft(bool $isDraft = null) + { + $this->isDraft = $isDraft ?? false; + return $this; + } + + /** + * Sets the sorting number + * + * @param int|null $num + * @return self + */ + protected function setNum(int $num = null) + { + $this->num = $num === null ? $num : (int)$num; + return $this; + } + + /** + * Sets the parent page object + * + * @param \Kirby\Cms\Page|null $parent + * @return self + */ + protected function setParent(Page $parent = null) + { + $this->parent = $parent; + return $this; + } + + /** + * Sets the absolute path to the page + * + * @param string|null $root + * @return self + */ + protected function setRoot(string $root = null) + { + $this->root = $root; + return $this; + } + + /** + * Sets the required Page slug + * + * @param string $slug + * @return self + */ + protected function setSlug(string $slug) + { + $this->slug = $slug; + return $this; + } + + /** + * Sets the intended template + * + * @param string|null $template + * @return self + */ + protected function setTemplate(string $template = null) + { + if ($template !== null) { + $this->intendedTemplate = $this->kirby()->template($template); + } + + return $this; + } + + /** + * Sets the Url + * + * @param string|null $url + * @return self + */ + protected function setUrl(string $url = null) + { + if (is_string($url) === true) { + $url = rtrim($url, '/'); + } + + $this->url = $url; + return $this; + } + + /** + * Returns the slug of the page + * + * @param string|null $languageCode + * @return string + */ + public function slug(string $languageCode = null): string + { + if ($this->kirby()->multilang() === true) { + if ($languageCode === null) { + $languageCode = $this->kirby()->languageCode(); + } + + if ($translation = $this->translations()->find($languageCode)) { + return $translation->slug() ?? $this->slug; + } + } + + return $this->slug; + } + + /** + * Returns the page status, which + * can be `draft`, `listed` or `unlisted` + * + * @return string + */ + public function status(): string + { + if ($this->isDraft() === true) { + return 'draft'; + } + + if ($this->isUnlisted() === true) { + return 'unlisted'; + } + + return 'listed'; + } + + /** + * Returns the final template + * + * @return \Kirby\Cms\Template + */ + public function template() + { + if ($this->template !== null) { + return $this->template; + } + + $intended = $this->intendedTemplate(); + + if ($intended->exists() === true) { + return $this->template = $intended; + } + + return $this->template = $this->kirby()->template('default'); + } + + /** + * Returns the title field or the slug as fallback + * + * @return \Kirby\Cms\Field + */ + public function title() + { + return $this->content()->get('title')->or($this->slug()); + } + + /** + * Converts the most important + * properties to array + * + * @return array + */ + public function toArray(): array + { + return [ + 'children' => $this->children()->keys(), + 'content' => $this->content()->toArray(), + 'files' => $this->files()->keys(), + 'id' => $this->id(), + 'mediaUrl' => $this->mediaUrl(), + 'mediaRoot' => $this->mediaRoot(), + 'num' => $this->num(), + 'parent' => $this->parent() ? $this->parent()->id(): null, + 'slug' => $this->slug(), + 'template' => $this->template(), + 'translations' => $this->translations()->toArray(), + 'uid' => $this->uid(), + 'uri' => $this->uri(), + 'url' => $this->url() + ]; + } + + /** + * Returns a verification token, which + * is used for the draft authentication + * + * @return string + */ + protected function token(): string + { + return $this->kirby()->contentToken($this, $this->id() . $this->template()); + } + + /** + * Returns the UID of the page. + * The UID is basically the same as the + * slug, but stays the same on + * multi-language sites. Whereas the slug + * can be translated. + * + * @see self::slug() + * @return string + */ + public function uid(): string + { + return $this->slug; + } + + /** + * The uri is the same as the id, except + * that it will be translated in multi-language setups + * + * @param string|null $languageCode + * @return string + */ + public function uri(string $languageCode = null): string + { + // set the id, depending on the parent + if ($parent = $this->parent()) { + return $parent->uri($languageCode) . '/' . $this->slug($languageCode); + } + + return $this->slug($languageCode); + } + + /** + * Returns the Url + * + * @param array|string|null $options + * @return string + */ + public function url($options = null): string + { + if ($this->kirby()->multilang() === true) { + if (is_string($options) === true) { + return $this->urlForLanguage($options); + } else { + return $this->urlForLanguage(null, $options); + } + } + + if ($options !== null) { + return Url::to($this->url(), $options); + } + + if (is_string($this->url) === true) { + return $this->url; + } + + if ($this->isHomePage() === true) { + return $this->url = $this->site()->url(); + } + + if ($parent = $this->parent()) { + if ($parent->isHomePage() === true) { + return $this->url = $this->kirby()->url('base') . '/' . $parent->uid() . '/' . $this->uid(); + } else { + return $this->url = $this->parent()->url() . '/' . $this->uid(); + } + } + + return $this->url = $this->kirby()->url('base') . '/' . $this->uid(); + } + + /** + * Builds the Url for a specific language + * + * @internal + * @param string|null $language + * @param array|null $options + * @return string + */ + public function urlForLanguage($language = null, array $options = null): string + { + if ($options !== null) { + return Url::to($this->urlForLanguage($language), $options); + } + + if ($this->isHomePage() === true) { + return $this->url = $this->site()->urlForLanguage($language); + } + + if ($parent = $this->parent()) { + if ($parent->isHomePage() === true) { + return $this->url = $this->site()->urlForLanguage($language) . '/' . $parent->slug($language) . '/' . $this->slug($language); + } else { + return $this->url = $this->parent()->urlForLanguage($language) . '/' . $this->slug($language); + } + } + + return $this->url = $this->site()->urlForLanguage($language) . '/' . $this->slug($language); + } +} diff --git a/kirby/src/Cms/PageActions.php b/kirby/src/Cms/PageActions.php new file mode 100644 index 0000000..ccaec93 --- /dev/null +++ b/kirby/src/Cms/PageActions.php @@ -0,0 +1,838 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait PageActions +{ + /** + * Changes the sorting number + * The sorting number must already be correct + * when the method is called + * + * @param int|null $num + * @return self + * @throws \Kirby\Exception\LogicException If a draft is being sorted or the directory cannot be moved + */ + public function changeNum(int $num = null) + { + if ($this->isDraft() === true) { + throw new LogicException('Drafts cannot change their sorting number'); + } + + // don't run the action if everything stayed the same + if ($this->num() === $num) { + return $this; + } + + return $this->commit('changeNum', ['page' => $this, 'num' => $num], function ($oldPage, $num) { + $newPage = $oldPage->clone([ + 'num' => $num, + 'dirname' => null, + 'root' => null + ]); + + // actually move the page on disk + if ($oldPage->exists() === true) { + if (Dir::move($oldPage->root(), $newPage->root()) === true) { + // Updates the root path of the old page with the root path + // of the moved new page to use fly actions on old page in loop + $oldPage->setRoot($newPage->root()); + } else { + throw new LogicException('The page directory cannot be moved'); + } + } + + // overwrite the child in the parent page + $newPage + ->parentModel() + ->children() + ->set($newPage->id(), $newPage); + + return $newPage; + }); + } + + /** + * Changes the slug/uid of the page + * + * @param string $slug + * @param string|null $languageCode + * @return self + * @throws \Kirby\Exception\LogicException If the directory cannot be moved + */ + public function changeSlug(string $slug, string $languageCode = null) + { + // always sanitize the slug + $slug = Str::slug($slug); + + // in multi-language installations the slug for the non-default + // languages is stored in the text file. The changeSlugForLanguage + // method takes care of that. + if ($language = $this->kirby()->language($languageCode)) { + if ($language->isDefault() === false) { + return $this->changeSlugForLanguage($slug, $languageCode); + } + } + + // if the slug stays exactly the same, + // nothing needs to be done. + if ($slug === $this->slug()) { + return $this; + } + + $arguments = ['page' => $this, 'slug' => $slug, 'languageCode' => null]; + return $this->commit('changeSlug', $arguments, function ($oldPage, $slug) { + $newPage = $oldPage->clone([ + 'slug' => $slug, + 'dirname' => null, + 'root' => null + ]); + + if ($oldPage->exists() === true) { + // remove the lock of the old page + if ($lock = $oldPage->lock()) { + $lock->remove(); + } + + // actually move stuff on disk + if (Dir::move($oldPage->root(), $newPage->root()) !== true) { + throw new LogicException('The page directory cannot be moved'); + } + + // remove from the siblings + $oldPage->parentModel()->children()->remove($oldPage); + + Dir::remove($oldPage->mediaRoot()); + } + + // overwrite the new page in the parent collection + if ($newPage->isDraft() === true) { + $newPage->parentModel()->drafts()->set($newPage->id(), $newPage); + } else { + $newPage->parentModel()->children()->set($newPage->id(), $newPage); + } + + return $newPage; + }); + } + + /** + * Change the slug for a specific language + * + * @param string $slug + * @param string|null $languageCode + * @return self + * @throws \Kirby\Exception\NotFoundException If the language for the given language code cannot be found + * @throws \Kirby\Exception\InvalidArgumentException If the slug for the default language is being changed + */ + protected function changeSlugForLanguage(string $slug, string $languageCode = null) + { + $language = $this->kirby()->language($languageCode); + + if (!$language) { + throw new NotFoundException('The language: "' . $languageCode . '" does not exist'); + } + + if ($language->isDefault() === true) { + throw new InvalidArgumentException('Use the changeSlug method to change the slug for the default language'); + } + + $arguments = ['page' => $this, 'slug' => $slug, 'languageCode' => $languageCode]; + return $this->commit('changeSlug', $arguments, function ($page, $slug, $languageCode) { + // remove the slug if it's the same as the folder name + if ($slug === $page->uid()) { + $slug = null; + } + + return $page->save(['slug' => $slug], $languageCode); + }); + } + + /** + * Change the status of the current page + * to either draft, listed or unlisted + * + * @param string $status "draft", "listed" or "unlisted" + * @param int|null $position Optional sorting number + * @return self + * @throws \Kirby\Exception\InvalidArgumentException If an invalid status is being passed + */ + public function changeStatus(string $status, int $position = null) + { + switch ($status) { + case 'draft': + return $this->changeStatusToDraft(); + case 'listed': + return $this->changeStatusToListed($position); + case 'unlisted': + return $this->changeStatusToUnlisted(); + default: + throw new InvalidArgumentException('Invalid status: ' . $status); + } + } + + /** + * @return self + */ + protected function changeStatusToDraft() + { + $arguments = ['page' => $this, 'status' => 'draft', 'position' => null]; + $page = $this->commit('changeStatus', $arguments, function ($page) { + return $page->unpublish(); + }); + + return $page; + } + + /** + * @param int $position + * @return self + */ + protected function changeStatusToListed(int $position = null) + { + // create a sorting number for the page + $num = $this->createNum($position); + + // don't sort if not necessary + if ($this->status() === 'listed' && $num === $this->num()) { + return $this; + } + + $arguments = ['page' => $this, 'status' => 'listed', 'position' => $num]; + $page = $this->commit('changeStatus', $arguments, function ($page, $status, $position) { + return $page->publish()->changeNum($position); + }); + + if ($this->blueprint()->num() === 'default') { + $page->resortSiblingsAfterListing($num); + } + + return $page; + } + + /** + * @return self + */ + protected function changeStatusToUnlisted() + { + if ($this->status() === 'unlisted') { + return $this; + } + + $arguments = ['page' => $this, 'status' => 'unlisted', 'position' => null]; + $page = $this->commit('changeStatus', $arguments, function ($page) { + return $page->publish()->changeNum(null); + }); + + $this->resortSiblingsAfterUnlisting(); + + return $page; + } + + /** + * Changes the page template + * + * @param string $template + * @return self + * @throws \Kirby\Exception\LogicException If the textfile cannot be renamed/moved + */ + public function changeTemplate(string $template) + { + if ($template === $this->intendedTemplate()->name()) { + return $this; + } + + return $this->commit('changeTemplate', ['page' => $this, 'template' => $template], function ($oldPage, $template) { + if ($this->kirby()->multilang() === true) { + $newPage = $this->clone([ + 'template' => $template + ]); + + foreach ($this->kirby()->languages()->codes() as $code) { + $content = $oldPage->content($code)->convertTo($template); + + if (F::remove($oldPage->contentFile($code)) !== true) { + throw new LogicException('The old text file could not be removed'); + } + + // save the language file + $newPage->save($content, $code); + } + + // return a fresh copy of the object + return $newPage->clone(); + } else { + $newPage = $this->clone([ + 'content' => $this->content()->convertTo($template), + 'template' => $template + ]); + + if (F::remove($oldPage->contentFile()) !== true) { + throw new LogicException('The old text file could not be removed'); + } + + return $newPage->save(); + } + }); + } + + /** + * Change the page title + * + * @param string $title + * @param string|null $languageCode + * @return self + */ + public function changeTitle(string $title, string $languageCode = null) + { + $arguments = ['page' => $this, 'title' => $title, 'languageCode' => $languageCode]; + return $this->commit('changeTitle', $arguments, function ($page, $title, $languageCode) { + return $page->save(['title' => $title], $languageCode); + }); + } + + /** + * Commits a page action, by following these steps + * + * 1. checks the action rules + * 2. sends the before hook + * 3. commits the store action + * 4. sends the after hook + * 5. returns the result + * + * @param string $action + * @param array $arguments + * @param \Closure $callback + * @return mixed + */ + protected function commit(string $action, array $arguments, Closure $callback) + { + $old = $this->hardcopy(); + $kirby = $this->kirby(); + $argumentValues = array_values($arguments); + + $this->rules()->$action(...$argumentValues); + $kirby->trigger('page.' . $action . ':before', $arguments); + + $result = $callback(...$argumentValues); + + if ($action === 'create') { + $argumentsAfter = ['page' => $result]; + } elseif ($action === 'duplicate') { + $argumentsAfter = ['duplicatePage' => $result, 'originalPage' => $old]; + } elseif ($action === 'delete') { + $argumentsAfter = ['status' => $result, 'page' => $old]; + } else { + $argumentsAfter = ['newPage' => $result, 'oldPage' => $old]; + } + $kirby->trigger('page.' . $action . ':after', $argumentsAfter); + + $kirby->cache('pages')->flush(); + return $result; + } + + /** + * Copies the page to a new parent + * + * @param array $options + * @return \Kirby\Cms\Page + * @throws \Kirby\Exception\DuplicateException If the page already exists + */ + public function copy(array $options = []) + { + $slug = $options['slug'] ?? $this->slug(); + $isDraft = $options['isDraft'] ?? $this->isDraft(); + $parent = $options['parent'] ?? null; + $parentModel = $options['parent'] ?? $this->site(); + $num = $options['num'] ?? null; + $children = $options['children'] ?? false; + $files = $options['files'] ?? false; + + // clean up the slug + $slug = Str::slug($slug); + + if ($parentModel->findPageOrDraft($slug)) { + throw new DuplicateException([ + 'key' => 'page.duplicate', + 'data' => [ + 'slug' => $slug + ] + ]); + } + + $tmp = new static([ + 'isDraft' => $isDraft, + 'num' => $num, + 'parent' => $parent, + 'slug' => $slug, + ]); + + $ignore = [ + $this->kirby()->locks()->file($this) + ]; + + // don't copy files + if ($files === false) { + foreach ($this->files() as $file) { + $ignore[] = $file->root(); + + // append all content files + array_push($ignore, ...$file->contentFiles()); + } + } + + Dir::copy($this->root(), $tmp->root(), $children, $ignore); + + $copy = $parentModel->clone()->findPageOrDraft($slug); + + // remove all translated slugs + if ($this->kirby()->multilang() === true) { + foreach ($this->kirby()->languages() as $language) { + if ($language->isDefault() === false && $copy->translation($language)->exists() === true) { + $copy = $copy->save(['slug' => null], $language->code()); + } + } + } + + // add copy to siblings + if ($isDraft === true) { + $parentModel->drafts()->append($copy->id(), $copy); + } else { + $parentModel->children()->append($copy->id(), $copy); + } + + return $copy; + } + + /** + * Creates and stores a new page + * + * @param array $props + * @return self + */ + public static function create(array $props) + { + // clean up the slug + $props['slug'] = Str::slug($props['slug'] ?? $props['content']['title'] ?? null); + $props['template'] = $props['model'] = strtolower($props['template'] ?? 'default'); + $props['isDraft'] = ($props['draft'] ?? true); + + // create a temporary page object + $page = Page::factory($props); + + // create a form for the page + $form = Form::for($page, [ + 'values' => $props['content'] ?? [] + ]); + + // inject the content + $page = $page->clone(['content' => $form->strings(true)]); + + // run the hooks and creation action + $page = $page->commit('create', ['page' => $page, 'input' => $props], function ($page, $props) { + + // always create pages in the default language + if ($page->kirby()->multilang() === true) { + $languageCode = $page->kirby()->defaultLanguage()->code(); + } else { + $languageCode = null; + } + + // write the content file + $page = $page->save($page->content()->toArray(), $languageCode); + + // flush the parent cache to get children and drafts right + if ($page->isDraft() === true) { + $page->parentModel()->drafts()->append($page->id(), $page); + } else { + $page->parentModel()->children()->append($page->id(), $page); + } + + return $page; + }); + + // publish the new page if a number is given + if (isset($props['num']) === true) { + $page = $page->changeStatus('listed', $props['num']); + } + + return $page; + } + + /** + * Creates a child of the current page + * + * @param array $props + * @return self + */ + public function createChild(array $props) + { + $props = array_merge($props, [ + 'url' => null, + 'num' => null, + 'parent' => $this, + 'site' => $this->site(), + ]); + + $modelClass = Page::$models[$props['template']] ?? Page::class; + return $modelClass::create($props); + } + + /** + * Create the sorting number for the page + * depending on the blueprint settings + * + * @param int|null $num + * @return int + */ + public function createNum(int $num = null): int + { + $mode = $this->blueprint()->num(); + + switch ($mode) { + case 'zero': + return 0; + case 'date': + case 'datetime': + $format = $mode === 'date' ? 'Ymd' : 'YmdHi'; + $lang = $this->kirby()->defaultLanguage() ?? null; + $field = $this->content($lang)->get('date'); + $date = $field->isEmpty() ? 'now' : $field; + return date($format, strtotime($date)); + break; + case 'default': + + $max = $this + ->parentModel() + ->children() + ->listed() + ->merge($this) + ->count(); + + // default positioning at the end + if ($num === null) { + $num = $max; + } + + // avoid zeros or negative numbers + if ($num < 1) { + return 1; + } + + // avoid higher numbers than possible + if ($num > $max) { + return $max; + } + + return $num; + default: + // get instance with default language + $app = $this->kirby()->clone(); + $app->setCurrentLanguage(); + + $template = Str::template($mode, [ + 'kirby' => $app, + 'page' => $app->page($this->id()), + 'site' => $app->site(), + ], ''); + + return (int)$template; + } + } + + /** + * Deletes the page + * + * @param bool $force + * @return bool + */ + public function delete(bool $force = false): bool + { + return $this->commit('delete', ['page' => $this, 'force' => $force], function ($page, $force) { + + // delete all files individually + foreach ($page->files() as $file) { + $file->delete(); + } + + // delete all children individually + foreach ($page->children() as $child) { + $child->delete(true); + } + + // actually remove the page from disc + if ($page->exists() === true) { + + // delete all public media files + Dir::remove($page->mediaRoot()); + + // delete the content folder for this page + Dir::remove($page->root()); + + // if the page is a draft and the _drafts folder + // is now empty. clean it up. + if ($page->isDraft() === true) { + $draftsDir = dirname($page->root()); + + if (Dir::isEmpty($draftsDir) === true) { + Dir::remove($draftsDir); + } + } + } + + if ($page->isDraft() === true) { + $page->parentModel()->drafts()->remove($page); + } else { + $page->parentModel()->children()->remove($page); + $page->resortSiblingsAfterUnlisting(); + } + + return true; + }); + } + + /** + * Duplicates the page with the given + * slug and optionally copies all files + * + * @param string|null $slug + * @param array $options + * @return \Kirby\Cms\Page + */ + public function duplicate(string $slug = null, array $options = []) + { + + // create the slug for the duplicate + $slug = Str::slug($slug ?? $this->slug() . '-copy'); + + $arguments = ['originalPage' => $this, 'input' => $slug, 'options' => $options]; + return $this->commit('duplicate', $arguments, function ($page, $slug, $options) { + return $this->copy([ + 'parent' => $this->parent(), + 'slug' => $slug, + 'isDraft' => true, + 'files' => $options['files'] ?? false, + 'children' => $options['children'] ?? false, + ]); + }); + } + + /** + * @return self + * @throws \Kirby\Exception\LogicException If the folder cannot be moved + */ + public function publish() + { + if ($this->isDraft() === false) { + return $this; + } + + $page = $this->clone([ + 'isDraft' => false, + 'root' => null + ]); + + // actually do it on disk + if ($this->exists() === true) { + if (Dir::move($this->root(), $page->root()) !== true) { + throw new LogicException('The draft folder cannot be moved'); + } + + // Get the draft folder and check if there are any other drafts + // left. Otherwise delete it. + $draftDir = dirname($this->root()); + + if (Dir::isEmpty($draftDir) === true) { + Dir::remove($draftDir); + } + } + + // remove the page from the parent drafts and add it to children + $page->parentModel()->drafts()->remove($page); + $page->parentModel()->children()->append($page->id(), $page); + + return $page; + } + + /** + * Clean internal caches + * @return self + */ + public function purge() + { + $this->blueprint = null; + $this->children = null; + $this->content = null; + $this->drafts = null; + $this->files = null; + $this->inventory = null; + $this->translations = null; + + return $this; + } + + /** + * @param int|null $position + * @return bool + * @throws \Kirby\Exception\LogicException If the page is not included in the siblings collection + */ + protected function resortSiblingsAfterListing(int $position = null): bool + { + // get all siblings including the current page + $siblings = $this + ->parentModel() + ->children() + ->listed() + ->append($this) + ->filter(function ($page) { + return $page->blueprint()->num() === 'default'; + }); + + // get a non-associative array of ids + $keys = $siblings->keys(); + $index = array_search($this->id(), $keys); + + // if the page is not included in the siblings something went wrong + if ($index === false) { + throw new LogicException('The page is not included in the sorting index'); + } + + if ($position > count($keys)) { + $position = count($keys); + } + + // move the current page number in the array of keys + // subtract 1 from the num and the position, because of the + // zero-based array keys + $sorted = A::move($keys, $index, $position - 1); + + foreach ($sorted as $key => $id) { + if ($id === $this->id()) { + continue; + } else { + if ($sibling = $siblings->get($id)) { + $sibling->changeNum($key + 1); + } + } + } + + $parent = $this->parentModel(); + $parent->children = $parent->children()->sortBy('num', 'asc'); + + return true; + } + + /** + * @return bool + */ + public function resortSiblingsAfterUnlisting(): bool + { + $index = 0; + $parent = $this->parentModel(); + $siblings = $parent + ->children() + ->listed() + ->not($this) + ->filter(function ($page) { + return $page->blueprint()->num() === 'default'; + }); + + if ($siblings->count() > 0) { + foreach ($siblings as $sibling) { + $index++; + $sibling->changeNum($index); + } + + $parent->children = $siblings->sortBy('num', 'asc'); + } + + return true; + } + + /** + * @param null $position + * @return self + */ + public function sort($position = null) + { + return $this->changeStatus('listed', $position); + } + + /** + * Convert a page from listed or + * unlisted to draft. + * + * @return self + * @throws \Kirby\Exception\LogicException If the folder cannot be moved + */ + public function unpublish() + { + if ($this->isDraft() === true) { + return $this; + } + + $page = $this->clone([ + 'isDraft' => true, + 'num' => null, + 'dirname' => null, + 'root' => null + ]); + + // actually do it on disk + if ($this->exists() === true) { + if (Dir::move($this->root(), $page->root()) !== true) { + throw new LogicException('The page folder cannot be moved to drafts'); + } + } + + // remove the page from the parent children and add it to drafts + $page->parentModel()->children()->remove($page); + $page->parentModel()->drafts()->append($page->id(), $page); + + $page->resortSiblingsAfterUnlisting(); + + return $page; + } + + /** + * Updates the page data + * + * @param array|null $input + * @param string|null $language + * @param bool $validate + * @return self + */ + public function update(array $input = null, string $language = null, bool $validate = false) + { + if ($this->isDraft() === true) { + $validate = false; + } + + $page = parent::update($input, $language, $validate); + + // if num is created from page content, update num on content update + if ($page->isListed() === true && in_array($page->blueprint()->num(), ['zero', 'default']) === false) { + $page = $page->changeNum($page->createNum()); + } + + return $page; + } +} diff --git a/kirby/src/Cms/PageBlueprint.php b/kirby/src/Cms/PageBlueprint.php new file mode 100644 index 0000000..bef7ba1 --- /dev/null +++ b/kirby/src/Cms/PageBlueprint.php @@ -0,0 +1,209 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class PageBlueprint extends Blueprint +{ + /** + * Creates a new page blueprint object + * with the given props + * + * @param array $props + */ + public function __construct(array $props) + { + parent::__construct($props); + + // normalize all available page options + $this->props['options'] = $this->normalizeOptions( + $props['options'] ?? true, + // defaults + [ + 'changeSlug' => null, + 'changeStatus' => null, + 'changeTemplate' => null, + 'changeTitle' => null, + 'create' => null, + 'delete' => null, + 'duplicate' => null, + 'read' => null, + 'preview' => null, + 'sort' => null, + 'update' => null, + ], + // aliases (from v2) + [ + 'status' => 'changeStatus', + 'template' => 'changeTemplate', + 'title' => 'changeTitle', + 'url' => 'changeSlug', + ] + ); + + // normalize the ordering number + $this->props['num'] = $this->normalizeNum($props['num'] ?? 'default'); + + // normalize the available status array + $this->props['status'] = $this->normalizeStatus($props['status'] ?? null); + } + + /** + * Returns the page numbering mode + * + * @return string + */ + public function num(): string + { + return $this->props['num']; + } + + /** + * Normalizes the ordering number + * + * @param mixed $num + * @return string + */ + protected function normalizeNum($num): string + { + $aliases = [ + '0' => 'zero', + 'sort' => 'default', + ]; + + if (isset($aliases[$num]) === true) { + return $aliases[$num]; + } + + return $num; + } + + /** + * Normalizes the available status options for the page + * + * @param mixed $status + * @return array + */ + protected function normalizeStatus($status): array + { + $defaults = [ + 'draft' => [ + 'label' => $this->i18n('page.status.draft'), + 'text' => $this->i18n('page.status.draft.description'), + ], + 'unlisted' => [ + 'label' => $this->i18n('page.status.unlisted'), + 'text' => $this->i18n('page.status.unlisted.description'), + ], + 'listed' => [ + 'label' => $this->i18n('page.status.listed'), + 'text' => $this->i18n('page.status.listed.description'), + ] + ]; + + // use the defaults, when the status is not defined + if (empty($status) === true) { + $status = $defaults; + } + + // extend the status definition + $status = $this->extend($status); + + // clean up and translate each status + foreach ($status as $key => $options) { + + // skip invalid status definitions + if (in_array($key, ['draft', 'listed', 'unlisted']) === false || $options === false) { + unset($status[$key]); + continue; + } + + if ($options === true) { + $status[$key] = $defaults[$key]; + continue; + } + + // convert everything to a simple array + if (is_array($options) === false) { + $status[$key] = [ + 'label' => $options, + 'text' => null + ]; + } + + // always make sure to have a proper label + if (empty($status[$key]['label']) === true) { + $status[$key]['label'] = $defaults[$key]['label']; + } + + // also make sure to have the text field set + if (isset($status[$key]['text']) === false) { + $status[$key]['text'] = null; + } + + // translate text and label if necessary + $status[$key]['label'] = $this->i18n($status[$key]['label'], $status[$key]['label']); + $status[$key]['text'] = $this->i18n($status[$key]['text'], $status[$key]['text']); + } + + // the draft status is required + if (isset($status['draft']) === false) { + $status = ['draft' => $defaults['draft']] + $status; + } + + // remove the draft status for the home and error pages + if ($this->model->isHomeOrErrorPage() === true) { + unset($status['draft']); + } + + return $status; + } + + /** + * Returns the options object + * that handles page options and permissions + * + * @return array + */ + public function options(): array + { + return $this->props['options']; + } + + /** + * Returns the preview settings + * The preview setting controls the "Open" + * button in the panel and redirects it to a + * different URL if necessary. + * + * @return string|bool + */ + public function preview() + { + $preview = $this->props['options']['preview'] ?? true; + + if (is_string($preview) === true) { + return $this->model->toString($preview); + } + + return $preview; + } + + /** + * Returns the status array + * + * @return array + */ + public function status(): array + { + return $this->props['status']; + } +} diff --git a/kirby/src/Cms/PagePermissions.php b/kirby/src/Cms/PagePermissions.php new file mode 100644 index 0000000..acb0adf --- /dev/null +++ b/kirby/src/Cms/PagePermissions.php @@ -0,0 +1,80 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class PagePermissions extends ModelPermissions +{ + /** + * @var string + */ + protected $category = 'pages'; + + /** + * @return bool + */ + protected function canChangeSlug(): bool + { + return $this->model->isHomeOrErrorPage() !== true; + } + + /** + * @return bool + */ + protected function canChangeStatus(): bool + { + return $this->model->isErrorPage() !== true; + } + + /** + * @return bool + */ + protected function canChangeTemplate(): bool + { + if ($this->model->isHomeOrErrorPage() === true) { + return false; + } + + if (count($this->model->blueprints()) <= 1) { + return false; + } + + return true; + } + + /** + * @return bool + */ + protected function canDelete(): bool + { + return $this->model->isHomeOrErrorPage() !== true; + } + + /** + * @return bool + */ + protected function canSort(): bool + { + if ($this->model->isErrorPage() === true) { + return false; + } + + if ($this->model->isListed() !== true) { + return false; + } + + if ($this->model->blueprint()->num() !== 'default') { + return false; + } + + return true; + } +} diff --git a/kirby/src/Cms/PagePicker.php b/kirby/src/Cms/PagePicker.php new file mode 100644 index 0000000..40ece5f --- /dev/null +++ b/kirby/src/Cms/PagePicker.php @@ -0,0 +1,265 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class PagePicker extends Picker +{ + /** + * @var \Kirby\Cms\Pages + */ + protected $items; + + /** + * @var \Kirby\Cms\Pages + */ + protected $itemsForQuery; + + /** + * @var \Kirby\Cms\Page|\Kirby\Cms\Site|null + */ + protected $parent; + + /** + * Extends the basic defaults + * + * @return array + */ + public function defaults(): array + { + return array_merge(parent::defaults(), [ + // Page ID of the selected parent. Used to navigate + 'parent' => null, + // enable/disable subpage navigation + 'subpages' => true, + ]); + } + + /** + * Returns the parent model object that + * is currently selected in the page picker. + * It normally starts at the site, but can + * also be any subpage. When a query is given + * and subpage navigation is deactivated, + * there will be no model available at all. + * + * @return \Kirby\Cms\Page|\Kirby\Cms\Site|null + */ + public function model() + { + // no subpages navigation = no model + if ($this->options['subpages'] === false) { + return null; + } + + // the model for queries is a bit more tricky to find + if (empty($this->options['query']) === false) { + return $this->modelForQuery(); + } + + return $this->parent(); + } + + /** + * Returns a model object for the given + * query, depending on the parent and subpages + * options. + * + * @return \Kirby\Cms\Page|\Kirby\Cms\Site|null + */ + public function modelForQuery() + { + if ($this->options['subpages'] === true && empty($this->options['parent']) === false) { + return $this->parent(); + } + + if ($items = $this->items()) { + return $items->parent(); + } + + return null; + } + + /** + * Returns basic information about the + * parent model that is currently selected + * in the page picker. + * + * @param \Kirby\Cms\Site|\Kirby\Cms\Page|null + * @return array|null + */ + public function modelToArray($model = null): ?array + { + if ($model === null) { + return null; + } + + // the selected model is the site. there's nothing above + if (is_a($model, 'Kirby\Cms\Site') === true) { + return [ + 'id' => null, + 'parent' => null, + 'title' => $model->title()->value() + ]; + } + + // the top-most page has been reached + // the missing id indicates that there's nothing above + if ($model->id() === $this->start()->id()) { + return [ + 'id' => null, + 'parent' => null, + 'title' => $model->title()->value() + ]; + } + + // the model is a regular page + return [ + 'id' => $model->id(), + 'parent' => $model->parentModel()->id(), + 'title' => $model->title()->value() + ]; + } + + /** + * Search all pages for the picker + * + * @return \Kirby\Cms\Pages|null + */ + public function items() + { + // cache + if ($this->items !== null) { + return $this->items; + } + + // no query? simple parent-based search for pages + if (empty($this->options['query']) === true) { + $items = $this->itemsForParent(); + + // when subpage navigation is enabled, a parent + // might be passed in addition to the query. + // The parent then takes priority. + } elseif ($this->options['subpages'] === true && empty($this->options['parent']) === false) { + $items = $this->itemsForParent(); + + // search by query + } else { + $items = $this->itemsForQuery(); + } + + // filter protected pages + $items = $items->filterBy('isReadable', true); + + // search + $items = $this->search($items); + + // paginate the result + return $this->items = $this->paginate($items); + } + + /** + * Search for pages by parent + * + * @return \Kirby\Cms\Pages + */ + public function itemsForParent() + { + return $this->parent()->children(); + } + + /** + * Search for pages by query string + * + * @return \Kirby\Cms\Pages + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function itemsForQuery() + { + // cache + if ($this->itemsForQuery !== null) { + return $this->itemsForQuery; + } + + $model = $this->options['model']; + $items = $model->query($this->options['query']); + + // help mitigate some typical query usage issues + // by converting site and page objects to proper + // pages by returning their children + if (is_a($items, 'Kirby\Cms\Site') === true) { + $items = $items->children(); + } elseif (is_a($items, 'Kirby\Cms\Page') === true) { + $items = $items->children(); + } elseif (is_a($items, 'Kirby\Cms\Pages') === false) { + throw new InvalidArgumentException('Your query must return a set of pages'); + } + + return $this->itemsForQuery = $items; + } + + /** + * Returns the parent model. + * The model will be used to fetch + * subpages unless there's a specific + * query to find pages instead. + * + * @return \Kirby\Cms\Page|\Kirby\Cms\Site + */ + public function parent() + { + if ($this->parent !== null) { + return $this->parent; + } + + return $this->parent = $this->kirby->page($this->options['parent']) ?? $this->site; + } + + /** + * Calculates the top-most model (page or site) + * that can be accessed when navigating + * through pages. + * + * @return \Kirby\Cms\Page|\Kirby\Cms\Site + */ + public function start() + { + if (empty($this->options['query']) === false) { + if ($items = $this->itemsForQuery()) { + return $items->parent(); + } + + return $this->site; + } + + return $this->site; + } + + /** + * Returns an associative array + * with all information for the picker. + * This will be passed directly to the API. + * + * @return array + */ + public function toArray(): array + { + $array = parent::toArray(); + $array['model'] = $this->modelToArray($this->model()); + + return $array; + } +} diff --git a/kirby/src/Cms/PageRules.php b/kirby/src/Cms/PageRules.php new file mode 100644 index 0000000..0e863b5 --- /dev/null +++ b/kirby/src/Cms/PageRules.php @@ -0,0 +1,428 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class PageRules +{ + /** + * Validates if the sorting number of the page can be changed + * + * @param \Kirby\Cms\Page $page + * @param int|null $num + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the given number is invalid + */ + public static function changeNum(Page $page, int $num = null): bool + { + if ($num !== null && $num < 0) { + throw new InvalidArgumentException(['key' => 'page.num.invalid']); + } + + return true; + } + + /** + * Validates if the slug for the page can be changed + * + * @param \Kirby\Cms\Page $page + * @param string $slug + * @return bool + * @throws \Kirby\Exception\DuplicateException If a page with this slug already exists + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the slug + */ + public static function changeSlug(Page $page, string $slug): bool + { + if ($page->permissions()->changeSlug() !== true) { + throw new PermissionException([ + 'key' => 'page.changeSlug.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } + + self::validateSlugLength($slug); + + $siblings = $page->parentModel()->children(); + $drafts = $page->parentModel()->drafts(); + + if ($duplicate = $siblings->find($slug)) { + if ($duplicate->is($page) === false) { + throw new DuplicateException([ + 'key' => 'page.duplicate', + 'data' => [ + 'slug' => $slug + ] + ]); + } + } + + if ($duplicate = $drafts->find($slug)) { + if ($duplicate->is($page) === false) { + throw new DuplicateException([ + 'key' => 'page.draft.duplicate', + 'data' => [ + 'slug' => $slug + ] + ]); + } + } + + return true; + } + + /** + * Validates if the status for the page can be changed + * + * @param \Kirby\Cms\Page $page + * @param string $status + * @param int|null $position + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the given status is invalid + */ + public static function changeStatus(Page $page, string $status, int $position = null): bool + { + if (isset($page->blueprint()->status()[$status]) === false) { + throw new InvalidArgumentException(['key' => 'page.status.invalid']); + } + + switch ($status) { + case 'draft': + return static::changeStatusToDraft($page); + case 'listed': + return static::changeStatusToListed($page, $position); + case 'unlisted': + return static::changeStatusToUnlisted($page); + default: + throw new InvalidArgumentException(['key' => 'page.status.invalid']); + } + } + + /** + * Validates if a page can be converted to a draft + * + * @param \Kirby\Cms\Page $page + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the status or the page cannot be converted to a draft + */ + public static function changeStatusToDraft(Page $page) + { + if ($page->permissions()->changeStatus() !== true) { + throw new PermissionException([ + 'key' => 'page.changeStatus.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } + + if ($page->isHomeOrErrorPage() === true) { + throw new PermissionException([ + 'key' => 'page.changeStatus.toDraft.invalid', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } + + return true; + } + + /** + * Validates if the status of a page can be changed to listed + * + * @param \Kirby\Cms\Page $page + * @param int $position + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the given position is invalid + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the status or the status for the page cannot be changed by any user + */ + public static function changeStatusToListed(Page $page, int $position) + { + // no need to check for status changing permissions, + // instead we need to check for sorting permissions + if ($page->isListed() === true) { + if ($page->isSortable() !== true) { + throw new PermissionException([ + 'key' => 'page.sort.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } + + return true; + } + + if ($page->permissions()->changeStatus() !== true) { + throw new PermissionException([ + 'key' => 'page.changeStatus.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } + + if ($position !== null && $position < 0) { + throw new InvalidArgumentException(['key' => 'page.num.invalid']); + } + + if ($page->isDraft() === true && empty($page->errors()) === false) { + throw new PermissionException([ + 'key' => 'page.changeStatus.incomplete', + 'details' => $page->errors() + ]); + } + + return true; + } + + /** + * Validates if the status of a page can be changed to unlisted + * + * @param \Kirby\Cms\Page $page + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the status + */ + public static function changeStatusToUnlisted(Page $page) + { + if ($page->permissions()->changeStatus() !== true) { + throw new PermissionException([ + 'key' => 'page.changeStatus.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } + + return true; + } + + /** + * Validates if the template of the page can be changed + * + * @param \Kirby\Cms\Page $page + * @param string $template + * @return bool + * @throws \Kirby\Exception\LogicException If the template of the page cannot be changed at all + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the template + */ + public static function changeTemplate(Page $page, string $template): bool + { + if ($page->permissions()->changeTemplate() !== true) { + throw new PermissionException([ + 'key' => 'page.changeTemplate.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } + + if (count($page->blueprints()) <= 1) { + throw new LogicException([ + 'key' => 'page.changeTemplate.invalid', + 'data' => ['slug' => $page->slug()] + ]); + } + + return true; + } + + /** + * Validates if the title of the page can be changed + * + * @param \Kirby\Cms\Page $page + * @param string $title + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the new title is empty + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the title + */ + public static function changeTitle(Page $page, string $title): bool + { + if (Str::length($title) === 0) { + throw new InvalidArgumentException([ + 'key' => 'page.changeTitle.empty', + ]); + } + + if ($page->permissions()->changeTitle() !== true) { + throw new PermissionException([ + 'key' => 'page.changeTitle.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } + + return true; + } + + /** + * Validates if the page can be created + * + * @param \Kirby\Cms\Page $page + * @return bool + * @throws \Kirby\Exception\DuplicateException If the same page or a draft already exists + * @throws \Kirby\Exception\InvalidArgumentException If the slug is invalid + * @throws \Kirby\Exception\PermissionException If the user is not allowed to create this page + */ + public static function create(Page $page): bool + { + if (Str::length($page->slug()) < 1) { + throw new InvalidArgumentException([ + 'key' => 'page.slug.invalid', + ]); + } + + self::validateSlugLength($page->slug()); + + if ($page->exists() === true) { + throw new DuplicateException([ + 'key' => 'page.draft.duplicate', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } + + if ($page->permissions()->create() !== true) { + throw new PermissionException([ + 'key' => 'page.create.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } + + $siblings = $page->parentModel()->children(); + $drafts = $page->parentModel()->drafts(); + $slug = $page->slug(); + + if ($duplicate = $siblings->find($slug)) { + throw new DuplicateException([ + 'key' => 'page.duplicate', + 'data' => ['slug' => $slug] + ]); + } + + if ($duplicate = $drafts->find($slug)) { + throw new DuplicateException([ + 'key' => 'page.draft.duplicate', + 'data' => ['slug' => $slug] + ]); + } + + return true; + } + + /** + * Validates if the page can be deleted + * + * @param \Kirby\Cms\Page $page + * @param bool $force + * @return bool + * @throws \Kirby\Exception\LogicException If the page has children and should not be force-deleted + * @throws \Kirby\Exception\PermissionException If the user is not allowed to delete the page + */ + public static function delete(Page $page, bool $force = false): bool + { + if ($page->permissions()->delete() !== true) { + throw new PermissionException([ + 'key' => 'page.delete.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } + + if (($page->hasChildren() === true || $page->hasDrafts() === true) && $force === false) { + throw new LogicException(['key' => 'page.delete.hasChildren']); + } + + return true; + } + + /** + * Validates if the page can be duplicated + * + * @param \Kirby\Cms\Page $page + * @param string $slug + * @param array $options + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to duplicate the page + */ + public static function duplicate(Page $page, string $slug, array $options = []): bool + { + if ($page->permissions()->duplicate() !== true) { + throw new PermissionException([ + 'key' => 'page.duplicate.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } + + return true; + } + + /** + * Validates if the page can be updated + * + * @param \Kirby\Cms\Page $page + * @param array $content + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to update the page + */ + public static function update(Page $page, array $content = []): bool + { + if ($page->permissions()->update() !== true) { + throw new PermissionException([ + 'key' => 'page.update.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } + + return true; + } + + /** + * Ensures that the slug doesn't exceed the maximum length to make + * sure that the directory name will be accepted by the filesystem + * + * @param string $slug New slug to check + * @return void + * @throws \Kirby\Exception\InvalidArgumentException If the slug is too long + */ + protected static function validateSlugLength(string $slug): void + { + if ($slugsMaxlength = App::instance()->option('slugs.maxlength', 255)) { + $maxlength = (int)$slugsMaxlength; + + if (Str::length($slug) > $maxlength) { + throw new InvalidArgumentException([ + 'key' => 'page.slug.maxlength', + 'data' => [ + 'length' => $maxlength + ] + ]); + } + } + } +} diff --git a/kirby/src/Cms/PageSiblings.php b/kirby/src/Cms/PageSiblings.php new file mode 100644 index 0000000..f592d10 --- /dev/null +++ b/kirby/src/Cms/PageSiblings.php @@ -0,0 +1,237 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait PageSiblings +{ + /** + * Checks if there's a next listed + * page in the siblings collection + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return bool + */ + public function hasNextListed($collection = null): bool + { + return $this->nextListed($collection) !== null; + } + + /** + * Checks if there's a next unlisted + * page in the siblings collection + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return bool + */ + public function hasNextUnlisted($collection = null): bool + { + return $this->nextUnlisted($collection) !== null; + } + + /** + * Checks if there's a previous listed + * page in the siblings collection + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return bool + */ + public function hasPrevListed($collection = null): bool + { + return $this->prevListed($collection) !== null; + } + + /** + * Checks if there's a previous unlisted + * page in the siblings collection + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return bool + */ + public function hasPrevUnlisted($collection = null): bool + { + return $this->prevUnlisted($collection) !== null; + } + + /** + * Returns the next listed page if it exists + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return \Kirby\Cms\Page|null + */ + public function nextListed($collection = null) + { + return $this->nextAll($collection)->listed()->first(); + } + + /** + * Returns the next unlisted page if it exists + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return \Kirby\Cms\Page|null + */ + public function nextUnlisted($collection = null) + { + return $this->nextAll($collection)->unlisted()->first(); + } + + /** + * Returns the previous listed page + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return \Kirby\Cms\Page|null + */ + public function prevListed($collection = null) + { + return $this->prevAll($collection)->listed()->last(); + } + + /** + * Returns the previous unlisted page + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return \Kirby\Cms\Page|null + */ + public function prevUnlisted($collection = null) + { + return $this->prevAll($collection)->unlisted()->first(); + } + + /** + * Private siblings collector + * + * @return \Kirby\Cms\Collection + */ + protected function siblingsCollection() + { + if ($this->isDraft() === true) { + return $this->parentModel()->drafts(); + } else { + return $this->parentModel()->children(); + } + } + + /** + * Returns siblings with the same template + * + * @param bool $self + * @return \Kirby\Cms\Pages + */ + public function templateSiblings(bool $self = true) + { + return $this->siblings($self)->filterBy('intendedTemplate', $this->intendedTemplate()->name()); + } + + /** + * @deprecated 3.0.0 Use `Page::hasNextUnlisted()` instead + * @return bool + * @codeCoverageIgnore + */ + public function hasNextInvisible(): bool + { + deprecated('$page->hasNextInvisible() is deprecated, use $page->hasNextUnlisted() instead. $page->hasNextInvisible() will be removed in Kirby 3.5.0.'); + + return $this->hasNextUnlisted(); + } + + /** + * @deprecated 3.0.0 Use `Page::hasNextListed()` instead + * @return bool + * @codeCoverageIgnore + */ + public function hasNextVisible(): bool + { + deprecated('$page->hasNextVisible() is deprecated, use $page->hasNextListed() instead. $page->hasNextVisible() will be removed in Kirby 3.5.0.'); + + return $this->hasNextListed(); + } + + /** + * @deprecated 3.0.0 Use `Page::hasPrevUnlisted()` instead + * @return bool + * @codeCoverageIgnore + */ + public function hasPrevInvisible(): bool + { + deprecated('$page->hasPrevInvisible() is deprecated, use $page->hasPrevUnlisted() instead. $page->hasPrevInvisible() will be removed in Kirby 3.5.0.'); + + return $this->hasPrevUnlisted(); + } + + /** + * @deprecated 3.0.0 Use `Page::hasPrevListed()` instead + * @return bool + * @codeCoverageIgnore + */ + public function hasPrevVisible(): bool + { + deprecated('$page->hasPrevVisible() is deprecated, use $page->hasPrevListed() instead. $page->hasPrevVisible() will be removed in Kirby 3.5.0.'); + + return $this->hasPrevListed(); + } + + /** + * @deprecated 3.0.0 Use `Page::nextUnlisted()` instead + * @return self|null + * @codeCoverageIgnore + */ + public function nextInvisible() + { + deprecated('$page->nextInvisible() is deprecated, use $page->nextUnlisted() instead. $page->nextInvisible() will be removed in Kirby 3.5.0.'); + + return $this->nextUnlisted(); + } + + + /** + * @deprecated 3.0.0 Use `Page::nextListed()` instead + * @return self|null + * @codeCoverageIgnore + */ + public function nextVisible() + { + deprecated('$page->nextVisible() is deprecated, use $page->nextListed() instead. $page->nextVisible() will be removed in Kirby 3.5.0.'); + + return $this->nextListed(); + } + + /** + * @deprecated 3.0.0 Use `Page::prevUnlisted()` instead + * @return self|null + * @codeCoverageIgnore + */ + public function prevInvisible() + { + deprecated('$page->prevInvisible() is deprecated, use $page->prevUnlisted() instead. $page->prevInvisible() will be removed in Kirby 3.5.0.'); + + return $this->prevUnlisted(); + } + + /** + * @deprecated 3.0.0 Use `Page::prevListed()` instead + * @return self|null + * @codeCoverageIgnore + */ + public function prevVisible() + { + deprecated('$page->prevVisible() is deprecated, use $page->prevListed() instead. $page->prevVisible() will be removed in Kirby 3.5.0.'); + + return $this->prevListed(); + } +} diff --git a/kirby/src/Cms/Pages.php b/kirby/src/Cms/Pages.php new file mode 100644 index 0000000..30fb10e --- /dev/null +++ b/kirby/src/Cms/Pages.php @@ -0,0 +1,527 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Pages extends Collection +{ + /** + * Cache for the index + * + * @var \Kirby\Cms\Pages|null + */ + protected $index = null; + + /** + * All registered pages methods + * + * @var array + */ + public static $methods = []; + + /** + * Adds a single page or + * an entire second collection to the + * current collection + * + * @param mixed $object + * @return self + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function add($object) + { + // add a page collection + if (is_a($object, static::class) === true) { + $this->data = array_merge($this->data, $object->data); + + // add a page by id + } elseif (is_string($object) === true && $page = page($object)) { + $this->__set($page->id(), $page); + + // add a page object + } elseif (is_a($object, 'Kirby\Cms\Page') === true) { + $this->__set($object->id(), $object); + + // give a useful error message on invalid input + } elseif (in_array($object, [null, false, true], true) !== true) { + throw new InvalidArgumentException('You must pass a Page object to the Pages collection'); + } + + return $this; + } + + /** + * Returns all audio files of all children + * + * @return \Kirby\Cms\Files + */ + public function audio() + { + return $this->files()->filterBy('type', 'audio'); + } + + /** + * Returns all children for each page in the array + * + * @return \Kirby\Cms\Pages + */ + public function children() + { + $children = new Pages([], $this->parent); + + foreach ($this->data as $page) { + foreach ($page->children() as $childKey => $child) { + $children->data[$childKey] = $child; + } + } + + return $children; + } + + /** + * Returns all code files of all children + * + * @return \Kirby\Cms\Files + */ + public function code() + { + return $this->files()->filterBy('type', 'code'); + } + + /** + * Returns all documents of all children + * + * @return \Kirby\Cms\Files + */ + public function documents() + { + return $this->files()->filterBy('type', 'document'); + } + + /** + * Fetch all drafts for all pages in the collection + * + * @return \Kirby\Cms\Pages + */ + public function drafts() + { + $drafts = new Pages([], $this->parent); + + foreach ($this->data as $page) { + foreach ($page->drafts() as $draftKey => $draft) { + $drafts->data[$draftKey] = $draft; + } + } + + return $drafts; + } + + /** + * Creates a pages collection from an array of props + * + * @param array $pages + * @param \Kirby\Cms\Model|null $model + * @param bool $draft + * @return self + */ + public static function factory(array $pages, Model $model = null, bool $draft = false) + { + $model = $model ?? App::instance()->site(); + $children = new static([], $model); + $kirby = $model->kirby(); + + if (is_a($model, 'Kirby\Cms\Page') === true) { + $parent = $model; + $site = $model->site(); + } else { + $parent = null; + $site = $model; + } + + foreach ($pages as $props) { + $props['kirby'] = $kirby; + $props['parent'] = $parent; + $props['site'] = $site; + $props['isDraft'] = $draft; + + $page = Page::factory($props); + + $children->data[$page->id()] = $page; + } + + return $children; + } + + /** + * Returns all files of all children + * + * @return \Kirby\Cms\Files + */ + public function files() + { + $files = new Files([], $this->parent); + + foreach ($this->data as $page) { + foreach ($page->files() as $fileKey => $file) { + $files->data[$fileKey] = $file; + } + } + + return $files; + } + + /** + * Finds a page in the collection by id. + * This works recursively for children and + * children of children, etc. + * + * @param string|null $id + * @return mixed + */ + public function findById(string $id = null) + { + // remove trailing or leading slashes + $id = trim($id, '/'); + + // strip extensions from the id + if (strpos($id, '.') !== false) { + $info = pathinfo($id); + + if ($info['dirname'] !== '.') { + $id = $info['dirname'] . '/' . $info['filename']; + } else { + $id = $info['filename']; + } + } + + // try the obvious way + if ($page = $this->get($id)) { + return $page; + } + + $multiLang = App::instance()->multilang(); + + if ($multiLang === true && $page = $this->findBy('slug', $id)) { + return $page; + } + + $start = is_a($this->parent, 'Kirby\Cms\Page') === true ? $this->parent->id() : ''; + $page = $this->findByIdRecursive($id, $start, $multiLang); + + return $page; + } + + /** + * Finds a child or child of a child recursively. + * + * @param string $id + * @param string|null $startAt + * @param bool $multiLang + * @return mixed + */ + public function findByIdRecursive(string $id, string $startAt = null, bool $multiLang = false) + { + $path = explode('/', $id); + $item = null; + $query = $startAt; + + foreach ($path as $key) { + $collection = $item ? $item->children() : $this; + $query = ltrim($query . '/' . $key, '/'); + $item = $collection->get($query) ?? null; + + if ($item === null && $multiLang === true) { + $item = $collection->findBy('slug', $key); + } + + if ($item === null) { + return null; + } + } + + return $item; + } + + /** + * Uses the specialized find by id method + * + * @param string|null $key + * @return mixed + */ + public function findByKey(string $key = null) + { + return $this->findById($key); + } + + /** + * Alias for Pages::findById + * + * @param string $id + * @return \Kirby\Cms\Page|null + */ + public function findByUri(string $id) + { + return $this->findById($id); + } + + /** + * Finds the currently open page + * + * @return \Kirby\Cms\Page|null + */ + public function findOpen() + { + return $this->findBy('isOpen', true); + } + + /** + * Custom getter that is able to find + * extension pages + * + * @param string $key + * @param mixed $default + * @return \Kirby\Cms\Page|null + */ + public function get($key, $default = null) + { + if ($key === null) { + return null; + } + + if ($item = parent::get($key)) { + return $item; + } + + return App::instance()->extension('pages', $key); + } + + /** + * Returns all images of all children + * + * @return \Kirby\Cms\Files + */ + public function images() + { + return $this->files()->filterBy('type', 'image'); + } + + /** + * Create a recursive flat index of all + * pages and subpages, etc. + * + * @param bool $drafts + * @return \Kirby\Cms\Pages + */ + public function index(bool $drafts = false) + { + if (is_a($this->index, 'Kirby\Cms\Pages') === true) { + return $this->index; + } + + $this->index = new Pages([], $this->parent); + + foreach ($this->data as $pageKey => $page) { + $this->index->data[$pageKey] = $page; + $index = $page->index($drafts); + + if ($index) { + foreach ($index as $childKey => $child) { + $this->index->data[$childKey] = $child; + } + } + } + + return $this->index; + } + + /** + * @deprecated 3.0.0 Use `Pages::unlisted()` instead + * + * @return self + * @codeCoverageIgnore + */ + public function invisible() + { + deprecated('$pages->invisible() is deprecated, use $pages->unlisted() instead. $pages->invisible() will be removed in Kirby 3.5.0.'); + + return $this->unlisted(); + } + + /** + * Returns all listed pages in the collection + * + * @return \Kirby\Cms\Pages + */ + public function listed() + { + return $this->filterBy('isListed', '==', true); + } + + /** + * Returns all unlisted pages in the collection + * + * @return \Kirby\Cms\Pages + */ + public function unlisted() + { + return $this->filterBy('isUnlisted', '==', true); + } + + /** + * Include all given items in the collection + * + * @param mixed ...$args + * @return self + */ + public function merge(...$args) + { + // merge multiple arguments at once + if (count($args) > 1) { + $collection = clone $this; + foreach ($args as $arg) { + $collection = $collection->merge($arg); + } + return $collection; + } + + // merge all parent drafts + if ($args[0] === 'drafts') { + if ($parent = $this->parent()) { + return $this->merge($parent->drafts()); + } + + return $this; + } + + // merge an entire collection + if (is_a($args[0], static::class) === true) { + $collection = clone $this; + $collection->data = array_merge($collection->data, $args[0]->data); + return $collection; + } + + // append a single page + if (is_a($args[0], 'Kirby\Cms\Page') === true) { + $collection = clone $this; + return $collection->set($args[0]->id(), $args[0]); + } + + // merge an array + if (is_array($args[0]) === true) { + $collection = clone $this; + foreach ($args[0] as $arg) { + $collection = $collection->merge($arg); + } + return $collection; + } + + if (is_string($args[0]) === true) { + return $this->merge(App::instance()->site()->find($args[0])); + } + + return $this; + } + + /** + * Filter all pages by excluding the given template + * @since 3.3.0 + * + * @param string|array $templates + * @return \Kirby\Cms\Pages + */ + public function notTemplate($templates) + { + if (empty($templates) === true) { + return $this; + } + + if (is_array($templates) === false) { + $templates = [$templates]; + } + + return $this->filter(function ($page) use ($templates) { + return !in_array($page->intendedTemplate()->name(), $templates); + }); + } + + /** + * Returns an array with all page numbers + * + * @return array + */ + public function nums(): array + { + return $this->pluck('num'); + } + + /* + * Returns all listed and unlisted pages in the collection + * + * @return \Kirby\Cms\Pages + */ + public function published() + { + return $this->filterBy('isDraft', '==', false); + } + + /** + * Filter all pages by the given template + * + * @param string|array $templates + * @return \Kirby\Cms\Pages + */ + public function template($templates) + { + if (empty($templates) === true) { + return $this; + } + + if (is_array($templates) === false) { + $templates = [$templates]; + } + + return $this->filter(function ($page) use ($templates) { + return in_array($page->intendedTemplate()->name(), $templates); + }); + } + + /** + * Returns all video files of all children + * + * @return \Kirby\Cms\Files + */ + public function videos() + { + return $this->files()->filterBy('type', 'video'); + } + + /** + * @deprecated 3.0.0 Use `Pages::listed()` instead + * + * @return \Kirby\Cms\Pages + * @codeCoverageIgnore + */ + public function visible() + { + deprecated('$pages->visible() is deprecated, use $pages->listed() instead. $pages->visible() will be removed in Kirby 3.5.0.'); + + return $this->listed(); + } +} diff --git a/kirby/src/Cms/Pagination.php b/kirby/src/Cms/Pagination.php new file mode 100644 index 0000000..b064dc1 --- /dev/null +++ b/kirby/src/Cms/Pagination.php @@ -0,0 +1,179 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Pagination extends BasePagination +{ + /** + * Pagination method (param, query, none) + * + * @var string + */ + protected $method; + + /** + * The base URL + * + * @var string + */ + protected $url; + + /** + * Variable name for query strings + * + * @var string + */ + protected $variable; + + /** + * Creates the pagination object. As a new + * property you can now pass the base Url. + * That Url must be the Url of the first + * page of the collection without additional + * pagination information/query parameters in it. + * + * ```php + * $pagination = new Pagination([ + * 'page' => 1, + * 'limit' => 10, + * 'total' => 120, + * 'method' => 'query', + * 'variable' => 'p', + * 'url' => new Uri('https://getkirby.com/blog') + * ]); + * ``` + * + * @param array $params + */ + public function __construct(array $params = []) + { + $kirby = App::instance(); + $config = $kirby->option('pagination', []); + $request = $kirby->request(); + + $params['limit'] = $params['limit'] ?? $config['limit'] ?? 20; + $params['method'] = $params['method'] ?? $config['method'] ?? 'param'; + $params['variable'] = $params['variable'] ?? $config['variable'] ?? 'page'; + + if (empty($params['url']) === true) { + $params['url'] = new Uri($kirby->url('current'), [ + 'params' => $request->params(), + 'query' => $request->query()->toArray(), + ]); + } + + if ($params['method'] === 'query') { + $params['page'] = $params['page'] ?? $params['url']->query()->get($params['variable']); + } elseif ($params['method'] === 'param') { + $params['page'] = $params['page'] ?? $params['url']->params()->get($params['variable']); + } + + parent::__construct($params); + + $this->method = $params['method']; + $this->url = $params['url']; + $this->variable = $params['variable']; + } + + /** + * Returns the Url for the first page + * + * @return string + */ + public function firstPageUrl(): string + { + return $this->pageUrl(1); + } + + /** + * Returns the Url for the last page + * + * @return string + */ + public function lastPageUrl(): string + { + return $this->pageUrl($this->lastPage()); + } + + /** + * Returns the Url for the next page. + * Returns null if there's no next page. + * + * @return string|null + */ + public function nextPageUrl(): ?string + { + if ($page = $this->nextPage()) { + return $this->pageUrl($page); + } + + return null; + } + + /** + * Returns the URL of the current page. + * If the `$page` variable is set, the URL + * for that page will be returned. + * + * @param int|null $page + * @return string|null + */ + public function pageUrl(int $page = null): ?string + { + if ($page === null) { + return $this->pageUrl($this->page()); + } + + $url = clone $this->url; + $variable = $this->variable; + + if ($this->hasPage($page) === false) { + return null; + } + + $pageValue = $page === 1 ? null : $page; + + if ($this->method === 'query') { + $url->query->$variable = $pageValue; + } elseif ($this->method === 'param') { + $url->params->$variable = $pageValue; + } else { + return null; + } + + return $url->toString(); + } + + /** + * Returns the Url for the previous page. + * Returns null if there's no previous page. + * + * @return string|null + */ + public function prevPageUrl(): ?string + { + if ($page = $this->prevPage()) { + return $this->pageUrl($page); + } + + return null; + } +} diff --git a/kirby/src/Cms/Panel.php b/kirby/src/Cms/Panel.php new file mode 100644 index 0000000..acd11d4 --- /dev/null +++ b/kirby/src/Cms/Panel.php @@ -0,0 +1,139 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Panel +{ + /** + * Returns custom css path for panel ui + * + * @param \Kirby\Cms\App $kirby + * @return bool|string + */ + public static function customCss(App $kirby) + { + if ($css = $kirby->option('panel.css')) { + $asset = asset($css); + + if ($asset->exists() === true) { + return $asset->url() . '?' . $asset->modified(); + } + } + + return false; + } + + /** + * Returns predefined icons path as sprite svg file + * + * @param \Kirby\Cms\App $kirby + * @return string + */ + public static function icons(App $kirby): string + { + return F::read($kirby->root('kirby') . '/panel/dist/img/icons.svg'); + } + + /** + * Links all dist files in the media folder + * and returns the link to the requested asset + * + * @param \Kirby\Cms\App $kirby + * @return bool + * @throws \Exception If Panel assets could not be moved to the public directory + */ + public static function link(App $kirby): bool + { + $mediaRoot = $kirby->root('media') . '/panel'; + $panelRoot = $kirby->root('panel') . '/dist'; + $versionHash = $kirby->versionHash(); + $versionRoot = $mediaRoot . '/' . $versionHash; + + // check if the version already exists + if (is_dir($versionRoot) === true) { + return false; + } + + // delete the panel folder and all previous versions + Dir::remove($mediaRoot); + + // recreate the panel folder + Dir::make($mediaRoot, true); + + // create a symlink to the dist folder + if (Dir::copy($panelRoot, $versionRoot) !== true) { + throw new Exception('Panel assets could not be linked'); + } + + return true; + } + + /** + * Renders the main panel view + * + * @param \Kirby\Cms\App $kirby + * @return \Kirby\Http\Response + */ + public static function render(App $kirby) + { + try { + if (static::link($kirby) === true) { + usleep(1); + go($kirby->url('index') . '/' . $kirby->path()); + } + } catch (Throwable $e) { + die('The Panel assets cannot be installed properly. ' . $e->getMessage()); + } + + // get the uri object for the panel url + $uri = new Uri($url = $kirby->url('panel')); + + // fetch all plugins + $plugins = new PanelPlugins(); + + $view = new View($kirby->root('kirby') . '/views/panel.php', [ + 'kirby' => $kirby, + 'config' => $kirby->option('panel'), + 'assetUrl' => $kirby->url('media') . '/panel/' . $kirby->versionHash(), + 'customCss' => static::customCss($kirby), + 'icons' => static::icons($kirby), + 'pluginCss' => $plugins->url('css'), + 'pluginJs' => $plugins->url('js'), + 'panelUrl' => $uri->path()->toString(true) . '/', + 'nonce' => $kirby->nonce(), + 'options' => [ + 'url' => $url, + 'site' => $kirby->url('index'), + 'api' => $kirby->url('api'), + 'csrf' => $kirby->option('api.csrf') ?? csrf(), + 'translation' => 'en', + 'debug' => $kirby->option('debug', false), + 'search' => [ + 'limit' => $kirby->option('panel.search.limit') ?? 10 + ] + ] + ]); + + return new Response($view->render()); + } +} diff --git a/kirby/src/Cms/PanelPlugins.php b/kirby/src/Cms/PanelPlugins.php new file mode 100644 index 0000000..e05caea --- /dev/null +++ b/kirby/src/Cms/PanelPlugins.php @@ -0,0 +1,108 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class PanelPlugins +{ + /** + * Cache of all collected plugin files + * + * @var array + */ + public $files; + + /** + * Collects and returns the plugin files for all plugins + * + * @return array + */ + public function files(): array + { + if ($this->files !== null) { + return $this->files; + } + + $this->files = []; + + foreach (App::instance()->plugins() as $plugin) { + $this->files[] = $plugin->root() . '/index.css'; + $this->files[] = $plugin->root() . '/index.js'; + } + + return $this->files; + } + + /** + * Returns the last modification + * of the collected plugin files + * + * @return int + */ + public function modified(): int + { + $files = $this->files(); + $modified = [0]; + + foreach ($files as $file) { + $modified[] = F::modified($file); + } + + return max($modified); + } + + /** + * Read the files from all plugins and concatenate them + * + * @param string $type + * @return string + */ + public function read(string $type): string + { + $dist = []; + + foreach ($this->files() as $file) { + if (F::extension($file) === $type) { + if ($content = F::read($file)) { + if ($type === 'js') { + $content = trim($content); + + // make sure that each plugin is ended correctly + if (Str::endsWith($content, ';') === false) { + $content .= ';'; + } + } + + $dist[] = $content; + } + } + } + + return implode(PHP_EOL . PHP_EOL, $dist); + } + + /** + * Absolute url to the cache file + * This is used by the panel to link the plugins + * + * @param string $type + * @return string + */ + public function url(string $type): string + { + return App::instance()->url('media') . '/plugins/index.' . $type . '?' . $this->modified(); + } +} diff --git a/kirby/src/Cms/Permissions.php b/kirby/src/Cms/Permissions.php new file mode 100644 index 0000000..e44b827 --- /dev/null +++ b/kirby/src/Cms/Permissions.php @@ -0,0 +1,230 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Permissions +{ + /** + * @var array + */ + public static $extendedActions = []; + + /** + * @var array + */ + protected $actions = [ + 'access' => [ + 'panel' => true, + 'settings' => true, + 'site' => true, + 'users' => true, + ], + 'files' => [ + 'changeName' => true, + 'create' => true, + 'delete' => true, + 'read' => true, + 'replace' => true, + 'update' => true + ], + 'languages' => [ + 'create' => true, + 'delete' => true + ], + 'pages' => [ + 'changeSlug' => true, + 'changeStatus' => true, + 'changeTemplate' => true, + 'changeTitle' => true, + 'create' => true, + 'delete' => true, + 'duplicate' => true, + 'preview' => true, + 'read' => true, + 'sort' => true, + 'update' => true + ], + 'site' => [ + 'changeTitle' => true, + 'update' => true + ], + 'users' => [ + 'changeEmail' => true, + 'changeLanguage' => true, + 'changeName' => true, + 'changePassword' => true, + 'changeRole' => true, + 'create' => true, + 'delete' => true, + 'update' => true + ], + 'user' => [ + 'changeEmail' => true, + 'changeLanguage' => true, + 'changeName' => true, + 'changePassword' => true, + 'changeRole' => true, + 'delete' => true, + 'update' => true + ] + ]; + + /** + * Permissions constructor + * + * @param array $settings + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function __construct($settings = []) + { + // dynamically register the extended actions + foreach (static::$extendedActions as $key => $actions) { + if (isset($this->actions[$key]) === true) { + throw new InvalidArgumentException('The action ' . $key . ' is already a core action'); + } + + $this->actions[$key] = $actions; + } + + if (is_array($settings) === true) { + return $this->setCategories($settings); + } + + if (is_bool($settings) === true) { + return $this->setAll($settings); + } + } + + /** + * @param string|null $category + * @param string|null $action + * @return bool + */ + public function for(string $category = null, string $action = null): bool + { + if ($action === null) { + if ($this->hasCategory($category) === false) { + return false; + } + + return $this->actions[$category]; + } + + if ($this->hasAction($category, $action) === false) { + return false; + } + + return $this->actions[$category][$action]; + } + + /** + * @param string $category + * @param string $action + * @return bool + */ + protected function hasAction(string $category, string $action): bool + { + return $this->hasCategory($category) === true && array_key_exists($action, $this->actions[$category]) === true; + } + + /** + * @param string $category + * @return bool + */ + protected function hasCategory(string $category): bool + { + return array_key_exists($category, $this->actions) === true; + } + + /** + * @param string $category + * @param string $action + * @param $setting + * @return self + */ + protected function setAction(string $category, string $action, $setting) + { + // wildcard to overwrite the entire category + if ($action === '*') { + return $this->setCategory($category, $setting); + } + + $this->actions[$category][$action] = $setting; + + return $this; + } + + /** + * @param bool $setting + * @return self + */ + protected function setAll(bool $setting) + { + foreach ($this->actions as $categoryName => $actions) { + $this->setCategory($categoryName, $setting); + } + + return $this; + } + + /** + * @param array $settings + * @return self + */ + protected function setCategories(array $settings) + { + foreach ($settings as $categoryName => $categoryActions) { + if (is_bool($categoryActions) === true) { + $this->setCategory($categoryName, $categoryActions); + } + + if (is_array($categoryActions) === true) { + foreach ($categoryActions as $actionName => $actionSetting) { + $this->setAction($categoryName, $actionName, $actionSetting); + } + } + } + + return $this; + } + + /** + * @param string $category + * @param bool $setting + * @return self + * @throws \Kirby\Exception\InvalidArgumentException + */ + protected function setCategory(string $category, bool $setting) + { + if ($this->hasCategory($category) === false) { + throw new InvalidArgumentException('Invalid permissions category'); + } + + foreach ($this->actions[$category] as $actionName => $actionSetting) { + $this->actions[$category][$actionName] = $setting; + } + + return $this; + } + + /** + * @return array + */ + public function toArray(): array + { + return $this->actions; + } +} diff --git a/kirby/src/Cms/Picker.php b/kirby/src/Cms/Picker.php new file mode 100644 index 0000000..c729396 --- /dev/null +++ b/kirby/src/Cms/Picker.php @@ -0,0 +1,176 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +abstract class Picker +{ + /** + * @var \Kirby\Cms\App + */ + protected $kirby; + + /** + * @var array + */ + protected $options; + + /** + * @var \Kirby\Cms\Site + */ + protected $site; + + /** + * Creates a new Picker instance + * + * @param array $params + */ + public function __construct(array $params = []) + { + $this->options = array_merge($this->defaults(), $params); + $this->kirby = $this->options['model']->kirby(); + $this->site = $this->kirby->site(); + } + + /** + * Return the array of default values + * + * @return array + */ + protected function defaults(): array + { + // default params + return [ + // image settings (ratio, cover, etc.) + 'image' => [], + // query template for the info field + 'info' => false, + // number of users displayed per pagination page + 'limit' => 20, + // optional mapping function for the result array + 'map' => null, + // the reference model + 'model' => site(), + // current page when paginating + 'page' => 1, + // a query string to fetch specific items + 'query' => null, + // search query + 'search' => null, + // query template for the text field + 'text' => null + ]; + } + + /** + * Fetches all items for the picker + * + * @return \Kirby\Cms\Collection|null + */ + abstract public function items(); + + /** + * Converts all given items to an associative + * array that is already optimized for the + * panel picker component. + * + * @param \Kirby\Cms\Collection|null $items + * @return array + */ + public function itemsToArray($items = null): array + { + if ($items === null) { + return []; + } + + $result = []; + + foreach ($items as $index => $item) { + if (empty($this->options['map']) === false) { + $result[] = $this->options['map']($item); + } else { + $result[] = $item->panelPickerData([ + 'image' => $this->options['image'], + 'info' => $this->options['info'], + 'model' => $this->options['model'], + 'text' => $this->options['text'], + ]); + } + } + + return $result; + } + + /** + * Apply pagination to the collection + * of items according to the options. + * + * @param \Kirby\Cms\Collection $items + * @return \Kirby\Cms\Collection + */ + public function paginate(Collection $items) + { + return $items->paginate([ + 'limit' => $this->options['limit'], + 'page' => $this->options['page'] + ]); + } + + /** + * Return the most relevant pagination + * info as array + * + * @param \Kirby\Cms\Pagination $pagination + * @return array + */ + public function paginationToArray(Pagination $pagination): array + { + return [ + 'limit' => $pagination->limit(), + 'page' => $pagination->page(), + 'total' => $pagination->total() + ]; + } + + /** + * Search through the collection of items + * if not deactivate in the options + * + * @param \Kirby\Cms\Collection $items + * @return \Kirby\Cms\Collection + */ + public function search(Collection $items) + { + if (empty($this->options['search']) === false) { + return $items->search($this->options['search']); + } + + return $items; + } + + /** + * Returns an associative array + * with all information for the picker. + * This will be passed directly to the API. + * + * @return array + */ + public function toArray(): array + { + $items = $this->items(); + + return [ + 'data' => $this->itemsToArray($items), + 'pagination' => $this->paginationToArray($items->pagination()), + ]; + } +} diff --git a/kirby/src/Cms/Plugin.php b/kirby/src/Cms/Plugin.php new file mode 100644 index 0000000..e60ddae --- /dev/null +++ b/kirby/src/Cms/Plugin.php @@ -0,0 +1,158 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Plugin extends Model +{ + protected $extends; + protected $info; + protected $name; + protected $root; + + /** + * @param string $key + * @param array|null $arguments + * @return mixed|null + */ + public function __call(string $key, array $arguments = null) + { + return $this->info()[$key] ?? null; + } + + /** + * Plugin constructor + * + * @param string $name + * @param array $extends + */ + public function __construct(string $name, array $extends = []) + { + $this->setName($name); + $this->extends = $extends; + $this->root = $extends['root'] ?? dirname(debug_backtrace()[0]['file']); + + unset($this->extends['root']); + } + + /** + * @return array + */ + public function extends(): array + { + return $this->extends; + } + + /** + * @return array + */ + public function info(): array + { + if (is_array($this->info) === true) { + return $this->info; + } + + try { + $info = Data::read($this->manifest()); + } catch (Exception $e) { + // there is no manifest file or it is invalid + $info = []; + } + + return $this->info = $info; + } + + /** + * @return string + */ + public function manifest(): string + { + return $this->root() . '/composer.json'; + } + + /** + * @return string + */ + public function mediaRoot(): string + { + return App::instance()->root('media') . '/plugins/' . $this->name(); + } + + /** + * @return string + */ + public function mediaUrl(): string + { + return App::instance()->url('media') . '/plugins/' . $this->name(); + } + + /** + * @return string + */ + public function name(): string + { + return $this->name; + } + + /** + * @param string $key + * @return mixed + */ + public function option(string $key) + { + return $this->kirby()->option($this->prefix() . '.' . $key); + } + + /** + * @return string + */ + public function prefix(): string + { + return str_replace('/', '.', $this->name()); + } + + /** + * @return string + */ + public function root(): string + { + return $this->root; + } + + /** + * @param string $name + * @return self + * @throws \Kirby\Exception\InvalidArgumentException + */ + protected function setName(string $name) + { + if (preg_match('!^[a-z0-9-]+\/[a-z0-9-]+$!i', $name) == false) { + throw new InvalidArgumentException('The plugin name must follow the format "a-z0-9-/a-z0-9-"'); + } + + $this->name = $name; + return $this; + } + + /** + * @return array + */ + public function toArray(): array + { + return $this->propertiesToArray(); + } +} diff --git a/kirby/src/Cms/PluginAssets.php b/kirby/src/Cms/PluginAssets.php new file mode 100644 index 0000000..20f53c8 --- /dev/null +++ b/kirby/src/Cms/PluginAssets.php @@ -0,0 +1,84 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class PluginAssets +{ + /** + * Clean old/deprecated assets on every resolve + * + * @param string $pluginName + * @return void + */ + public static function clean(string $pluginName): void + { + if ($plugin = App::instance()->plugin($pluginName)) { + $root = $plugin->root() . '/assets'; + $media = $plugin->mediaRoot(); + $assets = Dir::index($media, true); + + foreach ($assets as $asset) { + $original = $root . '/' . $asset; + + if (file_exists($original) === false) { + $assetRoot = $media . '/' . $asset; + + if (is_file($assetRoot) === true) { + F::remove($assetRoot); + } else { + Dir::remove($assetRoot); + } + } + } + } + } + + /** + * Create a symlink for a plugin asset and + * return the public URL + * + * @param string $pluginName + * @param string $filename + * @return \Kirby\Cms\Response|null + */ + public static function resolve(string $pluginName, string $filename) + { + if ($plugin = App::instance()->plugin($pluginName)) { + $source = $plugin->root() . '/assets/' . $filename; + + if (F::exists($source, $plugin->root()) === true) { + // do some spring cleaning for older files + static::clean($pluginName); + + $target = $plugin->mediaRoot() . '/' . $filename; + $url = $plugin->mediaUrl() . '/' . $filename; + + // create the plugin directory first + Dir::make($plugin->mediaRoot(), true); + + if (F::link($source, $target, 'symlink') === true) { + return Response::redirect($url); + } + + return Response::file($source); + } + } + + return null; + } +} diff --git a/kirby/src/Cms/R.php b/kirby/src/Cms/R.php new file mode 100644 index 0000000..9c9c354 --- /dev/null +++ b/kirby/src/Cms/R.php @@ -0,0 +1,25 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class R extends Facade +{ + /** + * @return \Kirby\Http\Request + */ + public static function instance() + { + return App::instance()->request(); + } +} diff --git a/kirby/src/Cms/Responder.php b/kirby/src/Cms/Responder.php new file mode 100644 index 0000000..3b91da7 --- /dev/null +++ b/kirby/src/Cms/Responder.php @@ -0,0 +1,222 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Responder +{ + /** + * HTTP status code + * + * @var int + */ + protected $code = null; + + /** + * Response body + * + * @var string + */ + protected $body = null; + + /** + * HTTP headers + * + * @var array + */ + protected $headers = []; + + /** + * Content type + * + * @var string + */ + protected $type = null; + + /** + * Creates and sends the response + * + * @return string + */ + public function __toString(): string + { + return (string)$this->send(); + } + + /** + * Setter and getter for the response body + * + * @param string|null $body + * @return string|self + */ + public function body(string $body = null) + { + if ($body === null) { + return $this->body; + } + + $this->body = $body; + return $this; + } + + /** + * Setter and getter for the status code + * + * @param int|null $code + * @return int|self + */ + public function code(int $code = null) + { + if ($code === null) { + return $this->code; + } + + $this->code = $code; + return $this; + } + + /** + * Construct response from an array + * + * @param array $response + */ + public function fromArray(array $response): void + { + $this->body($response['body'] ?? null); + $this->code($response['code'] ?? null); + $this->headers($response['headers'] ?? null); + $this->type($response['type'] ?? null); + } + + /** + * Setter and getter for a single header + * + * @param string $key + * @param string|false|null $value + * @return string|self + */ + public function header(string $key, $value = null) + { + if ($value === null) { + return $this->headers[$key] ?? null; + } + + if ($value === false) { + unset($this->headers[$key]); + return $this; + } + + $this->headers[$key] = $value; + return $this; + } + + /** + * Setter and getter for all headers + * + * @param array|null $headers + * @return array|self + */ + public function headers(array $headers = null) + { + if ($headers === null) { + return $this->headers; + } + + $this->headers = $headers; + return $this; + } + + /** + * Shortcut to configure a json response + * + * @param array|null $json + * @return string|self + */ + public function json(array $json = null) + { + if ($json !== null) { + $this->body(json_encode($json)); + } + + return $this->type('application/json'); + } + + /** + * Shortcut to create a redirect response + * + * @param string|null $location + * @param int|null $code + * @return self + */ + public function redirect(?string $location = null, ?int $code = null) + { + $location = Url::to($location ?? '/'); + $location = Url::unIdn($location); + + return $this + ->header('Location', (string)$location) + ->code($code ?? 302); + } + + /** + * Creates and returns the response object from the config + * + * @param string|null $body + * @return \Kirby\Cms\Response + */ + public function send(string $body = null) + { + if ($body !== null) { + $this->body($body); + } + + return new Response($this->toArray()); + } + + /** + * Converts the response configuration + * to an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'body' => $this->body, + 'code' => $this->code, + 'headers' => $this->headers, + 'type' => $this->type, + ]; + } + + /** + * Setter and getter for the content type + * + * @param string|null $type + * @return string|self + */ + public function type(string $type = null) + { + if ($type === null) { + return $this->type; + } + + if (Str::contains($type, '/') === false) { + $type = Mime::fromExtension($type); + } + + $this->type = $type; + return $this; + } +} diff --git a/kirby/src/Cms/Response.php b/kirby/src/Cms/Response.php new file mode 100644 index 0000000..fe2deca --- /dev/null +++ b/kirby/src/Cms/Response.php @@ -0,0 +1,30 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Response extends \Kirby\Http\Response +{ + /** + * Adjusted redirect creation which + * parses locations with the Url::to method + * first. + * + * @param string|null $location + * @param int|null $code + * @return self + */ + public static function redirect(?string $location = null, ?int $code = null) + { + return parent::redirect(Url::to($location ?? '/'), $code); + } +} diff --git a/kirby/src/Cms/Role.php b/kirby/src/Cms/Role.php new file mode 100644 index 0000000..35a9a49 --- /dev/null +++ b/kirby/src/Cms/Role.php @@ -0,0 +1,232 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Role extends Model +{ + protected $description; + protected $name; + protected $permissions; + protected $title; + + public function __construct(array $props) + { + $this->setProperties($props); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + /** + * @return string + */ + public function __toString(): string + { + return $this->name(); + } + + /** + * @param array $inject + * @return self + */ + public static function admin(array $inject = []) + { + try { + return static::load('admin'); + } catch (Exception $e) { + return static::factory(static::defaults()['admin'], $inject); + } + } + + /** + * @return array + */ + protected static function defaults(): array + { + return [ + 'admin' => [ + 'name' => 'admin', + 'description' => I18n::translate('role.admin.description'), + 'title' => I18n::translate('role.admin.title'), + 'permissions' => true, + ], + 'nobody' => [ + 'name' => 'nobody', + 'description' => I18n::translate('role.nobody.description'), + 'title' => I18n::translate('role.nobody.title'), + 'permissions' => false, + ] + ]; + } + + /** + * @return mixed + */ + public function description() + { + return $this->description; + } + + /** + * @param array $props + * @param array $inject + * @return self + */ + public static function factory(array $props, array $inject = []) + { + return new static($props + $inject); + } + + /** + * @return string + */ + public function id(): string + { + return $this->name(); + } + + /** + * @return bool + */ + public function isAdmin(): bool + { + return $this->name() === 'admin'; + } + + /** + * @return bool + */ + public function isNobody(): bool + { + return $this->name() === 'nobody'; + } + + /** + * @param string $file + * @param array $inject + * @return self + */ + public static function load(string $file, array $inject = []) + { + $data = Data::read($file); + $data['name'] = F::name($file); + + return static::factory($data, $inject); + } + + /** + * @return string + */ + public function name(): string + { + return $this->name; + } + + /** + * @param array $inject + * @return self + */ + public static function nobody(array $inject = []) + { + try { + return static::load('nobody'); + } catch (Exception $e) { + return static::factory(static::defaults()['nobody'], $inject); + } + } + + /** + * @return \Kirby\Cms\Permissions + */ + public function permissions() + { + return $this->permissions; + } + + /** + * @param [type] $description + * @return self + */ + protected function setDescription($description = null) + { + $this->description = I18n::translate($description, $description); + return $this; + } + + /** + * @param string $name + * @return self + */ + protected function setName(string $name) + { + $this->name = $name; + return $this; + } + + /** + * @param [type] $permissions + * @return self + */ + protected function setPermissions($permissions = null) + { + $this->permissions = new Permissions($permissions); + return $this; + } + + /** + * @param [type] $title + * @return self + */ + protected function setTitle($title = null) + { + $this->title = I18n::translate($title, $title); + return $this; + } + + /** + * @return string + */ + public function title(): string + { + return $this->title = $this->title ?? ucfirst($this->name()); + } + + /** + * Converts the most important role + * properties to an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'description' => $this->description(), + 'id' => $this->id(), + 'name' => $this->name(), + 'permissions' => $this->permissions()->toArray(), + 'title' => $this->title(), + ]; + } +} diff --git a/kirby/src/Cms/Roles.php b/kirby/src/Cms/Roles.php new file mode 100644 index 0000000..de9a08e --- /dev/null +++ b/kirby/src/Cms/Roles.php @@ -0,0 +1,139 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Roles extends Collection +{ + /** + * Returns a filtered list of all + * roles that can be created by the + * current user + * + * @return self + * @throws \Exception + */ + public function canBeChanged() + { + if (App::instance()->user()) { + return $this->filter(function ($role) { + $newUser = new User([ + 'email' => 'test@getkirby.com', + 'role' => $role->id() + ]); + + return $newUser->permissions()->can('changeRole'); + }); + } + + return $this; + } + + /** + * Returns a filtered list of all + * roles that can be created by the + * current user + * + * @return self + * @throws \Exception + */ + public function canBeCreated() + { + if (App::instance()->user()) { + return $this->filter(function ($role) { + $newUser = new User([ + 'email' => 'test@getkirby.com', + 'role' => $role->id() + ]); + + return $newUser->permissions()->can('create'); + }); + } + + return $this; + } + + /** + * @param array $roles + * @param array $inject + * @return self + */ + public static function factory(array $roles, array $inject = []) + { + $collection = new static(); + + // read all user blueprints + foreach ($roles as $props) { + $role = Role::factory($props, $inject); + $collection->set($role->id(), $role); + } + + // always include the admin role + if ($collection->find('admin') === null) { + $collection->set('admin', Role::admin()); + } + + // return the collection sorted by name + return $collection->sortBy('name', 'asc'); + } + + /** + * @param string|null $root + * @param array $inject + * @return self + */ + public static function load(string $root = null, array $inject = []) + { + $roles = new static(); + + // load roles from plugins + foreach (App::instance()->extensions('blueprints') as $blueprintName => $blueprint) { + if (substr($blueprintName, 0, 6) !== 'users/') { + continue; + } + + if (is_array($blueprint) === true) { + $role = Role::factory($blueprint, $inject); + } else { + $role = Role::load($blueprint, $inject); + } + + $roles->set($role->id(), $role); + } + + // load roles from directory + if ($root !== null) { + foreach (glob($root . '/*.yml') as $file) { + $filename = basename($file); + + if ($filename === 'default.yml') { + continue; + } + + $role = Role::load($file, $inject); + $roles->set($role->id(), $role); + } + } + + // always include the admin role + if ($roles->find('admin') === null) { + $roles->set('admin', Role::admin($inject)); + } + + // return the collection sorted by name + return $roles->sortBy('name', 'asc'); + } +} diff --git a/kirby/src/Cms/S.php b/kirby/src/Cms/S.php new file mode 100644 index 0000000..8830077 --- /dev/null +++ b/kirby/src/Cms/S.php @@ -0,0 +1,26 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class S extends Facade +{ + /** + * @return \Kirby\Session\Session + */ + public static function instance() + { + return App::instance()->session(); + } +} diff --git a/kirby/src/Cms/Search.php b/kirby/src/Cms/Search.php new file mode 100644 index 0000000..d4fd894 --- /dev/null +++ b/kirby/src/Cms/Search.php @@ -0,0 +1,62 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Search +{ + /** + * @param string|null $query + * @param array $params + * @return \Kirby\Cms\Files + */ + public static function files(string $query = null, $params = []) + { + return App::instance()->site()->index()->files()->search($query, $params); + } + + /** + * Native search method to search for anything within the collection + * + * @param \Kirby\Cms\Collection $collection + * @param string|null $query + * @param mixed $params + * @return \Kirby\Cms\Collection|bool + */ + public static function collection(Collection $collection, string $query = null, $params = []) + { + $kirby = App::instance(); + return $kirby->component('search')($kirby, $collection, $query, $params); + } + + /** + * @param string|null $query + * @param array $params + * @return \Kirby\Cms\Pages + */ + public static function pages(string $query = null, $params = []) + { + return App::instance()->site()->index()->search($query, $params); + } + + /** + * @param string|null $query + * @param array $params + * @return \Kirby\Cms\Users + */ + public static function users(string $query = null, $params = []) + { + return App::instance()->users()->search($query, $params); + } +} diff --git a/kirby/src/Cms/Section.php b/kirby/src/Cms/Section.php new file mode 100644 index 0000000..a8d62e7 --- /dev/null +++ b/kirby/src/Cms/Section.php @@ -0,0 +1,94 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Section extends Component +{ + /** + * Registry for all component mixins + * + * @var array + */ + public static $mixins = []; + + /** + * Registry for all component types + * + * @var array + */ + public static $types = []; + + + /** + * Section constructor. + * + * @param string $type + * @param array $attrs + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function __construct(string $type, array $attrs = []) + { + if (isset($attrs['model']) === false) { + throw new InvalidArgumentException('Undefined section model'); + } + + // use the type as fallback for the name + $attrs['name'] = $attrs['name'] ?? $type; + $attrs['type'] = $type; + + parent::__construct($type, $attrs); + } + + /** + * @return \Kirby\Cms\App + */ + public function kirby() + { + return $this->model->kirby(); + } + + /** + * @return \Kirby\Cms\Model + */ + public function model() + { + return $this->model; + } + + /** + * @return array + */ + public function toArray(): array + { + $array = parent::toArray(); + + unset($array['model']); + + return $array; + } + + /** + * @return array + */ + public function toResponse(): array + { + return array_merge([ + 'status' => 'ok', + 'code' => 200, + 'name' => $this->name, + 'type' => $this->type + ], $this->toArray()); + } +} diff --git a/kirby/src/Cms/Site.php b/kirby/src/Cms/Site.php new file mode 100644 index 0000000..eab8597 --- /dev/null +++ b/kirby/src/Cms/Site.php @@ -0,0 +1,673 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Site extends ModelWithContent +{ + const CLASS_ALIAS = 'site'; + + use SiteActions; + use HasChildren; + use HasFiles; + use HasMethods; + + /** + * The SiteBlueprint object + * + * @var SiteBlueprint + */ + protected $blueprint; + + /** + * The error page object + * + * @var Page + */ + protected $errorPage; + + /** + * The id of the error page, which is + * fetched in the errorPage method + * + * @var string + */ + protected $errorPageId = 'error'; + + /** + * The home page object + * + * @var Page + */ + protected $homePage; + + /** + * The id of the home page, which is + * fetched in the errorPage method + * + * @var string + */ + protected $homePageId = 'home'; + + /** + * Cache for the inventory array + * + * @var array + */ + protected $inventory; + + /** + * The current page object + * + * @var Page + */ + protected $page; + + /** + * The absolute path to the site directory + * + * @var string + */ + protected $root; + + /** + * The page url + * + * @var string + */ + protected $url; + + /** + * Modified getter to also return fields + * from the content + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + // public property access + if (isset($this->$method) === true) { + return $this->$method; + } + + // site methods + if ($this->hasMethod($method)) { + return $this->callMethod($method, $arguments); + } + + // return site content otherwise + return $this->content()->get($method, $arguments); + } + + /** + * Creates a new Site object + * + * @param array $props + */ + public function __construct(array $props = []) + { + $this->setProperties($props); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return array_merge($this->toArray(), [ + 'content' => $this->content(), + 'children' => $this->children(), + 'files' => $this->files(), + ]); + } + + /** + * Makes it possible to convert the site model + * to a string. Mostly useful for debugging + * + * @return string + */ + public function __toString(): string + { + return $this->url(); + } + + /** + * Returns the url to the api endpoint + * + * @internal + * @param bool $relative + * @return string + */ + public function apiUrl(bool $relative = false): string + { + if ($relative === true) { + return 'site'; + } else { + return $this->kirby()->url('api') . '/site'; + } + } + + /** + * Returns the blueprint object + * + * @return \Kirby\Cms\SiteBlueprint + */ + public function blueprint() + { + if (is_a($this->blueprint, 'Kirby\Cms\SiteBlueprint') === true) { + return $this->blueprint; + } + + return $this->blueprint = SiteBlueprint::factory('site', null, $this); + } + + /** + * Builds a breadcrumb collection + * + * @return \Kirby\Cms\Pages + */ + public function breadcrumb() + { + // get all parents and flip the order + $crumb = $this->page()->parents()->flip(); + + // add the home page + $crumb->prepend($this->homePage()->id(), $this->homePage()); + + // add the active page + $crumb->append($this->page()->id(), $this->page()); + + return $crumb; + } + + /** + * Prepares the content for the write method + * + * @internal + * @param array $data + * @param string|null $languageCode + * @return array + */ + public function contentFileData(array $data, string $languageCode = null): array + { + return A::prepend($data, [ + 'title' => $data['title'] ?? null, + ]); + } + + /** + * Filename for the content file + * + * @internal + * @return string + */ + public function contentFileName(): string + { + return 'site'; + } + + /** + * Returns the error page object + * + * @return \Kirby\Cms\Page|null + */ + public function errorPage() + { + if (is_a($this->errorPage, 'Kirby\Cms\Page') === true) { + return $this->errorPage; + } + + if ($error = $this->find($this->errorPageId())) { + return $this->errorPage = $error; + } + + return null; + } + + /** + * Returns the global error page id + * + * @internal + * @return string + */ + public function errorPageId(): string + { + return $this->errorPageId ?? 'error'; + } + + /** + * Checks if the site exists on disk + * + * @return bool + */ + public function exists(): bool + { + return is_dir($this->root()) === true; + } + + /** + * Returns the home page object + * + * @return \Kirby\Cms\Page|null + */ + public function homePage() + { + if (is_a($this->homePage, 'Kirby\Cms\Page') === true) { + return $this->homePage; + } + + if ($home = $this->find($this->homePageId())) { + return $this->homePage = $home; + } + + return null; + } + + /** + * Returns the global home page id + * + * @internal + * @return string + */ + public function homePageId(): string + { + return $this->homePageId ?? 'home'; + } + + /** + * Creates an inventory of all files + * and children in the site directory + * + * @internal + * @return array + */ + public function inventory(): array + { + if ($this->inventory !== null) { + return $this->inventory; + } + + $kirby = $this->kirby(); + + return $this->inventory = Dir::inventory( + $this->root(), + $kirby->contentExtension(), + $kirby->contentIgnore(), + $kirby->multilang() + ); + } + + /** + * Compares the current object with the given site object + * + * @param mixed $site + * @return bool + */ + public function is($site): bool + { + if (is_a($site, 'Kirby\Cms\Site') === false) { + return false; + } + + return $this === $site; + } + + /** + * Returns the root to the media folder for the site + * + * @internal + * @return string + */ + public function mediaRoot(): string + { + return $this->kirby()->root('media') . '/site'; + } + + /** + * The site's base url for any files + * + * @internal + * @return string + */ + public function mediaUrl(): string + { + return $this->kirby()->url('media') . '/site'; + } + + /** + * Gets the last modification date of all pages + * in the content folder. + * + * @param string|null $format + * @param string|null $handler + * @return mixed + */ + public function modified(string $format = null, string $handler = null) + { + return Dir::modified($this->root(), $format, $handler ?? $this->kirby()->option('date.handler', 'date')); + } + + /** + * Returns the current page if `$path` + * is not specified. Otherwise it will try + * to find a page by the given path. + * + * If no current page is set with the page + * prop, the home page will be returned if + * it can be found. (see `Site::homePage()`) + * + * @param string|null $path + * @return \Kirby\Cms\Page|null + */ + public function page(string $path = null) + { + if ($path !== null) { + return $this->find($path); + } + + if (is_a($this->page, 'Kirby\Cms\Page') === true) { + return $this->page; + } + + try { + return $this->page = $this->homePage(); + } catch (LogicException $e) { + return $this->page = null; + } + } + + /** + * Alias for `Site::children()` + * + * @return \Kirby\Cms\Pages + */ + public function pages() + { + return $this->children(); + } + + /** + * Returns the full path without leading slash + * + * @internal + * @return string + */ + public function panelPath(): string + { + return 'site'; + } + + /** + * Returns the url to the editing view + * in the panel + * + * @internal + * @param bool $relative + * @return string + */ + public function panelUrl(bool $relative = false): string + { + if ($relative === true) { + return '/' . $this->panelPath(); + } else { + return $this->kirby()->url('panel') . '/' . $this->panelPath(); + } + } + + /** + * Returns the permissions object for this site + * + * @return \Kirby\Cms\SitePermissions + */ + public function permissions() + { + return new SitePermissions($this); + } + + /** + * Preview Url + * + * @internal + * @return string|null + */ + public function previewUrl(): ?string + { + $preview = $this->blueprint()->preview(); + + if ($preview === false) { + return null; + } + + if ($preview === true) { + $url = $this->url(); + } else { + $url = $preview; + } + + return $url; + } + + /** + * Returns the absolute path to the content directory + * + * @return string + */ + public function root(): string + { + return $this->root = $this->root ?? $this->kirby()->root('content'); + } + + /** + * Returns the SiteRules class instance + * which is being used in various methods + * to check for valid actions and input. + * + * @return \Kirby\Cms\SiteRules + */ + protected function rules() + { + return new SiteRules(); + } + + /** + * Search all pages in the site + * + * @param string|null $query + * @param array $params + * @return \Kirby\Cms\Pages + */ + public function search(string $query = null, $params = []) + { + return $this->index()->search($query, $params); + } + + /** + * Sets the Blueprint object + * + * @param array|null $blueprint + * @return self + */ + protected function setBlueprint(array $blueprint = null) + { + if ($blueprint !== null) { + $blueprint['model'] = $this; + $this->blueprint = new SiteBlueprint($blueprint); + } + + return $this; + } + + /** + * Sets the id of the error page, which + * is used in the errorPage method + * to get the default error page if nothing + * else is set. + * + * @param string $id + * @return self + */ + protected function setErrorPageId(string $id = 'error') + { + $this->errorPageId = $id; + return $this; + } + + /** + * Sets the id of the home page, which + * is used in the homePage method + * to get the default home page if nothing + * else is set. + * + * @param string $id + * @return self + */ + protected function setHomePageId(string $id = 'home') + { + $this->homePageId = $id; + return $this; + } + + /** + * Sets the current page object + * + * @internal + * @param \Kirby\Cms\Page|null $page + * @return self + */ + public function setPage(Page $page = null) + { + $this->page = $page; + return $this; + } + + /** + * Sets the Url + * + * @param string|null $url + * @return self + */ + protected function setUrl($url = null) + { + $this->url = $url; + return $this; + } + + /** + * Converts the most important site + * properties to an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'children' => $this->children()->keys(), + 'content' => $this->content()->toArray(), + 'errorPage' => $this->errorPage() ? $this->errorPage()->id(): false, + 'files' => $this->files()->keys(), + 'homePage' => $this->homePage() ? $this->homePage()->id(): false, + 'page' => $this->page() ? $this->page()->id(): false, + 'title' => $this->title()->value(), + 'url' => $this->url(), + ]; + } + + /** + * Returns the Url + * + * @param string|null $language + * @return string + */ + public function url($language = null): string + { + if ($language !== null || $this->kirby()->multilang() === true) { + return $this->urlForLanguage($language); + } + + return $this->url ?? $this->kirby()->url(); + } + + /** + * Returns the translated url + * + * @internal + * @param string|null $languageCode + * @param array|null $options + * @return string + */ + public function urlForLanguage(string $languageCode = null, array $options = null): string + { + if ($language = $this->kirby()->language($languageCode)) { + return $language->url(); + } + + return $this->kirby()->url(); + } + + /** + * Sets the current page by + * id or page object and + * returns the current page + * + * @internal + * @param string|\Kirby\Cms\Page $page + * @param string|null $languageCode + * @return \Kirby\Cms\Page + */ + public function visit($page, string $languageCode = null) + { + if ($languageCode !== null) { + $this->kirby()->setCurrentTranslation($languageCode); + $this->kirby()->setCurrentLanguage($languageCode); + } + + // convert ids to a Page object + if (is_string($page)) { + $page = $this->find($page); + } + + // handle invalid pages + if (is_a($page, 'Kirby\Cms\Page') === false) { + throw new InvalidArgumentException('Invalid page object'); + } + + // set the current active page + $this->setPage($page); + + // return the page + return $page; + } + + /** + * Checks if any content of the site has been + * modified after the given unix timestamp + * This is mainly used to auto-update the cache + * + * @param mixed $time + * @return bool + */ + public function wasModifiedAfter($time): bool + { + return Dir::wasModifiedAfter($this->root(), $time); + } +} diff --git a/kirby/src/Cms/SiteActions.php b/kirby/src/Cms/SiteActions.php new file mode 100644 index 0000000..f693d8e --- /dev/null +++ b/kirby/src/Cms/SiteActions.php @@ -0,0 +1,98 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait SiteActions +{ + /** + * Commits a site action, by following these steps + * + * 1. checks the action rules + * 2. sends the before hook + * 3. commits the store action + * 4. sends the after hook + * 5. returns the result + * + * @param string $action + * @param mixed ...$arguments + * @param Closure $callback + * @return mixed + */ + protected function commit(string $action, array $arguments, Closure $callback) + { + $old = $this->hardcopy(); + $kirby = $this->kirby(); + $argumentValues = array_values($arguments); + + $this->rules()->$action(...$argumentValues); + $kirby->trigger('site.' . $action . ':before', $arguments); + + $result = $callback(...$argumentValues); + + $kirby->trigger('site.' . $action . ':after', ['newSite' => $result, 'oldSite' => $old]); + + $kirby->cache('pages')->flush(); + return $result; + } + + /** + * Change the site title + * + * @param string $title + * @param string|null $languageCode + * @return self + */ + public function changeTitle(string $title, string $languageCode = null) + { + $arguments = ['site' => $this, 'title' => $title, 'languageCode' => $languageCode]; + return $this->commit('changeTitle', $arguments, function ($site, $title, $languageCode) { + return $site->save(['title' => $title], $languageCode); + }); + } + + /** + * Creates a main page + * + * @param array $props + * @return \Kirby\Cms\Page + */ + public function createChild(array $props) + { + $props = array_merge($props, [ + 'url' => null, + 'num' => null, + 'parent' => null, + 'site' => $this, + ]); + + return Page::create($props); + } + + /** + * Clean internal caches + * + * @return self + */ + public function purge() + { + $this->blueprint = null; + $this->children = null; + $this->content = null; + $this->files = null; + $this->inventory = null; + $this->translations = null; + + return $this; + } +} diff --git a/kirby/src/Cms/SiteBlueprint.php b/kirby/src/Cms/SiteBlueprint.php new file mode 100644 index 0000000..9b58538 --- /dev/null +++ b/kirby/src/Cms/SiteBlueprint.php @@ -0,0 +1,60 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class SiteBlueprint extends Blueprint +{ + /** + * Creates a new page blueprint object + * with the given props + * + * @param array $props + */ + public function __construct(array $props) + { + parent::__construct($props); + + // normalize all available page options + $this->props['options'] = $this->normalizeOptions( + $props['options'] ?? true, + // defaults + [ + 'changeTitle' => null, + 'update' => null, + ], + // aliases + [ + 'title' => 'changeTitle', + ] + ); + } + + /** + * Returns the preview settings + * The preview setting controls the "Open" + * button in the panel and redirects it to a + * different URL if necessary. + * + * @return string|bool + */ + public function preview() + { + $preview = $this->props['options']['preview'] ?? true; + + if (is_string($preview) === true) { + return $this->model->toString($preview); + } + + return $preview; + } +} diff --git a/kirby/src/Cms/SitePermissions.php b/kirby/src/Cms/SitePermissions.php new file mode 100644 index 0000000..aa61ea8 --- /dev/null +++ b/kirby/src/Cms/SitePermissions.php @@ -0,0 +1,17 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class SitePermissions extends ModelPermissions +{ + protected $category = 'site'; +} diff --git a/kirby/src/Cms/SiteRules.php b/kirby/src/Cms/SiteRules.php new file mode 100644 index 0000000..f4797fd --- /dev/null +++ b/kirby/src/Cms/SiteRules.php @@ -0,0 +1,58 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class SiteRules +{ + /** + * Validates if the site title can be changed + * + * @param \Kirby\Cms\Site $site + * @param string $title + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the title is empty + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the title + */ + public static function changeTitle(Site $site, string $title): bool + { + if ($site->permissions()->changeTitle() !== true) { + throw new PermissionException(['key' => 'site.changeTitle.permission']); + } + + if (Str::length($title) === 0) { + throw new InvalidArgumentException(['key' => 'site.changeTitle.empty']); + } + + return true; + } + + /** + * Validates if the site can be updated + * + * @param \Kirby\Cms\Site $site + * @param array $content + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to update the site + */ + public static function update(Site $site, array $content = []): bool + { + if ($site->permissions()->update() !== true) { + throw new PermissionException(['key' => 'site.update.permission']); + } + + return true; + } +} diff --git a/kirby/src/Cms/Structure.php b/kirby/src/Cms/Structure.php new file mode 100644 index 0000000..5050690 --- /dev/null +++ b/kirby/src/Cms/Structure.php @@ -0,0 +1,64 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Structure extends Collection +{ + /** + * Creates a new Collection with the given objects + * + * @param array $objects + * @param object|null $parent + */ + public function __construct($objects = [], $parent = null) + { + $this->parent = $parent; + $this->set($objects); + } + + /** + * The internal setter for collection items. + * This makes sure that nothing unexpected ends + * up in the collection. You can pass arrays or + * StructureObjects + * + * @param string $id + * @param array|StructureObject $props + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function __set(string $id, $props) + { + if (is_a($props, 'Kirby\Cms\StructureObject') === true) { + $object = $props; + } else { + if (is_array($props) === false) { + throw new InvalidArgumentException('Invalid structure data'); + } + + $object = new StructureObject([ + 'content' => $props, + 'id' => $props['id'] ?? $id, + 'parent' => $this->parent, + 'structure' => $this + ]); + } + + return parent::__set($object->id(), $object); + } +} diff --git a/kirby/src/Cms/StructureObject.php b/kirby/src/Cms/StructureObject.php new file mode 100644 index 0000000..5496f6f --- /dev/null +++ b/kirby/src/Cms/StructureObject.php @@ -0,0 +1,210 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class StructureObject extends Model +{ + use HasSiblings; + + /** + * The content + * + * @var Content + */ + protected $content; + + /** + * @var string + */ + protected $id; + + /** + * @var Page|Site|File|User + */ + protected $parent; + + /** + * The parent Structure collection + * + * @var Structure + */ + protected $structure; + + /** + * Modified getter to also return fields + * from the object's content + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + // public property access + if (isset($this->$method) === true) { + return $this->$method; + } + + return $this->content()->get($method, $arguments); + } + + /** + * Creates a new StructureObject with the given props + * + * @param array $props + */ + public function __construct(array $props) + { + $this->setProperties($props); + } + + /** + * Returns the content + * + * @return \Kirby\Cms\Content + */ + public function content() + { + if (is_a($this->content, 'Kirby\Cms\Content') === true) { + return $this->content; + } + + if (is_array($this->content) !== true) { + $this->content = []; + } + + return $this->content = new Content($this->content, $this->parent()); + } + + /** + * Returns the required id + * + * @return string + */ + public function id(): string + { + return $this->id; + } + + /** + * Compares the current object with the given structure object + * + * @param mixed $structure + * @return bool + */ + public function is($structure): bool + { + if (is_a($structure, 'Kirby\Cms\StructureObject') === false) { + return false; + } + + return $this === $structure; + } + + /** + * Returns the parent Model object + * + * @return \Kirby\Cms\Model + */ + public function parent() + { + return $this->parent; + } + + /** + * Sets the Content object with the given parent + * + * @param array|null $content + * @return self + */ + protected function setContent(array $content = null) + { + $this->content = $content; + return $this; + } + + /** + * Sets the id of the object. + * The id is required. The structure + * class will use the index, if no id is + * specified. + * + * @param string $id + * @return self + */ + protected function setId(string $id) + { + $this->id = $id; + return $this; + } + + /** + * Sets the parent Model. This can either be a + * Page, Site, File or User object + * + * @param \Kirby\Cms\Model|null $parent + * @return self + */ + protected function setParent(Model $parent = null) + { + $this->parent = $parent; + return $this; + } + + /** + * Sets the parent Structure collection + * + * @param \Kirby\Cms\Structure|null $structure + * @return self + */ + protected function setStructure(Structure $structure = null) + { + $this->structure = $structure; + return $this; + } + + /** + * Returns the parent Structure collection as siblings + * + * @return \Kirby\Cms\Structure + */ + protected function siblingsCollection() + { + return $this->structure; + } + + /** + * Converts all fields in the object to a + * plain associative array. The id is + * injected into the array afterwards + * to make sure it's always present and + * not overloaded in the content. + * + * @return array + */ + public function toArray(): array + { + $array = $this->content()->toArray(); + $array['id'] = $this->id(); + + ksort($array); + + return $array; + } +} diff --git a/kirby/src/Cms/System.php b/kirby/src/Cms/System.php new file mode 100644 index 0000000..70f6779 --- /dev/null +++ b/kirby/src/Cms/System.php @@ -0,0 +1,495 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class System +{ + /** + * @var \Kirby\Cms\App + */ + protected $app; + + /** + * @param \Kirby\Cms\App $app + */ + public function __construct(App $app) + { + $this->app = $app; + + // try to create all folders that could be missing + $this->init(); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + /** + * Get an status array of all checks + * + * @return array + */ + public function status(): array + { + return [ + 'accounts' => $this->accounts(), + 'content' => $this->content(), + 'curl' => $this->curl(), + 'sessions' => $this->sessions(), + 'mbstring' => $this->mbstring(), + 'media' => $this->media(), + 'php' => $this->php(), + 'server' => $this->server(), + ]; + } + + /** + * Check for a writable accounts folder + * + * @return bool + */ + public function accounts(): bool + { + return is_writable($this->app->root('accounts')); + } + + /** + * Check for a writable content folder + * + * @return bool + */ + public function content(): bool + { + return is_writable($this->app->root('content')); + } + + /** + * Check for an existing curl extension + * + * @return bool + */ + public function curl(): bool + { + return extension_loaded('curl'); + } + + /** + * Returns the app's human-readable + * index URL without scheme + * + * @return string + */ + public function indexUrl(): string + { + $url = $this->app->url('index'); + + if (Url::isAbsolute($url)) { + $uri = Url::toObject($url); + } else { + // index URL was configured without host, use the current host + $uri = Uri::current([ + 'path' => $url, + 'query' => null + ]); + } + + return $uri->setScheme(null)->setSlash(false)->toString(); + } + + /** + * Create the most important folders + * if they don't exist yet + * + * @return void + * @throws \Kirby\Exception\PermissionException + */ + public function init() + { + // init /site/accounts + try { + Dir::make($this->app->root('accounts')); + } catch (Throwable $e) { + throw new PermissionException('The accounts directory could not be created'); + } + + // init /content + try { + Dir::make($this->app->root('content')); + } catch (Throwable $e) { + throw new PermissionException('The content directory could not be created'); + } + + // init /media + try { + Dir::make($this->app->root('media')); + } catch (Throwable $e) { + throw new PermissionException('The media directory could not be created'); + } + } + + /** + * Check if the panel is installable. + * On a public server the panel.install + * option must be explicitly set to true + * to get the installer up and running. + * + * @return bool + */ + public function isInstallable(): bool + { + return $this->isLocal() === true || $this->app->option('panel.install', false) === true; + } + + /** + * Check if Kirby is already installed + * + * @return bool + */ + public function isInstalled(): bool + { + return $this->app->users()->count() > 0; + } + + /** + * Check if this is a local installation + * + * @return bool + */ + public function isLocal(): bool + { + $server = $this->app->server(); + $visitor = $this->app->visitor(); + $host = $server->host(); + + if ($host === 'localhost') { + return true; + } + + if (Str::endsWith($host, '.local') === true) { + return true; + } + + if (Str::endsWith($host, '.test') === true) { + return true; + } + + if (in_array($visitor->ip(), ['::1', '127.0.0.1']) === true) { + // ensure that there is no reverse proxy in between + + if ( + isset($_SERVER['HTTP_X_FORWARDED_FOR']) === true && + in_array($_SERVER['HTTP_X_FORWARDED_FOR'], ['::1', '127.0.0.1']) === false + ) { + return false; + } + + if ( + isset($_SERVER['HTTP_CLIENT_IP']) === true && + in_array($_SERVER['HTTP_CLIENT_IP'], ['::1', '127.0.0.1']) === false + ) { + return false; + } + + // no reverse proxy or the real client also comes from localhost + return true; + } + + return false; + } + + /** + * Check if all tests pass + * + * @return bool + */ + public function isOk(): bool + { + return in_array(false, array_values($this->status()), true) === false; + } + + /** + * Normalizes the app's index URL for + * licensing purposes + * + * @param string|null $url Input URL, by default the app's index URL + * @return string Normalized URL + */ + protected function licenseUrl(string $url = null): string + { + if ($url === null) { + $url = $this->indexUrl(); + } + + // remove common "testing" subdomains as well as www. + // to ensure that installations of the same site have + // the same license URL; only for installations at /, + // subdirectory installations are difficult to normalize + if (Str::contains($url, '/') === false) { + if (Str::startsWith($url, 'www.')) { + return substr($url, 4); + } + + if (Str::startsWith($url, 'dev.')) { + return substr($url, 4); + } + + if (Str::startsWith($url, 'test.')) { + return substr($url, 5); + } + + if (Str::startsWith($url, 'staging.')) { + return substr($url, 8); + } + } + + return $url; + } + + /** + * Loads the license file and returns + * the license information if available + * + * @return string|bool License key or `false` if the current user has + * permissions for access.settings, otherwise just a + * boolean that tells whether a valid license is active + */ + public function license() + { + try { + $license = Json::read($this->app->root('config') . '/.license'); + } catch (Throwable $e) { + return false; + } + + // check for all required fields for the validation + if (isset( + $license['license'], + $license['order'], + $license['date'], + $license['email'], + $license['domain'], + $license['signature'] + ) !== true) { + return false; + } + + // build the license verification data + $data = [ + 'license' => $license['license'], + 'order' => $license['order'], + 'email' => hash('sha256', $license['email'] . 'kwAHMLyLPBnHEskzH9pPbJsBxQhKXZnX'), + 'domain' => $license['domain'], + 'date' => $license['date'] + ]; + + + // get the public key + $pubKey = F::read($this->app->root('kirby') . '/kirby.pub'); + + // verify the license signature + if (openssl_verify(json_encode($data), hex2bin($license['signature']), $pubKey, 'RSA-SHA256') !== 1) { + return false; + } + + // verify the URL + if ($this->licenseUrl() !== $this->licenseUrl($license['domain'])) { + return false; + } + + // only return the actual license key if the + // current user has appropriate permissions + $user = $this->app->user(); + if ($user && $user->role()->permissions()->for('access', 'settings') === true) { + return $license['license']; + } else { + return true; + } + } + + /** + * Check for an existing mbstring extension + * + * @return bool + */ + public function mbString(): bool + { + return extension_loaded('mbstring'); + } + + /** + * Check for a writable media folder + * + * @return bool + */ + public function media(): bool + { + return is_writable($this->app->root('media')); + } + + /** + * Check for a valid PHP version + * + * @return bool + */ + public function php(): bool + { + return version_compare(phpversion(), '7.1.0', '>='); + } + + /** + * Validates the license key + * and adds it to the .license file in the config + * folder if possible. + * + * @param string|null $license + * @param string|null $email + * @return bool + * @throws \Kirby\Exception\Exception + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function register(string $license = null, string $email = null): bool + { + if (Str::startsWith($license, 'K3-PRO-') === false) { + throw new InvalidArgumentException([ + 'key' => 'license.format' + ]); + } + + if (V::email($email) === false) { + throw new InvalidArgumentException([ + 'key' => 'license.email' + ]); + } + + $response = Remote::get('https://licenses.getkirby.com/register', [ + 'data' => [ + 'license' => $license, + 'email' => $email, + 'domain' => $this->indexUrl() + ] + ]); + + if ($response->code() !== 200) { + throw new Exception($response->content()); + } + + // decode the response + $json = Json::decode($response->content()); + + // replace the email with the plaintext version + $json['email'] = $email; + + // where to store the license file + $file = $this->app->root('config') . '/.license'; + + // save the license information + Json::write($file, $json); + + if ($this->license() === false) { + throw new InvalidArgumentException([ + 'key' => 'license.verification' + ]); + } + + return true; + } + + /** + * Check for a valid server environment + * + * @return bool + */ + public function server(): bool + { + if ($servers = $this->app->option('servers')) { + $servers = A::wrap($servers); + } else { + $servers = [ + 'apache', + 'caddy', + 'litespeed', + 'nginx', + 'php' + ]; + } + + $software = $_SERVER['SERVER_SOFTWARE'] ?? null; + + return preg_match('!(' . implode('|', $servers) . ')!i', $software) > 0; + } + + /** + * Check for a writable sessions folder + * + * @return bool + */ + public function sessions(): bool + { + return is_writable($this->app->root('sessions')); + } + + /** + * Return the status as array + * + * @return array + */ + public function toArray(): array + { + return $this->status(); + } + + /** + * Upgrade to the new folder separator + * + * @param string $root + * @return void + */ + public static function upgradeContent(string $root) + { + $index = Dir::read($root); + + foreach ($index as $dir) { + $oldRoot = $root . '/' . $dir; + $newRoot = preg_replace('!\/([0-9]+)\-!', '/$1_', $oldRoot); + + if (is_dir($oldRoot) === true) { + Dir::move($oldRoot, $newRoot); + static::upgradeContent($newRoot); + } + } + } +} diff --git a/kirby/src/Cms/Template.php b/kirby/src/Cms/Template.php new file mode 100644 index 0000000..b7df18e --- /dev/null +++ b/kirby/src/Cms/Template.php @@ -0,0 +1,201 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Template +{ + /** + * Global template data + * + * @var array + */ + public static $data = []; + + /** + * The name of the template + * + * @var string + */ + protected $name; + + /** + * Template type (html, json, etc.) + * + * @var string + */ + protected $type; + + /** + * Default template type if no specific type is set + * + * @var string + */ + protected $defaultType; + + /** + * Creates a new template object + * + * @param string $name + * @param string $type + * @param string $defaultType + */ + public function __construct(string $name, string $type = 'html', string $defaultType = 'html') + { + $this->name = strtolower($name); + $this->type = $type; + $this->defaultType = $defaultType; + } + + /** + * Converts the object to a simple string + * This is used in template filters for example + * + * @return string + */ + public function __toString(): string + { + return $this->name; + } + + /** + * Checks if the template exists + * + * @return bool + */ + public function exists(): bool + { + return file_exists($this->file()); + } + + /** + * Returns the expected template file extension + * + * @return string + */ + public function extension(): string + { + return 'php'; + } + + /** + * Returns the default template type + * + * @return string + */ + public function defaultType(): string + { + return $this->defaultType; + } + + /** + * Returns the place where templates are located + * in the site folder and and can be found in extensions + * + * @return string + */ + public function store(): string + { + return 'templates'; + } + + /** + * Detects the location of the template file + * if it exists. + * + * @return string|null + */ + public function file(): ?string + { + if ($this->hasDefaultType() === true) { + try { + // Try the default template in the default template directory. + return F::realpath($this->root() . '/' . $this->name() . '.' . $this->extension(), $this->root()); + } catch (Exception $e) { + // ignore errors, continue searching + } + + // Look for the default template provided by an extension. + $path = App::instance()->extension($this->store(), $this->name()); + + if ($path !== null) { + return $path; + } + } + + $name = $this->name() . '.' . $this->type(); + + try { + // Try the template with type extension in the default template directory. + return F::realpath($this->root() . '/' . $name . '.' . $this->extension(), $this->root()); + } catch (Exception $e) { + // Look for the template with type extension provided by an extension. + // This might be null if the template does not exist. + return App::instance()->extension($this->store(), $name); + } + } + + /** + * Returns the template name + * + * @return string + */ + public function name(): string + { + return $this->name; + } + + /** + * @param array $data + * @return string + */ + public function render(array $data = []): string + { + return Tpl::load($this->file(), $data); + } + + /** + * Returns the root to the templates directory + * + * @return string + */ + public function root(): string + { + return App::instance()->root($this->store()); + } + + /** + * Returns the template type + * + * @return string + */ + public function type(): string + { + return $this->type; + } + + /** + * Checks if the template uses the default type + * + * @return bool + */ + public function hasDefaultType(): bool + { + $type = $this->type(); + + return $type === null || $type === $this->defaultType(); + } +} diff --git a/kirby/src/Cms/Translation.php b/kirby/src/Cms/Translation.php new file mode 100644 index 0000000..b4b3a33 --- /dev/null +++ b/kirby/src/Cms/Translation.php @@ -0,0 +1,193 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Translation +{ + /** + * @var string + */ + protected $code; + + /** + * @var array + */ + protected $data = []; + + /** + * @param string $code + * @param array $data + */ + public function __construct(string $code, array $data) + { + $this->code = $code; + $this->data = $data; + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + /** + * Returns the translation author + * + * @return string + */ + public function author(): string + { + return $this->get('translation.author', 'Kirby'); + } + + /** + * Returns the official translation code + * + * @return string + */ + public function code(): string + { + return $this->code; + } + + /** + * Returns an array with all + * translation strings + * + * @return array + */ + public function data(): array + { + return $this->data; + } + + /** + * Returns the translation data and merges + * it with the data from the default translation + * + * @return array + */ + public function dataWithFallback(): array + { + if ($this->code === 'en') { + return $this->data; + } + + // get the fallback array + $fallback = App::instance()->translation('en')->data(); + + return array_merge($fallback, $this->data); + } + + /** + * Returns the writing direction + * (ltr or rtl) + * + * @return string + */ + public function direction(): string + { + return $this->get('translation.direction', 'ltr'); + } + + /** + * Returns a single translation + * string by key + * + * @param string $key + * @param string|null $default + * @return string|null + */ + public function get(string $key, string $default = null): ?string + { + return $this->data[$key] ?? $default; + } + + /** + * Returns the translation id, + * which is also the code + * + * @return string + */ + public function id(): string + { + return $this->code; + } + + /** + * Loads the translation from the + * json file in Kirby's translations folder + * + * @param string $code + * @param string $root + * @param array $inject + * @return self + */ + public static function load(string $code, string $root, array $inject = []) + { + try { + return new Translation($code, array_merge(Data::read($root), $inject)); + } catch (Exception $e) { + return new Translation($code, []); + } + } + + /** + * Returns the PHP locale of the translation + * + * @return string + */ + public function locale(): string + { + $default = $this->code; + if (Str::contains($default, '_') !== true) { + $default .= '_' . strtoupper($this->code); + } + + return $this->get('translation.locale', $default); + } + + /** + * Returns the human-readable translation name. + * + * @return string + */ + public function name(): string + { + return $this->get('translation.name', $this->code); + } + + /** + * Converts the most important + * properties to an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'code' => $this->code(), + 'data' => $this->data(), + 'name' => $this->name(), + 'author' => $this->author(), + ]; + } +} diff --git a/kirby/src/Cms/Translations.php b/kirby/src/Cms/Translations.php new file mode 100644 index 0000000..d55ce61 --- /dev/null +++ b/kirby/src/Cms/Translations.php @@ -0,0 +1,78 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Translations extends Collection +{ + /** + * @param string $code + * @return void + */ + public function start(string $code): void + { + F::move($this->parent->contentFile('', true), $this->parent->contentFile($code, true)); + } + + /** + * @param string $code + * @return void + */ + public function stop(string $code): void + { + F::move($this->parent->contentFile($code, true), $this->parent->contentFile('', true)); + } + + /** + * @param array $translations + * @return self + */ + public static function factory(array $translations) + { + $collection = new static(); + + foreach ($translations as $code => $props) { + $translation = new Translation($code, $props); + $collection->data[$translation->code()] = $translation; + } + + return $collection; + } + + /** + * @param string $root + * @param array $inject + * @return self + */ + public static function load(string $root, array $inject = []) + { + $collection = new static(); + + foreach (Dir::read($root) as $filename) { + if (F::extension($filename) !== 'json') { + continue; + } + + $locale = F::name($filename); + $translation = Translation::load($locale, $root . '/' . $filename, $inject[$locale] ?? []); + + $collection->data[$locale] = $translation; + } + + return $collection; + } +} diff --git a/kirby/src/Cms/Url.php b/kirby/src/Cms/Url.php new file mode 100755 index 0000000..3f743db --- /dev/null +++ b/kirby/src/Cms/Url.php @@ -0,0 +1,69 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Url extends BaseUrl +{ + public static $home = null; + + /** + * Returns the Url to the homepage + * + * @return string + */ + public static function home(): string + { + return App::instance()->url(); + } + + /** + * Creates an absolute Url to a template asset if it exists. This is used in the `css()` and `js()` helpers + * + * @param string $assetPath + * @param string $extension + * @return string|null + */ + public static function toTemplateAsset(string $assetPath, string $extension) + { + $kirby = App::instance(); + $page = $kirby->site()->page(); + $path = $assetPath . '/' . $page->template() . '.' . $extension; + $file = $kirby->root('assets') . '/' . $path; + $url = $kirby->url('assets') . '/' . $path; + + return file_exists($file) === true ? $url : null; + } + + /** + * Smart resolver for internal and external urls + * + * @param string|null $path + * @param array|string|null $options Either an array of options for the Uri class or a language string + * @return string + */ + public static function to(string $path = null, $options = null): string + { + $kirby = App::instance(); + + return $kirby->component('url')($kirby, $path, $options, function (string $path = null, $options = null) use ($kirby) { + return $kirby->nativeComponent('url')($kirby, $path, $options); + }); + } +} diff --git a/kirby/src/Cms/User.php b/kirby/src/Cms/User.php new file mode 100644 index 0000000..33ef672 --- /dev/null +++ b/kirby/src/Cms/User.php @@ -0,0 +1,926 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class User extends ModelWithContent +{ + const CLASS_ALIAS = 'user'; + + use HasFiles; + use HasMethods; + use HasSiblings; + use UserActions; + + /** + * @var UserBlueprint + */ + protected $blueprint; + + /** + * @var array + */ + protected $credentials; + + /** + * @var string + */ + protected $email; + + /** + * @var string + */ + protected $hash; + + /** + * @var string + */ + protected $id; + + /** + * @var array|null + */ + protected $inventory; + + /** + * @var string + */ + protected $language; + + /** + * All registered user methods + * + * @var array + */ + public static $methods = []; + + /** + * Registry with all User models + * + * @var array + */ + public static $models = []; + + /** + * @var \Kirby\Cms\Field + */ + protected $name; + + /** + * @var string + */ + protected $password; + + /** + * The user role + * + * @var string + */ + protected $role; + + /** + * Modified getter to also return fields + * from the content + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + // public property access + if (isset($this->$method) === true) { + return $this->$method; + } + + // user methods + if ($this->hasMethod($method)) { + return $this->callMethod($method, $arguments); + } + + // return site content otherwise + return $this->content()->get($method, $arguments); + } + + /** + * Creates a new User object + * + * @param array $props + */ + public function __construct(array $props) + { + $props['id'] = $props['id'] ?? $this->createId(); + $this->setProperties($props); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return array_merge($this->toArray(), [ + 'avatar' => $this->avatar(), + 'content' => $this->content(), + 'role' => $this->role() + ]); + } + + /** + * Returns the url to the api endpoint + * + * @internal + * @param bool $relative + * @return string + */ + public function apiUrl(bool $relative = false): string + { + if ($relative === true) { + return 'users/' . $this->id(); + } else { + return $this->kirby()->url('api') . '/users/' . $this->id(); + } + } + + /** + * Returns the File object for the avatar or null + * + * @return \Kirby\Cms\File|null + */ + public function avatar() + { + return $this->files()->template('avatar')->first(); + } + + /** + * Returns the UserBlueprint object + * + * @return \Kirby\Cms\Blueprint + */ + public function blueprint() + { + if (is_a($this->blueprint, 'Kirby\Cms\Blueprint') === true) { + return $this->blueprint; + } + + try { + return $this->blueprint = UserBlueprint::factory('users/' . $this->role(), 'users/default', $this); + } catch (Exception $e) { + return $this->blueprint = new UserBlueprint([ + 'model' => $this, + 'name' => 'default', + 'title' => 'Default', + ]); + } + } + + /** + * Prepares the content for the write method + * + * @internal + * @param array $data + * @param string $languageCode|null Not used so far + * @return array + */ + public function contentFileData(array $data, string $languageCode = null): array + { + // remove stuff that has nothing to do in the text files + unset( + $data['email'], + $data['language'], + $data['name'], + $data['password'], + $data['role'] + ); + + return $data; + } + + /** + * Filename for the content file + * + * @internal + * @return string + */ + public function contentFileName(): string + { + return 'user'; + } + + protected function credentials(): array + { + return $this->credentials = $this->credentials ?? $this->readCredentials(); + } + + /** + * Returns the user email address + * + * @return string + */ + public function email(): ?string + { + return $this->email = $this->email ?? $this->credentials()['email'] ?? null; + } + + /** + * Checks if the user exists + * + * @return bool + */ + public function exists(): bool + { + return is_file($this->contentFile('default')) === true; + } + + /** + * Constructs a User object and also + * takes User models into account. + * + * @internal + * @param mixed $props + * @return self + */ + public static function factory($props) + { + if (empty($props['model']) === false) { + return static::model($props['model'], $props); + } + + return new static($props); + } + + /** + * Hashes the user's password unless it is `null`, + * which will leave it as `null` + * + * @internal + * @param string|null $password + * @return string|null + */ + public static function hashPassword($password): ?string + { + if ($password !== null) { + $password = password_hash($password, PASSWORD_DEFAULT); + } + + return $password; + } + + /** + * Returns the user id + * + * @return string + */ + public function id(): string + { + return $this->id; + } + + /** + * Returns the inventory of files + * children and content files + * + * @return array + */ + public function inventory(): array + { + if ($this->inventory !== null) { + return $this->inventory; + } + + $kirby = $this->kirby(); + + return $this->inventory = Dir::inventory( + $this->root(), + $kirby->contentExtension(), + $kirby->contentIgnore(), + $kirby->multilang() + ); + } + + /** + * Compares the current object with the given user object + * + * @param \Kirby\Cms\User|null $user + * @return bool + */ + public function is(User $user = null): bool + { + if ($user === null) { + return false; + } + + return $this->id() === $user->id(); + } + + /** + * Checks if this user has the admin role + * + * @return bool + */ + public function isAdmin(): bool + { + return $this->role()->id() === 'admin'; + } + + /** + * Checks if the current user is the virtual + * Kirby user + * + * @return bool + */ + public function isKirby(): bool + { + return $this->email() === 'kirby@getkirby.com'; + } + + /** + * Checks if the current user is this user + * + * @return bool + */ + public function isLoggedIn(): bool + { + return $this->is($this->kirby()->user()); + } + + /** + * Checks if the user is the last one + * with the admin role + * + * @return bool + */ + public function isLastAdmin(): bool + { + return $this->role()->isAdmin() === true && $this->kirby()->users()->filterBy('role', 'admin')->count() <= 1; + } + + /** + * Checks if the user is the last user + * + * @return bool + */ + public function isLastUser(): bool + { + return $this->kirby()->users()->count() === 1; + } + + /** + * Returns the user language + * + * @return string + */ + public function language(): string + { + return $this->language ?? $this->language = $this->credentials()['language'] ?? $this->kirby()->option('panel.language', 'en'); + } + + /** + * Logs the user in + * + * @param string $password + * @param \Kirby\Session\Session|array|null $session Session options or session object to set the user in + * @return bool + */ + public function login(string $password, $session = null): bool + { + $this->validatePassword($password); + $this->loginPasswordless($session); + + return true; + } + + /** + * Logs the user in without checking the password + * + * @param \Kirby\Session\Session|array|null $session Session options or session object to set the user in + * @return void + */ + public function loginPasswordless($session = null): void + { + $kirby = $this->kirby(); + + $session = $this->sessionFromOptions($session); + + $kirby->trigger('user.login:before', ['user' => $this, 'session' => $session]); + + $session->regenerateToken(); // privilege change + $session->data()->set('user.id', $this->id()); + $this->kirby()->auth()->setUser($this); + + $kirby->trigger('user.login:after', ['user' => $this, 'session' => $session]); + } + + /** + * Logs the user out + * + * @param \Kirby\Session\Session|array|null $session Session options or session object to unset the user in + * @return void + */ + public function logout($session = null): void + { + $kirby = $this->kirby(); + $session = $this->sessionFromOptions($session); + + $kirby->trigger('user.logout:before', ['user' => $this, 'session' => $session]); + + // remove the user from the session for future requests + $session->data()->remove('user.id'); + + // clear the cached user object from the app state of the current request + $this->kirby()->auth()->flush(); + + if ($session->data()->get() === []) { + // session is now empty, we might as well destroy it + $session->destroy(); + + $kirby->trigger('user.logout:after', ['user' => $this, 'session' => null]); + } else { + // privilege change + $session->regenerateToken(); + + $kirby->trigger('user.logout:after', ['user' => $this, 'session' => $session]); + } + } + + /** + * Returns the root to the media folder for the user + * + * @internal + * @return string + */ + public function mediaRoot(): string + { + return $this->kirby()->root('media') . '/users/' . $this->id(); + } + + /** + * Returns the media url for the user object + * + * @internal + * @return string + */ + public function mediaUrl(): string + { + return $this->kirby()->url('media') . '/users/' . $this->id(); + } + + /** + * Creates a user model if it has been registered + * + * @internal + * @param string $name + * @param array $props + * @return \Kirby\Cms\User + */ + public static function model(string $name, array $props = []) + { + if ($class = (static::$models[$name] ?? null)) { + $object = new $class($props); + + if (is_a($object, 'Kirby\Cms\User') === true) { + return $object; + } + } + + return new static($props); + } + + /** + * Returns the last modification date of the user + * + * @param string $format + * @param string|null $handler + * @param string|null $languageCode + * @return int|string + */ + public function modified(string $format = 'U', string $handler = null, string $languageCode = null) + { + $modifiedContent = F::modified($this->contentFile($languageCode)); + $modifiedIndex = F::modified($this->root() . '/index.php'); + $modifiedTotal = max([$modifiedContent, $modifiedIndex]); + $handler = $handler ?? $this->kirby()->option('date.handler', 'date'); + + return $handler($format, $modifiedTotal); + } + + /** + * Returns the user's name + * + * @return \Kirby\Cms\Field + */ + public function name() + { + if (is_string($this->name) === true) { + return new Field($this, 'name', $this->name); + } + + if ($this->name !== null) { + return $this->name; + } + + return $this->name = new Field($this, 'name', $this->credentials()['name'] ?? null); + } + + /** + * Create a dummy nobody + * + * @internal + * @return self + */ + public static function nobody() + { + return new static([ + 'email' => 'nobody@getkirby.com', + 'role' => 'nobody' + ]); + } + + /** + * Panel icon definition + * + * @internal + * @param array $params + * @return array + */ + public function panelIcon(array $params = null): array + { + $params['type'] = 'user'; + + return parent::panelIcon($params); + } + + /** + * Returns the image file object based on provided query + * + * @internal + * @param string|null $query + * @return \Kirby\Cms\File|\Kirby\Cms\Asset|null + */ + protected function panelImageSource(string $query = null) + { + if ($query === null) { + return $this->avatar(); + } + + return parent::panelImageSource($query); + } + + /** + * Returns the full path without leading slash + * + * @internal + * @return string + */ + public function panelPath(): string + { + return 'users/' . $this->id(); + } + + /** + * Returns prepared data for the panel user picker + * + * @param array|null $params + * @return array + */ + public function panelPickerData(array $params = null): array + { + $image = $this->panelImage($params['image'] ?? []); + $icon = $this->panelIcon($image); + + return [ + 'icon' => $icon, + 'id' => $this->id(), + 'image' => $image, + 'email' => $this->email(), + 'info' => $this->toString($params['info'] ?? false), + 'link' => $this->panelUrl(true), + 'text' => $this->toString($params['text'] ?? '{{ user.username }}'), + 'username' => $this->username(), + ]; + } + + /** + * Returns the url to the editing view + * in the panel + * + * @internal + * @param bool $relative + * @return string + */ + public function panelUrl(bool $relative = false): string + { + if ($relative === true) { + return '/' . $this->panelPath(); + } else { + return $this->kirby()->url('panel') . '/' . $this->panelPath(); + } + } + + /** + * Returns the encrypted user password + * + * @return string|null + */ + public function password(): ?string + { + if ($this->password !== null) { + return $this->password; + } + + return $this->password = $this->readPassword(); + } + + /** + * @return \Kirby\Cms\UserPermissions + */ + public function permissions() + { + return new UserPermissions($this); + } + + /** + * Returns the user role + * + * @return \Kirby\Cms\Role + */ + public function role() + { + if (is_a($this->role, 'Kirby\Cms\Role') === true) { + return $this->role; + } + + $roleName = $this->role ?? $this->credentials()['role'] ?? 'visitor'; + + if ($role = $this->kirby()->roles()->find($roleName)) { + return $this->role = $role; + } + + return $this->role = Role::nobody(); + } + + /** + * Returns all available roles + * for this user, that can be selected + * by the authenticated user + * + * @return \Kirby\Cms\Roles + */ + public function roles() + { + $kirby = $this->kirby(); + $roles = $kirby->roles(); + + // a collection with just the one role of the user + $myRole = $roles->filterBy('id', $this->role()->id()); + + // if there's an authenticated user … + if ($user = $kirby->user()) { + + // admin users can select pretty much any role + if ($user->isAdmin() === true) { + // except if the user is the last admin + if ($this->isLastAdmin() === true) { + // in which case they have to stay admin + return $myRole; + } + + // return all roles for mighty admins + return $roles; + } + } + + // any other user can only keep their role + return $myRole; + } + + /** + * The absolute path to the user directory + * + * @return string + */ + public function root(): string + { + return $this->kirby()->root('accounts') . '/' . $this->id(); + } + + /** + * Returns the UserRules class to + * validate any important action. + * + * @return \Kirby\Cms\UserRules + */ + protected function rules() + { + return new UserRules(); + } + + /** + * Sets the Blueprint object + * + * @param array|null $blueprint + * @return self + */ + protected function setBlueprint(array $blueprint = null) + { + if ($blueprint !== null) { + $blueprint['model'] = $this; + $this->blueprint = new UserBlueprint($blueprint); + } + + return $this; + } + + /** + * Sets the user email + * + * @param string $email|null + * @return self + */ + protected function setEmail(string $email = null) + { + if ($email !== null) { + $this->email = Str::lower(trim($email)); + } + return $this; + } + + /** + * Sets the user id + * + * @param string $id|null + * @return self + */ + protected function setId(string $id = null) + { + $this->id = $id; + return $this; + } + + /** + * Sets the user language + * + * @param string $language|null + * @return self + */ + protected function setLanguage(string $language = null) + { + $this->language = $language !== null ? trim($language) : null; + return $this; + } + + /** + * Sets the user name + * + * @param string $name|null + * @return self + */ + protected function setName(string $name = null) + { + $this->name = $name !== null ? trim(strip_tags($name)) : null; + return $this; + } + + /** + * Sets the user's password hash + * + * @param string $password|null + * @return self + */ + protected function setPassword(string $password = null) + { + $this->password = $password; + return $this; + } + + /** + * Sets the user role + * + * @param string $role|null + * @return self + */ + protected function setRole(string $role = null) + { + $this->role = $role !== null ? Str::lower(trim($role)) : null; + return $this; + } + + /** + * Converts session options into a session object + * + * @param \Kirby\Session\Session|array $session Session options or session object to unset the user in + * @return \Kirby\Session\Session + */ + protected function sessionFromOptions($session) + { + // use passed session options or session object if set + if (is_array($session) === true) { + $session = $this->kirby()->session($session); + } elseif (is_a($session, 'Kirby\Session\Session') === false) { + $session = $this->kirby()->session(['detect' => true]); + } + + return $session; + } + + /** + * Returns the parent Users collection + * + * @return \Kirby\Cms\Users + */ + protected function siblingsCollection() + { + return $this->kirby()->users(); + } + + /** + * Converts the most important user properties + * to an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'avatar' => $this->avatar() ? $this->avatar()->toArray() : null, + 'content' => $this->content()->toArray(), + 'email' => $this->email(), + 'id' => $this->id(), + 'language' => $this->language(), + 'role' => $this->role()->name(), + 'username' => $this->username() + ]; + } + + /** + * String template builder + * + * @param string|null $template + * @param array|null $data + * @param string $fallback Fallback for tokens in the template that cannot be replaced + * @return string + */ + public function toString(string $template = null, array $data = [], string $fallback = ''): string + { + if ($template === null) { + $template = $this->email(); + } + + return parent::toString($template, $data); + } + + /** + * Returns the username + * which is the given name or the email + * as a fallback + * + * @return string|null + */ + public function username(): ?string + { + return $this->name()->or($this->email())->value(); + } + + /** + * Compares the given password with the stored one + * + * @param string $password|null + * @return bool + * + * @throws \Kirby\Exception\NotFoundException If the user has no password + * @throws \Kirby\Exception\InvalidArgumentException If the entered password is not valid + * or does not match the user password + */ + public function validatePassword(string $password = null): bool + { + if (empty($this->password()) === true) { + throw new NotFoundException(['key' => 'user.password.undefined']); + } + + if (Str::length($password) < 8) { + throw new InvalidArgumentException(['key' => 'user.password.invalid']); + } + + if (password_verify($password, $this->password()) !== true) { + throw new InvalidArgumentException(['key' => 'user.password.notSame']); + } + + return true; + } +} diff --git a/kirby/src/Cms/UserActions.php b/kirby/src/Cms/UserActions.php new file mode 100644 index 0000000..534ac5e --- /dev/null +++ b/kirby/src/Cms/UserActions.php @@ -0,0 +1,354 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +trait UserActions +{ + /** + * Changes the user email address + * + * @param string $email + * @return self + */ + public function changeEmail(string $email) + { + return $this->commit('changeEmail', ['user' => $this, 'email' => Idn::decodeEmail($email)], function ($user, $email) { + $user = $user->clone([ + 'email' => $email + ]); + + $user->updateCredentials([ + 'email' => $email + ]); + + return $user; + }); + } + + /** + * Changes the user language + * + * @param string $language + * @return self + */ + public function changeLanguage(string $language) + { + return $this->commit('changeLanguage', ['user' => $this, 'language' => $language], function ($user, $language) { + $user = $user->clone([ + 'language' => $language, + ]); + + $user->updateCredentials([ + 'language' => $language + ]); + + return $user; + }); + } + + /** + * Changes the screen name of the user + * + * @param string $name + * @return self + */ + public function changeName(string $name) + { + return $this->commit('changeName', ['user' => $this, 'name' => $name], function ($user, $name) { + $user = $user->clone([ + 'name' => $name + ]); + + $user->updateCredentials([ + 'name' => $name + ]); + + return $user; + }); + } + + /** + * Changes the user password + * + * @param string $password + * @return self + */ + public function changePassword(string $password) + { + return $this->commit('changePassword', ['user' => $this, 'password' => $password], function ($user, $password) { + $user = $user->clone([ + 'password' => $password = User::hashPassword($password) + ]); + + $user->writePassword($password); + + return $user; + }); + } + + /** + * Changes the user role + * + * @param string $role + * @return self + */ + public function changeRole(string $role) + { + return $this->commit('changeRole', ['user' => $this, 'role' => $role], function ($user, $role) { + $user = $user->clone([ + 'role' => $role, + ]); + + $user->updateCredentials([ + 'role' => $role + ]); + + return $user; + }); + } + + /** + * Commits a user action, by following these steps + * + * 1. checks the action rules + * 2. sends the before hook + * 3. commits the action + * 4. sends the after hook + * 5. returns the result + * + * @param string $action + * @param array $arguments + * @param \Closure $callback + * @return mixed + * @throws \Kirby\Exception\PermissionException + */ + protected function commit(string $action, array $arguments = [], Closure $callback) + { + if ($this->isKirby() === true) { + throw new PermissionException('The Kirby user cannot be changed'); + } + + $old = $this->hardcopy(); + $kirby = $this->kirby(); + $argumentValues = array_values($arguments); + + $this->rules()->$action(...$argumentValues); + $kirby->trigger('user.' . $action . ':before', $arguments); + + $result = $callback(...$argumentValues); + + if ($action === 'create') { + $argumentsAfter = ['user' => $result]; + } elseif ($action === 'delete') { + $argumentsAfter = ['status' => $result, 'user' => $old]; + } else { + $argumentsAfter = ['newUser' => $result, 'oldUser' => $old]; + } + $kirby->trigger('user.' . $action . ':after', $argumentsAfter); + + $kirby->cache('pages')->flush(); + return $result; + } + + /** + * Creates a new User from the given props and returns a new User object + * + * @param array|null $props + * @return self + */ + public static function create(array $props = null) + { + $data = $props; + + if (isset($props['email']) === true) { + $data['email'] = Idn::decodeEmail($props['email']); + } + + if (isset($props['password']) === true) { + $data['password'] = User::hashPassword($props['password']); + } + + $props['role'] = $props['model'] = strtolower($props['role'] ?? 'default'); + + $user = User::factory($data); + + // create a form for the user + $form = Form::for($user, [ + 'values' => $props['content'] ?? [] + ]); + + // inject the content + $user = $user->clone(['content' => $form->strings(true)]); + + // run the hook + return $user->commit('create', ['user' => $user, 'input' => $props], function ($user, $props) { + $user->writeCredentials([ + 'email' => $user->email(), + 'language' => $user->language(), + 'name' => $user->name()->value(), + 'role' => $user->role()->id(), + ]); + + $user->writePassword($user->password()); + + // always create users in the default language + if ($user->kirby()->multilang() === true) { + $languageCode = $user->kirby()->defaultLanguage()->code(); + } else { + $languageCode = null; + } + + // add the user to users collection + $user->kirby()->users()->add($user); + + // write the user data + return $user->save($user->content()->toArray(), $languageCode); + }); + } + + /** + * Returns a random user id + * + * @return string + */ + public function createId(): string + { + $length = 8; + $id = Str::random($length); + + while ($this->kirby()->users()->has($id)) { + $length++; + $id = Str::random($length); + } + + return $id; + } + + /** + * Deletes the user + * + * @return bool + * @throws \Kirby\Exception\LogicException + */ + public function delete(): bool + { + return $this->commit('delete', ['user' => $this], function ($user) { + if ($user->exists() === false) { + return true; + } + + // delete all public assets for this user + Dir::remove($user->mediaRoot()); + + // delete the user directory + if (Dir::remove($user->root()) !== true) { + throw new LogicException('The user directory for "' . $user->email() . '" could not be deleted'); + } + + // remove the user from users collection + $user->kirby()->users()->remove($user); + + return true; + }); + } + + /** + * Read the account information from disk + * + * @return array + */ + protected function readCredentials(): array + { + $path = $this->root() . '/index.php'; + + if (is_file($path) === true) { + $credentials = F::load($path); + + return is_array($credentials) === false ? [] : $credentials; + } else { + return []; + } + } + + /** + * Reads the user password from disk + * + * @return string|null + */ + protected function readPassword(): ?string + { + return F::read($this->root() . '/.htpasswd'); + } + + /** + * Updates the user data + * + * @param array|null $input + * @param string|null $language + * @param bool $validate + * @return self + */ + public function update(array $input = null, string $language = null, bool $validate = false) + { + $user = parent::update($input, $language, $validate); + + // set auth user data only if the current user is this user + if ($user->isLoggedIn() === true) { + $this->kirby()->auth()->setUser($user); + } + + return $user; + } + + /** + * This always merges the existing credentials + * with the given input. + * + * @param array $credentials + * @return bool + */ + protected function updateCredentials(array $credentials): bool + { + return $this->writeCredentials(array_merge($this->credentials(), $credentials)); + } + + /** + * Writes the account information to disk + * + * @param array $credentials + * @return bool + */ + protected function writeCredentials(array $credentials): bool + { + return Data::write($this->root() . '/index.php', $credentials); + } + + /** + * Writes the password to disk + * + * @param string|null $password + * @return bool + */ + protected function writePassword(string $password = null): bool + { + return F::write($this->root() . '/.htpasswd', $password); + } +} diff --git a/kirby/src/Cms/UserBlueprint.php b/kirby/src/Cms/UserBlueprint.php new file mode 100644 index 0000000..ae55569 --- /dev/null +++ b/kirby/src/Cms/UserBlueprint.php @@ -0,0 +1,47 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class UserBlueprint extends Blueprint +{ + /** + * UserBlueprint constructor. + * + * @param array $props + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function __construct(array $props) + { + // normalize and translate the description + $props['description'] = $this->i18n($props['description'] ?? null); + + // register the other props + parent::__construct($props); + + // normalize all available page options + $this->props['options'] = $this->normalizeOptions( + $props['options'] ?? true, + // defaults + [ + 'create' => null, + 'changeEmail' => null, + 'changeLanguage' => null, + 'changeName' => null, + 'changePassword' => null, + 'changeRole' => null, + 'delete' => null, + 'update' => null, + ] + ); + } +} diff --git a/kirby/src/Cms/UserPermissions.php b/kirby/src/Cms/UserPermissions.php new file mode 100644 index 0000000..e26ff9c --- /dev/null +++ b/kirby/src/Cms/UserPermissions.php @@ -0,0 +1,67 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class UserPermissions extends ModelPermissions +{ + /** + * @var string + */ + protected $category = 'users'; + + /** + * UserPermissions constructor + * + * @param \Kirby\Cms\Model $model + */ + public function __construct(Model $model) + { + parent::__construct($model); + + // change the scope of the permissions, when the current user is this user + $this->category = $this->user && $this->user->is($model) ? 'user' : 'users'; + } + + /** + * @return bool + */ + protected function canChangeRole(): bool + { + return $this->model->roles()->count() > 1; + } + + /** + * @return bool + */ + protected function canCreate(): bool + { + // the admin can always create new users + if ($this->user->isAdmin() === true) { + return true; + } + + // users who are not admins cannot create admins + if ($this->model->isAdmin() === false) { + return false; + } + + return true; + } + + /** + * @return bool + */ + protected function canDelete(): bool + { + return $this->model->isLastAdmin() !== true; + } +} diff --git a/kirby/src/Cms/UserPicker.php b/kirby/src/Cms/UserPicker.php new file mode 100644 index 0000000..a23799a --- /dev/null +++ b/kirby/src/Cms/UserPicker.php @@ -0,0 +1,69 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class UserPicker extends Picker +{ + /** + * Extends the basic defaults + * + * @return array + */ + public function defaults(): array + { + $defaults = parent::defaults(); + $defaults['text'] = '{{ user.username }}'; + + return $defaults; + } + + /** + * Search all users for the picker + * + * @return \Kirby\Cms\Users|null + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function items() + { + $model = $this->options['model']; + + // find the right default query + if (empty($this->options['query']) === false) { + $query = $this->options['query']; + } elseif (is_a($model, 'Kirby\Cms\User') === true) { + $query = 'user.siblings'; + } else { + $query = 'kirby.users'; + } + + // fetch all users for the picker + $users = $model->query($query); + + // catch invalid data + if (is_a($users, 'Kirby\Cms\Users') === false) { + throw new InvalidArgumentException('Your query must return a set of users'); + } + + // search + $users = $this->search($users); + + // sort + $users = $users->sortBy('username', 'asc'); + + // paginate + return $this->paginate($users); + } +} diff --git a/kirby/src/Cms/UserRules.php b/kirby/src/Cms/UserRules.php new file mode 100644 index 0000000..2e262cf --- /dev/null +++ b/kirby/src/Cms/UserRules.php @@ -0,0 +1,359 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class UserRules +{ + /** + * Validates if the email address can be changed + * + * @param \Kirby\Cms\User $user + * @param string $email + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the address + */ + public static function changeEmail(User $user, string $email): bool + { + if ($user->permissions()->changeEmail() !== true) { + throw new PermissionException([ + 'key' => 'user.changeEmail.permission', + 'data' => ['name' => $user->username()] + ]); + } + + return static::validEmail($user, $email); + } + + /** + * Validates if the language can be changed + * + * @param \Kirby\Cms\User $user + * @param string $language + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the language + */ + public static function changeLanguage(User $user, string $language): bool + { + if ($user->permissions()->changeLanguage() !== true) { + throw new PermissionException([ + 'key' => 'user.changeLanguage.permission', + 'data' => ['name' => $user->username()] + ]); + } + + return static::validLanguage($user, $language); + } + + /** + * Validates if the name can be changed + * + * @param \Kirby\Cms\User $user + * @param string $name + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the name + */ + public static function changeName(User $user, string $name): bool + { + if ($user->permissions()->changeName() !== true) { + throw new PermissionException([ + 'key' => 'user.changeName.permission', + 'data' => ['name' => $user->username()] + ]); + } + + return true; + } + + /** + * Validates if the password can be changed + * + * @param \Kirby\Cms\User $user + * @param string $password + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the password + */ + public static function changePassword(User $user, string $password): bool + { + if ($user->permissions()->changePassword() !== true) { + throw new PermissionException([ + 'key' => 'user.changePassword.permission', + 'data' => ['name' => $user->username()] + ]); + } + + return static::validPassword($user, $password); + } + + /** + * Validates if the role can be changed + * + * @param \Kirby\Cms\User $user + * @param string $role + * @return bool + * @throws \Kirby\Exception\LogicException If the user is the last admin + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the role + */ + public static function changeRole(User $user, string $role): bool + { + // protect admin from role changes by non-admin + if ( + $user->kirby()->user()->isAdmin() === false && + $user->isAdmin() === true + ) { + throw new PermissionException([ + 'key' => 'user.changeRole.permission', + 'data' => ['name' => $user->username()] + ]); + } + + // prevent non-admins making a user to admin + if ( + $user->kirby()->user()->isAdmin() === false && + $role === 'admin' + ) { + throw new PermissionException([ + 'key' => 'user.changeRole.toAdmin' + ]); + } + + static::validRole($user, $role); + + if ($role !== 'admin' && $user->isLastAdmin() === true) { + throw new LogicException([ + 'key' => 'user.changeRole.lastAdmin', + 'data' => ['name' => $user->username()] + ]); + } + + if ($user->permissions()->changeRole() !== true) { + throw new PermissionException([ + 'key' => 'user.changeRole.permission', + 'data' => ['name' => $user->username()] + ]); + } + + return true; + } + + /** + * Validates if the user can be created + * + * @param \Kirby\Cms\User $user + * @param array $props + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to create a new user + */ + public static function create(User $user, array $props = []): bool + { + static::validId($user, $user->id()); + static::validEmail($user, $user->email(), true); + static::validLanguage($user, $user->language()); + + if (empty($props['password']) === false) { + static::validPassword($user, $props['password']); + } + + // get the current user if it exists + $currentUser = $user->kirby()->user(); + + // admins are allowed everything + if ($currentUser && $currentUser->isAdmin() === true) { + return true; + } + + // only admins are allowed to add admins + $role = $props['role'] ?? null; + + if ($role === 'admin' && $currentUser && $currentUser->isAdmin() === false) { + throw new PermissionException([ + 'key' => 'user.create.permission' + ]); + } + + // check user permissions (if not on install) + if ($user->kirby()->users()->count() > 0) { + if ($user->permissions()->create() !== true) { + throw new PermissionException([ + 'key' => 'user.create.permission' + ]); + } + } + + return true; + } + + /** + * Validates if the user can be deleted + * + * @param \Kirby\Cms\User $user + * @return bool + * @throws \Kirby\Exception\LogicException If this is the last user or last admin, which cannot be deleted + * @throws \Kirby\Exception\PermissionException If the user is not allowed to delete this user + */ + public static function delete(User $user): bool + { + if ($user->isLastAdmin() === true) { + throw new LogicException(['key' => 'user.delete.lastAdmin']); + } + + if ($user->isLastUser() === true) { + throw new LogicException([ + 'key' => 'user.delete.lastUser' + ]); + } + + if ($user->permissions()->delete() !== true) { + throw new PermissionException([ + 'key' => 'user.delete.permission', + 'data' => ['name' => $user->username()] + ]); + } + + return true; + } + + /** + * Validates if the user can be updated + * + * @param \Kirby\Cms\User $user + * @param array $values + * @param array $strings + * @return bool + * @throws \Kirby\Exception\PermissionException If the user it not allowed to update this user + */ + public static function update(User $user, array $values = [], array $strings = []): bool + { + if ($user->permissions()->update() !== true) { + throw new PermissionException([ + 'key' => 'user.update.permission', + 'data' => ['name' => $user->username()] + ]); + } + + return true; + } + + /** + * Validates an email address + * + * @param \Kirby\Cms\User $user + * @param string $email + * @param bool $strict + * @return bool + * @throws \Kirby\Exception\DuplicateException If the email address already exists + * @throws \Kirby\Exception\InvalidArgumentException If the email address is invalid + */ + public static function validEmail(User $user, string $email, bool $strict = false): bool + { + if (V::email($email ?? null) === false) { + throw new InvalidArgumentException([ + 'key' => 'user.email.invalid', + ]); + } + + if ($strict === true) { + $duplicate = $user->kirby()->users()->find($email); + } else { + $duplicate = $user->kirby()->users()->not($user)->find($email); + } + + if ($duplicate) { + throw new DuplicateException([ + 'key' => 'user.duplicate', + 'data' => ['email' => $email] + ]); + } + + return true; + } + + /** + * Validates a user id + * + * @param \Kirby\Cms\User $user + * @param string $id + * @return bool + * @throws \Kirby\Exception\DuplicateException If the user already exists + */ + public static function validId(User $user, string $id): bool + { + if ($user->kirby()->users()->find($id)) { + throw new DuplicateException('A user with this id exists'); + } + + return true; + } + + /** + * Validates a user language code + * + * @param \Kirby\Cms\User $user + * @param string $language + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the language does not exist + */ + public static function validLanguage(User $user, string $language): bool + { + if (in_array($language, $user->kirby()->translations()->keys(), true) === false) { + throw new InvalidArgumentException([ + 'key' => 'user.language.invalid', + ]); + } + + return true; + } + + /** + * Validates a password + * + * @param \Kirby\Cms\User $user + * @param string $password + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the password is too short + */ + public static function validPassword(User $user, string $password): bool + { + if (Str::length($password ?? null) < 8) { + throw new InvalidArgumentException([ + 'key' => 'user.password.invalid', + ]); + } + + return true; + } + + /** + * Validates a user role + * + * @param \Kirby\Cms\User $user + * @param string $role + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the user role does not exist + */ + public static function validRole(User $user, string $role): bool + { + if (is_a($user->kirby()->roles()->find($role), 'Kirby\Cms\Role') === true) { + return true; + } + + throw new InvalidArgumentException([ + 'key' => 'user.role.invalid', + ]); + } +} diff --git a/kirby/src/Cms/Users.php b/kirby/src/Cms/Users.php new file mode 100644 index 0000000..56c0099 --- /dev/null +++ b/kirby/src/Cms/Users.php @@ -0,0 +1,140 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Users extends Collection +{ + /** + * All registered users methods + * + * @var array + */ + public static $methods = []; + + public function create(array $data) + { + return User::create($data); + } + + /** + * Adds a single user or + * an entire second collection to the + * current collection + * + * @param mixed $object + * @return self + */ + public function add($object) + { + // add a page collection + if (is_a($object, static::class) === true) { + $this->data = array_merge($this->data, $object->data); + + // add a user by id + } elseif (is_string($object) === true && $user = App::instance()->user($object)) { + $this->__set($user->id(), $user); + + // add a user object + } elseif (is_a($object, 'Kirby\Cms\User') === true) { + $this->__set($object->id(), $object); + } + + return $this; + } + + /** + * Takes an array of user props and creates a nice and clean user collection from it + * + * @param array $users + * @param array $inject + * @return self + */ + public static function factory(array $users, array $inject = []) + { + $collection = new static(); + + // read all user blueprints + foreach ($users as $props) { + $user = User::factory($props + $inject); + $collection->set($user->id(), $user); + } + + return $collection; + } + + /** + * Finds a user in the collection by id or email address + * + * @param string $key + * @return \Kirby\Cms\User|null + */ + public function findByKey(string $key) + { + if (Str::contains($key, '@') === true) { + return parent::findBy('email', Str::lower($key)); + } + + return parent::findByKey($key); + } + + /** + * Loads a user from disk by passing the absolute path (root) + * + * @param string $root + * @param array $inject + * @return self + */ + public static function load(string $root, array $inject = []) + { + $users = new static(); + + foreach (Dir::read($root) as $userDirectory) { + if (is_dir($root . '/' . $userDirectory) === false) { + continue; + } + + // get role information + $path = $root . '/' . $userDirectory . '/index.php'; + if (is_file($path) === true) { + $credentials = F::load($path); + } + + // create user model based on role + $user = User::factory([ + 'id' => $userDirectory, + 'model' => $credentials['role'] ?? null + ] + $inject); + + $users->set($user->id(), $user); + } + + return $users; + } + + /** + * Shortcut for `$users->filterBy('role', 'admin')` + * + * @param string $role + * @return self + */ + public function role(string $role) + { + return $this->filterBy('role', $role); + } +} diff --git a/kirby/src/Cms/Visitor.php b/kirby/src/Cms/Visitor.php new file mode 100644 index 0000000..19eeeb0 --- /dev/null +++ b/kirby/src/Cms/Visitor.php @@ -0,0 +1,25 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://getkirby.com/license + */ +class Visitor extends Facade +{ + /** + * @return \Kirby\Http\Visitor + */ + public static function instance() + { + return App::instance()->visitor(); + } +} diff --git a/kirby/src/Data/Data.php b/kirby/src/Data/Data.php new file mode 100644 index 0000000..07a4807 --- /dev/null +++ b/kirby/src/Data/Data.php @@ -0,0 +1,127 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Data +{ + /** + * Handler Type Aliases + * + * @var array + */ + public static $aliases = [ + 'md' => 'txt', + 'mdown' => 'txt', + 'rss' => 'xml', + 'yml' => 'yaml', + ]; + + /** + * All registered handlers + * + * @var array + */ + public static $handlers = [ + 'json' => 'Kirby\Data\Json', + 'php' => 'Kirby\Data\PHP', + 'txt' => 'Kirby\Data\Txt', + 'xml' => 'Kirby\Data\Xml', + 'yaml' => 'Kirby\Data\Yaml', + ]; + + /** + * Handler getter + * + * @param string $type + * @return \Kirby\Data\Handler + */ + public static function handler(string $type) + { + // normalize the type + $type = strtolower($type); + + // find a handler or alias + $handler = static::$handlers[$type] ?? + static::$handlers[static::$aliases[$type] ?? null] ?? + null; + + if (class_exists($handler)) { + return new $handler(); + } + + throw new Exception('Missing handler for type: "' . $type . '"'); + } + + /** + * Decodes data with the specified handler + * + * @param mixed $string + * @param string $type + * @return array + */ + public static function decode($string = null, string $type): array + { + return static::handler($type)->decode($string); + } + + /** + * Encodes data with the specified handler + * + * @param mixed $data + * @param string $type + * @return string + */ + public static function encode($data = null, string $type): string + { + return static::handler($type)->encode($data); + } + + /** + * Reads data from a file; + * the data handler is automatically chosen by + * the extension if not specified + * + * @param string $file + * @param string $type + * @return array + */ + public static function read(string $file, string $type = null): array + { + return static::handler($type ?? F::extension($file))->read($file); + } + + /** + * Writes data to a file; + * the data handler is automatically chosen by + * the extension if not specified + * + * @param string $file + * @param mixed $data + * @param string $type + * @return bool + */ + public static function write(string $file = null, $data = [], string $type = null): bool + { + return static::handler($type ?? F::extension($file))->write($file, $data); + } +} diff --git a/kirby/src/Data/Handler.php b/kirby/src/Data/Handler.php new file mode 100644 index 0000000..8bf5f7c --- /dev/null +++ b/kirby/src/Data/Handler.php @@ -0,0 +1,65 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +abstract class Handler +{ + /** + * Parses an encoded string and returns a multi-dimensional array + * + * Needs to throw an Exception if the file can't be parsed. + * + * @param mixed $string + * @return array + */ + abstract public static function decode($string): array; + + /** + * Converts an array to an encoded string + * + * @param mixed $data + * @return string + */ + abstract public static function encode($data): string; + + /** + * Reads data from a file + * + * @param string $file + * @return array + */ + public static function read(string $file): array + { + if (is_file($file) !== true) { + throw new Exception('The file "' . $file . '" does not exist'); + } + + return static::decode(F::read($file)); + } + + /** + * Writes data to a file + * + * @param string $file + * @param mixed $data + * @return bool + */ + public static function write(string $file = null, $data = []): bool + { + return F::write($file, static::encode($data)); + } +} diff --git a/kirby/src/Data/Json.php b/kirby/src/Data/Json.php new file mode 100644 index 0000000..542ce53 --- /dev/null +++ b/kirby/src/Data/Json.php @@ -0,0 +1,57 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Json extends Handler +{ + /** + * Converts an array to an encoded JSON string + * + * @param mixed $data + * @return string + */ + public static function encode($data): string + { + return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + + /** + * Parses an encoded JSON string and returns a multi-dimensional array + * + * @param mixed $string + * @return array + */ + public static function decode($string): array + { + if ($string === null) { + return []; + } + + if (is_array($string) === true) { + return $string; + } + + if (is_string($string) === false) { + throw new InvalidArgumentException('Invalid JSON data; please pass a string'); + } + + $result = json_decode($string, true); + + if (is_array($result) === true) { + return $result; + } else { + throw new InvalidArgumentException('JSON string is invalid'); + } + } +} diff --git a/kirby/src/Data/PHP.php b/kirby/src/Data/PHP.php new file mode 100644 index 0000000..8b2a840 --- /dev/null +++ b/kirby/src/Data/PHP.php @@ -0,0 +1,94 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class PHP extends Handler +{ + /** + * Converts an array to PHP file content + * + * @param mixed $data + * @param string $indent For internal use only + * @return string + */ + public static function encode($data, string $indent = ''): string + { + switch (gettype($data)) { + case 'array': + $indexed = array_keys($data) === range(0, count($data) - 1); + $array = []; + + foreach ($data as $key => $value) { + $array[] = "$indent " . ($indexed ? '' : static::encode($key) . ' => ') . static::encode($value, "$indent "); + } + + return "[\n" . implode(",\n", $array) . "\n" . $indent . ']'; + case 'boolean': + return $data ? 'true' : 'false'; + case 'int': + case 'double': + return $data; + default: + return var_export($data, true); + } + } + + /** + * PHP strings shouldn't be decoded manually + * + * @param mixed $array + * @return array + */ + public static function decode($array): array + { + throw new BadMethodCallException('The PHP::decode() method is not implemented'); + } + + /** + * Reads data from a file + * + * @param string $file + * @return array + */ + public static function read(string $file): array + { + if (is_file($file) !== true) { + throw new Exception('The file "' . $file . '" does not exist'); + } + + return (array)F::load($file, []); + } + + /** + * Creates a PHP file with the given data + * + * @param string $file + * @param mixed $data + * @return bool + */ + public static function write(string $file = null, $data = []): bool + { + $php = static::encode($data); + $php = " + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Txt extends Handler +{ + /** + * Converts an array to an encoded Kirby txt string + * + * @param mixed $data + * @return string + */ + public static function encode($data): string + { + $result = []; + + foreach (A::wrap($data) as $key => $value) { + if (empty($key) === true || $value === null) { + continue; + } + + $key = Str::ucfirst(Str::slug($key)); + $value = static::encodeValue($value); + $result[$key] = static::encodeResult($key, $value); + } + + return implode("\n\n----\n\n", $result); + } + + /** + * Helper for converting the value + * + * @param array|string $value + * @return string + */ + protected static function encodeValue($value): string + { + // avoid problems with arrays + if (is_array($value) === true) { + $value = Data::encode($value, 'yaml'); + // avoid problems with localized floats + } elseif (is_float($value) === true) { + $value = Str::float($value); + } + + // escape accidental dividers within a field + $value = preg_replace('!(?<=\n|^)----!', '\\----', $value); + + return $value; + } + + /** + * Helper for converting the key and value to the result string + * + * @param string $key + * @param string $value + * @return string + */ + protected static function encodeResult(string $key, string $value): string + { + $result = $key . ':'; + + // multi-line content + if (preg_match('!\R!', $value) === 1) { + $result .= "\n\n"; + } else { + $result .= ' '; + } + + $result .= trim($value); + + return $result; + } + + /** + * Parses a Kirby txt string and returns a multi-dimensional array + * + * @param mixed $string + * @return array + */ + public static function decode($string): array + { + if ($string === null) { + return []; + } + + if (is_array($string) === true) { + return $string; + } + + if (is_string($string) === false) { + throw new InvalidArgumentException('Invalid TXT data; please pass a string'); + } + + // remove BOM + $string = str_replace("\xEF\xBB\xBF", '', $string); + // explode all fields by the line separator + $fields = preg_split('!\n----\s*\n*!', $string); + // start the data array + $data = []; + + // loop through all fields and add them to the content + foreach ($fields as $field) { + $pos = strpos($field, ':'); + $key = str_replace(['-', ' '], '_', strtolower(trim(substr($field, 0, $pos)))); + + // Don't add fields with empty keys + if (empty($key) === true) { + continue; + } + + $value = trim(substr($field, $pos + 1)); + + // unescape escaped dividers within a field + $data[$key] = preg_replace('!(?<=\n|^)\\\\----!', '----', $value); + } + + return $data; + } +} diff --git a/kirby/src/Data/Xml.php b/kirby/src/Data/Xml.php new file mode 100644 index 0000000..9c36be7 --- /dev/null +++ b/kirby/src/Data/Xml.php @@ -0,0 +1,64 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Xml extends Handler +{ + /** + * Converts an array to an encoded XML string + * + * @param mixed $data + * @return string + */ + public static function encode($data): string + { + return XmlConverter::create($data, 'data'); + } + + /** + * Parses an encoded XML string and returns a multi-dimensional array + * + * @param mixed $string + * @return array + */ + public static function decode($string): array + { + if ($string === null) { + return []; + } + + if (is_array($string) === true) { + return $string; + } + + if (is_string($string) === false) { + throw new InvalidArgumentException('Invalid XML data; please pass a string'); + } + + $result = XmlConverter::parse($string); + + if (is_array($result) === true) { + // remove the root's name if it is the default to ensure that + // the decoded data is the same as the input to the encode() method + if ($result['@name'] === 'data') { + unset($result['@name']); + } + + return $result; + } else { + throw new InvalidArgumentException('XML string is invalid'); + } + } +} diff --git a/kirby/src/Data/Yaml.php b/kirby/src/Data/Yaml.php new file mode 100644 index 0000000..163b968 --- /dev/null +++ b/kirby/src/Data/Yaml.php @@ -0,0 +1,74 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Yaml extends Handler +{ + /** + * Converts an array to an encoded YAML string + * + * @param mixed $data + * @return string + */ + public static function encode($data): string + { + // fetch the current locale setting for numbers + $locale = setlocale(LC_NUMERIC, 0); + + // change to english numerics to avoid issues with floats + setlocale(LC_NUMERIC, 'C'); + + // $data, $indent, $wordwrap, $no_opening_dashes + $yaml = Spyc::YAMLDump($data, false, false, true); + + // restore the previous locale settings + setlocale(LC_NUMERIC, $locale); + + return $yaml; + } + + /** + * Parses an encoded YAML string and returns a multi-dimensional array + * + * @param mixed $string + * @return array + */ + public static function decode($string): array + { + if ($string === null) { + return []; + } + + if (is_array($string) === true) { + return $string; + } + + if (is_string($string) === false) { + throw new InvalidArgumentException('Invalid YAML data; please pass a string'); + } + + // remove BOM + $string = str_replace("\xEF\xBB\xBF", '', $string); + $result = Spyc::YAMLLoadString($string); + + if (is_array($result)) { + return $result; + } else { + // apparently Spyc always returns an array, even for invalid YAML syntax + // so this Exception should currently never be thrown + throw new InvalidArgumentException('The YAML data cannot be parsed'); // @codeCoverageIgnore + } + } +} diff --git a/kirby/src/Database/Database.php b/kirby/src/Database/Database.php new file mode 100644 index 0000000..6360754 --- /dev/null +++ b/kirby/src/Database/Database.php @@ -0,0 +1,665 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Database +{ + /** + * The number of affected rows for the last query + * + * @var int|null + */ + protected $affected; + + /** + * Whitelist for column names + * + * @var array + */ + protected $columnWhitelist = []; + + /** + * The established connection + * + * @var \PDO|null + */ + protected $connection; + + /** + * A global array of started connections + * + * @var array + */ + public static $connections = []; + + /** + * Database name + * + * @var string + */ + protected $database; + + /** + * @var string + */ + protected $dsn; + + /** + * Set to true to throw exceptions on failed queries + * + * @var bool + */ + protected $fail = false; + + /** + * The connection id + * + * @var string + */ + protected $id; + + /** + * The last error + * + * @var \Exception|null + */ + protected $lastError; + + /** + * The last insert id + * + * @var int|null + */ + protected $lastId; + + /** + * The last query + * + * @var string + */ + protected $lastQuery; + + /** + * The last result set + * + * @var mixed + */ + protected $lastResult; + + /** + * Optional prefix for table names + * + * @var string + */ + protected $prefix; + + /** + * The PDO query statement + * + * @var \PDOStatement|null + */ + protected $statement; + + /** + * List of existing tables in the database + * + * @var array|null + */ + protected $tables; + + /** + * An array with all queries which are being made + * + * @var array + */ + protected $trace = []; + + /** + * The database type (mysql, sqlite) + * + * @var string + */ + protected $type; + + /** + * @var array + */ + public static $types = []; + + /** + * Creates a new Database instance + * + * @param array $params + * @return void + */ + public function __construct(array $params = []) + { + $this->connect($params); + } + + /** + * Returns one of the started instances + * + * @param string|null $id + * @return self|null + */ + public static function instance(string $id = null) + { + return $id === null ? A::last(static::$connections) : static::$connections[$id] ?? null; + } + + /** + * Returns all started instances + * + * @return array + */ + public static function instances(): array + { + return static::$connections; + } + + /** + * Connects to a database + * + * @param array|null $params This can either be a config key or an array of parameters for the connection + * @return \PDO|null + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function connect(array $params = null) + { + $defaults = [ + 'database' => null, + 'type' => 'mysql', + 'prefix' => null, + 'user' => null, + 'password' => null, + 'id' => uniqid() + ]; + + $options = array_merge($defaults, $params); + + // store the database information + $this->database = $options['database']; + $this->type = $options['type']; + $this->prefix = $options['prefix']; + $this->id = $options['id']; + + if (isset(static::$types[$this->type]) === false) { + throw new InvalidArgumentException('Invalid database type: ' . $this->type); + } + + // fetch the dsn and store it + $this->dsn = static::$types[$this->type]['dsn']($options); + + // try to connect + $this->connection = new PDO($this->dsn, $options['user'], $options['password']); + $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $this->connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); + + // store the connection + static::$connections[$this->id] = $this; + + // return the connection + return $this->connection; + } + + /** + * Returns the currently active connection + * + * @return \PDO|null + */ + public function connection(): ?PDO + { + return $this->connection; + } + + /** + * Sets the exception mode + * + * @param bool $fail + * @return \Kirby\Database\Database + */ + public function fail(bool $fail = true) + { + $this->fail = $fail; + return $this; + } + + /** + * Returns the used database type + * + * @return string + */ + public function type(): string + { + return $this->type; + } + + /** + * Returns the used table name prefix + * + * @return string|null + */ + public function prefix(): ?string + { + return $this->prefix; + } + + /** + * Escapes a value to be used for a safe query + * NOTE: Prepared statements using bound parameters are more secure and solid + * + * @param string $value + * @return string + */ + public function escape(string $value): string + { + return substr($this->connection()->quote($value), 1, -1); + } + + /** + * Adds a value to the db trace and also returns the entire trace if nothing is specified + * + * @param array|null $data + * @return array + */ + public function trace(array $data = null): array + { + // return the full trace + if ($data === null) { + return $this->trace; + } + + // add a new entry to the trace + $this->trace[] = $data; + + return $this->trace; + } + + /** + * Returns the number of affected rows for the last query + * + * @return int|null + */ + public function affected(): ?int + { + return $this->affected; + } + + /** + * Returns the last id if available + * + * @return int|null + */ + public function lastId(): ?int + { + return $this->lastId; + } + + /** + * Returns the last query + * + * @return string|null + */ + public function lastQuery(): ?string + { + return $this->lastQuery; + } + + /** + * Returns the last set of results + * + * @return mixed + */ + public function lastResult() + { + return $this->lastResult; + } + + /** + * Returns the last db error + * + * @return \Throwable + */ + public function lastError() + { + return $this->lastError; + } + + /** + * Returns the name of the database + * + * @return string|null + */ + public function name(): ?string + { + return $this->database; + } + + /** + * Private method to execute database queries. + * This is used by the query() and execute() methods + * + * @param string $query + * @param array $bindings + * @return bool + */ + protected function hit(string $query, array $bindings = []): bool + { + // try to prepare and execute the sql + try { + $this->statement = $this->connection->prepare($query); + $this->statement->execute($bindings); + + $this->affected = $this->statement->rowCount(); + $this->lastId = Str::startsWith($query, 'insert ', true) ? $this->connection->lastInsertId() : null; + $this->lastError = null; + + // store the final sql to add it to the trace later + $this->lastQuery = $this->statement->queryString; + } catch (Throwable $e) { + + // store the error + $this->affected = 0; + $this->lastError = $e; + $this->lastId = null; + $this->lastQuery = $query; + + // only throw the extension if failing is allowed + if ($this->fail === true) { + throw $e; + } + } + + // add a new entry to the singleton trace array + $this->trace([ + 'query' => $this->lastQuery, + 'bindings' => $bindings, + 'error' => $this->lastError + ]); + + // return true or false on success or failure + return $this->lastError === null; + } + + /** + * Executes a sql query, which is expected to return a set of results + * + * @param string $query + * @param array $bindings + * @param array $params + * @return mixed + */ + public function query(string $query, array $bindings = [], array $params = []) + { + $defaults = [ + 'flag' => null, + 'method' => 'fetchAll', + 'fetch' => 'Kirby\Toolkit\Obj', + 'iterator' => 'Kirby\Toolkit\Collection', + ]; + + $options = array_merge($defaults, $params); + + if ($this->hit($query, $bindings) === false) { + return false; + } + + // define the default flag for the fetch method + if ($options['fetch'] instanceof Closure || $options['fetch'] === 'array') { + $flags = PDO::FETCH_ASSOC; + } else { + $flags = PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE; + } + + // add optional flags + if (empty($options['flag']) === false) { + $flags |= $options['flag']; + } + + // set the fetch mode + if ($options['fetch'] instanceof Closure || $options['fetch'] === 'array') { + $this->statement->setFetchMode($flags); + } else { + $this->statement->setFetchMode($flags, $options['fetch']); + } + + // fetch that stuff + $results = $this->statement->{$options['method']}(); + + // apply the fetch closure to all results if given + if ($options['fetch'] instanceof Closure) { + foreach ($results as $key => $result) { + $results[$key] = $options['fetch']($result, $key); + } + } + + if ($options['iterator'] === 'array') { + return $this->lastResult = $results; + } + + return $this->lastResult = new $options['iterator']($results); + } + + /** + * Executes a sql query, which is expected to not return a set of results + * + * @param string $query + * @param array $bindings + * @return bool + */ + public function execute(string $query, array $bindings = []): bool + { + return $this->lastResult = $this->hit($query, $bindings); + } + + /** + * Returns the correct Sql generator instance + * for the type of database + * + * @return \Kirby\Database\Sql + */ + public function sql() + { + $className = static::$types[$this->type]['sql'] ?? 'Sql'; + return new $className($this); + } + + /** + * Sets the current table, which should be queried. Returns a + * Query object, which can be used to build a full query + * for that table + * + * @param string $table + * @return \Kirby\Database\Query + */ + public function table(string $table) + { + return new Query($this, $this->prefix() . $table); + } + + /** + * Checks if a table exists in the current database + * + * @param string $table + * @return bool + */ + public function validateTable(string $table): bool + { + if ($this->tables === null) { + // Get the list of tables from the database + $sql = $this->sql()->tables($this->database); + $results = $this->query($sql['query'], $sql['bindings']); + + if ($results) { + $this->tables = $results->pluck('name'); + } else { + return false; + } + } + + return in_array($table, $this->tables) === true; + } + + /** + * Checks if a column exists in a specified table + * + * @param string $table + * @param string $column + * @return bool + */ + public function validateColumn(string $table, string $column): bool + { + if (isset($this->columnWhitelist[$table]) === false) { + if ($this->validateTable($table) === false) { + $this->columnWhitelist[$table] = []; + return false; + } + + // Get the column whitelist from the database + $sql = $this->sql()->columns($table); + $results = $this->query($sql['query'], $sql['bindings']); + + if ($results) { + $this->columnWhitelist[$table] = $results->pluck('name'); + } else { + return false; + } + } + + return in_array($column, $this->columnWhitelist[$table]) === true; + } + + /** + * Creates a new table + * + * @param string $table + * @param array $columns + * @return bool + */ + public function createTable($table, $columns = []): bool + { + $sql = $this->sql()->createTable($table, $columns); + $queries = Str::split($sql['query'], ';'); + + foreach ($queries as $query) { + $query = trim($query); + + if ($this->execute($query, $sql['bindings']) === false) { + return false; + } + } + + // update cache + if (in_array($table, $this->tables ?? []) !== true) { + $this->tables[] = $table; + } + + return true; + } + + /** + * Drops a table + * + * @param string $table + * @return bool + */ + public function dropTable(string $table): bool + { + $sql = $this->sql()->dropTable($table); + if ($this->execute($sql['query'], $sql['bindings']) !== true) { + return false; + } + + // update cache + $key = array_search($table, $this->tables ?? []); + if ($key !== false) { + unset($this->tables[$key]); + } + + return true; + } + + /** + * Magic way to start queries for tables by + * using a method named like the table. + * I.e. $db->users()->all() + * + * @param mixed $method + * @param mixed $arguments + * @return \Kirby\Database\Query + */ + public function __call($method, $arguments = null) + { + return $this->table($method); + } +} + +/** + * MySQL database connector + */ +Database::$types['mysql'] = [ + 'sql' => 'Kirby\Database\Sql\Mysql', + 'dsn' => function (array $params) { + if (isset($params['host']) === false && isset($params['socket']) === false) { + throw new InvalidArgumentException('The mysql connection requires either a "host" or a "socket" parameter'); + } + + if (isset($params['database']) === false) { + throw new InvalidArgumentException('The mysql connection requires a "database" parameter'); + } + + $parts = []; + + if (empty($params['host']) === false) { + $parts[] = 'host=' . $params['host']; + } + + if (empty($params['port']) === false) { + $parts[] = 'port=' . $params['port']; + } + + if (empty($params['socket']) === false) { + $parts[] = 'unix_socket=' . $params['socket']; + } + + if (empty($params['database']) === false) { + $parts[] = 'dbname=' . $params['database']; + } + + $parts[] = 'charset=' . ($params['charset'] ?? 'utf8'); + + return 'mysql:' . implode(';', $parts); + } +]; + +/** + * SQLite database connector + */ +Database::$types['sqlite'] = [ + 'sql' => 'Kirby\Database\Sql\Sqlite', + 'dsn' => function (array $params) { + if (isset($params['database']) === false) { + throw new InvalidArgumentException('The sqlite connection requires a "database" parameter'); + } + + return 'sqlite:' . $params['database']; + } +]; diff --git a/kirby/src/Database/Db.php b/kirby/src/Database/Db.php new file mode 100644 index 0000000..00065db --- /dev/null +++ b/kirby/src/Database/Db.php @@ -0,0 +1,283 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Db +{ + /** + * Query shortcuts + * + * @var array + */ + public static $queries = []; + + /** + * The singleton Database object + * + * @var \Kirby\Database\Database + */ + public static $connection = null; + + /** + * (Re)connect the database + * + * @param array|null $params Pass `[]` to use the default params from the config, + * don't pass any argument to get the current connection + * @return \Kirby\Database\Database + */ + public static function connect(?array $params = null) + { + if ($params === null && static::$connection !== null) { + return static::$connection; + } + + // try to connect with the default + // connection settings if no params are set + $defaults = [ + 'type' => Config::get('db.type', 'mysql'), + 'host' => Config::get('db.host', 'localhost'), + 'user' => Config::get('db.user', 'root'), + 'password' => Config::get('db.password', ''), + 'database' => Config::get('db.database', ''), + 'prefix' => Config::get('db.prefix', '') + ]; + $params = $params ?? $defaults; + + return static::$connection = new Database($params); + } + + /** + * Returns the current database connection + * + * @return \Kirby\Database\Database|null + */ + public static function connection() + { + return static::$connection; + } + + /** + * Sets the current table which should be queried. Returns a + * Query object, which can be used to build a full query for + * that table. + * + * @param string $table + * @return \Kirby\Database\Query + */ + public static function table(string $table) + { + $db = static::connect(); + return $db->table($table); + } + + /** + * Executes a raw SQL query which expects a set of results + * + * @param string $query + * @param array $bindings + * @param array $params + * @return mixed + */ + public static function query(string $query, array $bindings = [], array $params = []) + { + $db = static::connect(); + return $db->query($query, $bindings, $params); + } + + /** + * Executes a raw SQL query which expects no set of results (i.e. update, insert, delete) + * + * @param string $query + * @param array $bindings + * @return bool + */ + public static function execute(string $query, array $bindings = []): bool + { + $db = static::connect(); + return $db->execute($query, $bindings); + } + + /** + * Magic calls for other static Db methods are + * redirected to either a predefined query or + * the respective method of the Database object + * + * @param string $method + * @param mixed $arguments + * @return mixed + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function __callStatic(string $method, $arguments) + { + if (isset(static::$queries[$method])) { + return static::$queries[$method](...$arguments); + } + + if (static::$connection !== null && method_exists(static::$connection, $method) === true) { + return call_user_func_array([static::$connection, $method], $arguments); + } + + throw new InvalidArgumentException('Invalid static Db method: ' . $method); + } +} + +/** + * Shortcut for SELECT clauses + * @codeCoverageIgnore + * + * @param string $table The name of the table which should be queried + * @param mixed $columns Either a string with columns or an array of column names + * @param mixed $where The WHERE clause; can be a string or an array + * @param string $order + * @param int $offset + * @param int $limit + * @return mixed + */ +Db::$queries['select'] = function (string $table, $columns = '*', $where = null, string $order = null, int $offset = 0, int $limit = null) { + return Db::table($table)->select($columns)->where($where)->order($order)->offset($offset)->limit($limit)->all(); +}; + +/** + * Shortcut for selecting a single row in a table + * @codeCoverageIgnore + * + * @param string $table The name of the table which should be queried + * @param mixed $columns Either a string with columns or an array of column names + * @param mixed $where The WHERE clause; can be a string or an array + * @param string $order + * @param int $offset + * @param int $limit + * @return mixed + */ +Db::$queries['first'] = Db::$queries['row'] = Db::$queries['one'] = function (string $table, $columns = '*', $where = null, string $order = null) { + return Db::table($table)->select($columns)->where($where)->order($order)->first(); +}; + +/** + * Returns only values from a single column + * @codeCoverageIgnore + * + * @param string $table The name of the table which should be queried + * @param string $column The name of the column to select from + * @param mixed $where The WHERE clause; can be a string or an array + * @param string $order + * @param int $offset + * @param int $limit + * @return mixed + */ +Db::$queries['column'] = function (string $table, string $column, $where = null, string $order = null, int $offset = 0, int $limit = null) { + return Db::table($table)->where($where)->order($order)->offset($offset)->limit($limit)->column($column); +}; + +/** + * Shortcut for inserting a new row into a table + * @codeCoverageIgnore + * + * @param string $table The name of the table which should be queried + * @param array $values An array of values which should be inserted + * @return int ID of the inserted row + */ +Db::$queries['insert'] = function (string $table, array $values): int { + return Db::table($table)->insert($values); +}; + +/** + * Shortcut for updating a row in a table + * @codeCoverageIgnore + * + * @param string $table The name of the table which should be queried + * @param array $values An array of values which should be inserted + * @param mixed $where An optional WHERE clause + * @return bool + */ +Db::$queries['update'] = function (string $table, array $values, $where = null): bool { + return Db::table($table)->where($where)->update($values); +}; + +/** + * Shortcut for deleting rows in a table + * @codeCoverageIgnore + * + * @param string $table The name of the table which should be queried + * @param mixed $where An optional WHERE clause + * @return bool + */ +Db::$queries['delete'] = function (string $table, $where = null): bool { + return Db::table($table)->where($where)->delete(); +}; + +/** + * Shortcut for counting rows in a table + * @codeCoverageIgnore + * + * @param string $table The name of the table which should be queried + * @param mixed $where An optional WHERE clause + * @return int + */ +Db::$queries['count'] = function (string $table, $where = null): int { + return Db::table($table)->where($where)->count(); +}; + +/** + * Shortcut for calculating the minimum value in a column + * @codeCoverageIgnore + * + * @param string $table The name of the table which should be queried + * @param string $column The name of the column of which the minimum should be calculated + * @param mixed $where An optional WHERE clause + * @return float + */ +Db::$queries['min'] = function (string $table, string $column, $where = null): float { + return Db::table($table)->where($where)->min($column); +}; + +/** + * Shortcut for calculating the maximum value in a column + * @codeCoverageIgnore + * + * @param string $table The name of the table which should be queried + * @param string $column The name of the column of which the maximum should be calculated + * @param mixed $where An optional WHERE clause + * @return float + */ +Db::$queries['max'] = function (string $table, string $column, $where = null): float { + return Db::table($table)->where($where)->max($column); +}; + +/** + * Shortcut for calculating the average value in a column + * @codeCoverageIgnore + * + * @param string $table The name of the table which should be queried + * @param string $column The name of the column of which the average should be calculated + * @param mixed $where An optional WHERE clause + * @return float + */ +Db::$queries['avg'] = function (string $table, string $column, $where = null): float { + return Db::table($table)->where($where)->avg($column); +}; + +/** + * Shortcut for calculating the sum of all values in a column + * @codeCoverageIgnore + * + * @param string $table The name of the table which should be queried + * @param string $column The name of the column of which the sum should be calculated + * @param mixed $where An optional WHERE clause + * @return float + */ +Db::$queries['sum'] = function (string $table, string $column, $where = null): float { + return Db::table($table)->where($where)->sum($column); +}; diff --git a/kirby/src/Database/Query.php b/kirby/src/Database/Query.php new file mode 100644 index 0000000..56f775f --- /dev/null +++ b/kirby/src/Database/Query.php @@ -0,0 +1,1070 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Query +{ + const ERROR_INVALID_QUERY_METHOD = 0; + + /** + * Parent Database object + * + * @var \Kirby\Database\Database + */ + protected $database = null; + + /** + * The object which should be fetched for each row + * or function to call for each row + * + * @var string|\Closure + */ + protected $fetch = 'Kirby\Toolkit\Obj'; + + /** + * The iterator class, which should be used for result sets + * + * @var string + */ + protected $iterator = 'Kirby\Toolkit\Collection'; + + /** + * An array of bindings for the final query + * + * @var array + */ + protected $bindings = []; + + /** + * The table name + * + * @var string + */ + protected $table; + + /** + * The name of the primary key column + * + * @var string + */ + protected $primaryKeyName = 'id'; + + /** + * An array with additional join parameters + * + * @var array + */ + protected $join; + + /** + * A list of columns, which should be selected + * + * @var array|string + */ + protected $select; + + /** + * Boolean for distinct select clauses + * + * @var bool + */ + protected $distinct; + + /** + * Boolean for if exceptions should be thrown on failing queries + * + * @var bool + */ + protected $fail = false; + + /** + * A list of values for update and insert clauses + * + * @var array + */ + protected $values; + + /** + * WHERE clause + * + * @var mixed + */ + protected $where; + + /** + * GROUP BY clause + * + * @var mixed + */ + protected $group; + + /** + * HAVING clause + * + * @var mixed + */ + protected $having; + + /** + * ORDER BY clause + * + * @var mixed + */ + protected $order; + + /** + * The offset, which should be applied to the select query + * + * @var int + */ + protected $offset = 0; + + /** + * The limit, which should be applied to the select query + * + * @var int + */ + protected $limit; + + /** + * Boolean to enable query debugging + * + * @var bool + */ + protected $debug = false; + + /** + * Constructor + * + * @param \Kirby\Database\Database $database Database object + * @param string $table Optional name of the table, which should be queried + */ + public function __construct(Database $database, string $table) + { + $this->database = $database; + $this->table($table); + } + + /** + * Reset the query class after each db hit + */ + protected function reset() + { + $this->bindings = []; + $this->join = null; + $this->select = null; + $this->distinct = null; + $this->fail = false; + $this->values = null; + $this->where = null; + $this->group = null; + $this->having = null; + $this->order = null; + $this->offset = 0; + $this->limit = null; + $this->debug = false; + } + + /** + * Enables query debugging. + * If enabled, the query will return an array with all important info about + * the query instead of actually executing the query and returning results + * + * @param bool $debug + * @return \Kirby\Database\Query + */ + public function debug(bool $debug = true) + { + $this->debug = $debug; + return $this; + } + + /** + * Enables distinct select clauses. + * + * @param bool $distinct + * @return \Kirby\Database\Query + */ + public function distinct(bool $distinct = true) + { + $this->distinct = $distinct; + return $this; + } + + /** + * Enables failing queries. + * If enabled queries will no longer fail silently but throw an exception + * + * @param bool $fail + * @return \Kirby\Database\Query + */ + public function fail(bool $fail = true) + { + $this->fail = $fail; + return $this; + } + + /** + * Sets the object class, which should be fetched; + * set this to `'array'` to get a simple array instead of an object; + * pass a function that receives the `$data` and the `$key` to generate arbitrary data structures + * + * @param string|\Closure $fetch + * @return \Kirby\Database\Query + */ + public function fetch($fetch) + { + $this->fetch = $fetch; + return $this; + } + + /** + * Sets the iterator class, which should be used for multiple results + * Set this to array to get a simple array instead of an iterator object + * + * @param string $iterator + * @return \Kirby\Database\Query + */ + public function iterator(string $iterator) + { + $this->iterator = $iterator; + return $this; + } + + /** + * Sets the name of the table, which should be queried + * + * @param string $table + * @return \Kirby\Database\Query + * @throws \Kirby\Exception\InvalidArgumentException if the table does not exist + */ + public function table(string $table) + { + if ($this->database->validateTable($table) === false) { + throw new InvalidArgumentException('Invalid table: ' . $table); + } + + $this->table = $table; + return $this; + } + + /** + * Sets the name of the primary key column + * + * @param string $primaryKeyName + * @return \Kirby\Database\Query + */ + public function primaryKeyName(string $primaryKeyName) + { + $this->primaryKeyName = $primaryKeyName; + return $this; + } + + /** + * Sets the columns, which should be selected from the table + * By default all columns will be selected + * + * @param mixed $select Pass either a string of columns or an array + * @return \Kirby\Database\Query + */ + public function select($select) + { + $this->select = $select; + return $this; + } + + /** + * Adds a new join clause to the query + * + * @param string $table Name of the table, which should be joined + * @param string $on The on clause for this join + * @param string $type The join type. Uses an inner join by default + * @return self + */ + public function join(string $table, string $on, string $type = 'JOIN') + { + $join = [ + 'table' => $table, + 'on' => $on, + 'type' => $type + ]; + + $this->join[] = $join; + return $this; + } + + /** + * Shortcut for creating a left join clause + * + * @param string $table Name of the table, which should be joined + * @param string $on The on clause for this join + * @return \Kirby\Database\Query + */ + public function leftJoin(string $table, string $on) + { + return $this->join($table, $on, 'left'); + } + + /** + * Shortcut for creating a right join clause + * + * @param string $table Name of the table, which should be joined + * @param string $on The on clause for this join + * @return \Kirby\Database\Query + */ + public function rightJoin(string $table, string $on) + { + return $this->join($table, $on, 'right'); + } + + /** + * Shortcut for creating an inner join clause + * + * @param string $table Name of the table, which should be joined + * @param string $on The on clause for this join + * @return \Kirby\Database\Query + */ + public function innerJoin($table, $on) + { + return $this->join($table, $on, 'inner'); + } + + /** + * Sets the values which should be used for the update or insert clause + * + * @param mixed $values Can either be a string or an array of values + * @return \Kirby\Database\Query + */ + public function values($values = []) + { + if ($values !== null) { + $this->values = $values; + } + return $this; + } + + /** + * Attaches additional bindings to the query. + * Also can be used as getter for all attached bindings by not passing an argument. + * + * @param mixed $bindings Array of bindings or null to use this method as getter + * @return array|\Kirby\Database\Query + */ + public function bindings(array $bindings = null) + { + if (is_array($bindings) === true) { + $this->bindings = array_merge($this->bindings, $bindings); + return $this; + } + + return $this->bindings; + } + + /** + * Attaches an additional where clause + * + * All available ways to add where clauses + * + * ->where('username like "myuser"'); (args: 1) + * ->where(['username' => 'myuser']); (args: 1) + * ->where(function($where) { $where->where('id', '=', 1) }) (args: 1) + * ->where('username like ?', 'myuser') (args: 2) + * ->where('username', 'like', 'myuser'); (args: 3) + * + * @param mixed ...$args + * @return \Kirby\Database\Query + */ + public function where(...$args) + { + $this->where = $this->filterQuery($args, $this->where); + return $this; + } + + /** + * Shortcut to attach a where clause with an OR operator. + * Check out the where() method docs for additional info. + * + * @param mixed ...$args + * @return \Kirby\Database\Query + */ + public function orWhere(...$args) + { + $mode = A::last($args); + + // if there's a where clause mode attribute attached… + if (in_array($mode, ['AND', 'OR']) === true) { + // remove that from the list of arguments + array_pop($args); + } + + // make sure to always attach the OR mode indicator + $args[] = 'OR'; + + $this->where(...$args); + return $this; + } + + /** + * Shortcut to attach a where clause with an AND operator. + * Check out the where() method docs for additional info. + * + * @param mixed ...$args + * @return \Kirby\Database\Query + */ + public function andWhere(...$args) + { + $mode = A::last($args); + + // if there's a where clause mode attribute attached… + if (in_array($mode, ['AND', 'OR']) === true) { + // remove that from the list of arguments + array_pop($args); + } + + // make sure to always attach the AND mode indicator + $args[] = 'AND'; + + $this->where(...$args); + return $this; + } + + /** + * Attaches a group by clause + * + * @param string|null $group + * @return \Kirby\Database\Query + */ + public function group(string $group = null) + { + $this->group = $group; + return $this; + } + + /** + * Attaches an additional having clause + * + * All available ways to add having clauses + * + * ->having('username like "myuser"'); (args: 1) + * ->having(['username' => 'myuser']); (args: 1) + * ->having(function($having) { $having->having('id', '=', 1) }) (args: 1) + * ->having('username like ?', 'myuser') (args: 2) + * ->having('username', 'like', 'myuser'); (args: 3) + * + * @param mixed ...$args + * @return \Kirby\Database\Query + */ + public function having(...$args) + { + $this->having = $this->filterQuery($args, $this->having); + return $this; + } + + /** + * Attaches an order clause + * + * @param string|null $order + * @return \Kirby\Database\Query + */ + public function order(string $order = null) + { + $this->order = $order; + return $this; + } + + /** + * Sets the offset for select clauses + * + * @param int|null $offset + * @return \Kirby\Database\Query + */ + public function offset(int $offset = null) + { + $this->offset = $offset; + return $this; + } + + /** + * Sets the limit for select clauses + * + * @param int|null $limit + * @return \Kirby\Database\Query + */ + public function limit(int $limit = null) + { + $this->limit = $limit; + return $this; + } + + /** + * Builds the different types of SQL queries + * This uses the SQL class to build stuff. + * + * @param string $type (select, update, insert) + * @return array The final query + */ + public function build(string $type) + { + $sql = $this->database->sql(); + + switch ($type) { + case 'select': + return $sql->select([ + 'table' => $this->table, + 'columns' => $this->select, + 'join' => $this->join, + 'distinct' => $this->distinct, + 'where' => $this->where, + 'group' => $this->group, + 'having' => $this->having, + 'order' => $this->order, + 'offset' => $this->offset, + 'limit' => $this->limit, + 'bindings' => $this->bindings + ]); + case 'update': + return $sql->update([ + 'table' => $this->table, + 'where' => $this->where, + 'values' => $this->values, + 'bindings' => $this->bindings + ]); + case 'insert': + return $sql->insert([ + 'table' => $this->table, + 'values' => $this->values, + 'bindings' => $this->bindings + ]); + case 'delete': + return $sql->delete([ + 'table' => $this->table, + 'where' => $this->where, + 'bindings' => $this->bindings + ]); + } + } + + /** + * Builds a count query + * + * @return int + */ + public function count(): int + { + return (int)$this->aggregate('COUNT'); + } + + /** + * Builds a max query + * + * @param string $column + * @return float + */ + public function max(string $column): float + { + return (float)$this->aggregate('MAX', $column); + } + + /** + * Builds a min query + * + * @param string $column + * @return float + */ + public function min(string $column): float + { + return (float)$this->aggregate('MIN', $column); + } + + /** + * Builds a sum query + * + * @param string $column + * @return float + */ + public function sum(string $column): float + { + return (float)$this->aggregate('SUM', $column); + } + + /** + * Builds an average query + * + * @param string $column + * @return float + */ + public function avg(string $column): float + { + return (float)$this->aggregate('AVG', $column); + } + + /** + * Builds an aggregation query. + * This is used by all the aggregation methods above + * + * @param string $method + * @param string $column + * @param int $default An optional default value, which should be returned if the query fails + * @return mixed + */ + public function aggregate(string $method, string $column = '*', $default = 0) + { + // reset the sorting to avoid counting issues + $this->order = null; + + // validate column + if ($column !== '*') { + $sql = $this->database->sql(); + $column = $sql->columnName($this->table, $column); + } + + $fetch = $this->fetch; + $row = $this->select($method . '(' . $column . ') as aggregation')->fetch('Obj')->first(); + + if ($this->debug === true) { + return $row; + } + + $result = $row ? $row->get('aggregation') : $default; + + $this->fetch($fetch); + + return $result; + } + + /** + * Used as an internal shortcut for firing a db query + * + * @param string|array $sql + * @param array $params + * @return mixed + */ + protected function query($sql, array $params = []) + { + if (is_string($sql) === true) { + $sql = [ + 'query' => $sql, + 'bindings' => $this->bindings() + ]; + } + + if ($this->debug) { + return [ + 'query' => $sql['query'], + 'bindings' => $this->bindings(), + 'options' => $params + ]; + } + + if ($this->fail) { + $this->database->fail(); + } + + $result = $this->database->query($sql['query'], $sql['bindings'], $params); + + $this->reset(); + + return $result; + } + + /** + * Used as an internal shortcut for executing a db query + * + * @param string|array $sql + * @param array $params + * @return mixed + */ + protected function execute($sql, array $params = []) + { + if (is_string($sql) === true) { + $sql = [ + 'query' => $sql, + 'bindings' => $this->bindings() + ]; + } + + if ($this->debug === true) { + return [ + 'query' => $sql['query'], + 'bindings' => $sql['bindings'], + 'options' => $params + ]; + } + + if ($this->fail) { + $this->database->fail(); + } + + $result = $this->database->execute($sql['query'], $sql['bindings'], $params); + + $this->reset(); + + return $result; + } + + /** + * Selects only one row from a table + * + * @return object + */ + public function first() + { + return $this->query($this->offset(0)->limit(1)->build('select'), [ + 'fetch' => $this->fetch, + 'iterator' => 'array', + 'method' => 'fetch', + ]); + } + + /** + * Selects only one row from a table + * + * @return object + */ + public function row() + { + return $this->first(); + } + + /** + * Selects only one row from a table + * + * @return object + */ + public function one() + { + return $this->first(); + } + + /** + * Automatically adds pagination to a query + * + * @param int $page + * @param int $limit The number of rows, which should be returned for each page + * @return object Collection iterator with attached pagination object + */ + public function page(int $page, int $limit) + { + // clone this to create a counter query + $counter = clone $this; + + // count the total number of rows for this query + $count = $counter->debug(false)->count(); + + // pagination + $pagination = new Pagination([ + 'limit' => $limit, + 'page' => $page, + 'total' => $count, + ]); + + // apply it to the dataset and retrieve all rows. make sure to use Collection as the iterator to be able to attach the pagination object + $iterator = $this->iterator; + $collection = $this->offset($pagination->offset())->limit($pagination->limit())->iterator('Collection')->all(); + + $this->iterator($iterator); + + // return debug information if debug mode is active + if ($this->debug) { + $collection['totalcount'] = $count; + return $collection; + } + + // store all pagination vars in a separate object + if ($collection) { + $collection->paginate($pagination); + } + + // return the limited collection + return $collection; + } + + /** + * Returns all matching rows from a table + * + * @return mixed + */ + public function all() + { + return $this->query($this->build('select'), [ + 'fetch' => $this->fetch, + 'iterator' => $this->iterator, + ]); + } + + /** + * Returns only values from a single column + * + * @param string $column + * @return mixed + */ + public function column(string $column) + { + // if there isn't already an explicit order, order by the primary key + // instead of the column that was requested (which would be implied otherwise) + if ($this->order === null) { + $sql = $this->database->sql(); + $primaryKey = $sql->combineIdentifier($this->table, $this->primaryKeyName); + + $this->order($primaryKey . ' ASC'); + } + + $results = $this->query($this->select([$column])->build('select'), [ + 'iterator' => 'array', + 'fetch' => 'array', + ]); + + if ($this->debug === true) { + return $results; + } + + $results = array_column($results, $column); + + if ($this->iterator === 'array') { + return $results; + } + + $iterator = $this->iterator; + + return new $iterator($results); + } + + /** + * Find a single row by column and value + * + * @param string $column + * @param mixed $value + * @return mixed + */ + public function findBy(string $column, $value) + { + return $this->where([$column => $value])->first(); + } + + /** + * Find a single row by its primary key + * + * @param mixed $id + * @return mixed + */ + public function find($id) + { + return $this->findBy($this->primaryKeyName, $id); + } + + /** + * Fires an insert query + * + * @param mixed $values You can pass values here or set them with ->values() before + * @return mixed Returns the last inserted id on success or false. + */ + public function insert($values = null) + { + $query = $this->execute($this->values($values)->build('insert')); + + if ($this->debug === true) { + return $query; + } + + return $query ? $this->database->lastId() : false; + } + + /** + * Fires an update query + * + * @param mixed $values You can pass values here or set them with ->values() before + * @param mixed $where You can pass a where clause here or set it with ->where() before + * @return bool + */ + public function update($values = null, $where = null) + { + return $this->execute($this->values($values)->where($where)->build('update')); + } + + /** + * Fires a delete query + * + * @param mixed $where You can pass a where clause here or set it with ->where() before + * @return bool + */ + public function delete($where = null) + { + return $this->execute($this->where($where)->build('delete')); + } + + /** + * Enables magic queries like findByUsername or findByEmail + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + if (preg_match('!^findBy([a-z]+)!i', $method, $match)) { + $column = Str::lower($match[1]); + return $this->findBy($column, $arguments[0]); + } else { + throw new InvalidArgumentException('Invalid query method: ' . $method, static::ERROR_INVALID_QUERY_METHOD); + } + } + + /** + * Builder for where and having clauses + * + * @param array $args Arguments, see where() description + * @param mixed $current Current value (like $this->where) + * @return string + */ + protected function filterQuery(array $args, $current) + { + $mode = A::last($args); + $result = ''; + + // if there's a where clause mode attribute attached… + if (in_array($mode, ['AND', 'OR'])) { + // remove that from the list of arguments + array_pop($args); + } else { + $mode = 'AND'; + } + + switch (count($args)) { + case 1: + + if ($args[0] === null) { + return $current; + + // ->where('username like "myuser"'); + } elseif (is_string($args[0]) === true) { + + // simply add the entire string to the where clause + // escaping or using bindings has to be done before calling this method + $result = $args[0]; + + // ->where(['username' => 'myuser']); + } elseif (is_array($args[0]) === true) { + + // simple array mode (AND operator) + $sql = $this->database->sql()->values($this->table, $args[0], ' AND ', true, true); + + $result = $sql['query']; + + $this->bindings($sql['bindings']); + } elseif (is_callable($args[0]) === true) { + $query = clone $this; + call_user_func($args[0], $query); + + // copy over the bindings from the nested query + $this->bindings = array_merge($this->bindings, $query->bindings); + + $result = '(' . $query->where . ')'; + } + + break; + case 2: + + // ->where('username like :username', ['username' => 'myuser']) + if (is_string($args[0]) === true && is_array($args[1]) === true) { + + // prepared where clause + $result = $args[0]; + + // store the bindings + $this->bindings($args[1]); + + // ->where('username like ?', 'myuser') + } elseif (is_string($args[0]) === true && is_string($args[1]) === true) { + + // prepared where clause + $result = $args[0]; + + // store the bindings + $this->bindings([$args[1]]); + } + + break; + case 3: + + // ->where('username', 'like', 'myuser'); + if (is_string($args[0]) === true && is_string($args[1]) === true) { + + // validate column + $sql = $this->database->sql(); + $key = $sql->columnName($this->table, $args[0]); + + // ->where('username', 'in', ['myuser', 'myotheruser']); + if (is_array($args[2]) === true) { + $predicate = trim(strtoupper($args[1])); + + if (in_array($predicate, ['IN', 'NOT IN']) === false) { + throw new InvalidArgumentException('Invalid predicate ' . $predicate); + } + + // build a list of bound values + $values = []; + $bindings = []; + + foreach ($args[2] as $value) { + $valueBinding = $sql->bindingName('value'); + $bindings[$valueBinding] = $value; + $values[] = $valueBinding; + } + + // add that to the where clause in parenthesis + $result = $key . ' ' . $predicate . ' (' . implode(', ', $values) . ')'; + + $this->bindings($bindings); + + // ->where('username', 'like', 'myuser'); + } else { + $predicate = trim(strtoupper($args[1])); + $predicates = [ + '=', '>=', '>', '<=', '<', '<>', '!=', '<=>', + 'IS', 'IS NOT', + 'BETWEEN', 'NOT BETWEEN', + 'LIKE', 'NOT LIKE', + 'SOUNDS LIKE', + 'REGEXP', 'NOT REGEXP' + ]; + + if (in_array($predicate, $predicates) === false) { + throw new InvalidArgumentException('Invalid predicate/operator ' . $predicate); + } + + $valueBinding = $sql->bindingName('value'); + $bindings[$valueBinding] = $args[2]; + + $result = $key . ' ' . $predicate . ' ' . $valueBinding; + + $this->bindings($bindings); + } + } + + break; + + } + + // attach the where clause + if (empty($current) === false) { + return $current . ' ' . $mode . ' ' . $result; + } else { + return $result; + } + } +} diff --git a/kirby/src/Database/Sql.php b/kirby/src/Database/Sql.php new file mode 100644 index 0000000..2c4236a --- /dev/null +++ b/kirby/src/Database/Sql.php @@ -0,0 +1,955 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +abstract class Sql +{ + /** + * List of literals which should not be escaped in queries + * + * @var array + */ + public static $literals = ['NOW()', null]; + + /** + * The parent database connection + * + * @var \Kirby\Database\Database + */ + protected $database; + + /** + * List of used bindings; used to avoid + * duplicate binding names + * + * @var array + */ + protected $bindings = []; + + /** + * Constructor + * @codeCoverageIgnore + * + * @param \Kirby\Database\Database $database + */ + public function __construct($database) + { + $this->database = $database; + } + + /** + * Returns a randomly generated binding name + * + * @param string $label String that only contains alphanumeric chars and + * underscores to use as a human-readable identifier + * @return string Binding name that is guaranteed to be unique for this connection + */ + public function bindingName(string $label): string + { + // make sure that the binding name is safe to prevent injections; + // otherwise use a generic label + if (!$label || preg_match('/^[a-zA-Z0-9_]+$/', $label) !== 1) { + $label = 'invalid'; + } + + // generate random bindings until the name is unique + do { + $binding = ':' . $label . '_' . Str::random(8, 'alphaNum'); + } while (in_array($binding, $this->bindings) === true); + + // cache the generated binding name for future invocations + $this->bindings[] = $binding; + return $binding; + } + + /** + * Returns a query to list the columns of a specified table; + * the query needs to return rows with a column `name` + * + * @param string $table Table name + * @return array + */ + abstract public function columns(string $table): array; + + /** + * Returns a query snippet for a column default value + * + * @param string $name Column name + * @param array $column Column definition array with an optional `default` key + * @return array Array with a `query` string and a `bindings` array + */ + public function columnDefault(string $name, array $column): array + { + if (isset($column['default']) === false) { + return [ + 'query' => null, + 'bindings' => [] + ]; + } + + $binding = $this->bindingName($name . '_default'); + + return [ + 'query' => 'DEFAULT ' . $binding, + 'bindings' => [ + $binding => $column['default'] + ] + ]; + } + + /** + * Returns the cleaned identifier based on the table and column name + * + * @param string $table Table name + * @param string $column Column name + * @param bool $enforceQualified If true, a qualified identifier is returned in all cases + * @return string|null Identifier or null if the table or column is invalid + */ + public function columnName(string $table, string $column, bool $enforceQualified = false): ?string + { + // ensure we have clean $table and $column values without qualified identifiers + list($table, $column) = $this->splitIdentifier($table, $column); + + // combine the identifiers again + if ($this->database->validateColumn($table, $column) === true) { + return $this->combineIdentifier($table, $column, $enforceQualified !== true); + } + + // the table or column does not exist + return null; + } + + /** + * Abstracted column types to simplify table + * creation for multiple database drivers + * @codeCoverageIgnore + * + * @return array + */ + public function columnTypes(): array + { + return [ + 'id' => '{{ name }} INT(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY', + 'varchar' => '{{ name }} varchar(255) {{ null }} {{ default }} {{ unique }}', + 'text' => '{{ name }} TEXT {{ unique }}', + 'int' => '{{ name }} INT(11) UNSIGNED {{ null }} {{ default }} {{ unique }}', + 'timestamp' => '{{ name }} TIMESTAMP {{ null }} {{ default }} {{ unique }}' + ]; + } + + /** + * Combines an identifier (table and column) + * + * @param $table string + * @param $column string + * @param $values boolean Whether the identifier is going to be used for a VALUES clause; + * only relevant for SQLite + * @return string + */ + public function combineIdentifier(string $table, string $column, bool $values = false): string + { + return $this->quoteIdentifier($table) . '.' . $this->quoteIdentifier($column); + } + + /** + * Creates the CREATE TABLE syntax for a single column + * + * @param string $name Column name + * @param array $column Column definition array; valid keys: + * - `type` (required): Column template to use + * - `null`: Whether the column may be NULL (boolean) + * - `key`: Index this column is part of; special values `'primary'` for PRIMARY KEY and `true` for automatic naming + * - `unique`: Whether the index (or if not set the column itself) has a UNIQUE constraint + * - `default`: Default value of this column + * @return array Array with `query` and `key` strings, a `unique` boolean and a `bindings` array + * @throws \Kirby\Exception\InvalidArgumentException if no column type is given or the column type is not supported. + */ + public function createColumn(string $name, array $column): array + { + // column type + if (isset($column['type']) === false) { + throw new InvalidArgumentException('No column type given for column ' . $name); + } + $template = $this->columnTypes()[$column['type']] ?? null; + if (!$template) { + throw new InvalidArgumentException('Unsupported column type: ' . $column['type']); + } + + // null option + if (A::get($column, 'null') === false) { + $null = 'NOT NULL'; + } else { + $null = 'NULL'; + } + + // indexes/keys + if (isset($column['key']) === true) { + if (is_string($column['key']) === true) { + $column['key'] = strtolower($column['key']); + } elseif ($column['key'] === true) { + $column['key'] = $name . '_index'; + } + } + + // unique + $uniqueKey = false; + $uniqueColumn = null; + if (isset($column['unique']) === true && $column['unique'] === true) { + if (isset($column['key']) === true) { + // this column is part of an index, make that unique + $uniqueKey = true; + } else { + // make the column itself unique + $uniqueColumn = 'UNIQUE'; + } + } + + // default value + $columnDefault = $this->columnDefault($name, $column); + + $query = trim(Str::template($template, [ + 'name' => $this->quoteIdentifier($name), + 'null' => $null, + 'default' => $columnDefault['query'], + 'unique' => $uniqueColumn + ], '')); + + return [ + 'query' => $query, + 'bindings' => $columnDefault['bindings'], + 'key' => $column['key'] ?? null, + 'unique' => $uniqueKey + ]; + } + + /** + * Creates the inner query for the columns in a CREATE TABLE query + * + * @param array $columns Array of column definition arrays, see `Kirby\Database\Sql::createColumn()` + * @return array Array with a `query` string and `bindings`, `keys` and `unique` arrays + */ + public function createTableInner(array $columns): array + { + $query = []; + $bindings = []; + $keys = []; + $unique = []; + + foreach ($columns as $name => $column) { + $sql = $this->createColumn($name, $column); + + // collect query and bindings + $query[] = $sql['query']; + $bindings += $sql['bindings']; + + // make a list of keys per key name + if ($sql['key'] !== null) { + if (isset($keys[$sql['key']]) !== true) { + $keys[$sql['key']] = []; + } + + $keys[$sql['key']][] = $name; + if ($sql['unique'] === true) { + $unique[$sql['key']] = true; + } + } + } + + return [ + 'query' => implode(',' . PHP_EOL, $query), + 'bindings' => $bindings, + 'keys' => $keys, + 'unique' => $unique + ]; + } + + /** + * Creates a CREATE TABLE query + * + * @param string $table Table name + * @param array $columns Array of column definition arrays, see `Kirby\Database\Sql::createColumn()` + * @return array Array with a `query` string and a `bindings` array + */ + public function createTable(string $table, array $columns = []): array + { + $inner = $this->createTableInner($columns); + + // add keys + foreach ($inner['keys'] as $key => $columns) { + // quote each column name and make a list string out of the column names + $columns = implode(', ', array_map(function ($name) { + return $this->quoteIdentifier($name); + }, $columns)); + + if ($key === 'primary') { + $key = 'PRIMARY KEY'; + } else { + $unique = isset($inner['unique'][$key]) === true ? 'UNIQUE ' : ''; + $key = $unique . 'INDEX ' . $this->quoteIdentifier($key); + } + + $inner['query'] .= ',' . PHP_EOL . $key . ' (' . $columns . ')'; + } + + return [ + 'query' => 'CREATE TABLE ' . $this->quoteIdentifier($table) . ' (' . PHP_EOL . $inner['query'] . PHP_EOL . ')', + 'bindings' => $inner['bindings'] + ]; + } + + /** + * Builds a DELETE clause + * + * @param array $params List of parameters for the DELETE clause. See defaults for more info. + * @return array + */ + public function delete(array $params = []): array + { + $defaults = [ + 'table' => '', + 'where' => null, + 'bindings' => [] + ]; + + $options = array_merge($defaults, $params); + $bindings = $options['bindings']; + $query = ['DELETE']; + + // from + $this->extend($query, $bindings, $this->from($options['table'])); + + // where + $this->extend($query, $bindings, $this->where($options['where'])); + + return [ + 'query' => $this->query($query), + 'bindings' => $bindings + ]; + } + + /** + * Creates the sql for dropping a single table + * + * @param string $table + * @return array + */ + public function dropTable(string $table): array + { + return [ + 'query' => 'DROP TABLE ' . $this->tableName($table), + 'bindings' => [] + ]; + } + + /** + * Extends a given query and bindings + * by reference + * + * @param array $query + * @param array $bindings + * @param array $input + * @return void + */ + public function extend(&$query, array &$bindings = [], $input) + { + if (empty($input['query']) === false) { + $query[] = $input['query']; + $bindings = array_merge($bindings, $input['bindings']); + } + } + + /** + * Creates the from syntax + * + * @param string $table + * @return array + */ + public function from(string $table): array + { + return [ + 'query' => 'FROM ' . $this->tableName($table), + 'bindings' => [] + ]; + } + + /** + * Creates the group by syntax + * + * @param string $group + * @return array + */ + public function group(string $group = null): array + { + if (empty($group) === true) { + return [ + 'query' => null, + 'bindings' => [] + ]; + } + + return [ + 'query' => 'GROUP BY ' . $group, + 'bindings' => [] + ]; + } + + /** + * Creates the having syntax + * + * @param string|null $having + * @return array + */ + public function having(string $having = null): array + { + if (empty($having) === true) { + return [ + 'query' => null, + 'bindings' => [] + ]; + } + + return [ + 'query' => 'HAVING ' . $having, + 'bindings' => [] + ]; + } + + /** + * Creates an insert query + * + * @param array $params + * @return array + */ + public function insert(array $params = []): array + { + $table = $params['table'] ?? null; + $values = $params['values'] ?? null; + $bindings = $params['bindings']; + $query = ['INSERT INTO ' . $this->tableName($table)]; + + // add the values + $this->extend($query, $bindings, $this->values($table, $values, ', ', false)); + + return [ + 'query' => $this->query($query), + 'bindings' => $bindings + ]; + } + + /** + * Creates a join query + * + * @param string $table + * @param string $type + * @param string $on + * @return array + * @throws \Kirby\Exception\InvalidArgumentException if an invalid join type is given + */ + public function join(string $type, string $table, string $on): array + { + $types = [ + 'JOIN', + 'INNER JOIN', + 'OUTER JOIN', + 'LEFT OUTER JOIN', + 'LEFT JOIN', + 'RIGHT OUTER JOIN', + 'RIGHT JOIN', + 'FULL OUTER JOIN', + 'FULL JOIN', + 'NATURAL JOIN', + 'CROSS JOIN', + 'SELF JOIN' + ]; + + $type = strtoupper(trim($type)); + + // validate join type + if (in_array($type, $types) === false) { + throw new InvalidArgumentException('Invalid join type ' . $type); + } + + return [ + 'query' => $type . ' ' . $this->tableName($table) . ' ON ' . $on, + 'bindings' => [], + ]; + } + + /** + * Create the syntax for multiple joins + * + * @param array|null $joins + * @return array + */ + public function joins(array $joins = null): array + { + $query = []; + $bindings = []; + + foreach ((array)$joins as $join) { + $this->extend($query, $bindings, $this->join($join['type'] ?? 'JOIN', $join['table'] ?? null, $join['on'] ?? null)); + } + + return [ + 'query' => implode(' ', array_filter($query)), + 'bindings' => [], + ]; + } + + /** + * Creates a limit and offset query instruction + * + * @param int $offset + * @param int|null $limit + * @return array + */ + public function limit(int $offset = 0, int $limit = null): array + { + // no need to add it to the query + if ($offset === 0 && $limit === null) { + return [ + 'query' => null, + 'bindings' => [] + ]; + } + + $limit = $limit ?? '18446744073709551615'; + + $offsetBinding = $this->bindingName('offset'); + $limitBinding = $this->bindingName('limit'); + + return [ + 'query' => 'LIMIT ' . $offsetBinding . ', ' . $limitBinding, + 'bindings' => [ + $limitBinding => $limit, + $offsetBinding => $offset, + ] + ]; + } + + /** + * Creates the order by syntax + * + * @param string $order + * @return array + */ + public function order(string $order = null): array + { + if (empty($order) === true) { + return [ + 'query' => null, + 'bindings' => [] + ]; + } + + return [ + 'query' => 'ORDER BY ' . $order, + 'bindings' => [] + ]; + } + + /** + * Converts a query array into a final string + * + * @param array $query + * @param string $separator + * @return string + */ + public function query(array $query, string $separator = ' ') + { + return implode($separator, array_filter($query)); + } + + /** + * Quotes an identifier (table *or* column) + * + * @param $identifier string + * @return string + */ + public function quoteIdentifier(string $identifier): string + { + // * is special, don't quote that + if ($identifier === '*') { + return $identifier; + } + + // escape backticks inside the identifier name + $identifier = str_replace('`', '``', $identifier); + + // wrap in backticks + return '`' . $identifier . '`'; + } + + /** + * Builds a select clause + * + * @param array $params List of parameters for the select clause. Check out the defaults for more info. + * @return array An array with the query and the bindings + */ + public function select(array $params = []): array + { + $defaults = [ + 'table' => '', + 'columns' => '*', + 'join' => null, + 'distinct' => false, + 'where' => null, + 'group' => null, + 'having' => null, + 'order' => null, + 'offset' => 0, + 'limit' => null, + 'bindings' => [] + ]; + + $options = array_merge($defaults, $params); + $bindings = $options['bindings']; + $query = ['SELECT']; + + // select distinct values + if ($options['distinct'] === true) { + $query[] = 'DISTINCT'; + } + + // columns + $query[] = $this->selected($options['table'], $options['columns']); + + // from + $this->extend($query, $bindings, $this->from($options['table'])); + + // joins + $this->extend($query, $bindings, $this->joins($options['join'])); + + // where + $this->extend($query, $bindings, $this->where($options['where'])); + + // group + $this->extend($query, $bindings, $this->group($options['group'])); + + // having + $this->extend($query, $bindings, $this->having($options['having'])); + + // order + $this->extend($query, $bindings, $this->order($options['order'])); + + // offset and limit + $this->extend($query, $bindings, $this->limit($options['offset'], $options['limit'])); + + return [ + 'query' => $this->query($query), + 'bindings' => $bindings + ]; + } + + /** + * Creates a columns definition from string or array + * + * @param string $table + * @param array|string|null $columns + * @return string + */ + public function selected($table, $columns = null): string + { + // all columns + if (empty($columns) === true) { + return '*'; + } + + // array of columns + if (is_array($columns) === true) { + + // validate columns + $result = []; + + foreach ($columns as $column) { + list($table, $columnPart) = $this->splitIdentifier($table, $column); + + if ($this->validateColumn($table, $columnPart) === true) { + $result[] = $this->combineIdentifier($table, $columnPart); + } + } + + return implode(', ', $result); + } else { + return $columns; + } + } + + /** + * Splits a (qualified) identifier into table and column + * + * @param $table string Default table if the identifier is not qualified + * @param $identifier string + * @return array + * @throws \Kirby\Exception\InvalidArgumentException if an invalid identifier is given + */ + public function splitIdentifier($table, $identifier): array + { + // split by dot, but only outside of quotes + $parts = preg_split('/(?:`[^`]*`|"[^"]*")(*SKIP)(*F)|\./', $identifier); + + switch (count($parts)) { + // non-qualified identifier + case 1: + return [$table, $this->unquoteIdentifier($parts[0])]; + + // qualified identifier + case 2: + return [$this->unquoteIdentifier($parts[0]), $this->unquoteIdentifier($parts[1])]; + + // every other number is an error + default: + throw new InvalidArgumentException('Invalid identifier ' . $identifier); + } + } + + /** + * Returns a query to list the tables of the current database; + * the query needs to return rows with a column `name` + * + * @return array + */ + abstract public function tables(): array; + + /** + * Validates and quotes a table name + * + * @param string $table + * @return string + * @throws \Kirby\Exception\InvalidArgumentException if an invalid table name is given + */ + public function tableName(string $table): string + { + // validate table + if ($this->database->validateTable($table) === false) { + throw new InvalidArgumentException('Invalid table ' . $table); + } + + return $this->quoteIdentifier($table); + } + + /** + * Unquotes an identifier (table *or* column) + * + * @param $identifier string + * @return string + */ + public function unquoteIdentifier(string $identifier): string + { + // remove quotes around the identifier + if (in_array(Str::substr($identifier, 0, 1), ['"', '`']) === true) { + $identifier = Str::substr($identifier, 1); + } + + if (in_array(Str::substr($identifier, -1), ['"', '`']) === true) { + $identifier = Str::substr($identifier, 0, -1); + } + + // unescape duplicated quotes + return str_replace(['""', '``'], ['"', '`'], $identifier); + } + + /** + * Builds an update clause + * + * @param array $params List of parameters for the update clause. See defaults for more info. + * @return array + */ + public function update(array $params = []): array + { + $defaults = [ + 'table' => null, + 'values' => null, + 'where' => null, + 'bindings' => [] + ]; + + $options = array_merge($defaults, $params); + $bindings = $options['bindings']; + + // start the query + $query = ['UPDATE ' . $this->tableName($options['table']) . ' SET']; + + // add the values + $this->extend($query, $bindings, $this->values($options['table'], $options['values'])); + + // add the where clause + $this->extend($query, $bindings, $this->where($options['where'])); + + return [ + 'query' => $this->query($query), + 'bindings' => $bindings + ]; + } + + /** + * Validates a given column name in a table + * + * @param string $table + * @param string $column + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the column is invalid + */ + public function validateColumn(string $table, string $column): bool + { + if ($this->database->validateColumn($table, $column) !== true) { + throw new InvalidArgumentException('Invalid column ' . $column); + } + + return true; + } + + /** + * Builds a safe list of values for insert, select or update queries + * + * @param string $table Table name + * @param mixed $values A value string or array of values + * @param string $separator A separator which should be used to join values + * @param bool $set If true builds a set list of values for update clauses + * @param bool $enforceQualified Always use fully qualified column names + */ + public function values(string $table, $values, string $separator = ', ', bool $set = true, bool $enforceQualified = false): array + { + if (is_array($values) === false) { + return [ + 'query' => $values, + 'bindings' => [] + ]; + } + + if ($set === true) { + return $this->valueSet($table, $values, $separator, $enforceQualified); + } else { + return $this->valueList($table, $values, $separator, $enforceQualified); + } + } + + /** + * Creates a list of fields and values + * + * @param string $table + * @param string|array $values + * @param string $separator + * @param bool $enforceQualified + * @param array + */ + public function valueList(string $table, $values, string $separator = ',', bool $enforceQualified = false): array + { + $fields = []; + $query = []; + $bindings = []; + + foreach ($values as $key => $value) { + $fields[] = $this->columnName($table, $key, $enforceQualified); + + if (in_array($value, static::$literals, true) === true) { + $query[] = $value ?: 'null'; + continue; + } + + if (is_array($value) === true) { + $value = json_encode($value); + } + + // add the binding + $bindings[$bindingName = $this->bindingName('value')] = $value; + + // create the query + $query[] = $bindingName; + } + + return [ + 'query' => '(' . implode($separator, $fields) . ') VALUES (' . implode($separator, $query) . ')', + 'bindings' => $bindings + ]; + } + + /** + * Creates a set of values + * + * @param string $table + * @param string|array $values + * @param string $separator + * @param bool $enforceQualified + * @param array + * @return array + */ + public function valueSet(string $table, $values, string $separator = ',', bool $enforceQualified = false): array + { + $query = []; + $bindings = []; + + foreach ($values as $column => $value) { + $key = $this->columnName($table, $column, $enforceQualified); + + if (in_array($value, static::$literals, true) === true) { + $query[] = $key . ' = ' . ($value ?: 'null'); + continue; + } + + if (is_array($value) === true) { + $value = json_encode($value); + } + + // add the binding + $bindings[$bindingName = $this->bindingName('value')] = $value; + + // create the query + $query[] = $key . ' = ' . $bindingName; + } + + return [ + 'query' => implode($separator, $query), + 'bindings' => $bindings + ]; + } + + /** + * @param string|array|null $where + * @param array $bindings + * @return array + */ + public function where($where, array $bindings = []): array + { + if (empty($where) === true) { + return [ + 'query' => null, + 'bindings' => [], + ]; + } + + if (is_string($where) === true) { + return [ + 'query' => 'WHERE ' . $where, + 'bindings' => $bindings + ]; + } + + $query = []; + + foreach ($where as $key => $value) { + $binding = $this->bindingName('where_' . $key); + $bindings[$binding] = $value; + + $query[] = $key . ' = ' . $binding; + } + + return [ + 'query' => 'WHERE ' . implode(' AND ', $query), + 'bindings' => $bindings + ]; + } +} diff --git a/kirby/src/Database/Sql/Mysql.php b/kirby/src/Database/Sql/Mysql.php new file mode 100644 index 0000000..853a39b --- /dev/null +++ b/kirby/src/Database/Sql/Mysql.php @@ -0,0 +1,59 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Mysql extends Sql +{ + /** + * Returns a query to list the columns of a specified table; + * the query needs to return rows with a column `name` + * + * @param string $table Table name + * @return array + */ + public function columns(string $table): array + { + $databaseBinding = $this->bindingName('database'); + $tableBinding = $this->bindingName('table'); + + $query = 'SELECT COLUMN_NAME AS name FROM INFORMATION_SCHEMA.COLUMNS '; + $query .= 'WHERE TABLE_SCHEMA = ' . $databaseBinding . ' AND TABLE_NAME = ' . $tableBinding; + + return [ + 'query' => $query, + 'bindings' => [ + $databaseBinding => $this->database->name(), + $tableBinding => $table, + ] + ]; + } + + /** + * Returns a query to list the tables of the current database; + * the query needs to return rows with a column `name` + * + * @return array + */ + public function tables(): array + { + $binding = $this->bindingName('database'); + + return [ + 'query' => 'SELECT TABLE_NAME AS name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ' . $binding, + 'bindings' => [ + $binding => $this->database->name() + ] + ]; + } +} diff --git a/kirby/src/Database/Sql/Sqlite.php b/kirby/src/Database/Sql/Sqlite.php new file mode 100644 index 0000000..1c691ed --- /dev/null +++ b/kirby/src/Database/Sql/Sqlite.php @@ -0,0 +1,143 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Sqlite extends Sql +{ + /** + * Returns a query to list the columns of a specified table; + * the query needs to return rows with a column `name` + * + * @param string $table Table name + * @return array + */ + public function columns(string $table): array + { + return [ + 'query' => 'PRAGMA table_info(' . $this->tableName($table) . ')', + 'bindings' => [], + ]; + } + + /** + * Abstracted column types to simplify table + * creation for multiple database drivers + * @codeCoverageIgnore + * + * @return array + */ + public function columnTypes(): array + { + return [ + 'id' => '{{ name }} INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE', + 'varchar' => '{{ name }} TEXT {{ null }} {{ default }} {{ unique }}', + 'text' => '{{ name }} TEXT {{ null }} {{ default }} {{ unique }}', + 'int' => '{{ name }} INTEGER {{ null }} {{ default }} {{ unique }}', + 'timestamp' => '{{ name }} INTEGER {{ null }} {{ default }} {{ unique }}' + ]; + } + + /** + * Combines an identifier (table and column) + * + * @param $table string + * @param $column string + * @param $values boolean Whether the identifier is going to be used for a VALUES clause; + * only relevant for SQLite + * @return string + */ + public function combineIdentifier(string $table, string $column, bool $values = false): string + { + // SQLite doesn't support qualified column names for VALUES clauses + if ($values === true) { + return $this->quoteIdentifier($column); + } + + return $this->quoteIdentifier($table) . '.' . $this->quoteIdentifier($column); + } + + /** + * Creates a CREATE TABLE query + * + * @param string $table Table name + * @param array $columns Array of column definition arrays, see `Kirby\Database\Sql::createColumn()` + * @return array Array with a `query` string and a `bindings` array + */ + public function createTable(string $table, array $columns = []): array + { + $inner = $this->createTableInner($columns); + + // add keys + $keys = []; + foreach ($inner['keys'] as $key => $columns) { + // quote each column name and make a list string out of the column names + $columns = implode(', ', array_map(function ($name) { + return $this->quoteIdentifier($name); + }, $columns)); + + if ($key === 'primary') { + $inner['query'] .= ',' . PHP_EOL . 'PRIMARY KEY (' . $columns . ')'; + } else { + // SQLite only supports index creation using a separate CREATE INDEX query + $unique = isset($inner['unique'][$key]) === true ? 'UNIQUE ' : ''; + $keys[] = 'CREATE ' . $unique . 'INDEX ' . $this->quoteIdentifier($table . '_index_' . $key) . + ' ON ' . $this->quoteIdentifier($table) . ' (' . $columns . ')'; + } + } + + $query = 'CREATE TABLE ' . $this->quoteIdentifier($table) . ' (' . PHP_EOL . $inner['query'] . PHP_EOL . ')'; + if (empty($keys) === false) { + $query .= ';' . PHP_EOL . implode(';' . PHP_EOL, $keys); + } + + return [ + 'query' => $query, + 'bindings' => $inner['bindings'] + ]; + } + + /** + * Quotes an identifier (table *or* column) + * + * @param $identifier string + * @return string + */ + public function quoteIdentifier(string $identifier): string + { + // * is special + if ($identifier === '*') { + return $identifier; + } + + // escape quotes inside the identifier name + $identifier = str_replace('"', '""', $identifier); + + // wrap in quotes + return '"' . $identifier . '"'; + } + + /** + * Returns a query to list the tables of the current database; + * the query needs to return rows with a column `name` + * + * @return string + */ + public function tables(): array + { + return [ + 'query' => 'SELECT name FROM sqlite_master WHERE type = "table"', + 'bindings' => [] + ]; + } +} diff --git a/kirby/src/Email/Body.php b/kirby/src/Email/Body.php new file mode 100644 index 0000000..2eeb05c --- /dev/null +++ b/kirby/src/Email/Body.php @@ -0,0 +1,85 @@ +, + * Nico Hoffmann + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Body +{ + use Properties; + + /** + * @var string|null + */ + protected $html; + + /** + * @var string|null + */ + protected $text; + + /** + * Email body constructor + * + * @param array $props + */ + public function __construct(array $props = []) + { + $this->setProperties($props); + } + + /** + * Returns the HTML content of the email body + * + * @return string|null + */ + public function html() + { + return $this->html; + } + + /** + * Returns the plain text content of the email body + * + * @return string|null + */ + public function text() + { + return $this->text; + } + + /** + * Sets the HTML content for the email body + * + * @param string|null $html + * @return self + */ + protected function setHtml(string $html = null) + { + $this->html = $html; + return $this; + } + + /** + * Sets the plain text content for the email body + * + * @param string|null $text + * @return self + */ + protected function setText(string $text = null) + { + $this->text = $text; + return $this; + } +} diff --git a/kirby/src/Email/Email.php b/kirby/src/Email/Email.php new file mode 100644 index 0000000..1680369 --- /dev/null +++ b/kirby/src/Email/Email.php @@ -0,0 +1,452 @@ +, + * Nico Hoffmann + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Email +{ + use Properties; + + /** + * @var array|null + */ + protected $attachments; + + /** + * @var \Kirby\Email\Body|null + */ + protected $body; + + /** + * @var array|null + */ + protected $bcc; + + /** + * @var \Closure|null + */ + protected $beforeSend; + + /** + * @var array|null + */ + protected $cc; + + /** + * @var string|null + */ + protected $from; + + /** + * @var string|null + */ + protected $fromName; + + /** + * @var string|null + */ + protected $replyTo; + + /** + * @var string|null + */ + protected $replyToName; + + /** + * @var bool + */ + protected $isSent = false; + + /** + * @var string|null + */ + protected $subject; + + /** + * @var array|null + */ + protected $to; + + /** + * @var array|null + */ + protected $transport; + + /** + * Email constructor + * + * @param array $props + * @param bool $debug + */ + public function __construct(array $props = [], bool $debug = false) + { + $this->setProperties($props); + + if ($debug === false) { + $this->send(); // @codeCoverageIgnore + } + } + + /** + * Returns the email attachments + * + * @return array + */ + public function attachments(): array + { + return $this->attachments; + } + + /** + * Returns the email body + * + * @return \Kirby\Email\Body|null + */ + public function body() + { + return $this->body; + } + + /** + * Returns "bcc" recipients + * + * @return array + */ + public function bcc(): array + { + return $this->bcc; + } + + /** + * Returns the beforeSend callback closure, + * which has access to the PHPMailer instance + * + * @return \Closure|null + */ + public function beforeSend(): ?Closure + { + return $this->beforeSend; + } + + /** + * Returns "cc" recipients + * + * @return array + */ + public function cc(): array + { + return $this->cc; + } + + /** + * Returns default transport settings + * + * @return array + */ + protected function defaultTransport(): array + { + return [ + 'type' => 'mail' + ]; + } + + /** + * Returns the "from" email address + * + * @return string + */ + public function from(): string + { + return $this->from; + } + + /** + * Returns the "from" name + * + * @return string|null + */ + public function fromName(): ?string + { + return $this->fromName; + } + + /** + * Checks if the email has an HTML body + * + * @return bool + */ + public function isHtml() + { + return $this->body()->html() !== null; + } + + /** + * Checks if the email has been sent successfully + * + * @return bool + */ + public function isSent(): bool + { + return $this->isSent; + } + + /** + * Returns the "reply to" email address + * + * @return string + */ + public function replyTo(): string + { + return $this->replyTo; + } + + /** + * Returns the "reply to" name + * + * @return string|null + */ + public function replyToName(): ?string + { + return $this->replyToName; + } + + /** + * Converts single or multiple email addresses to a sanitized format + * + * @param string|array|null $email + * @param bool $multiple + * @return array|mixed|string + * @throws \Exception + */ + protected function resolveEmail($email = null, bool $multiple = true) + { + if ($email === null) { + return $multiple === true ? [] : ''; + } + + if (is_array($email) === false) { + $email = [$email => null]; + } + + $result = []; + foreach ($email as $address => $name) { + // convert simple email arrays to associative arrays + if (is_int($address) === true) { + // the value is the address, there is no name + $address = $name; + $result[$address] = null; + } else { + $result[$address] = $name; + } + + // ensure that the address is valid + if (V::email($address) === false) { + throw new Exception(sprintf('"%s" is not a valid email address', $address)); + } + } + + return $multiple === true ? $result : array_keys($result)[0]; + } + + /** + * Sends the email + * + * @return bool + */ + public function send(): bool + { + return $this->isSent = true; + } + + /** + * Sets the email attachments + * + * @param array|null $attachments + * @return self + */ + protected function setAttachments($attachments = null) + { + $this->attachments = $attachments ?? []; + return $this; + } + + /** + * Sets the email body + * + * @param string|array $body + * @return self + */ + protected function setBody($body) + { + if (is_string($body) === true) { + $body = ['text' => $body]; + } + + $this->body = new Body($body); + return $this; + } + + /** + * Sets "bcc" recipients + * + * @param string|array|null $bcc + * @return $this + */ + protected function setBcc($bcc = null) + { + $this->bcc = $this->resolveEmail($bcc); + return $this; + } + + /** + * Sets the "beforeSend" callback + * + * @param \Closure|null $beforeSend + * @return self + */ + protected function setBeforeSend(?Closure $beforeSend = null) + { + $this->beforeSend = $beforeSend; + return $this; + } + + /** + * Sets "cc" recipients + * + * @param string|array|null $cc + * @return self + */ + protected function setCc($cc = null) + { + $this->cc = $this->resolveEmail($cc); + return $this; + } + + /** + * Sets the "from" email address + * + * @param string $from + * @return self + */ + protected function setFrom(string $from) + { + $this->from = $this->resolveEmail($from, false); + return $this; + } + + /** + * Sets the "from" name + * + * @param string|null $fromName + * @return self + */ + protected function setFromName(string $fromName = null) + { + $this->fromName = $fromName; + return $this; + } + + /** + * Sets the "reply to" email address + * + * @param string|null $replyTo + * @return self + */ + protected function setReplyTo(string $replyTo = null) + { + $this->replyTo = $this->resolveEmail($replyTo, false); + return $this; + } + + /** + * Sets the "reply to" name + * + * @param string|null $replyToName + * @return self + */ + protected function setReplyToName(string $replyToName = null) + { + $this->replyToName = $replyToName; + return $this; + } + + /** + * Sets the email subject + * + * @param string $subject + * @return self + */ + protected function setSubject(string $subject) + { + $this->subject = $subject; + return $this; + } + + /** + * Sets the recipients of the email + * + * @param string|array $to + * @return self + */ + protected function setTo($to) + { + $this->to = $this->resolveEmail($to); + return $this; + } + + /** + * Sets the email transport settings + * + * @param array|null $transport + * @return self + */ + protected function setTransport($transport = null) + { + $this->transport = $transport; + return $this; + } + + /** + * Returns the email subject + * + * @return string + */ + public function subject(): string + { + return $this->subject; + } + + /** + * Returns the email recipients + * + * @return array + */ + public function to(): array + { + return $this->to; + } + + /** + * Returns the email transports settings + * + * @return array + */ + public function transport(): array + { + return $this->transport ?? $this->defaultTransport(); + } +} diff --git a/kirby/src/Email/PHPMailer.php b/kirby/src/Email/PHPMailer.php new file mode 100644 index 0000000..18df204 --- /dev/null +++ b/kirby/src/Email/PHPMailer.php @@ -0,0 +1,95 @@ +, + * Nico Hoffmann + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class PHPMailer extends Email +{ + /** + * Sends email via PHPMailer library + * + * @param bool $debug + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function send(bool $debug = false): bool + { + $mailer = new Mailer(true); + + // set sender's address + $mailer->setFrom($this->from(), $this->fromName() ?? ''); + + // optional reply-to address + if ($replyTo = $this->replyTo()) { + $mailer->addReplyTo($replyTo, $this->replyToName() ?? ''); + } + + // add (multiple) recepient, CC & BCC addresses + foreach ($this->to() as $email => $name) { + $mailer->addAddress($email, $name ?? ''); + } + foreach ($this->cc() as $email => $name) { + $mailer->addCC($email, $name ?? ''); + } + foreach ($this->bcc() as $email => $name) { + $mailer->addBCC($email, $name ?? ''); + } + + $mailer->Subject = $this->subject(); + $mailer->CharSet = 'UTF-8'; + + // set body according to html/text + if ($this->isHtml()) { + $mailer->isHTML(true); + $mailer->Body = $this->body()->html(); + $mailer->AltBody = $this->body()->text(); + } else { + $mailer->Body = $this->body()->text(); + } + + // add attachments + foreach ($this->attachments() as $attachment) { + $mailer->addAttachment($attachment); + } + + // smtp transport settings + if (($this->transport()['type'] ?? 'mail') === 'smtp') { + $mailer->isSMTP(); + $mailer->Host = $this->transport()['host'] ?? null; + $mailer->SMTPAuth = $this->transport()['auth'] ?? false; + $mailer->Username = $this->transport()['username'] ?? null; + $mailer->Password = $this->transport()['password'] ?? null; + $mailer->SMTPSecure = $this->transport()['security'] ?? 'ssl'; + $mailer->Port = $this->transport()['port'] ?? null; + } + + // accessible phpMailer instance + $beforeSend = $this->beforeSend(); + + if (empty($beforeSend) === false && is_a($beforeSend, 'Closure') === true) { + $mailer = $beforeSend->call($this, $mailer) ?? $mailer; + + if (is_a($mailer, 'PHPMailer\PHPMailer\PHPMailer') === false) { + throw new InvalidArgumentException('"beforeSend" option return should be instance of PHPMailer\PHPMailer\PHPMailer class'); + } + } + + if ($debug === true) { + return $this->isSent = true; + } + + return $this->isSent = $mailer->send(); // @codeCoverageIgnore + } +} diff --git a/kirby/src/Exception/BadMethodCallException.php b/kirby/src/Exception/BadMethodCallException.php new file mode 100644 index 0000000..1be6fb7 --- /dev/null +++ b/kirby/src/Exception/BadMethodCallException.php @@ -0,0 +1,21 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class BadMethodCallException extends Exception +{ + protected static $defaultKey = 'invalidMethod'; + protected static $defaultFallback = 'The method "{ method }" does not exist'; + protected static $defaultHttpCode = 400; + protected static $defaultData = ['method' => null]; +} diff --git a/kirby/src/Exception/DuplicateException.php b/kirby/src/Exception/DuplicateException.php new file mode 100644 index 0000000..7684c4c --- /dev/null +++ b/kirby/src/Exception/DuplicateException.php @@ -0,0 +1,21 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class DuplicateException extends Exception +{ + protected static $defaultKey = 'duplicate'; + protected static $defaultFallback = 'The entry exists'; + protected static $defaultHttpCode = 400; +} diff --git a/kirby/src/Exception/ErrorPageException.php b/kirby/src/Exception/ErrorPageException.php new file mode 100644 index 0000000..1c1532d --- /dev/null +++ b/kirby/src/Exception/ErrorPageException.php @@ -0,0 +1,21 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class ErrorPageException extends Exception +{ + protected static $defaultKey = 'errorPage'; + protected static $defaultFallback = 'Triggered error page'; + protected static $defaultHttpCode = 404; +} diff --git a/kirby/src/Exception/Exception.php b/kirby/src/Exception/Exception.php new file mode 100644 index 0000000..f6bc588 --- /dev/null +++ b/kirby/src/Exception/Exception.php @@ -0,0 +1,221 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Exception extends \Exception +{ + /** + * Data variables that can be used inside the exception message + * + * @var array + */ + protected $data; + + /** + * HTTP code that corresponds with the exception + * + * @var int + */ + protected $httpCode; + + /** + * Additional details that are not included in the exception message + * + * @var array + */ + protected $details; + + /** + * Whether the exception message could be translated into the user's language + * + * @var bool + */ + protected $isTranslated = true; + + /** + * Defaults that can be overridden by specific + * exception classes + */ + protected static $defaultKey = 'general'; + protected static $defaultFallback = 'An error occurred'; + protected static $defaultData = []; + protected static $defaultHttpCode = 500; + protected static $defaultDetails = []; + + /** + * Prefix for the exception key (e.g. 'error.general') + * + * @var string + */ + private static $prefix = 'error'; + + /** + * Class constructor + * + * @param array|string $args Full option array ('key', 'translate', 'fallback', + * 'data', 'httpCode', 'details' and 'previous') or + * just the message string + */ + public function __construct($args = []) + { + // set data and httpCode from provided arguments or defaults + $this->data = $args['data'] ?? static::$defaultData; + $this->httpCode = $args['httpCode'] ?? static::$defaultHttpCode; + $this->details = $args['details'] ?? static::$defaultDetails; + + // define the Exception key + $key = self::$prefix . '.' . ($args['key'] ?? static::$defaultKey); + + if (is_string($args) === true) { + $this->isTranslated = false; + parent::__construct($args); + } else { + // define whether message can/should be translated + $translate = ($args['translate'] ?? true) === true && class_exists('Kirby\Cms\App') === true; + + // fallback waterfall for message string + $message = null; + + if ($translate) { + // 1. translation for provided key in current language + // 2. translation for provided key in default language + if (isset($args['key']) === true) { + $message = I18n::translate(self::$prefix . '.' . $args['key']); + $this->isTranslated = true; + } + } + + // 3. provided fallback message + if ($message === null) { + $message = $args['fallback'] ?? null; + $this->isTranslated = false; + } + + if ($translate) { + // 4. translation for default key in current language + // 5. translation for default key in default language + if ($message === null) { + $message = I18n::translate(self::$prefix . '.' . static::$defaultKey); + $this->isTranslated = true; + } + } + + // 6. default fallback message + if ($message === null) { + $message = static::$defaultFallback; + $this->isTranslated = false; + } + + // format message with passed data + $message = Str::template($message, $this->data, '-', '{', '}'); + + // handover to Exception parent class constructor + parent::__construct($message, null, $args['previous'] ?? null); + } + + // set the Exception code to the key + $this->code = $key; + } + + /** + * Returns the file in which the Exception was created + * relative to the document root + * + * @return string + */ + final public function getFileRelative(): string + { + $file = $this->getFile(); + + if (empty($_SERVER['DOCUMENT_ROOT']) === false) { + $file = ltrim(Str::after($file, $_SERVER['DOCUMENT_ROOT']), '/'); + } + + return $file; + } + + /** + * Returns the data variables from the message + * + * @return array + */ + final public function getData(): array + { + return $this->data; + } + + /** + * Returns the additional details that are + * not included in the message + * + * @return array + */ + final public function getDetails(): array + { + return $this->details; + } + + /** + * Returns the exception key (error type) + * + * @return string + */ + final public function getKey(): string + { + return $this->getCode(); + } + + /** + * Returns the HTTP code that corresponds + * with the exception + * + * @return array + */ + final public function getHttpCode(): int + { + return $this->httpCode; + } + + /** + * Returns whether the exception message could + * be translated into the user's language + * + * @return bool + */ + final public function isTranslated(): bool + { + return $this->isTranslated; + } + + /** + * Converts the object to an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'exception' => static::class, + 'message' => $this->getMessage(), + 'key' => $this->getKey(), + 'file' => $this->getFileRelative(), + 'line' => $this->getLine(), + 'details' => $this->getDetails(), + 'code' => $this->getHttpCode() + ]; + } +} diff --git a/kirby/src/Exception/InvalidArgumentException.php b/kirby/src/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..e536f4f --- /dev/null +++ b/kirby/src/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class InvalidArgumentException extends Exception +{ + protected static $defaultKey = 'invalidArgument'; + protected static $defaultFallback = 'Invalid argument "{ argument }" in method "{ method }"'; + protected static $defaultHttpCode = 400; + protected static $defaultData = ['argument' => null, 'method' => null]; +} diff --git a/kirby/src/Exception/LogicException.php b/kirby/src/Exception/LogicException.php new file mode 100644 index 0000000..7d9ac20 --- /dev/null +++ b/kirby/src/Exception/LogicException.php @@ -0,0 +1,20 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class LogicException extends Exception +{ + protected static $defaultKey = 'logic'; + protected static $defaultFallback = 'This task cannot be finished'; + protected static $defaultHttpCode = 400; +} diff --git a/kirby/src/Exception/NotFoundException.php b/kirby/src/Exception/NotFoundException.php new file mode 100644 index 0000000..af08c04 --- /dev/null +++ b/kirby/src/Exception/NotFoundException.php @@ -0,0 +1,20 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class NotFoundException extends Exception +{ + protected static $defaultKey = 'notFound'; + protected static $defaultFallback = 'Not found'; + protected static $defaultHttpCode = 404; +} diff --git a/kirby/src/Exception/PermissionException.php b/kirby/src/Exception/PermissionException.php new file mode 100644 index 0000000..b53a053 --- /dev/null +++ b/kirby/src/Exception/PermissionException.php @@ -0,0 +1,21 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class PermissionException extends Exception +{ + protected static $defaultKey = 'permission'; + protected static $defaultFallback = 'You are not allowed to do this'; + protected static $defaultHttpCode = 403; +} diff --git a/kirby/src/Form/Field.php b/kirby/src/Form/Field.php new file mode 100644 index 0000000..a2dd56b --- /dev/null +++ b/kirby/src/Form/Field.php @@ -0,0 +1,477 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Field extends Component +{ + /** + * An array of all found errors + * + * @var array|null + */ + protected $errors; + + /** + * Parent collection with all fields of the current form + * + * @var \Kirby\Form\Fields|null + */ + protected $formFields; + + /** + * Registry for all component mixins + * + * @var array + */ + public static $mixins = []; + + /** + * Registry for all component types + * + * @var array + */ + public static $types = []; + + /** + * Field constructor + * + * @param string $type + * @param array $attrs + * @param \Kirby\Form\Fields|null $formFields + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function __construct(string $type, array $attrs = [], ?Fields $formFields = null) + { + if (isset(static::$types[$type]) === false) { + throw new InvalidArgumentException('The field type "' . $type . '" does not exist'); + } + + if (isset($attrs['model']) === false) { + throw new InvalidArgumentException('Field requires a model'); + } + + $this->formFields = $formFields; + + // use the type as fallback for the name + $attrs['name'] = $attrs['name'] ?? $type; + $attrs['type'] = $type; + + parent::__construct($type, $attrs); + } + + /** + * Returns field api call + * + * @return mixed + */ + public function api() + { + if (isset($this->options['api']) === true && is_callable($this->options['api']) === true) { + return $this->options['api']->call($this); + } + } + + /** + * Returns field data + * + * @param bool $default + * @return mixed + */ + public function data(bool $default = false) + { + $save = $this->options['save'] ?? true; + + if ($default === true && $this->isEmpty($this->value)) { + $value = $this->default(); + } else { + $value = $this->value; + } + + if ($save === false) { + return null; + } elseif (is_callable($save) === true) { + return $save->call($this, $value); + } else { + return $value; + } + } + + /** + * Default props and computed of the field + * + * @return array + */ + public static function defaults(): array + { + return [ + 'props' => [ + /** + * Optional text that will be shown after the input + */ + 'after' => function ($after = null) { + return I18n::translate($after, $after); + }, + /** + * Sets the focus on this field when the form loads. Only the first field with this label gets + */ + 'autofocus' => function (bool $autofocus = null): bool { + return $autofocus ?? false; + }, + /** + * Optional text that will be shown before the input + */ + 'before' => function ($before = null) { + return I18n::translate($before, $before); + }, + /** + * Default value for the field, which will be used when a page/file/user is created + */ + 'default' => function ($default = null) { + return $default; + }, + /** + * If `true`, the field is no longer editable and will not be saved + */ + 'disabled' => function (bool $disabled = null): bool { + return $disabled ?? false; + }, + /** + * Optional help text below the field + */ + 'help' => function ($help = null) { + return I18n::translate($help, $help); + }, + /** + * Optional icon that will be shown at the end of the field + */ + 'icon' => function (string $icon = null) { + return $icon; + }, + /** + * The field label can be set as string or associative array with translations + */ + 'label' => function ($label = null) { + return I18n::translate($label, $label); + }, + /** + * Optional placeholder value that will be shown when the field is empty + */ + 'placeholder' => function ($placeholder = null) { + return I18n::translate($placeholder, $placeholder); + }, + /** + * If `true`, the field has to be filled in correctly to be saved. + */ + 'required' => function (bool $required = null): bool { + return $required ?? false; + }, + /** + * If `false`, the field will be disabled in non-default languages and cannot be translated. This is only relevant in multi-language setups. + */ + 'translate' => function (bool $translate = true): bool { + return $translate; + }, + /** + * Conditions when the field will be shown (since 3.1.0) + */ + 'when' => function ($when = null) { + return $when; + }, + /** + * The width of the field in the field grid. Available widths: `1/1`, `1/2`, `1/3`, `1/4`, `2/3`, `3/4` + */ + 'width' => function (string $width = '1/1') { + return $width; + }, + 'value' => function ($value = null) { + return $value; + } + ], + 'computed' => [ + 'after' => function () { + if ($this->after !== null) { + return $this->model()->toString($this->after); + } + }, + 'before' => function () { + if ($this->before !== null) { + return $this->model()->toString($this->before); + } + }, + 'default' => function () { + if ($this->default === null) { + return; + } + + if (is_string($this->default) === false) { + return $this->default; + } + + return $this->model()->toString($this->default); + }, + 'help' => function () { + if ($this->help) { + $help = $this->model()->toString($this->help); + $help = $this->kirby()->kirbytext($help); + return $help; + } + }, + 'label' => function () { + if ($this->label !== null) { + return $this->model()->toString($this->label); + } + }, + 'placeholder' => function () { + if ($this->placeholder !== null) { + return $this->model()->toString($this->placeholder); + } + } + ] + ]; + } + + /** + * Parent collection with all fields of the current form + * + * @return \Kirby\Form\Fields|null + */ + public function formFields(): ?Fields + { + return $this->formFields; + } + + /** + * Validates when run for the first time and returns any errors + * + * @return array + */ + public function errors(): array + { + if ($this->errors === null) { + $this->validate(); + } + + return $this->errors; + } + + /** + * Checks if the field is empty + * + * @param mixed ...$args + * @return bool + */ + public function isEmpty(...$args): bool + { + if (count($args) === 0) { + $value = $this->value(); + } else { + $value = $args[0]; + } + + if (isset($this->options['isEmpty']) === true) { + return $this->options['isEmpty']->call($this, $value); + } + + return in_array($value, [null, '', []], true); + } + + /** + * Checks if the field is invalid + * + * @return bool + */ + public function isInvalid(): bool + { + return empty($this->errors()) === false; + } + + /** + * Checks if the field is required + * + * @return bool + */ + public function isRequired(): bool + { + return $this->required ?? false; + } + + /** + * Checks if the field is valid + * + * @return bool + */ + public function isValid(): bool + { + return empty($this->errors()) === true; + } + + /** + * Returns the Kirby instance + * + * @return \Kirby\Cms\App + */ + public function kirby() + { + return $this->model->kirby(); + } + + /** + * Returns the parent model + * + * @return mixed|null + */ + public function model() + { + return $this->model; + } + + /** + * Checks if the field needs a value before being saved; + * this is the case if all of the following requirements are met: + * - The field is saveable + * - The field is required + * - The field is currently empty + * - The field is not currently inactive because of a `when` rule + * + * @return bool + */ + protected function needsValue(): bool + { + // check simple conditions first + if ($this->save() === false || $this->isRequired() === false || $this->isEmpty() === false) { + return false; + } + + // check the data of the relevant fields if there is a `when` option + if (empty($this->when) === false && is_array($this->when) === true) { + $formFields = $this->formFields(); + + if ($formFields !== null) { + foreach ($this->when as $field => $value) { + $field = $formFields->get($field); + $inputValue = $field !== null ? $field->value() : ''; + + // if the input data doesn't match the requested `when` value, + // that means that this field is not required and can be saved + // (*all* `when` conditions must be met for this field to be required) + if ($inputValue !== $value) { + return false; + } + } + } + } + + // either there was no `when` condition or all conditions matched + return true; + } + + /** + * Checks if the field is saveable + * + * @return bool + */ + public function save(): bool + { + return ($this->options['save'] ?? true) !== false; + } + + /** + * Converts the field to a plain array + * + * @return array + */ + public function toArray(): array + { + $array = parent::toArray(); + + unset($array['model']); + + $array['errors'] = $this->errors(); + $array['invalid'] = $this->isInvalid(); + $array['saveable'] = $this->save(); + $array['signature'] = md5(json_encode($array)); + + ksort($array); + + return array_filter($array, function ($item) { + return $item !== null; + }); + } + + /** + * Runs the validations defined for the field + * + * @return void + */ + protected function validate(): void + { + $validations = $this->options['validations'] ?? []; + $this->errors = []; + + // validate required values + if ($this->needsValue() === true) { + $this->errors['required'] = I18n::translate('error.validation.required'); + } + + foreach ($validations as $key => $validation) { + if (is_int($key) === true) { + // predefined validation + try { + Validations::$validation($this, $this->value()); + } catch (Exception $e) { + $this->errors[$validation] = $e->getMessage(); + } + continue; + } + + if (is_a($validation, 'Closure') === true) { + try { + $validation->call($this, $this->value()); + } catch (Exception $e) { + $this->errors[$key] = $e->getMessage(); + } + } + } + + if ( + empty($this->validate) === false && + ($this->isEmpty() === false || $this->isRequired() === true) + ) { + $rules = A::wrap($this->validate); + $errors = V::errors($this->value(), $rules); + + if (empty($errors) === false) { + $this->errors = array_merge($this->errors, $errors); + } + } + } + + /** + * Returns the value of the field if saveable + * otherwise it returns null + * + * @return mixed + */ + public function value() + { + return $this->save() ? $this->value : null; + } +} diff --git a/kirby/src/Form/Fields.php b/kirby/src/Form/Fields.php new file mode 100644 index 0000000..eb5b7bc --- /dev/null +++ b/kirby/src/Form/Fields.php @@ -0,0 +1,57 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Fields extends Collection +{ + /** + * Internal setter for each object in the Collection. + * This takes care of validation and of setting + * the collection prop on each object correctly. + * + * @param string $name + * @param object|array $field + * @return self + */ + public function __set(string $name, $field) + { + if (is_array($field) === true) { + // use the array key as name if the name is not set + $field['name'] = $field['name'] ?? $name; + $field = new Field($field['type'], $field); + } + + return parent::__set($field->name(), $field); + } + + /** + * Converts the fields collection to an + * array and also does that for every + * included field. + * + * @param \Closure|null $map + * @return array + */ + public function toArray(Closure $map = null): array + { + $array = []; + + foreach ($this as $field) { + $array[$field->name()] = $field->toArray(); + } + + return $array; + } +} diff --git a/kirby/src/Form/Form.php b/kirby/src/Form/Form.php new file mode 100644 index 0000000..f8711d0 --- /dev/null +++ b/kirby/src/Form/Form.php @@ -0,0 +1,270 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Form +{ + /** + * An array of all found errors + * + * @var array|null + */ + protected $errors; + + /** + * Fields in the form + * + * @var \Kirby\Form\Fields|null + */ + protected $fields; + + /** + * All values of form + * + * @var array + */ + protected $values = []; + + /** + * Form constructor + * + * @param array $props + */ + public function __construct(array $props) + { + $fields = $props['fields'] ?? []; + $values = $props['values'] ?? []; + $input = $props['input'] ?? []; + $strict = $props['strict'] ?? false; + $inject = $props; + + // lowercase all value names + $values = array_change_key_case($values); + $input = array_change_key_case($input); + + unset($inject['fields'], $inject['values'], $inject['input']); + + $this->fields = new Fields(); + $this->values = []; + + foreach ($fields as $name => $props) { + + // inject stuff from the form constructor (model, etc.) + $props = array_merge($inject, $props); + + // inject the name + $props['name'] = $name = strtolower($name); + + // check if the field is disabled + $disabled = $props['disabled'] ?? false; + + // overwrite the field value if not set + if ($disabled === true) { + $props['value'] = $values[$name] ?? null; + } else { + $props['value'] = $input[$name] ?? $values[$name] ?? null; + } + + try { + $field = new Field($props['type'], $props, $this->fields); + } catch (Throwable $e) { + $field = static::exceptionField($e, $props); + } + + if ($field->save() !== false) { + $this->values[$name] = $field->value(); + } + + $this->fields->append($name, $field); + } + + if ($strict !== true) { + + // use all given values, no matter + // if there's a field or not. + $input = array_merge($values, $input); + + foreach ($input as $key => $value) { + if (isset($this->values[$key]) === false) { + $this->values[$key] = $value; + } + } + } + } + + /** + * Returns the data required to write to the content file + * Doesn't include default and null values + * + * @return array + */ + public function content(): array + { + return $this->data(false, false); + } + + /** + * Returns data for all fields in the form + * + * @param false $defaults + * @param bool $includeNulls + * @return array + */ + public function data($defaults = false, bool $includeNulls = true): array + { + $data = $this->values; + + foreach ($this->fields as $field) { + if ($field->save() === false || $field->unset() === true) { + if ($includeNulls === true) { + $data[$field->name()] = null; + } else { + unset($data[$field->name()]); + } + } else { + $data[$field->name()] = $field->data($defaults); + } + } + + return $data; + } + + /** + * An array of all found errors + * + * @return array + */ + public function errors(): array + { + if ($this->errors !== null) { + return $this->errors; + } + + $this->errors = []; + + foreach ($this->fields as $field) { + if (empty($field->errors()) === false) { + $this->errors[$field->name()] = [ + 'label' => $field->label(), + 'message' => $field->errors() + ]; + } + } + + return $this->errors; + } + + /** + * Shows the error with the field + * + * @param \Throwable $exception + * @param array $props + * @return \Kirby\Form\Field + */ + public static function exceptionField(Throwable $exception, array $props = []) + { + $props = array_merge($props, [ + 'label' => 'Error in "' . $props['name'] . '" field', + 'theme' => 'negative', + 'text' => strip_tags($exception->getMessage()), + ]); + + return new Field('info', $props); + } + + /** + * Returns form fields + * + * @return \Kirby\Form\Fields|null + */ + public function fields() + { + return $this->fields; + } + + /** + * Checks if the form is invalid + * + * @return bool + */ + public function isInvalid(): bool + { + return empty($this->errors()) === false; + } + + /** + * Checks if the form is valid + * + * @return bool + */ + public function isValid(): bool + { + return empty($this->errors()) === true; + } + + /** + * Converts the data of fields to strings + * + * @param false $defaults + * @return array + */ + public function strings($defaults = false): array + { + $strings = []; + + foreach ($this->data($defaults) as $key => $value) { + if ($value === null) { + $strings[$key] = null; + } elseif (is_array($value) === true) { + $strings[$key] = Data::encode($value, 'yaml'); + } else { + $strings[$key] = $value; + } + } + + return $strings; + } + + /** + * Converts the form to a plain array + * + * @return array + */ + public function toArray(): array + { + $array = [ + 'errors' => $this->errors(), + 'fields' => $this->fields->toArray(function ($item) { + return $item->toArray(); + }), + 'invalid' => $this->isInvalid() + ]; + + return $array; + } + + /** + * Returns form values + * + * @return array + */ + public function values(): array + { + return $this->values; + } +} diff --git a/kirby/src/Form/Options.php b/kirby/src/Form/Options.php new file mode 100644 index 0000000..090a26b --- /dev/null +++ b/kirby/src/Form/Options.php @@ -0,0 +1,204 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Options +{ + /** + * Returns the classes of predefined Kirby objects + * + * @return array + */ + protected static function aliases(): array + { + return [ + 'Kirby\Cms\File' => 'file', + 'Kirby\Toolkit\Obj' => 'arrayItem', + 'Kirby\Cms\Page' => 'page', + 'Kirby\Cms\StructureObject' => 'structureItem', + 'Kirby\Cms\User' => 'user', + ]; + } + + /** + * Brings options through api + * + * @param $api + * @param $model + * @return array + */ + public static function api($api, $model = null): array + { + $model = $model ?? App::instance()->site(); + $fetch = null; + $text = null; + $value = null; + + if (is_array($api) === true) { + $fetch = $api['fetch'] ?? null; + $text = $api['text'] ?? null; + $value = $api['value'] ?? null; + $url = $api['url'] ?? null; + } else { + $url = $api; + } + + $optionsApi = new OptionsApi([ + 'data' => static::data($model), + 'fetch' => $fetch, + 'url' => $url, + 'text' => $text, + 'value' => $value + ]); + + return $optionsApi->options(); + } + + /** + * @param $model + * @return array + */ + protected static function data($model): array + { + $kirby = $model->kirby(); + + // default data setup + $data = [ + 'kirby' => $kirby, + 'site' => $kirby->site(), + 'users' => $kirby->users(), + ]; + + // add the model by the proper alias + foreach (static::aliases() as $className => $alias) { + if (is_a($model, $className) === true) { + $data[$alias] = $model; + } + } + + return $data; + } + + /** + * Brings options by supporting both api and query + * + * @param $options + * @param array $props + * @param null $model + * @return array + */ + public static function factory($options, array $props = [], $model = null): array + { + switch ($options) { + case 'api': + $options = static::api($props['api']); + break; + case 'query': + $options = static::query($props['query'], $model); + break; + case 'children': + case 'grandChildren': + case 'siblings': + case 'index': + case 'files': + case 'images': + case 'documents': + case 'videos': + case 'audio': + case 'code': + case 'archives': + $options = static::query('page.' . $options, $model); + break; + case 'pages': + $options = static::query('site.index', $model); + break; + } + + if (is_array($options) === false) { + return []; + } + + $result = []; + + foreach ($options as $key => $option) { + if (is_array($option) === false || isset($option['value']) === false) { + $option = [ + 'value' => is_int($key) ? $option : $key, + 'text' => $option + ]; + } + + // translate the option text + if (is_array($option['text']) === true) { + $option['text'] = I18n::translate($option['text'], $option['text']); + } + + // add the option to the list + $result[] = $option; + } + + return $result; + } + + /** + * Brings options with query + * + * @param $query + * @param null $model + * @return array + */ + public static function query($query, $model = null): array + { + $model = $model ?? App::instance()->site(); + + // default text setup + $text = [ + 'arrayItem' => '{{ arrayItem.value }}', + 'file' => '{{ file.filename }}', + 'page' => '{{ page.title }}', + 'structureItem' => '{{ structureItem.title }}', + 'user' => '{{ user.username }}', + ]; + + // default value setup + $value = [ + 'arrayItem' => '{{ arrayItem.value }}', + 'file' => '{{ file.id }}', + 'page' => '{{ page.id }}', + 'structureItem' => '{{ structureItem.id }}', + 'user' => '{{ user.email }}', + ]; + + // resolve array query setup + if (is_array($query) === true) { + $text = $query['text'] ?? $text; + $value = $query['value'] ?? $value; + $query = $query['fetch'] ?? null; + } + + $optionsQuery = new OptionsQuery([ + 'aliases' => static::aliases(), + 'data' => static::data($model), + 'query' => $query, + 'text' => $text, + 'value' => $value + ]); + + return $optionsQuery->options(); + } +} diff --git a/kirby/src/Form/OptionsApi.php b/kirby/src/Form/OptionsApi.php new file mode 100644 index 0000000..f15f437 --- /dev/null +++ b/kirby/src/Form/OptionsApi.php @@ -0,0 +1,242 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class OptionsApi +{ + use Properties; + + /** + * @var + */ + protected $data; + + /** + * @var + */ + protected $fetch; + + /** + * @var + */ + protected $options; + + /** + * @var string + */ + protected $text = '{{ item.value }}'; + + /** + * @var + */ + protected $url; + + /** + * @var string + */ + protected $value = '{{ item.key }}'; + + /** + * OptionsApi constructor + * + * @param array $props + */ + public function __construct(array $props) + { + $this->setProperties($props); + } + + /** + * @return array + */ + public function data(): array + { + return $this->data; + } + + /** + * @return mixed + */ + public function fetch() + { + return $this->fetch; + } + + /** + * @param string $field + * @param array $data + * @return string + */ + protected function field(string $field, array $data) + { + $value = $this->$field(); + return Str::template($value, $data); + } + + /** + * @return array + * @throws \Exception + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function options(): array + { + if (is_array($this->options) === true) { + return $this->options; + } + + if (Url::isAbsolute($this->url()) === true) { + // URL, request via cURL + $data = Remote::get($this->url())->json(); + } else { + // local file, get contents locally + + // ensure the file exists before trying to load it as the + // file_get_contents() warnings need to be suppressed + if (is_file($this->url()) !== true) { + throw new Exception('Local file ' . $this->url() . ' was not found'); + } + + $content = @file_get_contents($this->url()); + + if (is_string($content) !== true) { + throw new Exception('Unexpected read error'); // @codeCoverageIgnore + } + + if (empty($content) === true) { + return []; + } + + $data = json_decode($content, true); + } + + if (is_array($data) === false) { + throw new InvalidArgumentException('Invalid options format'); + } + + $result = (new Query($this->fetch(), Nest::create($data)))->result(); + $options = []; + + foreach ($result as $item) { + $data = array_merge($this->data(), ['item' => $item]); + + $options[] = [ + 'text' => $this->field('text', $data), + 'value' => $this->field('value', $data), + ]; + } + + return $options; + } + + /** + * @param array $data + * @return self + */ + protected function setData(array $data) + { + $this->data = $data; + return $this; + } + + /** + * @param string|null $fetch + * @return self + */ + protected function setFetch(string $fetch = null) + { + $this->fetch = $fetch; + return $this; + } + + /** + * @param $options + * @return self + */ + protected function setOptions($options = null) + { + $this->options = $options; + return $this; + } + + /** + * @param $text + * @return self + */ + protected function setText($text = null) + { + $this->text = $text; + return $this; + } + + /** + * @param $url + * @return self + */ + protected function setUrl($url) + { + $this->url = $url; + return $this; + } + + /** + * @param null $value + * @return self + */ + protected function setValue($value = null) + { + $this->value = $value; + return $this; + } + + /** + * @return string + */ + public function text() + { + return $this->text; + } + + /** + * @return array + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function toArray(): array + { + return $this->options(); + } + + /** + * @return string + */ + public function url(): string + { + return Str::template($this->url, $this->data()); + } + + /** + * @return string + */ + public function value() + { + return $this->value; + } +} diff --git a/kirby/src/Form/OptionsQuery.php b/kirby/src/Form/OptionsQuery.php new file mode 100644 index 0000000..0709de1 --- /dev/null +++ b/kirby/src/Form/OptionsQuery.php @@ -0,0 +1,271 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class OptionsQuery +{ + use Properties; + + /** + * @var array + */ + protected $aliases = []; + + /** + * @var + */ + protected $data; + + /** + * @var + */ + protected $options; + + /** + * @var + */ + protected $query; + + /** + * @var + */ + protected $text; + + /** + * @var + */ + protected $value; + + /** + * OptionsQuery constructor + * + * @param array $props + */ + public function __construct(array $props) + { + $this->setProperties($props); + } + + /** + * @return array + */ + public function aliases(): array + { + return $this->aliases; + } + + /** + * @return array + */ + public function data(): array + { + return $this->data; + } + + /** + * @param string $object + * @param string $field + * @param array $data + * @return string + * @throws \Kirby\Exception\NotFoundException + */ + protected function template(string $object, string $field, array $data) + { + $value = $this->$field(); + + if (is_array($value) === true) { + if (isset($value[$object]) === false) { + throw new NotFoundException('Missing "' . $field . '" definition'); + } + + $value = $value[$object]; + } + + return Str::template($value, $data); + } + + /** + * @return array + */ + public function options(): array + { + if (is_array($this->options) === true) { + return $this->options; + } + + $data = $this->data(); + $query = new Query($this->query(), $data); + $result = $query->result(); + $result = $this->resultToCollection($result); + $options = []; + + foreach ($result as $item) { + $alias = $this->resolve($item); + $data = array_merge($data, [$alias => $item]); + + $options[] = [ + 'text' => $this->template($alias, 'text', $data), + 'value' => $this->template($alias, 'value', $data) + ]; + } + + return $this->options = $options; + } + + /** + * @return string + */ + public function query(): string + { + return $this->query; + } + + /** + * @param $object + * @return mixed|string|null + */ + public function resolve($object) + { + // fast access + if ($alias = ($this->aliases[get_class($object)] ?? null)) { + return $alias; + } + + // slow but precise resolving + foreach ($this->aliases as $className => $alias) { + if (is_a($object, $className) === true) { + return $alias; + } + } + + return 'item'; + } + + /** + * @param $result + * @throws \Kirby\Exception\InvalidArgumentException + */ + protected function resultToCollection($result) + { + if (is_array($result)) { + foreach ($result as $key => $item) { + if (is_scalar($item) === true) { + $result[$key] = new Obj([ + 'key' => new Field(null, 'key', $key), + 'value' => new Field(null, 'value', $item), + ]); + } + } + + $result = new Collection($result); + } + + if (is_a($result, 'Kirby\Toolkit\Collection') === false) { + throw new InvalidArgumentException('Invalid query result data'); + } + + return $result; + } + + /** + * @param array|null $aliases + * @return self + */ + protected function setAliases(array $aliases = null) + { + $this->aliases = $aliases; + return $this; + } + + /** + * @param array $data + * @return self + */ + protected function setData(array $data) + { + $this->data = $data; + return $this; + } + + /** + * @param $options + * @return self + */ + protected function setOptions($options = null) + { + $this->options = $options; + return $this; + } + + /** + * @param string $query + * @return self + */ + protected function setQuery(string $query) + { + $this->query = $query; + return $this; + } + + /** + * @param $text + * @return self + */ + protected function setText($text) + { + $this->text = $text; + return $this; + } + + /** + * @param $value + * @return self + */ + protected function setValue($value) + { + $this->value = $value; + return $this; + } + + /** + * @return mixed + */ + public function text() + { + return $this->text; + } + + public function toArray(): array + { + return $this->options(); + } + + /** + * @return mixed + */ + public function value() + { + return $this->value; + } +} diff --git a/kirby/src/Form/Validations.php b/kirby/src/Form/Validations.php new file mode 100644 index 0000000..d828298 --- /dev/null +++ b/kirby/src/Form/Validations.php @@ -0,0 +1,294 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Validations +{ + /** + * Validates if the field value is boolean + * + * @param \Kirby\Form\Field $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function boolean(Field $field, $value): bool + { + if ($field->isEmpty($value) === false) { + if (is_bool($value) === false) { + throw new InvalidArgumentException([ + 'key' => 'validation.boolean' + ]); + } + } + + return true; + } + + /** + * Validates if the field value is valid date + * + * @param \Kirby\Form\Field $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function date(Field $field, $value): bool + { + if ($field->isEmpty($value) === false) { + if (V::date($value) !== true) { + throw new InvalidArgumentException( + V::message('date', $value) + ); + } + } + + return true; + } + + /** + * Validates if the field value is valid email + * + * @param \Kirby\Form\Field $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function email(Field $field, $value): bool + { + if ($field->isEmpty($value) === false) { + if (V::email($value) === false) { + throw new InvalidArgumentException( + V::message('email', $value) + ); + } + } + + return true; + } + + /** + * Validates if the field value is maximum + * + * @param \Kirby\Form\Field $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function max(Field $field, $value): bool + { + if ($field->isEmpty($value) === false && $field->max() !== null) { + if (V::max($value, $field->max()) === false) { + throw new InvalidArgumentException( + V::message('max', $value, $field->max()) + ); + } + } + + return true; + } + + /** + * Validates if the field value is max length + * + * @param \Kirby\Form\Field $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function maxlength(Field $field, $value): bool + { + if ($field->isEmpty($value) === false && $field->maxlength() !== null) { + if (V::maxLength($value, $field->maxlength()) === false) { + throw new InvalidArgumentException( + V::message('maxlength', $value, $field->maxlength()) + ); + } + } + + return true; + } + + /** + * Validates if the field value is minimum + * + * @param \Kirby\Form\Field $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function min(Field $field, $value): bool + { + if ($field->isEmpty($value) === false && $field->min() !== null) { + if (V::min($value, $field->min()) === false) { + throw new InvalidArgumentException( + V::message('min', $value, $field->min()) + ); + } + } + + return true; + } + + /** + * Validates if the field value is min length + * + * @param \Kirby\Form\Field $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function minlength(Field $field, $value): bool + { + if ($field->isEmpty($value) === false && $field->minlength() !== null) { + if (V::minLength($value, $field->minlength()) === false) { + throw new InvalidArgumentException( + V::message('minlength', $value, $field->minlength()) + ); + } + } + + return true; + } + + /** + * Validates if the field value matches defined pattern + * + * @param \Kirby\Form\Field $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function pattern(Field $field, $value): bool + { + if ($field->isEmpty($value) === false && $field->pattern() !== null) { + if (V::match($value, '/' . $field->pattern() . '/i') === false) { + throw new InvalidArgumentException( + V::message('match') + ); + } + } + + return true; + } + + /** + * Validates if the field value is required + * + * @param \Kirby\Form\Field $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function required(Field $field, $value): bool + { + if ($field->isRequired() === true && $field->save() === true && $field->isEmpty($value) === true) { + throw new InvalidArgumentException([ + 'key' => 'validation.required' + ]); + } + + return true; + } + + /** + * Validates if the field value is in defined options + * + * @param \Kirby\Form\Field $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function option(Field $field, $value): bool + { + if ($field->isEmpty($value) === false) { + $values = array_column($field->options(), 'value'); + + if (in_array($value, $values, true) !== true) { + throw new InvalidArgumentException([ + 'key' => 'validation.option' + ]); + } + } + + return true; + } + + /** + * Validates if the field values is in defined options + * + * @param \Kirby\Form\Field $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function options(Field $field, $value): bool + { + if ($field->isEmpty($value) === false) { + $values = array_column($field->options(), 'value'); + foreach ($value as $val) { + if (in_array($val, $values, true) === false) { + throw new InvalidArgumentException([ + 'key' => 'validation.option' + ]); + } + } + } + + return true; + } + + /** + * Validates if the field value is valid time + * + * @param \Kirby\Form\Field $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function time(Field $field, $value): bool + { + if ($field->isEmpty($value) === false) { + if (V::time($value) !== true) { + throw new InvalidArgumentException( + V::message('time', $value) + ); + } + } + + return true; + } + + /** + * Validates if the field value is valid url + * + * @param \Kirby\Form\Field $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function url(Field $field, $value): bool + { + if ($field->isEmpty($value) === false) { + if (V::url($value) === false) { + throw new InvalidArgumentException( + V::message('url', $value) + ); + } + } + + return true; + } +} diff --git a/kirby/src/Http/Cookie.php b/kirby/src/Http/Cookie.php new file mode 100644 index 0000000..92fc936 --- /dev/null +++ b/kirby/src/Http/Cookie.php @@ -0,0 +1,214 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Cookie +{ + /** + * Key to use for cookie signing + * @var string + */ + public static $key = 'KirbyHttpCookieKey'; + + /** + * Set a new cookie + * + * + * + * cookie::set('mycookie', 'hello', ['lifetime' => 60]); + * // expires in 1 hour + * + * + * + * @param string $key The name of the cookie + * @param string $value The cookie content + * @param array $options Array of options: + * lifetime, path, domain, secure, httpOnly, sameSite + * @return bool true: cookie was created, + * false: cookie creation failed + */ + public static function set(string $key, string $value, array $options = []): bool + { + // extract options + $expires = static::lifetime($options['lifetime'] ?? 0); + $path = $options['path'] ?? '/'; + $domain = $options['domain'] ?? null; + $secure = $options['secure'] ?? false; + $httponly = $options['httpOnly'] ?? true; + $samesite = $options['sameSite'] ?? 'Lax'; + + // add an HMAC signature of the value + $value = static::hmac($value) . '+' . $value; + + // store that thing in the cookie global + $_COOKIE[$key] = $value; + + // store the cookie + // the array syntax is only supported by PHP 7.3+ + // TODO: Always use the first alternative when support for PHP 7.2 is dropped + if (version_compare(PHP_VERSION, '7.3.0', '>=') === true) { + $options = compact('expires', 'path', 'domain', 'secure', 'httponly', 'samesite'); + return setcookie($key, $value, $options); + } else { + return setcookie($key, $value, $expires, $path, $domain, $secure, $httponly); + } + } + + /** + * Calculates the lifetime for a cookie + * + * @param int $minutes Number of minutes or timestamp + * @return int + */ + public static function lifetime(int $minutes): int + { + if ($minutes > 1000000000) { + // absolute timestamp + return $minutes; + } elseif ($minutes > 0) { + // minutes from now + return time() + ($minutes * 60); + } else { + return 0; + } + } + + /** + * Stores a cookie forever + * + * + * + * cookie::forever('mycookie', 'hello'); + * // never expires + * + * + * + * @param string $key The name of the cookie + * @param string $value The cookie content + * @param array $options Array of options: + * path, domain, secure, httpOnly + * @return bool true: cookie was created, + * false: cookie creation failed + */ + public static function forever(string $key, string $value, array $options = []): bool + { + $options['lifetime'] = 253402214400; // 9999-12-31 + return static::set($key, $value, $options); + } + + /** + * Get a cookie value + * + * + * + * cookie::get('mycookie', 'peter'); + * // sample output: 'hello' or if the cookie is not set 'peter' + * + * + * + * @param string|null $key The name of the cookie + * @param string|null $default The default value, which should be returned + * if the cookie has not been found + * @return mixed The found value + */ + public static function get(string $key = null, string $default = null) + { + if ($key === null) { + return $_COOKIE; + } + $value = $_COOKIE[$key] ?? null; + return empty($value) ? $default : static::parse($value); + } + + /** + * Checks if a cookie exists + * + * @param string $key + * @return bool + */ + public static function exists(string $key): bool + { + return static::get($key) !== null; + } + + /** + * Creates a HMAC for the cookie value + * Used as a cookie signature to prevent easy tampering with cookie data + * + * @param string $value + * @return string + */ + protected static function hmac(string $value): string + { + return hash_hmac('sha1', $value, static::$key); + } + + /** + * Parses the hashed value from a cookie + * and tries to extract the value + * + * @param string $string + * @return mixed + */ + protected static function parse(string $string) + { + // if no hash-value separator is present, we can't parse the value + if (strpos($string, '+') === false) { + return null; + } + + // extract hash and value + $hash = Str::before($string, '+'); + $value = Str::after($string, '+'); + + // if the hash or the value is missing at all return null + // $value can be an empty string, $hash can't be! + if (!is_string($hash) || $hash === '' || !is_string($value)) { + return null; + } + + // compare the extracted hash with the hashed value + // don't accept value if the hash is invalid + if (hash_equals(static::hmac($value), $hash) !== true) { + return null; + } + + return $value; + } + + /** + * Remove a cookie + * + * + * + * cookie::remove('mycookie'); + * // mycookie is now gone + * + * + * + * @param string $key The name of the cookie + * @return bool true: the cookie has been removed, + * false: the cookie could not be removed + */ + public static function remove(string $key): bool + { + if (isset($_COOKIE[$key])) { + unset($_COOKIE[$key]); + return setcookie($key, '', 1, '/') && setcookie($key, false); + } + + return false; + } +} diff --git a/kirby/src/Http/Exceptions/NextRouteException.php b/kirby/src/Http/Exceptions/NextRouteException.php new file mode 100644 index 0000000..de85efa --- /dev/null +++ b/kirby/src/Http/Exceptions/NextRouteException.php @@ -0,0 +1,16 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class NextRouteException extends \Exception +{ +} diff --git a/kirby/src/Http/Header.php b/kirby/src/Http/Header.php new file mode 100644 index 0000000..6ee8680 --- /dev/null +++ b/kirby/src/Http/Header.php @@ -0,0 +1,316 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Header +{ + // configuration + public static $codes = [ + + // successful + '_200' => 'OK', + '_201' => 'Created', + '_202' => 'Accepted', + + // redirection + '_300' => 'Multiple Choices', + '_301' => 'Moved Permanently', + '_302' => 'Found', + '_303' => 'See Other', + '_304' => 'Not Modified', + '_307' => 'Temporary Redirect', + '_308' => 'Permanent Redirect', + + // client error + '_400' => 'Bad Request', + '_401' => 'Unauthorized', + '_402' => 'Payment Required', + '_403' => 'Forbidden', + '_404' => 'Not Found', + '_405' => 'Method Not Allowed', + '_406' => 'Not Acceptable', + '_410' => 'Gone', + '_418' => 'I\'m a teapot', + '_451' => 'Unavailable For Legal Reasons', + + // server error + '_500' => 'Internal Server Error', + '_501' => 'Not Implemented', + '_502' => 'Bad Gateway', + '_503' => 'Service Unavailable', + '_504' => 'Gateway Time-out' + ]; + + /** + * Sends a content type header + * + * @param string $mime + * @param string $charset + * @param bool $send + * @return string|void + */ + public static function contentType(string $mime, string $charset = 'UTF-8', bool $send = true) + { + if ($found = F::extensionToMime($mime)) { + $mime = $found; + } + + $header = 'Content-type: ' . $mime; + + if (empty($charset) === false) { + $header .= '; charset=' . $charset; + } + + if ($send === false) { + return $header; + } + + header($header); + } + + /** + * Creates headers by key and value + * + * @param string|array $key + * @param string|null $value + * @return string + */ + public static function create($key, string $value = null): string + { + if (is_array($key) === true) { + $headers = []; + + foreach ($key as $k => $v) { + $headers[] = static::create($k, $v); + } + + return implode("\r\n", $headers); + } + + // prevent header injection by stripping any newline characters from single headers + return str_replace(["\r", "\n"], '', $key . ': ' . $value); + } + + /** + * Shortcut for static::contentType() + * + * @param string $mime + * @param string $charset + * @param bool $send + * @return string|void + */ + public static function type(string $mime, string $charset = 'UTF-8', bool $send = true) + { + return static::contentType($mime, $charset, $send); + } + + /** + * Sends a status header + * + * Checks $code against a list of known status codes. To bypass this check + * and send a custom status code and message, use a $code string formatted + * as 3 digits followed by a space and a message, e.g. '999 Custom Status'. + * + * @param int|string $code The HTTP status code + * @param bool $send If set to false the header will be returned instead + * @return string|void + */ + public static function status($code = null, bool $send = true) + { + $codes = static::$codes; + $protocol = $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1'; + + // allow full control over code and message + if (is_string($code) === true && preg_match('/^\d{3} \w.+$/', $code) === 1) { + $message = substr(rtrim($code), 4); + $code = substr($code, 0, 3); + } else { + $code = array_key_exists('_' . $code, $codes) === false ? 500 : $code; + $message = $codes['_' . $code] ?? 'Something went wrong'; + } + + $header = $protocol . ' ' . $code . ' ' . $message; + + if ($send === false) { + return $header; + } + + // try to send the header + header($header); + } + + /** + * Sends a 200 header + * + * @param bool $send + * @return string|void + */ + public static function success(bool $send = true) + { + return static::status(200, $send); + } + + /** + * Sends a 201 header + * + * @param bool $send + * @return string|void + */ + public static function created(bool $send = true) + { + return static::status(201, $send); + } + + /** + * Sends a 202 header + * + * @param bool $send + * @return string|void + */ + public static function accepted(bool $send = true) + { + return static::status(202, $send); + } + + /** + * Sends a 400 header + * + * @param bool $send + * @return string|void + */ + public static function error(bool $send = true) + { + return static::status(400, $send); + } + + /** + * Sends a 403 header + * + * @param bool $send + * @return string|void + */ + public static function forbidden(bool $send = true) + { + return static::status(403, $send); + } + + /** + * Sends a 404 header + * + * @param bool $send + * @return string|void + */ + public static function notfound(bool $send = true) + { + return static::status(404, $send); + } + + /** + * Sends a 404 header + * + * @param bool $send + * @return string|void + */ + public static function missing(bool $send = true) + { + return static::status(404, $send); + } + + /** + * Sends a 410 header + * + * @param bool $send + * @return string|void + */ + public static function gone(bool $send = true) + { + return static::status(410, $send); + } + + /** + * Sends a 500 header + * + * @param bool $send + * @return string|void + */ + public static function panic(bool $send = true) + { + return static::status(500, $send); + } + + /** + * Sends a 503 header + * + * @param bool $send + * @return string|void + */ + public static function unavailable(bool $send = true) + { + return static::status(503, $send); + } + + /** + * Sends a redirect header + * + * @param string $url + * @param int $code + * @param bool $send + * @return string|void + */ + public static function redirect(string $url, int $code = 302, bool $send = true) + { + $status = static::status($code, false); + $location = 'Location:' . Url::unIdn($url); + + if ($send !== true) { + return $status . "\r\n" . $location; + } + + header($status); + header($location); + exit(); + } + + /** + * Sends download headers for anything that is downloadable + * + * @param array $params Check out the defaults array for available parameters + */ + public static function download(array $params = []) + { + $defaults = [ + 'name' => 'download', + 'size' => false, + 'mime' => 'application/force-download', + 'modified' => time() + ]; + + $options = array_merge($defaults, $params); + + header('Pragma: public'); + header('Cache-Control: no-cache, no-store, must-revalidate'); + header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $options['modified']) . ' GMT'); + header('Content-Disposition: attachment; filename="' . $options['name'] . '"'); + header('Content-Transfer-Encoding: binary'); + + static::contentType($options['mime']); + + if ($options['size']) { + header('Content-Length: ' . $options['size']); + } + + header('Connection: close'); + } +} diff --git a/kirby/src/Http/Idn.php b/kirby/src/Http/Idn.php new file mode 100644 index 0000000..e4b58aa --- /dev/null +++ b/kirby/src/Http/Idn.php @@ -0,0 +1,64 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Idn +{ + public static function decode(string $domain) + { + return (new Punycode())->decode($domain); + } + + public static function encode(string $domain) + { + return (new Punycode())->encode($domain); + } + + /** + * Decodes a email address to the Unicode format + * + * @param string $email + * @return string + */ + public static function decodeEmail(string $email): string + { + if (Str::contains($email, 'xn--') === true) { + $parts = Str::split($email, '@'); + $address = $parts[0]; + $domain = Idn::decode($parts[1] ?? ''); + $email = $address . '@' . $domain; + } + + return $email; + } + + /** + * Encodes a email address to the Punycode format + * + * @param string $email + * @return string + */ + public static function encodeEmail(string $email): string + { + if (mb_detect_encoding($email, 'ASCII', true) === false) { + $parts = Str::split($email, '@'); + $address = $parts[0]; + $domain = Idn::encode($parts[1] ?? ''); + $email = $address . '@' . $domain; + } + + return $email; + } +} diff --git a/kirby/src/Http/Params.php b/kirby/src/Http/Params.php new file mode 100644 index 0000000..5d0fa44 --- /dev/null +++ b/kirby/src/Http/Params.php @@ -0,0 +1,149 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Params extends Query +{ + /** + * @var null|string + */ + public static $separator; + + /** + * Creates a new params object + * + * @param array|string $params + */ + public function __construct($params) + { + if (is_string($params) === true) { + $params = static::extract($params)['params']; + } + + parent::__construct($params ?? []); + } + + /** + * Extract the params from a string or array + * + * @param string|array|null $path + * @return array + */ + public static function extract($path = null): array + { + if (empty($path) === true) { + return [ + 'path' => null, + 'params' => null, + 'slash' => false + ]; + } + + $slash = false; + + if (is_string($path) === true) { + $slash = substr($path, -1, 1) === '/'; + $path = Str::split($path, '/'); + } + + if (is_array($path) === true) { + $params = []; + $separator = static::separator(); + + foreach ($path as $index => $p) { + if (strpos($p, $separator) === false) { + continue; + } + + $paramParts = Str::split($p, $separator); + $paramKey = $paramParts[0]; + $paramValue = $paramParts[1] ?? null; + + $params[$paramKey] = $paramValue; + unset($path[$index]); + } + + return [ + 'path' => $path, + 'params' => $params, + 'slash' => $slash + ]; + } + + return [ + 'path' => null, + 'params' => null, + 'slash' => false + ]; + } + + /** + * Returns the param separator according + * to the operating system. + * + * Unix = ':' + * Windows = ';' + * + * @return string + */ + public static function separator(): string + { + if (static::$separator !== null) { + return static::$separator; + } + + if (DIRECTORY_SEPARATOR === '/') { + return static::$separator = ':'; + } else { + return static::$separator = ';'; + } + } + + /** + * Converts the params object to a params string + * which can then be used in the URL builder again + * + * @param bool $leadingSlash + * @param bool $trailingSlash + * @return string|null + */ + public function toString($leadingSlash = false, $trailingSlash = false): string + { + if ($this->isEmpty() === true) { + return ''; + } + + $params = []; + $separator = static::separator(); + + foreach ($this as $key => $value) { + if ($value !== null && $value !== '') { + $params[] = $key . $separator . $value; + } + } + + if (empty($params) === true) { + return ''; + } + + $params = implode('/', $params); + + $leadingSlash = $leadingSlash === true ? '/' : null; + $trailingSlash = $trailingSlash === true ? '/' : null; + + return $leadingSlash . $params . $trailingSlash; + } +} diff --git a/kirby/src/Http/Path.php b/kirby/src/Http/Path.php new file mode 100644 index 0000000..eaa014a --- /dev/null +++ b/kirby/src/Http/Path.php @@ -0,0 +1,47 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Path extends Collection +{ + public function __construct($items) + { + if (is_string($items) === true) { + $items = Str::split($items, '/'); + } + + parent::__construct($items ?? []); + } + + public function __toString(): string + { + return $this->toString(); + } + + public function toString(bool $leadingSlash = false, bool $trailingSlash = false): string + { + if (empty($this->data) === true) { + return ''; + } + + $path = implode('/', $this->data); + + $leadingSlash = $leadingSlash === true ? '/' : null; + $trailingSlash = $trailingSlash === true ? '/' : null; + + return $leadingSlash . $path . $trailingSlash; + } +} diff --git a/kirby/src/Http/Query.php b/kirby/src/Http/Query.php new file mode 100644 index 0000000..0956802 --- /dev/null +++ b/kirby/src/Http/Query.php @@ -0,0 +1,58 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Query extends Obj +{ + public function __construct($query) + { + if (is_string($query) === true) { + parse_str(ltrim($query, '?'), $query); + } + + parent::__construct($query ?? []); + } + + public function isEmpty(): bool + { + return empty((array)$this) === true; + } + + public function isNotEmpty(): bool + { + return empty((array)$this) === false; + } + + public function __toString(): string + { + return $this->toString(); + } + + public function toString($questionMark = false): string + { + $query = http_build_query($this); + + if (empty($query) === true) { + return ''; + } + + if ($questionMark === true) { + $query = '?' . $query; + } + + return $query; + } +} diff --git a/kirby/src/Http/Remote.php b/kirby/src/Http/Remote.php new file mode 100644 index 0000000..af1789f --- /dev/null +++ b/kirby/src/Http/Remote.php @@ -0,0 +1,404 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Remote +{ + const CA_INTERNAL = 1; + const CA_SYSTEM = 2; + + /** + * @var array + */ + public static $defaults = [ + 'agent' => null, + 'basicAuth' => null, + 'body' => true, + 'ca' => self::CA_INTERNAL, + 'data' => [], + 'encoding' => 'utf-8', + 'file' => null, + 'headers' => [], + 'method' => 'GET', + 'progress' => null, + 'test' => false, + 'timeout' => 10, + ]; + + /** + * @var string + */ + public $content; + + /** + * @var resource + */ + public $curl; + + /** + * @var array + */ + public $curlopt = []; + + /** + * @var int + */ + public $errorCode; + + /** + * @var string + */ + public $errorMessage; + + /** + * @var array + */ + public $headers = []; + + /** + * @var array + */ + public $info = []; + + /** + * @var array + */ + public $options = []; + + /** + * Magic getter for request info data + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + $method = str_replace('-', '_', Str::kebab($method)); + return $this->info[$method] ?? null; + } + + /** + * Constructor + * + * @param string $url + * @param array $options + */ + public function __construct(string $url, array $options = []) + { + $defaults = static::$defaults; + + // update the defaults with App config if set; + // request the App instance lazily + $app = App::instance(null, true); + if ($app !== null) { + $defaults = array_merge($defaults, $app->option('remote', [])); + } + + // set all options + $this->options = array_merge($defaults, $options); + + // add the url + $this->options['url'] = $url; + + // send the request + $this->fetch(); + } + + public static function __callStatic(string $method, array $arguments = []) + { + return new static($arguments[0], array_merge(['method' => strtoupper($method)], $arguments[1] ?? [])); + } + + /** + * Returns the http status code + * + * @return int|null + */ + public function code(): ?int + { + return $this->info['http_code'] ?? null; + } + + /** + * Returns the response content + * + * @return mixed + */ + public function content() + { + return $this->content; + } + + /** + * Sets up all curl options and sends the request + * + * @return self + */ + public function fetch() + { + // curl options + $this->curlopt = [ + CURLOPT_URL => $this->options['url'], + CURLOPT_ENCODING => $this->options['encoding'], + CURLOPT_CONNECTTIMEOUT => $this->options['timeout'], + CURLOPT_TIMEOUT => $this->options['timeout'], + CURLOPT_AUTOREFERER => true, + CURLOPT_RETURNTRANSFER => $this->options['body'], + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 10, + CURLOPT_HEADER => false, + CURLOPT_HEADERFUNCTION => function ($curl, $header) { + $parts = Str::split($header, ':'); + + if (empty($parts[0]) === false && empty($parts[1]) === false) { + $key = array_shift($parts); + $this->headers[$key] = implode(':', $parts); + } + + return strlen($header); + } + ]; + + // determine the TLS CA to use + if ($this->options['ca'] === self::CA_INTERNAL) { + $this->curlopt[CURLOPT_SSL_VERIFYPEER] = true; + $this->curlopt[CURLOPT_CAINFO] = dirname(__DIR__, 2) . '/cacert.pem'; + } elseif ($this->options['ca'] === self::CA_SYSTEM) { + $this->curlopt[CURLOPT_SSL_VERIFYPEER] = true; + } elseif ($this->options['ca'] === false) { + $this->curlopt[CURLOPT_SSL_VERIFYPEER] = false; + } elseif ( + is_string($this->options['ca']) === true && + is_file($this->options['ca']) === true + ) { + $this->curlopt[CURLOPT_SSL_VERIFYPEER] = true; + $this->curlopt[CURLOPT_CAINFO] = $this->options['ca']; + } elseif ( + is_string($this->options['ca']) === true && + is_dir($this->options['ca']) === true + ) { + $this->curlopt[CURLOPT_SSL_VERIFYPEER] = true; + $this->curlopt[CURLOPT_CAPATH] = $this->options['ca']; + } else { + throw new InvalidArgumentException('Invalid "ca" option for the Remote class'); + } + + // add the progress + if (is_callable($this->options['progress']) === true) { + $this->curlopt[CURLOPT_NOPROGRESS] = false; + $this->curlopt[CURLOPT_PROGRESSFUNCTION] = $this->options['progress']; + } + + // add all headers + if (empty($this->options['headers']) === false) { + // convert associative arrays to strings + $headers = []; + foreach ($this->options['headers'] as $key => $value) { + if (is_string($key) === true) { + $headers[] = $key . ': ' . $value; + } else { + $headers[] = $value; + } + } + + $this->curlopt[CURLOPT_HTTPHEADER] = $headers; + } + + // add HTTP Basic authentication + if (empty($this->options['basicAuth']) === false) { + $this->curlopt[CURLOPT_USERPWD] = $this->options['basicAuth']; + } + + // add the user agent + if (empty($this->options['agent']) === false) { + $this->curlopt[CURLOPT_USERAGENT] = $this->options['agent']; + } + + // do some request specific stuff + switch (strtoupper($this->options['method'])) { + case 'POST': + $this->curlopt[CURLOPT_POST] = true; + $this->curlopt[CURLOPT_CUSTOMREQUEST] = 'POST'; + $this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); + break; + case 'PUT': + $this->curlopt[CURLOPT_CUSTOMREQUEST] = 'PUT'; + $this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); + + // put a file + if ($this->options['file']) { + $this->curlopt[CURLOPT_INFILE] = fopen($this->options['file'], 'r'); + $this->curlopt[CURLOPT_INFILESIZE] = F::size($this->options['file']); + } + break; + case 'PATCH': + $this->curlopt[CURLOPT_CUSTOMREQUEST] = 'PATCH'; + $this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); + break; + case 'DELETE': + $this->curlopt[CURLOPT_CUSTOMREQUEST] = 'DELETE'; + $this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); + break; + case 'HEAD': + $this->curlopt[CURLOPT_CUSTOMREQUEST] = 'HEAD'; + $this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); + $this->curlopt[CURLOPT_NOBODY] = true; + break; + } + + if ($this->options['test'] === true) { + return $this; + } + + // start a curl request + $this->curl = curl_init(); + + curl_setopt_array($this->curl, $this->curlopt); + + $this->content = curl_exec($this->curl); + $this->info = curl_getinfo($this->curl); + $this->errorCode = curl_errno($this->curl); + $this->errorMessage = curl_error($this->curl); + + if ($this->errorCode) { + throw new Exception($this->errorMessage, $this->errorCode); + } + + curl_close($this->curl); + + return $this; + } + + /** + * Static method to send a GET request + * + * @param string $url + * @param array $params + * @return self + */ + public static function get(string $url, array $params = []) + { + $defaults = [ + 'method' => 'GET', + 'data' => [], + ]; + + $options = array_merge($defaults, $params); + $query = http_build_query($options['data']); + + if (empty($query) === false) { + $url = Url::hasQuery($url) === true ? $url . '&' . $query : $url . '?' . $query; + } + + // remove the data array from the options + unset($options['data']); + + return new static($url, $options); + } + + /** + * Returns all received headers + * + * @return array + */ + public function headers(): array + { + return $this->headers; + } + + /** + * Returns the request info + * + * @return array + */ + public function info(): array + { + return $this->info; + } + + /** + * Decode the response content + * + * @param bool $array decode as array or object + * @return array|\stdClass + */ + public function json(bool $array = true) + { + return json_decode($this->content(), $array); + } + + /** + * Returns the request method + * + * @return string + */ + public function method(): string + { + return $this->options['method']; + } + + /** + * Returns all options which have been + * set for the current request + * + * @return array + */ + public function options(): array + { + return $this->options; + } + + /** + * Internal method to handle post field data + * + * @param mixed $data + * @return mixed + */ + protected function postfields($data) + { + if (is_object($data) || is_array($data)) { + return http_build_query($data); + } else { + return $data; + } + } + + /** + * Static method to init this class and send a request + * + * @param string $url + * @param array $params + * @return self + */ + public static function request(string $url, array $params = []) + { + return new static($url, $params); + } + + /** + * Returns the request Url + * + * @return string + */ + public function url(): string + { + return $this->options['url']; + } +} diff --git a/kirby/src/Http/Request.php b/kirby/src/Http/Request.php new file mode 100644 index 0000000..dce994b --- /dev/null +++ b/kirby/src/Http/Request.php @@ -0,0 +1,413 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Request +{ + /** + * The auth object if available + * + * @var BearerAuth|BasicAuth|false|null + */ + protected $auth; + + /** + * The Body object is a wrapper around + * the request body, which parses the contents + * of the body and provides an API to fetch + * particular parts of the body + * + * Examples: + * + * `$request->body()->get('foo')` + * + * @var Body + */ + protected $body; + + /** + * The Files object is a wrapper around + * the $_FILES global. It sanitizes the + * $_FILES array and provides an API to fetch + * individual files by key + * + * Examples: + * + * `$request->files()->get('upload')['size']` + * `$request->file('upload')['size']` + * + * @var Files + */ + protected $files; + + /** + * The Method type + * + * @var string + */ + protected $method; + + /** + * All options that have been passed to + * the request in the constructor + * + * @var array + */ + protected $options; + + /** + * The Query object is a wrapper around + * the URL query string, which parses the + * string and provides a clean API to fetch + * particular parts of the query + * + * Examples: + * + * `$request->query()->get('foo')` + * + * @var Query + */ + protected $query; + + /** + * Request URL object + * + * @var Uri + */ + protected $url; + + /** + * Creates a new Request object + * You can either pass your own request + * data via the $options array or use + * the data from the incoming request. + * + * @param array $options + */ + public function __construct(array $options = []) + { + $this->options = $options; + $this->method = $this->detectRequestMethod($options['method'] ?? null); + + if (isset($options['body']) === true) { + $this->body = new Body($options['body']); + } + + if (isset($options['files']) === true) { + $this->files = new Files($options['files']); + } + + if (isset($options['query']) === true) { + $this->query = new Query($options['query']); + } + + if (isset($options['url']) === true) { + $this->url = new Uri($options['url']); + } + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return [ + 'body' => $this->body(), + 'files' => $this->files(), + 'method' => $this->method(), + 'query' => $this->query(), + 'url' => $this->url()->toString() + ]; + } + + /** + * Returns the Auth object if authentication is set + * + * @return \Kirby\Http\Request\Auth\BasicAuth|\Kirby\Http\Request\Auth\BearerAuth|null + */ + public function auth() + { + if ($this->auth !== null) { + return $this->auth; + } + + if ($auth = $this->options['auth'] ?? $this->header('authorization')) { + $type = Str::before($auth, ' '); + $token = Str::after($auth, ' '); + $class = 'Kirby\\Http\\Request\\Auth\\' . ucfirst($type) . 'Auth'; + + if (class_exists($class) === false) { + return $this->auth = false; + } + + return $this->auth = new $class($token); + } + + return $this->auth = false; + } + + /** + * Returns the Body object + * + * @return \Kirby\Http\Request\Body + */ + public function body() + { + return $this->body = $this->body ?? new Body(); + } + + /** + * Checks if the request has been made from the command line + * + * @return bool + */ + public function cli(): bool + { + return Server::cli(); + } + + /** + * Returns a CSRF token if stored in a header or the query + * + * @return string|null + */ + public function csrf(): ?string + { + return $this->header('x-csrf') ?? $this->query()->get('csrf'); + } + + /** + * Returns the request input as array + * + * @return array + */ + public function data(): array + { + return array_merge($this->body()->toArray(), $this->query()->toArray()); + } + + /** + * Detect the request method from various + * options: given method, query string, server vars + * + * @param string $method + * @return string + */ + public function detectRequestMethod(string $method = null): string + { + // all possible methods + $methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH']; + + // the request method can be overwritten with a header + $methodOverride = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ?? null); + + if ($method === null && in_array($methodOverride, $methods) === true) { + $method = $methodOverride; + } + + // final chain of options to detect the method + $method = $method ?? $_SERVER['REQUEST_METHOD'] ?? 'GET'; + + // uppercase the shit out of it + $method = strtoupper($method); + + // sanitize the method + if (in_array($method, $methods) === false) { + $method = 'GET'; + } + + return $method; + } + + /** + * Returns the domain + * + * @return string + */ + public function domain(): string + { + return $this->url()->domain(); + } + + /** + * Fetches a single file array + * from the Files object by key + * + * @param string $key + * @return array|null + */ + public function file(string $key) + { + return $this->files()->get($key); + } + + /** + * Returns the Files object + * + * @return \Kirby\Cms\Files + */ + public function files() + { + return $this->files = $this->files ?? new Files(); + } + + /** + * Returns any data field from the request + * if it exists + * + * @param string|null|array $key + * @param mixed $fallback + * @return mixed + */ + public function get($key = null, $fallback = null) + { + return A::get($this->data(), $key, $fallback); + } + + /** + * Returns a header by key if it exists + * + * @param string $key + * @param mixed $fallback + * @return mixed + */ + public function header(string $key, $fallback = null) + { + $headers = array_change_key_case($this->headers()); + return $headers[strtolower($key)] ?? $fallback; + } + + /** + * Return all headers with polyfill for + * missing getallheaders function + * + * @return array + */ + public function headers(): array + { + $headers = []; + + foreach ($_SERVER as $key => $value) { + if (substr($key, 0, 5) !== 'HTTP_' && substr($key, 0, 14) !== 'REDIRECT_HTTP_') { + continue; + } + + // remove HTTP_ + $key = str_replace(['REDIRECT_HTTP_', 'HTTP_'], '', $key); + + // convert to lowercase + $key = strtolower($key); + + // replace _ with spaces + $key = str_replace('_', ' ', $key); + + // uppercase first char in each word + $key = ucwords($key); + + // convert spaces to dashes + $key = str_replace(' ', '-', $key); + + $headers[$key] = $value; + } + + return $headers; + } + + /** + * Checks if the given method name + * matches the name of the request method. + * + * @param string $method + * @return bool + */ + public function is(string $method): bool + { + return strtoupper($this->method) === strtoupper($method); + } + + /** + * Returns the request method + * + * @return string + */ + public function method(): string + { + return $this->method; + } + + /** + * Shortcut to the Params object + */ + public function params() + { + return $this->url()->params(); + } + + /** + * Shortcut to the Path object + */ + public function path() + { + return $this->url()->path(); + } + + /** + * Returns the Query object + * + * @return \Kirby\Http\Request\Query + */ + public function query() + { + return $this->query = $this->query ?? new Query(); + } + + /** + * Checks for a valid SSL connection + * + * @return bool + */ + public function ssl(): bool + { + return $this->url()->scheme() === 'https'; + } + + /** + * Returns the current Uri object. + * If you pass props you can safely modify + * the Url with new parameters without destroying + * the original object. + * + * @param array $props + * @return \Kirby\Http\Uri + */ + public function url(array $props = null) + { + if ($props !== null) { + return $this->url()->clone($props); + } + + return $this->url = $this->url ?? Uri::current(); + } +} diff --git a/kirby/src/Http/Request/Auth/BasicAuth.php b/kirby/src/Http/Request/Auth/BasicAuth.php new file mode 100644 index 0000000..4df6e8f --- /dev/null +++ b/kirby/src/Http/Request/Auth/BasicAuth.php @@ -0,0 +1,78 @@ +credentials = base64_decode($token); + $this->username = Str::before($this->credentials, ':'); + $this->password = Str::after($this->credentials, ':'); + } + + /** + * Returns the entire unencoded credentials string + * + * @return string + */ + public function credentials(): string + { + return $this->credentials; + } + + /** + * Returns the password + * + * @return string|null + */ + public function password(): ?string + { + return $this->password; + } + + /** + * Returns the authentication type + * + * @return string + */ + public function type(): string + { + return 'basic'; + } + + /** + * Returns the username + * + * @return string|null + */ + public function username(): ?string + { + return $this->username; + } +} diff --git a/kirby/src/Http/Request/Auth/BearerAuth.php b/kirby/src/Http/Request/Auth/BearerAuth.php new file mode 100644 index 0000000..2c5b1c2 --- /dev/null +++ b/kirby/src/Http/Request/Auth/BearerAuth.php @@ -0,0 +1,54 @@ +token = $token; + } + + /** + * Converts the object to a string + * + * @return string + */ + public function __toString(): string + { + return ucfirst($this->type()) . ' ' . $this->token(); + } + + /** + * Returns the authentication token + * + * @return string + */ + public function token(): string + { + return $this->token; + } + + /** + * Returns the auth type + * + * @return string + */ + public function type(): string + { + return 'bearer'; + } +} diff --git a/kirby/src/Http/Request/Body.php b/kirby/src/Http/Request/Body.php new file mode 100644 index 0000000..f9b6692 --- /dev/null +++ b/kirby/src/Http/Request/Body.php @@ -0,0 +1,129 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Body +{ + use Data; + + /** + * The raw body content + * + * @var string|array + */ + protected $contents; + + /** + * The parsed content as array + * + * @var array + */ + protected $data; + + /** + * Creates a new request body object. + * You can pass your own array or string. + * If null is being passed, the class will + * fetch the body either from the $_POST global + * or from php://input. + * + * @param array|string|null $contents + */ + public function __construct($contents = null) + { + $this->contents = $contents; + } + + /** + * Fetches the raw contents for the body + * or uses the passed contents. + * + * @return string|array + */ + public function contents() + { + if ($this->contents === null) { + if (empty($_POST) === false) { + $this->contents = $_POST; + } else { + $this->contents = file_get_contents('php://input'); + } + } + + return $this->contents; + } + + /** + * Parses the raw contents once and caches + * the result. The parser will try to convert + * the body with the json decoder first and + * then run parse_str to get some results + * if the json decoder failed. + * + * @return array + */ + public function data(): array + { + if (is_array($this->data) === true) { + return $this->data; + } + + $contents = $this->contents(); + + // return content which is already in array form + if (is_array($contents) === true) { + return $this->data = $contents; + } + + // try to convert the body from json + $json = json_decode($contents, true); + + if (is_array($json) === true) { + return $this->data = $json; + } + + if (strstr($contents, '=') !== false) { + // try to parse the body as query string + parse_str($contents, $parsed); + + if (is_array($parsed)) { + return $this->data = $parsed; + } + } + + return $this->data = []; + } + + /** + * Converts the data array back + * to a http query string + * + * @return string + */ + public function toString(): string + { + return http_build_query($this->data()); + } + + /** + * Magic string converter + * + * @return string + */ + public function __toString(): string + { + return $this->toString(); + } +} diff --git a/kirby/src/Http/Request/Data.php b/kirby/src/Http/Request/Data.php new file mode 100644 index 0000000..0a6bc7f --- /dev/null +++ b/kirby/src/Http/Request/Data.php @@ -0,0 +1,84 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +trait Data +{ + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + /** + * The data provider method has to be + * implemented by each class using this Trait + * and has to return an associative array + * for the get method + * + * @return array + */ + abstract public function data(): array; + + /** + * The get method is the heart and soul of this + * Trait. You can use it to fetch a single value + * of the data array by key or multiple values by + * passing an array of keys. + * + * @param string|array $key + * @param mixed|null $default + * @return mixed + */ + public function get($key, $default = null) + { + if (is_array($key) === true) { + $result = []; + foreach ($key as $k) { + $result[$k] = $this->get($k); + } + return $result; + } + + return $this->data()[$key] ?? $default; + } + + /** + * Returns the data array. + * This is basically an alias for Data::data() + * + * @return array + */ + public function toArray(): array + { + return $this->data(); + } + + /** + * Converts the data array to json + * + * @return string + */ + public function toJson(): string + { + return json_encode($this->data()); + } +} diff --git a/kirby/src/Http/Request/Files.php b/kirby/src/Http/Request/Files.php new file mode 100644 index 0000000..d5304dd --- /dev/null +++ b/kirby/src/Http/Request/Files.php @@ -0,0 +1,73 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Files +{ + use Data; + + /** + * Sanitized array of all received files + * + * @var array + */ + protected $files; + + /** + * Creates a new Files object + * Pass your own array to mock + * uploads. + * + * @param array|null $files + */ + public function __construct($files = null) + { + if ($files === null) { + $files = $_FILES; + } + + $this->files = []; + + foreach ($files as $key => $file) { + if (is_array($file['name'])) { + foreach ($file['name'] as $i => $name) { + $this->files[$key][] = [ + 'name' => $file['name'][$i] ?? null, + 'type' => $file['type'][$i] ?? null, + 'tmp_name' => $file['tmp_name'][$i] ?? null, + 'error' => $file['error'][$i] ?? null, + 'size' => $file['size'][$i] ?? null, + ]; + } + } else { + $this->files[$key] = $file; + } + } + } + + /** + * The data method returns the files + * array. This is only needed to make + * the Data trait work for the Files::get($key) + * method. + * + * @return array + */ + public function data(): array + { + return $this->files; + } +} diff --git a/kirby/src/Http/Request/Query.php b/kirby/src/Http/Request/Query.php new file mode 100644 index 0000000..a6101d0 --- /dev/null +++ b/kirby/src/Http/Request/Query.php @@ -0,0 +1,98 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Query +{ + use Data; + + /** + * The Query data array + * + * @var array|null + */ + protected $data; + + /** + * Creates a new Query object. + * The passed data can be an array + * or a parsable query string. If + * null is passed, the current Query + * will be taken from $_GET + * + * @param array|string|null $data + */ + public function __construct($data = null) + { + if ($data === null) { + $this->data = $_GET; + } elseif (is_array($data)) { + $this->data = $data; + } else { + parse_str($data, $parsed); + $this->data = $parsed; + } + } + + /** + * Returns the Query data as array + * + * @return array + */ + public function data(): array + { + return $this->data; + } + + /** + * Returns `true` if the request doesn't contain query variables + * + * @return bool + */ + public function isEmpty(): bool + { + return empty($this->data) === true; + } + + /** + * Returns `true` if the request contains query variables + * + * @return bool + */ + public function isNotEmpty(): bool + { + return empty($this->data) === false; + } + + /** + * Converts the query data array + * back to a query string + * + * @return string + */ + public function toString(): string + { + return http_build_query($this->data()); + } + + /** + * Magic string converter + * + * @return string + */ + public function __toString(): string + { + return $this->toString(); + } +} diff --git a/kirby/src/Http/Response.php b/kirby/src/Http/Response.php new file mode 100644 index 0000000..9279202 --- /dev/null +++ b/kirby/src/Http/Response.php @@ -0,0 +1,317 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Response +{ + /** + * Store for all registered headers, + * which will be sent with the response + * + * @var array + */ + protected $headers = []; + + /** + * The response body + * + * @var string + */ + protected $body; + + /** + * The HTTP response code + * + * @var int + */ + protected $code; + + /** + * The content type for the response + * + * @var string + */ + protected $type; + + /** + * The content type charset + * + * @var string + */ + protected $charset = 'UTF-8'; + + /** + * Creates a new response object + * + * @param string $body + * @param string $type + * @param int $code + * @param array $headers + * @param string $charset + */ + public function __construct($body = '', ?string $type = null, ?int $code = null, ?array $headers = null, ?string $charset = null) + { + // array construction + if (is_array($body) === true) { + $params = $body; + $body = $params['body'] ?? ''; + $type = $params['type'] ?? $type; + $code = $params['code'] ?? $code; + $headers = $params['headers'] ?? $headers; + $charset = $params['charset'] ?? $charset; + } + + // regular construction + $this->body = $body; + $this->type = $type ?? 'text/html'; + $this->code = $code ?? 200; + $this->headers = $headers ?? []; + $this->charset = $charset ?? 'UTF-8'; + + // automatic mime type detection + if (strpos($this->type, '/') === false) { + $this->type = F::extensionToMime($this->type) ?? 'text/html'; + } + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + /** + * Makes it possible to convert the + * entire response object to a string + * to send the headers and print the body + * + * @return string + */ + public function __toString(): string + { + try { + return $this->send(); + } catch (Throwable $e) { + return ''; + } + } + + /** + * Getter for the body + * + * @return string + */ + public function body(): string + { + return $this->body; + } + + /** + * Getter for the content type charset + * + * @return string + */ + public function charset(): string + { + return $this->charset; + } + + /** + * Getter for the HTTP status code + * + * @return int + */ + public function code(): int + { + return $this->code; + } + + /** + * Creates a response that triggers + * a file download for the given file + * + * @param string $file + * @param string $filename + * @param array $props Custom overrides for response props (e.g. headers) + * @return self + */ + public static function download(string $file, string $filename = null, array $props = []) + { + if (file_exists($file) === false) { + throw new Exception('The file could not be found'); + } + + $filename = $filename ?? basename($file); + $modified = filemtime($file); + $body = file_get_contents($file); + $size = strlen($body); + + $props = array_replace_recursive([ + 'body' => $body, + 'type' => 'application/force-download', + 'headers' => [ + 'Pragma' => 'public', + 'Cache-Control' => 'no-cache, no-store, must-revalidate', + 'Last-Modified' => gmdate('D, d M Y H:i:s', $modified) . ' GMT', + 'Content-Disposition' => 'attachment; filename="' . $filename . '"', + 'Content-Transfer-Encoding' => 'binary', + 'Content-Length' => $size, + 'Connection' => 'close' + ] + ], $props); + + return new static($props); + } + + /** + * Creates a response for a file and + * sends the file content to the browser + * + * @param string $file + * @param array $props Custom overrides for response props (e.g. headers) + * @return self + */ + public static function file(string $file, array $props = []) + { + $props = array_merge([ + 'body' => F::read($file), + 'type' => F::extensionToMime(F::extension($file)) + ], $props); + + return new static($props); + } + + /** + * Getter for single headers + * + * @param string $key Name of the header + * @return string|null + */ + public function header(string $key): ?string + { + return $this->headers[$key] ?? null; + } + + /** + * Getter for all headers + * + * @return array + */ + public function headers(): array + { + return $this->headers; + } + + /** + * Creates a json response with appropriate + * header and automatic conversion of arrays. + * + * @param string|array $body + * @param int $code + * @param bool $pretty + * @param array $headers + * @return self + */ + public static function json($body = '', ?int $code = null, ?bool $pretty = null, array $headers = []) + { + if (is_array($body) === true) { + $body = json_encode($body, $pretty === true ? JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES : null); + } + + return new static([ + 'body' => $body, + 'code' => $code, + 'type' => 'application/json', + 'headers' => $headers + ]); + } + + /** + * Creates a redirect response, + * which will send the visitor to the + * given location. + * + * @param string $location + * @param int $code + * @return self + */ + public static function redirect(string $location = '/', int $code = 302) + { + return new static([ + 'code' => $code, + 'headers' => [ + 'Location' => Url::unIdn($location) + ] + ]); + } + + /** + * Sends all registered headers and + * returns the response body + * + * @return string + */ + public function send(): string + { + // send the status response code + http_response_code($this->code()); + + // send all custom headers + foreach ($this->headers() as $key => $value) { + header($key . ': ' . $value); + } + + // send the content type header + header('Content-Type:' . $this->type() . '; charset=' . $this->charset()); + + // print the response body + return $this->body(); + } + + /** + * Converts all relevant response attributes + * to an associative array for debugging, + * testing or whatever. + * + * @return array + */ + public function toArray(): array + { + return [ + 'type' => $this->type(), + 'charset' => $this->charset(), + 'code' => $this->code(), + 'headers' => $this->headers(), + 'body' => $this->body() + ]; + } + + /** + * Getter for the content type + * + * @return string + */ + public function type(): string + { + return $this->type; + } +} diff --git a/kirby/src/Http/Route.php b/kirby/src/Http/Route.php new file mode 100644 index 0000000..362b5a2 --- /dev/null +++ b/kirby/src/Http/Route.php @@ -0,0 +1,230 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Route +{ + /** + * The callback action function + * + * @var Closure + */ + protected $action; + + /** + * Listed of parsed arguments + * + * @var array + */ + protected $arguments = []; + + /** + * An array of all passed attributes + * + * @var array + */ + protected $attributes = []; + + /** + * The registered request method + * + * @var string + */ + protected $method; + + /** + * The registered pattern + * + * @var string + */ + protected $pattern; + + /** + * Wildcards, which can be used in + * Route patterns to make regular expressions + * a little more human + * + * @var array + */ + protected $wildcards = [ + 'required' => [ + '(:num)' => '(-?[0-9]+)', + '(:alpha)' => '([a-zA-Z]+)', + '(:alphanum)' => '([a-zA-Z0-9]+)', + '(:any)' => '([a-zA-Z0-9\.\-_%= \+\@\(\)]+)', + '(:all)' => '(.*)', + ], + 'optional' => [ + '/(:num?)' => '(?:/(-?[0-9]+)', + '/(:alpha?)' => '(?:/([a-zA-Z]+)', + '/(:alphanum?)' => '(?:/([a-zA-Z0-9]+)', + '/(:any?)' => '(?:/([a-zA-Z0-9\.\-_%= \+\@\(\)]+)', + '/(:all?)' => '(?:/(.*)', + ], + ]; + + /** + * Magic getter for route attributes + * + * @param string $key + * @param array $arguments + * @return mixed + */ + public function __call(string $key, array $arguments = null) + { + return $this->attributes[$key] ?? null; + } + + /** + * Creates a new Route object for the given + * pattern(s), method(s) and the callback action + * + * @param string|array $pattern + * @param string|array $method + * @param Closure $action + * @param array $attributes + */ + public function __construct($pattern, $method = 'GET', Closure $action, array $attributes = []) + { + $this->action = $action; + $this->attributes = $attributes; + $this->method = $method; + $this->pattern = $this->regex(ltrim($pattern, '/')); + } + + /** + * Getter for the action callback + * + * @return Closure + */ + public function action() + { + return $this->action; + } + + /** + * Returns all parsed arguments + * + * @return array + */ + public function arguments(): array + { + return $this->arguments; + } + + /** + * Getter for additional attributes + * + * @return array + */ + public function attributes(): array + { + return $this->attributes; + } + + /** + * Getter for the method + * + * @return string + */ + public function method(): string + { + return $this->method; + } + + /** + * Returns the route name if set + * + * @return string|null + */ + public function name(): ?string + { + return $this->attributes['name'] ?? null; + } + + /** + * Throws a specific exception to tell + * the router to jump to the next route + * @since 3.0.3 + * + * @return void + */ + public static function next(): void + { + throw new Exceptions\NextRouteException('next'); + } + + /** + * Getter for the pattern + * + * @return string + */ + public function pattern(): string + { + return $this->pattern; + } + + /** + * Converts the pattern into a full regular + * expression by replacing all the wildcards + * + * @param string $pattern + * @return string + */ + public function regex(string $pattern): string + { + $search = array_keys($this->wildcards['optional']); + $replace = array_values($this->wildcards['optional']); + + // For optional parameters, first translate the wildcards to their + // regex equivalent, sans the ")?" ending. We'll add the endings + // back on when we know the replacement count. + $pattern = str_replace($search, $replace, $pattern, $count); + + if ($count > 0) { + $pattern .= str_repeat(')?', $count); + } + + return strtr($pattern, $this->wildcards['required']); + } + + /** + * Tries to match the path with the regular expression and + * extracts all arguments for the Route action + * + * @param string $pattern + * @param string $path + * @return array|false + */ + public function parse(string $pattern, string $path) + { + // check for direct matches + if ($pattern === $path) { + return $this->arguments = []; + } + + // We only need to check routes with regular expression since all others + // would have been able to be matched by the search for literal matches + // we just did before we started searching. + if (strpos($pattern, '(') === false) { + return false; + } + + // If we have a match we'll return all results + // from the preg without the full first match. + if (preg_match('#^' . $this->regex($pattern) . '$#u', $path, $parameters)) { + return $this->arguments = array_slice($parameters, 1); + } + + return false; + } +} diff --git a/kirby/src/Http/Router.php b/kirby/src/Http/Router.php new file mode 100644 index 0000000..389d47a --- /dev/null +++ b/kirby/src/Http/Router.php @@ -0,0 +1,169 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Router +{ + public static $beforeEach; + public static $afterEach; + + /** + * Store for the current route, + * if one can be found + * + * @var Route|null + */ + protected $route; + + /** + * All registered routes, sorted by + * their request method. This makes + * it faster to find the right route + * later. + * + * @var array + */ + protected $routes = [ + 'GET' => [], + 'HEAD' => [], + 'POST' => [], + 'PUT' => [], + 'DELETE' => [], + 'CONNECT' => [], + 'OPTIONS' => [], + 'TRACE' => [], + 'PATCH' => [], + ]; + + /** + * Creates a new router object and + * registers all the given routes + * + * @param array $routes + */ + public function __construct(array $routes = []) + { + foreach ($routes as $props) { + if (isset($props['pattern'], $props['action']) === false) { + throw new InvalidArgumentException('Invalid route parameters'); + } + + $methods = array_map('trim', explode('|', strtoupper($props['method'] ?? 'GET'))); + $patterns = is_array($props['pattern']) === false ? [$props['pattern']] : $props['pattern']; + + if ($methods === ['ALL']) { + $methods = array_keys($this->routes); + } + + foreach ($methods as $method) { + foreach ($patterns as $pattern) { + $this->routes[$method][] = new Route($pattern, $method, $props['action'], $props); + } + } + } + } + + /** + * Calls the Router by path and method. + * This will try to find a Route object + * and then call the Route action with + * the appropriate arguments and a Result + * object. + * + * @param string $path + * @param string $method + * @param Closure|null $callback + * @return mixed + */ + public function call(string $path = null, string $method = 'GET', Closure $callback = null) + { + $path = $path ?? ''; + $ignore = []; + $result = null; + $loop = true; + + while ($loop === true) { + $route = $this->find($path, $method, $ignore); + + if (is_a(static::$beforeEach, 'Closure') === true) { + (static::$beforeEach)($route, $path, $method); + } + + try { + if ($callback) { + $result = $callback($route); + } else { + $result = $route->action()->call($route, ...$route->arguments()); + } + + $loop = false; + } catch (Exceptions\NextRouteException $e) { + $ignore[] = $route; + } + + if (is_a(static::$afterEach, 'Closure') === true) { + $final = $loop === false; + $result = (static::$afterEach)($route, $path, $method, $result, $final); + } + } + + return $result; + } + + /** + * Finds a Route object by path and method + * The Route's arguments method is used to + * find matches and return all the found + * arguments in the path. + * + * @param string $path + * @param string $method + * @param array $ignore + * @return \Kirby\Http\Route|null + */ + public function find(string $path, string $method, array $ignore = null) + { + if (isset($this->routes[$method]) === false) { + throw new InvalidArgumentException('Invalid routing method: ' . $method, 400); + } + + // remove leading and trailing slashes + $path = trim($path, '/'); + + foreach ($this->routes[$method] as $route) { + $arguments = $route->parse($route->pattern(), $path); + + if ($arguments !== false) { + if (empty($ignore) === true || in_array($route, $ignore) === false) { + return $this->route = $route; + } + } + } + + throw new Exception('No route found for path: "' . $path . '" and request method: "' . $method . '"', 404); + } + + /** + * Returns the current route. + * This will only return something, + * once Router::find() has been called + * and only if a route was found. + * + * @return \Kirby\Http\Route|null + */ + public function route() + { + return $this->route; + } +} diff --git a/kirby/src/Http/Server.php b/kirby/src/Http/Server.php new file mode 100644 index 0000000..1ccf919 --- /dev/null +++ b/kirby/src/Http/Server.php @@ -0,0 +1,169 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Server +{ + /** + * Cache for the cli status + * + * @var bool|null + */ + public static $cli; + + /** + * Returns the server's IP address + * + * @return string + */ + public static function address(): string + { + return static::get('SERVER_ADDR'); + } + + /** + * Checks if the request is being served by the CLI + * + * @return bool + */ + public static function cli(): bool + { + if (static::$cli !== null) { + return static::$cli; + } + + if (defined('STDIN') === true) { + return static::$cli = true; + } + + $term = getenv('TERM'); + + if (substr(PHP_SAPI, 0, 3) === 'cgi' && $term && $term !== 'unknown') { + return static::$cli = true; + } + + return static::$cli = false; + } + + /** + * Gets a value from the _SERVER array + * + * + * Server::get('document_root'); + * // sample output: /var/www/kirby + * + * Server::get(); + * // returns the whole server array + * + * + * @param mixed $key The key to look for. Pass false or null to + * return the entire server array. + * @param mixed $default Optional default value, which should be + * returned if no element has been found + * @return mixed + */ + public static function get($key = null, $default = null) + { + if ($key === null) { + return $_SERVER; + } + + $key = strtoupper($key); + $value = $_SERVER[$key] ?? $default; + return static::sanitize($key, $value); + } + + /** + * Help to sanitize some _SERVER keys + * + * @param string $key + * @param mixed $value + * @return mixed + */ + public static function sanitize(string $key, $value) + { + switch ($key) { + case 'SERVER_ADDR': + case 'SERVER_NAME': + case 'HTTP_HOST': + case 'HTTP_X_FORWARDED_HOST': + $value = strip_tags($value); + $value = preg_replace('![^\w.:-]+!iu', '', $value); + $value = trim($value, '-'); + $value = htmlspecialchars($value); + break; + case 'SERVER_PORT': + case 'HTTP_X_FORWARDED_PORT': + $value = (int)(preg_replace('![^0-9]+!', '', $value)); + break; + } + + return $value; + } + + /** + * Returns the correct port number + * + * @param bool $forwarded + * @return int + */ + public static function port(bool $forwarded = false): int + { + $port = $forwarded === true ? static::get('HTTP_X_FORWARDED_PORT') : null; + + if (empty($port) === true) { + $port = static::get('SERVER_PORT'); + } + + return $port; + } + + /** + * Checks for a https request + * + * @return bool + */ + public static function https(): bool + { + if (isset($_SERVER['HTTPS']) && !empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off') { + return true; + } elseif (static::port() === 443) { + return true; + } elseif (in_array(static::get('HTTP_X_FORWARDED_PROTO'), ['https', 'https, http'])) { + return true; + } else { + return false; + } + } + + /** + * Returns the correct host + * + * @param bool $forwarded + * @return string + */ + public static function host(bool $forwarded = false): string + { + $host = $forwarded === true ? static::get('HTTP_X_FORWARDED_HOST') : null; + + if (empty($host) === true) { + $host = static::get('SERVER_NAME'); + } + + if (empty($host) === true) { + $host = static::get('SERVER_ADDR'); + } + + return explode(':', $host)[0]; + } +} diff --git a/kirby/src/Http/Uri.php b/kirby/src/Http/Uri.php new file mode 100644 index 0000000..190ae89 --- /dev/null +++ b/kirby/src/Http/Uri.php @@ -0,0 +1,563 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Uri +{ + use Properties; + + /** + * Cache for the current Uri object + * + * @var Uri|null + */ + public static $current; + + /** + * The fragment after the hash + * + * @var string|false + */ + protected $fragment; + + /** + * The host address + * + * @var string + */ + protected $host; + + /** + * The optional password for basic authentication + * + * @var string|false + */ + protected $password; + + /** + * The optional list of params + * + * @var Params + */ + protected $params; + + /** + * The optional path + * + * @var Path + */ + protected $path; + + /** + * The optional port number + * + * @var int|false + */ + protected $port; + + /** + * All original properties + * + * @var array + */ + protected $props; + + /** + * The optional query string without leading ? + * + * @var Query + */ + protected $query; + + /** + * https or http + * + * @var string + */ + protected $scheme = 'http'; + + /** + * @var bool + */ + protected $slash = false; + + /** + * The optional username for basic authentication + * + * @var string|false + */ + protected $username; + + /** + * Magic caller to access all properties + * + * @param string $property + * @param array $arguments + * @return mixed + */ + public function __call(string $property, array $arguments = []) + { + return $this->$property ?? null; + } + + /** + * Make sure that cloning also clones + * the path and query objects + * + * @return void + */ + public function __clone() + { + $this->path = clone $this->path; + $this->query = clone $this->query; + $this->params = clone $this->params; + } + + /** + * Creates a new URI object + * + * @param array $props + * @param array $inject + */ + public function __construct($props = [], array $inject = []) + { + if (is_string($props) === true) { + $props = parse_url($props); + $props['username'] = $props['user'] ?? null; + $props['password'] = $props['pass'] ?? null; + + $props = array_merge($props, $inject); + } + + // parse the path and extract params + if (empty($props['path']) === false) { + $extract = Params::extract($props['path']); + $props['params'] = $props['params'] ?? $extract['params']; + $props['path'] = $extract['path']; + $props['slash'] = $props['slash'] ?? $extract['slash']; + } + + $this->setProperties($this->props = $props); + } + + /** + * Magic getter + * + * @param string $property + * @return mixed + */ + public function __get(string $property) + { + return $this->$property ?? null; + } + + /** + * Magic setter + * + * @param string $property + * @param mixed $value + */ + public function __set(string $property, $value) + { + if (method_exists($this, 'set' . $property) === true) { + $this->{'set' . $property}($value); + } + } + + /** + * Converts the URL object to string + * + * @return string + */ + public function __toString(): string + { + try { + return $this->toString(); + } catch (Throwable $e) { + return ''; + } + } + + /** + * Returns the auth details (username:password) + * + * @return string|null + */ + public function auth(): ?string + { + $auth = trim($this->username . ':' . $this->password); + return $auth !== ':' ? $auth : null; + } + + /** + * Returns the base url (scheme + host) + * without trailing slash + * + * @return string|null + */ + public function base(): ?string + { + if ($domain = $this->domain()) { + return $this->scheme ? $this->scheme . '://' . $domain : $domain; + } + + return null; + } + + /** + * Clones the Uri object and applies optional + * new props. + * + * @param array $props + * @return self + */ + public function clone(array $props = []) + { + $clone = clone $this; + + foreach ($props as $key => $value) { + $clone->__set($key, $value); + } + + return $clone; + } + + /** + * @param array $props + * @param bool $forwarded + * @return self + */ + public static function current(array $props = [], bool $forwarded = false) + { + if (static::$current !== null) { + return static::$current; + } + + $uri = Server::get('REQUEST_URI'); + $uri = preg_replace('!^(http|https)\:\/\/' . Server::get('HTTP_HOST') . '!', '', $uri); + $uri = parse_url('http://getkirby.com' . $uri); + + $url = new static(array_merge([ + 'scheme' => Server::https() === true ? 'https' : 'http', + 'host' => Server::host($forwarded), + 'port' => Server::port($forwarded), + 'path' => $uri['path'] ?? null, + 'query' => $uri['query'] ?? null, + ], $props)); + + return $url; + } + + /** + * Returns the domain without scheme, path or query + * + * @return string|null + */ + public function domain(): ?string + { + if (empty($this->host) === true || $this->host === '/') { + return null; + } + + $auth = $this->auth(); + $domain = ''; + + if ($auth !== null) { + $domain .= $auth . '@'; + } + + $domain .= $this->host; + + if ($this->port !== null && in_array($this->port, [80, 443]) === false) { + $domain .= ':' . $this->port; + } + + return $domain; + } + + /** + * @return bool + */ + public function hasFragment(): bool + { + return empty($this->fragment) === false; + } + + /** + * @return bool + */ + public function hasPath(): bool + { + return $this->path()->isNotEmpty(); + } + + /** + * @return bool + */ + public function hasQuery(): bool + { + return $this->query()->isNotEmpty(); + } + + /** + * Tries to convert the internationalized host + * name to the human-readable UTF8 representation + * + * @return self + */ + public function idn() + { + if (empty($this->host) === false) { + $this->setHost(Idn::decode($this->host)); + } + return $this; + } + + /** + * Creates an Uri object for the URL to the index.php + * or any other executed script. + * + * @param array $props + * @param bool $forwarded + * @return string + */ + public static function index(array $props = [], bool $forwarded = false) + { + if (Server::cli() === true) { + $path = null; + } else { + $path = Server::get('SCRIPT_NAME'); + // replace Windows backslashes + $path = str_replace('\\', '/', $path); + // remove the script + $path = dirname($path); + // replace those fucking backslashes again + $path = str_replace('\\', '/', $path); + // remove the leading and trailing slashes + $path = trim($path, '/'); + } + + if ($path === '.') { + $path = null; + } + + return static::current(array_merge($props, [ + 'path' => $path, + 'query' => null, + 'fragment' => null, + ]), $forwarded); + } + + + /** + * Checks if the host exists + * + * @return bool + */ + public function isAbsolute(): bool + { + return empty($this->host) === false; + } + + /** + * @param string|null $fragment + * @return self + */ + public function setFragment(string $fragment = null) + { + $this->fragment = $fragment ? ltrim($fragment, '#') : null; + return $this; + } + + /** + * @param string $host + * @return self + */ + public function setHost(string $host = null) + { + $this->host = $host; + return $this; + } + + /** + * @param \Kirby\Http\Params|string|array|null $params + * @return self + */ + public function setParams($params = null) + { + $this->params = is_a($params, 'Kirby\Http\Params') === true ? $params : new Params($params); + return $this; + } + + /** + * @param string|null $password + * @return self + */ + public function setPassword(string $password = null) + { + $this->password = $password; + return $this; + } + + /** + * @param \Kirby\Http\Path|string|array|null $path + * @return self + */ + public function setPath($path = null) + { + $this->path = is_a($path, 'Kirby\Http\Path') === true ? $path : new Path($path); + return $this; + } + + /** + * @param int|null $port + * @return self + */ + public function setPort(int $port = null) + { + if ($port === 0) { + $port = null; + } + + if ($port !== null) { + if ($port < 1 || $port > 65535) { + throw new InvalidArgumentException('Invalid port format: ' . $port); + } + } + + $this->port = $port; + return $this; + } + + /** + * @param \Kirby\Http\Query|string|array|null $query + * @return self + */ + public function setQuery($query = null) + { + $this->query = is_a($query, 'Kirby\Http\Query') === true ? $query : new Query($query); + return $this; + } + + /** + * @param string $scheme + * @return self + */ + public function setScheme(string $scheme = null) + { + if ($scheme !== null && in_array($scheme, ['http', 'https', 'ftp']) === false) { + throw new InvalidArgumentException('Invalid URL scheme: ' . $scheme); + } + + $this->scheme = $scheme; + return $this; + } + + /** + * Set if a trailing slash should be added to + * the path when the URI is being built + * + * @param bool $slash + * @return self + */ + public function setSlash(bool $slash = false) + { + $this->slash = $slash; + return $this; + } + + /** + * @param string|null $username + * @return self + */ + public function setUsername(string $username = null) + { + $this->username = $username; + return $this; + } + + /** + * Converts the Url object to an array + * + * @return array + */ + public function toArray(): array + { + $array = []; + + foreach ($this->propertyData as $key => $value) { + $value = $this->$key; + + if (is_object($value) === true) { + $value = $value->toArray(); + } + + $array[$key] = $value; + } + + return $array; + } + + public function toJson(...$arguments): string + { + return json_encode($this->toArray(), ...$arguments); + } + + /** + * Returns the full URL as string + * + * @return string + */ + public function toString(): string + { + $url = $this->base(); + $slash = true; + + if (empty($url) === true) { + $url = '/'; + $slash = false; + } + + $path = $this->path->toString($slash) . $this->params->toString(true); + + if ($this->slash && $slash === true) { + $path .= '/'; + } + + $url .= $path; + $url .= $this->query->toString(true); + + if (empty($this->fragment) === false) { + $url .= '#' . $this->fragment; + } + + return $url; + } + + /** + * Tries to convert a URL with an internationalized host + * name to the machine-readable Punycode representation + * + * @return self + */ + public function unIdn() + { + if (empty($this->host) === false) { + $this->setHost(Idn::encode($this->host)); + } + return $this; + } +} diff --git a/kirby/src/Http/Url.php b/kirby/src/Http/Url.php new file mode 100644 index 0000000..0335413 --- /dev/null +++ b/kirby/src/Http/Url.php @@ -0,0 +1,287 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Url +{ + /** + * The base Url to build absolute Urls from + * + * @var string + */ + public static $home = '/'; + + /** + * The current Uri object + * + * @var Uri + */ + public static $current = null; + + /** + * Facade for all Uri object methods + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public static function __callStatic(string $method, $arguments) + { + return (new Uri($arguments[0] ?? static::current()))->$method(...array_slice($arguments, 1)); + } + + /** + * Url Builder + * Actually just a factory for `new Uri($parts)` + * + * @param array $parts + * @param string|null $url + * @return string + */ + public static function build(array $parts = [], string $url = null): string + { + return (string)(new Uri($url ?? static::current()))->clone($parts); + } + + /** + * Returns the current url with all bells and whistles + * + * @return string + */ + public static function current(): string + { + return static::$current = static::$current ?? static::toObject()->toString(); + } + + /** + * Returns the url for the current directory + * + * @return string + */ + public static function currentDir(): string + { + return dirname(static::current()); + } + + /** + * Tries to fix a broken url without protocol + * + * @param string $url + * @return string + */ + public static function fix(string $url = null): string + { + // make sure to not touch absolute urls + return (!preg_match('!^(https|http|ftp)\:\/\/!i', $url)) ? 'http://' . $url : $url; + } + + /** + * Returns the home url if defined + * + * @return string + */ + public static function home(): string + { + return static::$home; + } + + /** + * Returns the url to the executed script + * + * @param array $props + * @param bool $forwarded + * @return string + */ + public static function index(array $props = [], bool $forwarded = false): string + { + return Uri::index($props, $forwarded)->toString(); + } + + /** + * Checks if an URL is absolute + * + * @param string $url + * @return bool + */ + public static function isAbsolute(string $url = null): bool + { + // matches the following groups of URLs: + // //example.com/uri + // http://example.com/uri, https://example.com/uri, ftp://example.com/uri + // mailto:example@example.com, geo:49.0158,8.3239?z=11 + return preg_match('!^(//|[a-z0-9+-.]+://|mailto:|tel:|geo:)!i', $url) === 1; + } + + /** + * Convert a relative path into an absolute URL + * + * @param string $path + * @param string $home + * @return string + */ + public static function makeAbsolute(string $path = null, string $home = null): string + { + if ($path === '' || $path === '/' || $path === null) { + return $home ?? static::home(); + } + + if (substr($path, 0, 1) === '#') { + return $path; + } + + if (static::isAbsolute($path)) { + return $path; + } + + // build the full url + $path = ltrim($path, '/'); + $home = $home ?? static::home(); + + if (empty($path) === true) { + return $home; + } + + return $home === '/' ? '/' . $path : $home . '/' . $path; + } + + /** + * Returns the path for the given url + * + * @param string|array|null $url + * @param bool $leadingSlash + * @param bool $trailingSlash + * @return xtring + */ + public static function path($url = null, bool $leadingSlash = false, bool $trailingSlash = false): string + { + return Url::toObject($url)->path()->toString($leadingSlash, $trailingSlash); + } + + /** + * Returns the query for the given url + * + * @param string|array|null $url + * @return string + */ + public static function query($url = null): string + { + return Url::toObject($url)->query()->toString(); + } + + /** + * Return the last url the user has been on if detectable + * + * @return string + */ + public static function last(): string + { + return $_SERVER['HTTP_REFERER'] ?? ''; + } + + /** + * Shortens the Url by removing all unnecessary parts + * + * @param string $url + * @param int $length + * @param bool $base + * @param string $rep + * @return string + */ + public static function short($url = null, int $length = 0, bool $base = false, string $rep = '…'): string + { + $uri = static::toObject($url); + + $uri->fragment = null; + $uri->query = null; + $uri->password = null; + $uri->port = null; + $uri->scheme = null; + $uri->username = null; + + // remove the trailing slash from the path + $uri->slash = false; + + $url = $base ? $uri->base() : $uri->toString(); + $url = str_replace('www.', '', $url); + + return Str::short($url, $length, $rep); + } + + /** + * Removes the path from the Url + * + * @param string $url + * @return string + */ + public static function stripPath($url = null): string + { + return static::toObject($url)->setPath(null)->toString(); + } + + /** + * Removes the query string from the Url + * + * @param string $url + * @return string + */ + public static function stripQuery($url = null): string + { + return static::toObject($url)->setQuery(null)->toString(); + } + + /** + * Removes the fragment (hash) from the Url + * + * @param string $url + * @return string + */ + public static function stripFragment($url = null): string + { + return static::toObject($url)->setFragment(null)->toString(); + } + + /** + * Smart resolver for internal and external urls + * + * @param string $path + * @param mixed $options + * @return string + */ + public static function to(string $path = null, $options = null): string + { + // keep relative urls + if (substr($path, 0, 2) === './' || substr($path, 0, 3) === '../') { + return $path; + } + + $url = static::makeAbsolute($path); + + if ($options === null) { + return $url; + } + + return (new Uri($url, $options))->toString(); + } + + /** + * Converts the Url to a Uri object + * + * @param string $url + * @return \Kirby\Http\Uri + */ + public static function toObject($url = null) + { + return $url === null ? Uri::current() : new Uri($url); + } +} diff --git a/kirby/src/Http/Visitor.php b/kirby/src/Http/Visitor.php new file mode 100644 index 0000000..0a7e8f3 --- /dev/null +++ b/kirby/src/Http/Visitor.php @@ -0,0 +1,252 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Visitor +{ + /** + * IP address + * @var string|null + */ + protected $ip; + + /** + * user agent + * @var string|null + */ + protected $userAgent; + + /** + * accepted language + * @var string|null + */ + protected $acceptedLanguage; + + /** + * accepted mime type + * @var string|null + */ + protected $acceptedMimeType; + + /** + * Creates a new visitor object. + * Optional arguments can be passed to + * modify the information about the visitor. + * + * By default everything is pulled from $_SERVER + * + * @param array $arguments + */ + public function __construct(array $arguments = []) + { + $this->ip($arguments['ip'] ?? $_SERVER['REMOTE_ADDR'] ?? ''); + $this->userAgent($arguments['userAgent'] ?? $_SERVER['HTTP_USER_AGENT'] ?? ''); + $this->acceptedLanguage($arguments['acceptedLanguage'] ?? $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''); + $this->acceptedMimeType($arguments['acceptedMimeType'] ?? $_SERVER['HTTP_ACCEPT'] ?? ''); + } + + /** + * Sets the accepted language if + * provided or returns the user's + * accepted language otherwise + * + * @param string|null $acceptedLanguage + * @return \Kirby\Toolkit\Obj|\Kirby\Http\Visitor|null + */ + public function acceptedLanguage(string $acceptedLanguage = null) + { + if ($acceptedLanguage === null) { + return $this->acceptedLanguages()->first(); + } + + $this->acceptedLanguage = $acceptedLanguage; + return $this; + } + + /** + * Returns an array of all accepted languages + * including their quality and locale + * + * @return \Kirby\Toolkit\Collection + */ + public function acceptedLanguages() + { + $accepted = Str::accepted($this->acceptedLanguage); + $languages = []; + + foreach ($accepted as $language) { + $value = $language['value']; + $parts = Str::split($value, '-'); + $code = isset($parts[0]) ? Str::lower($parts[0]) : null; + $region = isset($parts[1]) ? Str::upper($parts[1]) : null; + $locale = $region ? $code . '_' . $region : $code; + + $languages[$locale] = new Obj([ + 'code' => $code, + 'locale' => $locale, + 'original' => $value, + 'quality' => $language['quality'], + 'region' => $region, + ]); + } + + return new Collection($languages); + } + + /** + * Checks if the user accepts the given language + * + * @param string $code + * @return bool + */ + public function acceptsLanguage(string $code): bool + { + $mode = Str::contains($code, '_') === true ? 'locale' : 'code'; + + foreach ($this->acceptedLanguages() as $language) { + if ($language->$mode() === $code) { + return true; + } + } + + return false; + } + + /** + * Sets the accepted mime type if + * provided or returns the user's + * accepted mime type otherwise + * + * @param string|null $acceptedMimeType + * @return \Kirby\Toolkit\Obj|\Kirby\Http\Visitor + */ + public function acceptedMimeType(string $acceptedMimeType = null) + { + if ($acceptedMimeType === null) { + return $this->acceptedMimeTypes()->first(); + } + + $this->acceptedMimeType = $acceptedMimeType; + return $this; + } + + /** + * Returns a collection of all accepted mime types + * + * @return \Kirby\Toolkit\Collection + */ + public function acceptedMimeTypes() + { + $accepted = Str::accepted($this->acceptedMimeType); + $mimes = []; + + foreach ($accepted as $mime) { + $mimes[$mime['value']] = new Obj([ + 'type' => $mime['value'], + 'quality' => $mime['quality'], + ]); + } + + return new Collection($mimes); + } + + /** + * Checks if the user accepts the given mime type + * + * @param string $mimeType + * @return bool + */ + public function acceptsMimeType(string $mimeType): bool + { + return Mime::isAccepted($mimeType, $this->acceptedMimeType); + } + + /** + * Returns the MIME type from the provided list that + * is most accepted (= preferred) by the visitor + * @since 3.3.0 + * + * @param string ...$mimeTypes MIME types to query for + * @return string|null Preferred MIME type + */ + public function preferredMimeType(string ...$mimeTypes): ?string + { + foreach ($this->acceptedMimeTypes() as $acceptedMime) { + // look for direct matches + if (in_array($acceptedMime->type(), $mimeTypes)) { + return $acceptedMime->type(); + } + + // test each option against wildcard `Accept` values + foreach ($mimeTypes as $expectedMime) { + if (Mime::matches($expectedMime, $acceptedMime->type()) === true) { + return $expectedMime; + } + } + } + + return null; + } + + /** + * Returns true if the visitor prefers a JSON response over + * an HTML response based on the `Accept` request header + * @since 3.3.0 + * + * @return bool + */ + public function prefersJson(): bool + { + return $this->preferredMimeType('application/json', 'text/html') === 'application/json'; + } + + /** + * Sets the ip address if provided + * or returns the ip of the current + * visitor otherwise + * + * @param string|null $ip + * @return string|Visitor|null + */ + public function ip(string $ip = null) + { + if ($ip === null) { + return $this->ip; + } + $this->ip = $ip; + return $this; + } + + /** + * Sets the user agent if provided + * or returns the user agent string of + * the current visitor otherwise + * + * @param string|null $userAgent + * @return string|Visitor|null + */ + public function userAgent(string $userAgent = null) + { + if ($userAgent === null) { + return $this->userAgent; + } + $this->userAgent = $userAgent; + return $this; + } +} diff --git a/kirby/src/Image/Camera.php b/kirby/src/Image/Camera.php new file mode 100644 index 0000000..59d16d2 --- /dev/null +++ b/kirby/src/Image/Camera.php @@ -0,0 +1,93 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Camera +{ + /** + * Make exif data + * + * @var string|null + */ + protected $make; + + /** + * Model exif data + * + * @var string|null + */ + protected $model; + + /** + * Constructor + * + * @param array $exif + */ + public function __construct(array $exif) + { + $this->make = $exif['Make'] ?? null; + $this->model = $exif['Model'] ?? null; + } + + /** + * Returns the make of the camera + * + * @return string + */ + public function make(): ?string + { + return $this->make; + } + + /** + * Returns the camera model + * + * @return string + */ + public function model(): ?string + { + return $this->model; + } + + /** + * Converts the object into a nicely readable array + * + * @return array + */ + public function toArray(): array + { + return [ + 'make' => $this->make, + 'model' => $this->model + ]; + } + + /** + * Returns the full make + model name + * + * @return string + */ + public function __toString(): string + { + return trim($this->make . ' ' . $this->model); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } +} diff --git a/kirby/src/Image/Darkroom.php b/kirby/src/Image/Darkroom.php new file mode 100644 index 0000000..503eddc --- /dev/null +++ b/kirby/src/Image/Darkroom.php @@ -0,0 +1,145 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Darkroom +{ + public static $types = [ + 'gd' => 'Kirby\Image\Darkroom\GdLib', + 'im' => 'Kirby\Image\Darkroom\ImageMagick' + ]; + + protected $settings = []; + + /** + * Darkroom constructor + * + * @param array $settings + */ + public function __construct(array $settings = []) + { + $this->settings = array_merge($this->defaults(), $settings); + } + + /** + * Creates a new Darkroom instance for the given + * type/driver + * + * @param string $type + * @param array $settings + * @return mixed + * @throws \Exception + */ + public static function factory(string $type, array $settings = []) + { + if (isset(static::$types[$type]) === false) { + throw new Exception('Invalid Darkroom type'); + } + + $class = static::$types[$type]; + return new $class($settings); + } + + /** + * Returns the default thumb settings + * + * @return array + */ + protected function defaults(): array + { + return [ + 'autoOrient' => true, + 'crop' => false, + 'blur' => false, + 'grayscale' => false, + 'height' => null, + 'quality' => 90, + 'width' => null, + ]; + } + + /** + * Normalizes all thumb options + * + * @param array $options + * @return array + */ + protected function options(array $options = []): array + { + $options = array_merge($this->settings, $options); + + // normalize the crop option + if ($options['crop'] === true) { + $options['crop'] = 'center'; + } + + // normalize the blur option + if ($options['blur'] === true) { + $options['blur'] = 10; + } + + // normalize the greyscale option + if (isset($options['greyscale']) === true) { + $options['grayscale'] = $options['greyscale']; + unset($options['greyscale']); + } + + // normalize the bw option + if (isset($options['bw']) === true) { + $options['grayscale'] = $options['bw']; + unset($options['bw']); + } + + if ($options['quality'] === null) { + $options['quality'] = $this->settings['quality']; + } + + return $options; + } + + /** + * Calculates the dimensions of the final thumb based + * on the given options and returns a full array with + * all the final options to be used for the image generator + * + * @param string $file + * @param array $options + * @return array + */ + public function preprocess(string $file, array $options = []) + { + $options = $this->options($options); + $image = new Image($file); + $dimensions = $image->dimensions()->thumb($options); + + $options['width'] = $dimensions->width(); + $options['height'] = $dimensions->height(); + + return $options; + } + + /** + * This method must be replaced by the driver to run the + * actual image processing job. + * + * @param string $file + * @param array $options + * @return array + */ + public function process(string $file, array $options = []): array + { + return $this->preprocess($file, $options); + } +} diff --git a/kirby/src/Image/Darkroom/GdLib.php b/kirby/src/Image/Darkroom/GdLib.php new file mode 100644 index 0000000..eda6118 --- /dev/null +++ b/kirby/src/Image/Darkroom/GdLib.php @@ -0,0 +1,109 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class GdLib extends Darkroom +{ + /** + * Processes the image with the SimpleImage library + * + * @param string $file + * @param array $options + * @return array + */ + public function process(string $file, array $options = []): array + { + $options = $this->preprocess($file, $options); + + $image = new SimpleImage(); + $image->fromFile($file); + + $image = $this->resize($image, $options); + $image = $this->autoOrient($image, $options); + $image = $this->blur($image, $options); + $image = $this->grayscale($image, $options); + + $image->toFile($file, null, $options['quality']); + + return $options; + } + + /** + * Activates the autoOrient option in SimpleImage + * unless this is deactivated + * + * @param \claviska\SimpleImage $image + * @param $options + * @return \claviska\SimpleImage + */ + protected function autoOrient(SimpleImage $image, $options) + { + if ($options['autoOrient'] === false) { + return $image; + } + + return $image->autoOrient(); + } + + /** + * Wrapper around SimpleImage's resize and crop methods + * + * @param \claviska\SimpleImage $image + * @param array $options + * @return \claviska\SimpleImage + */ + protected function resize(SimpleImage $image, array $options) + { + if ($options['crop'] === false) { + return $image->resize($options['width'], $options['height']); + } + + return $image->thumbnail($options['width'], $options['height'] ?? $options['width'], $options['crop']); + } + + /** + * Applies the correct blur settings for SimpleImage + * + * @param \claviska\SimpleImage $image + * @param array $options + * @return \claviska\SimpleImage + */ + protected function blur(SimpleImage $image, array $options) + { + if ($options['blur'] === false) { + return $image; + } + + return $image->blur('gaussian', (int)$options['blur']); + } + + /** + * Applies grayscale conversion if activated in the options. + * + * @param \claviska\SimpleImage $image + * @param array $options + * @return \claviska\SimpleImage + */ + protected function grayscale(SimpleImage $image, array $options) + { + if ($options['grayscale'] === false) { + return $image; + } + + return $image->desaturate(); + } +} diff --git a/kirby/src/Image/Darkroom/ImageMagick.php b/kirby/src/Image/Darkroom/ImageMagick.php new file mode 100644 index 0000000..f27dd4c --- /dev/null +++ b/kirby/src/Image/Darkroom/ImageMagick.php @@ -0,0 +1,228 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class ImageMagick extends Darkroom +{ + /** + * Activates imagemagick's auto-orient feature unless + * it is deactivated via the options + * + * @param string $file + * @param array $options + * @return string + */ + protected function autoOrient(string $file, array $options) + { + if ($options['autoOrient'] === true) { + return '-auto-orient'; + } + } + + /** + * Applies the blur settings + * + * @param string $file + * @param array $options + * @return string + */ + protected function blur(string $file, array $options) + { + if ($options['blur'] !== false) { + return '-blur 0x' . $options['blur']; + } + } + + /** + * Keep animated gifs + * + * @param string $file + * @param array $options + * @return string + */ + protected function coalesce(string $file, array $options) + { + if (F::extension($file) === 'gif') { + return '-coalesce'; + } + } + + /** + * Creates the convert command with the right path to the binary file + * + * @param string $file + * @param array $options + * @return string + */ + protected function convert(string $file, array $options): string + { + return sprintf($options['bin'] . ' "%s"', $file); + } + + /** + * Returns additional default parameters for imagemagick + * + * @return array + */ + protected function defaults(): array + { + return parent::defaults() + [ + 'bin' => 'convert', + 'interlace' => false, + ]; + } + + /** + * Applies the correct settings for grayscale images + * + * @param string $file + * @param array $options + * @return string + */ + protected function grayscale(string $file, array $options) + { + if ($options['grayscale'] === true) { + return '-colorspace gray'; + } + } + + /** + * Applies the correct settings for interlaced JPEGs if + * activated via options + * + * @param string $file + * @param array $options + * @return string + */ + protected function interlace(string $file, array $options) + { + if ($options['interlace'] === true) { + return '-interlace line'; + } + } + + /** + * Creates and runs the full imagemagick command + * to process the image + * + * @param string $file + * @param array $options + * @return array + * @throws \Exception + */ + public function process(string $file, array $options = []): array + { + $options = $this->preprocess($file, $options); + $command = []; + + $command[] = $this->convert($file, $options); + $command[] = $this->strip($file, $options); + $command[] = $this->interlace($file, $options); + $command[] = $this->coalesce($file, $options); + $command[] = $this->grayscale($file, $options); + $command[] = $this->autoOrient($file, $options); + $command[] = $this->resize($file, $options); + $command[] = $this->quality($file, $options); + $command[] = $this->blur($file, $options); + $command[] = $this->save($file, $options); + + // remove all null values and join the parts + $command = implode(' ', array_filter($command)); + + // try to execute the command + exec($command, $output, $return); + + // log broken commands + if ($return !== 0) { + throw new Exception('The imagemagick convert command could not be executed: ' . $command); + } + + return $options; + } + + /** + * Applies the correct JPEG compression quality settings + * + * @param string $file + * @param array $options + * @return string + */ + protected function quality(string $file, array $options): string + { + return '-quality ' . $options['quality']; + } + + /** + * Creates the correct options to crop or resize the image + * and translates the crop positions for imagemagick + * + * @param string $file + * @param array $options + * @return string + */ + protected function resize(string $file, array $options): string + { + // simple resize + if ($options['crop'] === false) { + return sprintf('-resize %sx%s!', $options['width'], $options['height']); + } + + $gravities = [ + 'top left' => 'NorthWest', + 'top' => 'North', + 'top right' => 'NorthEast', + 'left' => 'West', + 'center' => 'Center', + 'right' => 'East', + 'bottom left' => 'SouthWest', + 'bottom' => 'South', + 'bottom right' => 'SouthEast' + ]; + + // translate the gravity option into something imagemagick understands + $gravity = $gravities[$options['crop']] ?? 'Center'; + + $command = sprintf('-resize %sx%s^', $options['width'], $options['height']); + $command .= sprintf(' -gravity %s -crop %sx%s+0+0', $gravity, $options['width'], $options['height']); + + return $command; + } + + /** + * Makes sure to not process too many images at once + * which could crash the server + * + * @param string $file + * @param array $options + * @return string + */ + protected function save(string $file, array $options): string + { + return sprintf('-limit thread 1 "%s"', $file); + } + + /** + * Removes all metadata from the image + * + * @param string $file + * @param array $options + * @return string + */ + protected function strip(string $file, array $options): string + { + return '-strip'; + } +} diff --git a/kirby/src/Image/Dimensions.php b/kirby/src/Image/Dimensions.php new file mode 100644 index 0000000..4a20662 --- /dev/null +++ b/kirby/src/Image/Dimensions.php @@ -0,0 +1,430 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Dimensions +{ + /** + * the height of the parent object + * + * @var int + */ + public $height = 0; + + /** + * the width of the parent object + * + * @var int + */ + public $width = 0; + + /** + * Constructor + * + * @param int $width + * @param int $height + */ + public function __construct(int $width, int $height) + { + $this->width = $width; + $this->height = $height; + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + /** + * Echos the dimensions as width × height + * + * @return string + */ + public function __toString(): string + { + return $this->width . ' × ' . $this->height; + } + + /** + * Crops the dimensions by width and height + * + * @param int $width + * @param int|null $height + * @return self + */ + public function crop(int $width, int $height = null) + { + $this->width = $width; + $this->height = $width; + + if ($height !== 0 && $height !== null) { + $this->height = $height; + } + + return $this; + } + + /** + * Returns the height + * + * @return int + */ + public function height() + { + return $this->height; + } + + /** + * Recalculates the width and height to fit into the given box. + * + * + * + * $dimensions = new Dimensions(1200, 768); + * $dimensions->fit(500); + * + * echo $dimensions->width(); + * // output: 500 + * + * echo $dimensions->height(); + * // output: 320 + * + * + * + * @param int $box the max width and/or height + * @param bool $force If true, the dimensions will be + * upscaled to fit the box if smaller + * @return self object with recalculated dimensions + */ + public function fit(int $box, bool $force = false) + { + if ($this->width == 0 || $this->height == 0) { + $this->width = $box; + $this->height = $box; + return $this; + } + + $ratio = $this->ratio(); + + if ($this->width > $this->height) { + // wider than tall + if ($this->width > $box || $force === true) { + $this->width = $box; + } + $this->height = (int)round($this->width / $ratio); + } elseif ($this->height > $this->width) { + // taller than wide + if ($this->height > $box || $force === true) { + $this->height = $box; + } + $this->width = (int)round($this->height * $ratio); + } elseif ($this->width > $box) { + // width = height but bigger than box + $this->width = $box; + $this->height = $box; + } + + return $this; + } + + /** + * Recalculates the width and height to fit the given height + * + * + * + * $dimensions = new Dimensions(1200, 768); + * $dimensions->fitHeight(500); + * + * echo $dimensions->width(); + * // output: 781 + * + * echo $dimensions->height(); + * // output: 500 + * + * + * + * @param int|null $fit the max height + * @param bool $force If true, the dimensions will be + * upscaled to fit the box if smaller + * @return self object with recalculated dimensions + */ + public function fitHeight(int $fit = null, bool $force = false) + { + return $this->fitSize('height', $fit, $force); + } + + /** + * Helper for fitWidth and fitHeight methods + * + * @param string $ref reference (width or height) + * @param int|null $fit the max width + * @param bool $force If true, the dimensions will be + * upscaled to fit the box if smaller + * @return self object with recalculated dimensions + */ + protected function fitSize(string $ref, int $fit = null, bool $force = false) + { + if ($fit === 0 || $fit === null) { + return $this; + } + + if ($this->$ref <= $fit && !$force) { + return $this; + } + + $ratio = $this->ratio(); + $mode = $ref === 'width'; + $this->width = $mode ? $fit : (int)round($fit * $ratio); + $this->height = !$mode ? $fit : (int)round($fit / $ratio); + + return $this; + } + + /** + * Recalculates the width and height to fit the given width + * + * + * + * $dimensions = new Dimensions(1200, 768); + * $dimensions->fitWidth(500); + * + * echo $dimensions->width(); + * // output: 500 + * + * echo $dimensions->height(); + * // output: 320 + * + * + * + * @param int|null $fit the max width + * @param bool $force If true, the dimensions will be + * upscaled to fit the box if smaller + * @return self object with recalculated dimensions + */ + public function fitWidth(int $fit = null, bool $force = false) + { + return $this->fitSize('width', $fit, $force); + } + + /** + * Recalculates the dimensions by the width and height + * + * @param int|null $width the max height + * @param int|null $height the max width + * @param bool $force + * @return self + */ + public function fitWidthAndHeight(int $width = null, int $height = null, bool $force = false) + { + if ($this->width > $this->height) { + $this->fitWidth($width, $force); + + // do another check for the max height + if ($this->height > $height) { + $this->fitHeight($height); + } + } else { + $this->fitHeight($height, $force); + + // do another check for the max width + if ($this->width > $width) { + $this->fitWidth($width); + } + } + + return $this; + } + + /** + * Detect the dimensions for an image file + * + * @param string $root + * @return self + */ + public static function forImage(string $root) + { + if (file_exists($root) === false) { + return new static(0, 0); + } + + $size = getimagesize($root); + return new static($size[0] ?? 0, $size[1] ?? 1); + } + + /** + * Detect the dimensions for a svg file + * + * @param string $root + * @return self + */ + public static function forSvg(string $root) + { + // avoid xml errors + libxml_use_internal_errors(true); + + $content = file_get_contents($root); + $height = 0; + $width = 0; + $xml = simplexml_load_string($content); + + if ($xml !== false) { + $attr = $xml->attributes(); + $width = (float)($attr->width); + $height = (float)($attr->height); + if (($width === 0.0 || $height === 0.0) && empty($attr->viewBox) === false) { + $box = explode(' ', $attr->viewBox); + $width = (float)($box[2] ?? 0); + $height = (float)($box[3] ?? 0); + } + } + + return new static($width, $height); + } + + /** + * Checks if the dimensions are landscape + * + * @return bool + */ + public function landscape(): bool + { + return $this->width > $this->height; + } + + /** + * Returns a string representation of the orientation + * + * @return string|false + */ + public function orientation() + { + if (!$this->ratio()) { + return false; + } + + if ($this->portrait()) { + return 'portrait'; + } + + if ($this->landscape()) { + return 'landscape'; + } + + return 'square'; + } + + /** + * Checks if the dimensions are portrait + * + * @return bool + */ + public function portrait(): bool + { + return $this->height > $this->width; + } + + /** + * Calculates and returns the ratio + * + * + * + * $dimensions = new Dimensions(1200, 768); + * echo $dimensions->ratio(); + * // output: 1.5625 + * + * + * + * @return float + */ + public function ratio(): float + { + if ($this->width !== 0 && $this->height !== 0) { + return $this->width / $this->height; + } + + return 0; + } + + /** + * @param int|null $width + * @param int|null $height + * @param bool $force + * @return self + */ + public function resize(int $width = null, int $height = null, bool $force = false) + { + return $this->fitWidthAndHeight($width, $height, $force); + } + + /** + * Checks if the dimensions are square + * + * @return bool + */ + public function square(): bool + { + return $this->width == $this->height; + } + + /** + * Resize and crop + * + * @param array $options + * @return self + */ + public function thumb(array $options = []) + { + $width = $options['width'] ?? null; + $height = $options['height'] ?? null; + $crop = $options['crop'] ?? false; + $method = $crop !== false ? 'crop' : 'resize'; + + if ($width === null && $height === null) { + return $this; + } + + return $this->$method($width, $height); + } + + /** + * Converts the dimensions object + * to a plain PHP array + * + * @return array + */ + public function toArray(): array + { + return [ + 'width' => $this->width(), + 'height' => $this->height(), + 'ratio' => $this->ratio(), + 'orientation' => $this->orientation(), + ]; + } + + /** + * Returns the width + * + * @return int + */ + public function width(): int + { + return $this->width; + } +} diff --git a/kirby/src/Image/Exif.php b/kirby/src/Image/Exif.php new file mode 100644 index 0000000..3452179 --- /dev/null +++ b/kirby/src/Image/Exif.php @@ -0,0 +1,296 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Exif +{ + /** + * the parent image object + * @var Image + */ + protected $image; + + /** + * the raw exif array + * @var array + */ + protected $data = []; + + /** + * the camera object with model and make + * @var Camera + */ + protected $camera; + + /** + * the location object + * @var Location + */ + protected $location; + + /** + * the timestamp + * + * @var string + */ + protected $timestamp; + + /** + * the exposure value + * + * @var string + */ + protected $exposure; + + /** + * the aperture value + * + * @var string + */ + protected $aperture; + + /** + * iso value + * + * @var string + */ + protected $iso; + + /** + * focal length + * + * @var string + */ + protected $focalLength; + + /** + * color or black/white + * @var bool + */ + protected $isColor; + + /** + * Constructor + * + * @param \Kirby\Image\Image $image + */ + public function __construct(Image $image) + { + $this->image = $image; + $this->data = $this->read(); + $this->parse(); + } + + /** + * Returns the raw data array from the parser + * + * @return array + */ + public function data(): array + { + return $this->data; + } + + /** + * Returns the Camera object + * + * @return \Kirby\Image\Camera|null + */ + public function camera() + { + if ($this->camera !== null) { + return $this->camera; + } + + return $this->camera = new Camera($this->data); + } + + /** + * Returns the location object + * + * @return \Kirby\Image\Location|null + */ + public function location() + { + if ($this->location !== null) { + return $this->location; + } + + return $this->location = new Location($this->data); + } + + /** + * Returns the timestamp + * + * @return string|null + */ + public function timestamp() + { + return $this->timestamp; + } + + /** + * Returns the exposure + * + * @return string|null + */ + public function exposure() + { + return $this->exposure; + } + + /** + * Returns the aperture + * + * @return string|null + */ + public function aperture() + { + return $this->aperture; + } + + /** + * Returns the iso value + * + * @return int|null + */ + public function iso() + { + return $this->iso; + } + + /** + * Checks if this is a color picture + * + * @return bool|null + */ + public function isColor() + { + return $this->isColor; + } + + /** + * Checks if this is a bw picture + * + * @return bool|null + */ + public function isBW(): ?bool + { + return ($this->isColor !== null) ? $this->isColor === false : null; + } + + /** + * Returns the focal length + * + * @return string|null + */ + public function focalLength() + { + return $this->focalLength; + } + + /** + * Read the exif data of the image object if possible + * + * @return mixed + */ + protected function read(): array + { + if (function_exists('exif_read_data') === false) { + return []; + } + + $data = @exif_read_data($this->image->root()); + return is_array($data) ? $data : []; + } + + /** + * Get all computed data + * + * @return array + */ + protected function computed(): array + { + return $this->data['COMPUTED'] ?? []; + } + + /** + * Pareses and stores all relevant exif data + */ + protected function parse() + { + $this->timestamp = $this->parseTimestamp(); + $this->exposure = $this->data['ExposureTime'] ?? null; + $this->iso = $this->data['ISOSpeedRatings'] ?? null; + $this->focalLength = $this->parseFocalLength(); + $this->aperture = $this->computed()['ApertureFNumber'] ?? null; + $this->isColor = V::accepted($this->computed()['IsColor'] ?? null); + } + + /** + * Return the timestamp when the picture has been taken + * + * @return string|int + */ + protected function parseTimestamp() + { + if (isset($this->data['DateTimeOriginal']) === true) { + return strtotime($this->data['DateTimeOriginal']); + } + + return $this->data['FileDateTime'] ?? $this->image->modified(); + } + + /** + * Teturn the focal length + * + * @return string|null + */ + protected function parseFocalLength() + { + return $this->data['FocalLength'] ?? $this->data['FocalLengthIn35mmFilm'] ?? null; + } + + /** + * Converts the object into a nicely readable array + * + * @return array + */ + public function toArray(): array + { + return [ + 'camera' => $this->camera() ? $this->camera()->toArray() : null, + 'location' => $this->location() ? $this->location()->toArray() : null, + 'timestamp' => $this->timestamp(), + 'exposure' => $this->exposure(), + 'aperture' => $this->aperture(), + 'iso' => $this->iso(), + 'focalLength' => $this->focalLength(), + 'isColor' => $this->isColor() + ]; + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return array_merge($this->toArray(), [ + 'camera' => $this->camera(), + 'location' => $this->location() + ]); + } +} diff --git a/kirby/src/Image/Image.php b/kirby/src/Image/Image.php new file mode 100644 index 0000000..9a512b5 --- /dev/null +++ b/kirby/src/Image/Image.php @@ -0,0 +1,310 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Image extends File +{ + /** + * optional url where the file is reachable + * @var string + */ + protected $url; + + /** + * @var \Kirby\Image\Exif|null + */ + protected $exif; + + /** + * @var \Kirby\Image\Dimensions|null + */ + protected $dimensions; + + /** + * Constructor + * + * @param string|null $root + * @param string|null $url + */ + public function __construct(string $root = null, string $url = null) + { + parent::__construct($root); + $this->url = $url; + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return array_merge($this->toArray(), [ + 'dimensions' => $this->dimensions(), + 'exif' => $this->exif(), + ]); + } + + /** + * Returns a full link to this file + * Perfect for debugging in connection with echo + * + * @return string + */ + public function __toString(): string + { + return $this->root; + } + + /** + * Returns the dimensions of the file if possible + * + * @return \Kirby\Image\Dimensions + */ + public function dimensions() + { + if ($this->dimensions !== null) { + return $this->dimensions; + } + + if (in_array($this->mime(), ['image/jpeg', 'image/jp2', 'image/png', 'image/gif', 'image/webp'])) { + return $this->dimensions = Dimensions::forImage($this->root); + } + + if ($this->extension() === 'svg') { + return $this->dimensions = Dimensions::forSvg($this->root); + } + + return $this->dimensions = new Dimensions(0, 0); + } + + /* + * Automatically sends all needed headers for the file to be downloaded + * and echos the file's content + * + * @param string|null $filename Optional filename for the download + * @return string + */ + public function download($filename = null): string + { + return Response::download($this->root, $filename ?? $this->filename()); + } + + /** + * Returns the exif object for this file (if image) + * + * @return \Kirby\Image\Exif + */ + public function exif() + { + if ($this->exif !== null) { + return $this->exif; + } + $this->exif = new Exif($this); + return $this->exif; + } + + /** + * Sends an appropriate header for the asset + * + * @param bool $send + * @return \Kirby\Http\Response|string + */ + public function header(bool $send = true) + { + $response = new Response('', $this->mime()); + return $send === true ? $response->send() : $response; + } + + /** + * Returns the height of the asset + * + * @return int + */ + public function height(): int + { + return $this->dimensions()->height(); + } + + /** + * @param array $attr + * @return string + */ + public function html(array $attr = []): string + { + return Html::img($this->url(), $attr); + } + + /** + * Returns the PHP imagesize array + * + * @return array + */ + public function imagesize(): array + { + return getimagesize($this->root); + } + + /** + * Checks if the dimensions of the asset are portrait + * + * @return bool + */ + public function isPortrait(): bool + { + return $this->dimensions()->portrait(); + } + + /** + * Checks if the dimensions of the asset are landscape + * + * @return bool + */ + public function isLandscape(): bool + { + return $this->dimensions()->landscape(); + } + + /** + * Checks if the dimensions of the asset are square + * + * @return bool + */ + public function isSquare(): bool + { + return $this->dimensions()->square(); + } + + /** + * Runs a set of validations on the image object + * + * @param array $rules + * @return bool + * @throws \Exception + */ + public function match(array $rules): bool + { + if (($rules['mime'] ?? null) !== null) { + if (Mime::isAccepted($this->mime(), $rules['mime']) !== true) { + throw new Exception(I18n::template('error.file.mime.invalid', [ + 'mime' => $this->mime() + ])); + } + } + + $rules = array_change_key_case($rules); + + $validations = [ + 'maxsize' => ['size', 'max'], + 'minsize' => ['size', 'min'], + 'maxwidth' => ['width', 'max'], + 'minwidth' => ['width', 'min'], + 'maxheight' => ['height', 'max'], + 'minheight' => ['height', 'min'], + 'orientation' => ['orientation', 'same'] + ]; + + foreach ($validations as $key => $arguments) { + $rule = $rules[$key] ?? null; + + if ($rule !== null) { + $property = $arguments[0]; + $validator = $arguments[1]; + + if (V::$validator($this->$property(), $rule) === false) { + throw new Exception(I18n::template('error.file.' . $key, [ + $property => $rule + ])); + } + } + } + + return true; + } + + /** + * Returns the ratio of the asset + * + * @return float + */ + public function ratio(): float + { + return $this->dimensions()->ratio(); + } + + /** + * Returns the orientation as string + * landscape | portrait | square + * + * @return string + */ + public function orientation(): string + { + return $this->dimensions()->orientation(); + } + + /** + * Converts the media object to a + * plain PHP array + * + * @return array + */ + public function toArray(): array + { + return array_merge(parent::toArray(), [ + 'dimensions' => $this->dimensions()->toArray(), + 'exif' => $this->exif()->toArray(), + ]); + } + + /** + * Converts the entire file array into + * a json string + * + * @return string + */ + public function toJson(): string + { + return json_encode($this->toArray()); + } + + /** + * Returns the url + * + * @return string + */ + public function url() + { + return $this->url; + } + + /** + * Returns the width of the asset + * + * @return int + */ + public function width(): int + { + return $this->dimensions()->width(); + } +} diff --git a/kirby/src/Image/Location.php b/kirby/src/Image/Location.php new file mode 100644 index 0000000..2c4e386 --- /dev/null +++ b/kirby/src/Image/Location.php @@ -0,0 +1,136 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Location +{ + /** + * latitude + * + * @var float|null + */ + protected $lat; + + /** + * longitude + * + * @var float|null + */ + protected $lng; + + /** + * Constructor + * + * @param array $exif The entire exif array + */ + public function __construct(array $exif) + { + if (isset($exif['GPSLatitude']) === true && + isset($exif['GPSLatitudeRef']) === true && + isset($exif['GPSLongitude']) === true && + isset($exif['GPSLongitudeRef']) === true + ) { + $this->lat = $this->gps($exif['GPSLatitude'], $exif['GPSLatitudeRef']); + $this->lng = $this->gps($exif['GPSLongitude'], $exif['GPSLongitudeRef']); + } + } + + /** + * Returns the latitude + * + * @return float|null + */ + public function lat() + { + return $this->lat; + } + + /** + * Returns the longitude + * + * @return float|null + */ + public function lng() + { + return $this->lng; + } + + /** + * Converts the gps coordinates + * + * @param string|array $coord + * @param string $hemi + * @return float + */ + protected function gps($coord, string $hemi): float + { + $degrees = count($coord) > 0 ? $this->num($coord[0]) : 0; + $minutes = count($coord) > 1 ? $this->num($coord[1]) : 0; + $seconds = count($coord) > 2 ? $this->num($coord[2]) : 0; + + $hemi = strtoupper($hemi); + $flip = ($hemi == 'W' || $hemi == 'S') ? -1 : 1; + + return $flip * ($degrees + $minutes / 60 + $seconds / 3600); + } + + /** + * Converts coordinates to floats + * + * @param string $part + * @return float + */ + protected function num(string $part): float + { + $parts = explode('/', $part); + + if (count($parts) == 1) { + return $parts[0]; + } + + return (float)($parts[0]) / (float)($parts[1]); + } + + /** + * Converts the object into a nicely readable array + * + * @return array + */ + public function toArray(): array + { + return [ + 'lat' => $this->lat(), + 'lng' => $this->lng() + ]; + } + + /** + * Echos the entire location as lat, lng + * + * @return string + */ + public function __toString(): string + { + return trim(trim($this->lat() . ', ' . $this->lng(), ',')); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } +} diff --git a/kirby/src/Session/AutoSession.php b/kirby/src/Session/AutoSession.php new file mode 100644 index 0000000..cf9b33a --- /dev/null +++ b/kirby/src/Session/AutoSession.php @@ -0,0 +1,171 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class AutoSession +{ + protected $sessions; + protected $options; + + protected $createdSession; + + /** + * Creates a new AutoSession instance + * + * @param \Kirby\Session\SessionStore|string $store SessionStore object or a path to the storage directory (uses the FileSessionStore) + * @param array $options Optional additional options: + * - `durationNormal`: Duration of normal sessions in seconds; defaults to 2 hours + * - `durationLong`: Duration of "remember me" sessions in seconds; defaults to 2 weeks + * - `timeout`: Activity timeout in seconds (integer or false for none); *only* used for normal sessions; defaults to `1800` (half an hour) + * - `cookieName`: Name to use for the session cookie; defaults to `kirby_session` + * - `gcInterval`: How often should the garbage collector be run?; integer or `false` for never; defaults to `100` + */ + public function __construct($store, array $options = []) + { + // merge options with defaults + $this->options = array_merge([ + 'durationNormal' => 7200, + 'durationLong' => 1209600, + 'timeout' => 1800, + 'cookieName' => 'kirby_session', + 'gcInterval' => 100 + ], $options); + + // create an internal instance of the low-level Sessions class + $this->sessions = new Sessions($store, [ + 'cookieName' => $this->options['cookieName'], + 'gcInterval' => $this->options['gcInterval'] + ]); + } + + /** + * Returns the automatic session + * + * @param array $options Optional additional options: + * - `detect`: Whether to allow sessions in the `Authorization` HTTP header (`true`) or only in the session cookie (`false`); defaults to `false` + * - `createMode`: When creating a new session, should it be set as a cookie or is it going to be transmitted manually to be used in a header?; defaults to `cookie` + * - `long`: Whether the session is a long "remember me" session or a normal session; defaults to `false` + * @return \Kirby\Session\Session + */ + public function get(array $options = []) + { + // merge options with defaults + $options = array_merge([ + 'detect' => false, + 'createMode' => 'cookie', + 'long' => false + ], $options); + + // determine expiry options based on the session type + if ($options['long'] === true) { + $duration = $this->options['durationLong']; + $timeout = false; + } else { + $duration = $this->options['durationNormal']; + $timeout = $this->options['timeout']; + } + + // get the current session + if ($options['detect'] === true) { + $session = $this->sessions->currentDetected(); + } else { + $session = $this->sessions->current(); + } + + // create a new session + if ($session === null) { + $session = $this->createdSession ?? $this->sessions->create([ + 'mode' => $options['createMode'], + 'startTime' => time(), + 'expiryTime' => time() + $duration, + 'timeout' => $timeout, + 'renewable' => true, + ]); + + // cache the newly created session to ensure that we don't create multiple + $this->createdSession = $session; + } + + // update the session configuration if the $options changed + // always use the less strict value for compatibility with features + // that depend on the less strict behavior + if ($duration > $session->duration()) { + // the duration needs to be extended + $session->duration($duration); + } + if ($session->timeout() !== false) { + // a timeout exists + if ($timeout === false) { + // it needs to be completely disabled + $session->timeout(false); + } elseif (is_int($timeout) && $timeout > $session->timeout()) { + // it needs to be extended + $session->timeout($timeout); + } + } + + // if the session has been created and was not yet initialized, + // update the mode to a custom mode + // don't update back to cookie mode because the "special" behavior always wins + if ($session->token() === null && $options['createMode'] !== 'cookie') { + $session->mode($options['createMode']); + } + + return $session; + } + + /** + * Creates a new empty session that is *not* automatically transmitted to the client + * Useful for custom applications like a password reset link + * Does *not* affect the automatic session + * + * @param array $options Optional additional options: + * - `startTime`: Time the session starts being valid (date string or timestamp); defaults to `now` + * - `expiryTime`: Time the session expires (date string or timestamp); defaults to `+ 2 hours` + * - `timeout`: Activity timeout in seconds (integer or false for none); defaults to `1800` (half an hour) + * - `renewable`: Should it be possible to extend the expiry date?; defaults to `true` + * @return \Kirby\Session\Session + */ + public function createManually(array $options = []) + { + // only ever allow manual transmission mode + // to prevent overwriting our "auto" session + $options['mode'] = 'manual'; + + return $this->sessions->create($options); + } + + /** + * Returns the specified Session object + * @since 3.3.1 + * + * @param string $token Session token, either including or without the key + * @return \Kirby\Session\Session + */ + public function getManually(string $token) + { + return $this->sessions->get($token, 'manual'); + } + + /** + * Deletes all expired sessions + * + * If the `gcInterval` is configured, this is done automatically + * when intializing the AutoSession class + * + * @return void + */ + public function collectGarbage() + { + $this->sessions->collectGarbage(); + } +} diff --git a/kirby/src/Session/FileSessionStore.php b/kirby/src/Session/FileSessionStore.php new file mode 100644 index 0000000..aeaee14 --- /dev/null +++ b/kirby/src/Session/FileSessionStore.php @@ -0,0 +1,484 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class FileSessionStore extends SessionStore +{ + protected $path; + + // state of the session files + protected $handles = []; + protected $isLocked = []; + + /** + * Creates a new instance of the file session store + * + * @param string $path Path to the storage directory + */ + public function __construct(string $path) + { + // create the directory if it doesn't already exist + Dir::make($path, true); + + // store the canonicalized path + $this->path = realpath($path); + + // make sure it is usable for storage + if (!is_writable($this->path)) { + throw new Exception([ + 'key' => 'session.filestore.dirNotWritable', + 'data' => ['path' => $this->path], + 'fallback' => 'The session storage directory "' . $path . '" is not writable', + 'translate' => false, + 'httpCode' => 500 + ]); + } + } + + /** + * Creates a new session ID with the given expiry time + * + * Needs to make sure that the session does not already exist + * and needs to reserve it by locking it exclusively. + * + * @param int $expiryTime Timestamp + * @return string Randomly generated session ID (without timestamp) + */ + public function createId(int $expiryTime): string + { + clearstatcache(); + do { + // use helper from the abstract SessionStore class + $id = static::generateId(); + + $name = $this->name($expiryTime, $id); + $path = $this->path($name); + } while (file_exists($path)); + + // reserve the file + touch($path); + $this->lock($expiryTime, $id); + + // ensure that no other thread already wrote to the same file, otherwise try again + // very unlikely scenario! + $contents = $this->get($expiryTime, $id); + if ($contents !== '') { + // @codeCoverageIgnoreStart + $this->unlock($expiryTime, $id); + return $this->createId($expiryTime); + // @codeCoverageIgnoreEnd + } + + return $id; + } + + /** + * Checks if the given session exists + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return bool true: session exists, + * false: session doesn't exist + */ + public function exists(int $expiryTime, string $id): bool + { + $name = $this->name($expiryTime, $id); + $path = $this->path($name); + + clearstatcache(); + return is_file($path) === true; + } + + /** + * Locks the given session exclusively + * + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return void + */ + public function lock(int $expiryTime, string $id) + { + $name = $this->name($expiryTime, $id); + $path = $this->path($name); + + // check if the file is already locked + if (isset($this->isLocked[$name])) { + return; + } + + // lock it exclusively + $handle = $this->handle($name); + $result = flock($handle, LOCK_EX); + + // make a note that the file is now locked + if ($result === true) { + $this->isLocked[$name] = true; + } else { + // @codeCoverageIgnoreStart + throw new Exception([ + 'key' => 'session.filestore.unexpectedFilesystemError', + 'fallback' => 'Unexpected file system error', + 'translate' => false, + 'httpCode' => 500 + ]); + // @codeCoverageIgnoreEnd + } + } + + /** + * Removes all locks on the given session + * + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return void + */ + public function unlock(int $expiryTime, string $id) + { + $name = $this->name($expiryTime, $id); + $path = $this->path($name); + + // check if the file is already unlocked or doesn't exist + if (!isset($this->isLocked[$name])) { + return; + } elseif ($this->exists($expiryTime, $id) === false) { + unset($this->isLocked[$name]); + return; + } + + // remove the exclusive lock + $handle = $this->handle($name); + $result = flock($handle, LOCK_UN); + + // make a note that the file is no longer locked + if ($result === true) { + unset($this->isLocked[$name]); + } else { + // @codeCoverageIgnoreStart + throw new Exception([ + 'key' => 'session.filestore.unexpectedFilesystemError', + 'fallback' => 'Unexpected file system error', + 'translate' => false, + 'httpCode' => 500 + ]); + // @codeCoverageIgnoreEnd + } + } + + /** + * Returns the stored session data of the given session + * + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return string + */ + public function get(int $expiryTime, string $id): string + { + $name = $this->name($expiryTime, $id); + $path = $this->path($name); + $handle = $this->handle($name); + + // set read lock to prevent other threads from corrupting the data while we read it + // only if we don't already have a write lock, which is even better + if (!isset($this->isLocked[$name])) { + $result = flock($handle, LOCK_SH); + + if ($result !== true) { + // @codeCoverageIgnoreStart + throw new Exception([ + 'key' => 'session.filestore.unexpectedFilesystemError', + 'fallback' => 'Unexpected file system error', + 'translate' => false, + 'httpCode' => 500 + ]); + // @codeCoverageIgnoreEnd + } + } + + clearstatcache(); + $filesize = filesize($path); + if ($filesize > 0) { + // always read the whole file + rewind($handle); + $string = fread($handle, $filesize); + } else { + // we don't need to read empty files + $string = ''; + } + + // remove the shared lock if we set one above + if (!isset($this->isLocked[$name])) { + $result = flock($handle, LOCK_UN); + + if ($result !== true) { + // @codeCoverageIgnoreStart + throw new Exception([ + 'key' => 'session.filestore.unexpectedFilesystemError', + 'fallback' => 'Unexpected file system error', + 'translate' => false, + 'httpCode' => 500 + ]); + // @codeCoverageIgnoreEnd + } + } + + return $string; + } + + /** + * Stores data to the given session + * + * Needs to make sure that the session exists. + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @param string $data Session data to write + * @return void + */ + public function set(int $expiryTime, string $id, string $data) + { + $name = $this->name($expiryTime, $id); + $path = $this->path($name); + $handle = $this->handle($name); + + // validate that we have an exclusive lock already + if (!isset($this->isLocked[$name])) { + throw new LogicException([ + 'key' => 'session.filestore.notLocked', + 'data' => ['name' => $name], + 'fallback' => 'Cannot write to session "' . $name . '", because it is not locked', + 'translate' => false, + 'httpCode' => 500 + ]); + } + + // delete all file contents first + if (rewind($handle) !== true || ftruncate($handle, 0) !== true) { + // @codeCoverageIgnoreStart + throw new Exception([ + 'key' => 'session.filestore.unexpectedFilesystemError', + 'fallback' => 'Unexpected file system error', + 'translate' => false, + 'httpCode' => 500 + ]); + // @codeCoverageIgnoreEnd + } + + // write the new contents + $result = fwrite($handle, $data); + if (!is_int($result) || $result === 0) { + // @codeCoverageIgnoreStart + throw new Exception([ + 'key' => 'session.filestore.unexpectedFilesystemError', + 'fallback' => 'Unexpected file system error', + 'translate' => false, + 'httpCode' => 500 + ]); + // @codeCoverageIgnoreEnd + } + } + + /** + * Deletes the given session + * + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return void + */ + public function destroy(int $expiryTime, string $id) + { + $name = $this->name($expiryTime, $id); + $path = $this->path($name); + + // close the file, otherwise we can't delete it on Windows; + // deletion is *not* thread-safe because of this, but + // resurrection of the file is prevented in $this->set() because of + // the check in $this->handle() every time any method is called + $this->unlock($expiryTime, $id); + $this->closeHandle($name); + + // we don't need to delete files that don't exist anymore + if ($this->exists($expiryTime, $id) === false) { + return; + } + + // file still exists, delete it + if (@unlink($path) !== true) { + // @codeCoverageIgnoreStart + throw new Exception([ + 'key' => 'session.filestore.unexpectedFilesystemError', + 'fallback' => 'Unexpected file system error', + 'translate' => false, + 'httpCode' => 500 + ]); + // @codeCoverageIgnoreEnd + } + } + + /** + * Deletes all expired sessions + * + * Needs to throw an Exception on error. + * + * @return void + */ + public function collectGarbage() + { + $iterator = new FilesystemIterator($this->path); + + $currentTime = time(); + foreach ($iterator as $file) { + // make sure that the file is a session file + // prevents deleting files like .gitignore or other unrelated files + if (preg_match('/^[0-9]+\.[a-z0-9]+\.sess$/', $file->getFilename()) !== 1) { + continue; + } + + // extract the data from the filename + $name = $file->getBasename('.sess'); + $expiryTime = (int)Str::before($name, '.'); + $id = Str::after($name, '.'); + + if ($expiryTime < $currentTime) { + // the session has expired, delete it + $this->destroy($expiryTime, $id); + } + } + } + + /** + * Cleans up the open locks and file handles + * + * @codeCoverageIgnore + */ + public function __destruct() + { + // unlock all locked files + foreach ($this->isLocked as $name => $locked) { + $expiryTime = (int)Str::before($name, '.'); + $id = Str::after($name, '.'); + + $this->unlock($expiryTime, $id); + } + + // close all file handles + foreach ($this->handles as $name => $handle) { + $this->closeHandle($name); + } + } + + /** + * Returns the combined name based on expiry time and ID + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return string + */ + protected function name(int $expiryTime, string $id): string + { + return $expiryTime . '.' . $id; + } + + /** + * Returns the full path to the session file + * + * @param string $name Combined name + * @return string + */ + protected function path(string $name): string + { + return $this->path . '/' . $name . '.sess'; + } + + /** + * Returns a PHP file handle for a session + * + * @param string $name Combined name + * @return resource File handle + */ + protected function handle(string $name) + { + // always verify that the file still exists, even if we already have a handle; + // ensures thread-safeness for recently deleted sessions, see $this->destroy() + $path = $this->path($name); + clearstatcache(); + if (!is_file($path)) { + throw new NotFoundException([ + 'key' => 'session.filestore.notFound', + 'data' => ['name' => $name], + 'fallback' => 'Session file "' . $name . '" does not exist', + 'translate' => false, + 'httpCode' => 404 + ]); + } + + // return from cache + if (isset($this->handles[$name])) { + return $this->handles[$name]; + } + + // open a new handle + $handle = @fopen($path, 'r+b'); + if (!is_resource($handle)) { + throw new Exception([ + 'key' => 'session.filestore.notOpened', + 'data' => ['name' => $name], + 'fallback' => 'Session file "' . $name . '" could not be opened', + 'translate' => false, + 'httpCode' => 500 + ]); + } + + return $this->handles[$name] = $handle; + } + + /** + * Closes an open file handle + * + * @param string $name Combined name + * @return void + */ + protected function closeHandle(string $name) + { + if (!isset($this->handles[$name])) { + return; + } + $handle = $this->handles[$name]; + + unset($this->handles[$name]); + $result = fclose($handle); + + if ($result !== true) { + // @codeCoverageIgnoreStart + throw new Exception([ + 'key' => 'session.filestore.unexpectedFilesystemError', + 'fallback' => 'Unexpected file system error', + 'translate' => false, + 'httpCode' => 500 + ]); + // @codeCoverageIgnoreEnd + } + } +} diff --git a/kirby/src/Session/Session.php b/kirby/src/Session/Session.php new file mode 100644 index 0000000..acb5e7c --- /dev/null +++ b/kirby/src/Session/Session.php @@ -0,0 +1,767 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Session +{ + // parent data + protected $sessions; + protected $mode; + + // parts of the token + protected $tokenExpiry; + protected $tokenId; + protected $tokenKey; + + // persistent data + protected $startTime; + protected $expiryTime; + protected $duration; + protected $timeout; + protected $lastActivity; + protected $renewable; + protected $data; + protected $newSession; + + // temporary state flags + protected $updatingLastActivity = false; + protected $destroyed = false; + protected $writeMode = false; + protected $needsRetransmission = false; + + /** + * Creates a new Session instance + * + * @param \Kirby\Session\Sessions $sessions Parent sessions object + * @param string|null $token Session token or null for a new session + * @param array $options Optional additional options: + * - `mode`: Token transmission mode (cookie or manual); defaults to `cookie` + * - `startTime`: Time the session starts being valid (date string or timestamp); defaults to `now` + * - `expiryTime`: Time the session expires (date string or timestamp); defaults to `+ 2 hours` + * - `timeout`: Activity timeout in seconds (integer or false for none); defaults to `1800` (half an hour) + * - `renewable`: Should it be possible to extend the expiry date?; defaults to `true` + */ + public function __construct(Sessions $sessions, $token, array $options) + { + $this->sessions = $sessions; + $this->mode = $options['mode'] ?? 'cookie'; + + if (is_string($token)) { + // existing session + + // set the token as instance vars + $this->parseToken($token); + + // initialize, but only try to write to the session if not read-only + // (only the case for moved sessions) + $this->init(); + if ($this->tokenKey !== null) { + $this->autoRenew(); + } + } elseif ($token === null) { + // new session + + // set data based on options + $this->startTime = static::timeToTimestamp($options['startTime'] ?? time()); + $this->expiryTime = static::timeToTimestamp($options['expiryTime'] ?? '+ 2 hours', $this->startTime); + $this->duration = $this->expiryTime - $this->startTime; + $this->timeout = $options['timeout'] ?? 1800; + $this->renewable = $options['renewable'] ?? true; + $this->data = new SessionData($this, []); + + // validate persistent data + if (time() > $this->expiryTime) { + // session must not already be expired, but the start time may be in the future + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::__construct', 'argument' => '$options[\'expiryTime\']'], + 'translate' => false + ]); + } + if ($this->duration < 0) { + // expiry time must be after start time + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::__construct', 'argument' => '$options[\'startTime\' & \'expiryTime\']'], + 'translate' => false + ]); + } + if (!is_int($this->timeout) && $this->timeout !== false) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::__construct', 'argument' => '$options[\'timeout\']'], + 'translate' => false + ]); + } + if (!is_bool($this->renewable)) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::__construct', 'argument' => '$options[\'renewable\']'], + 'translate' => false + ]); + } + + // set activity time if a timeout was requested + if (is_int($this->timeout)) { + $this->lastActivity = time(); + } + } else { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::__construct', 'argument' => '$token'], + 'translate' => false + ]); + } + + // ensure that all changes are committed on script termination + register_shutdown_function([$this, 'commit']); + } + + /** + * Gets the session token or null if the session doesn't have a token yet + * + * @return string|null + */ + public function token() + { + if ($this->tokenExpiry !== null) { + if (is_string($this->tokenKey)) { + return $this->tokenExpiry . '.' . $this->tokenId . '.' . $this->tokenKey; + } else { + return $this->tokenExpiry . '.' . $this->tokenId; + } + } else { + return null; + } + } + + /** + * Gets or sets the transmission mode + * Setting only works for new sessions that haven't been transmitted yet + * + * @param string $mode Optional new transmission mode + * @return string Transmission mode + */ + public function mode(string $mode = null) + { + if (is_string($mode)) { + // only allow this if this is a new session, otherwise the change + // might not be applied correctly to the current request + if ($this->token() !== null) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::mode', 'argument' => '$mode'], + 'translate' => false + ]); + } + + $this->mode = $mode; + } + + return $this->mode; + } + + /** + * Gets the session start time + * + * @return int Timestamp + */ + public function startTime(): int + { + return $this->startTime; + } + + /** + * Gets or sets the session expiry time + * Setting the expiry time also updates the duration and regenerates the session token + * + * @param string|int $expiryTime Optional new expiry timestamp or time string to set + * @return int Timestamp + */ + public function expiryTime($expiryTime = null): int + { + if (is_string($expiryTime) || is_int($expiryTime)) { + // convert to a timestamp + $expiryTime = static::timeToTimestamp($expiryTime); + + // verify that the expiry time is not in the past + if ($expiryTime <= time()) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::expiryTime', 'argument' => '$expiryTime'], + 'translate' => false + ]); + } + + $this->prepareForWriting(); + $this->expiryTime = $expiryTime; + $this->duration = $expiryTime - time(); + $this->regenerateTokenIfNotNew(); + } elseif ($expiryTime !== null) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::expiryTime', 'argument' => '$expiryTime'], + 'translate' => false + ]); + } + + return $this->expiryTime; + } + + /** + * Gets or sets the session duration + * Setting the duration also updates the expiry time and regenerates the session token + * + * @param int $duration Optional new duration in seconds to set + * @return int Number of seconds + */ + public function duration(int $duration = null): int + { + if (is_int($duration)) { + // verify that the duration is at least 1 second + if ($duration < 1) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::duration', 'argument' => '$duration'], + 'translate' => false + ]); + } + + $this->prepareForWriting(); + $this->duration = $duration; + $this->expiryTime = time() + $duration; + $this->regenerateTokenIfNotNew(); + } + + return $this->duration; + } + + /** + * Gets or sets the session timeout + * + * @param int|false $timeout Optional new timeout to set or false to disable timeout + * @return int|false Number of seconds or false for "no timeout" + */ + public function timeout($timeout = null) + { + if (is_int($timeout) || $timeout === false) { + // verify that the timeout is at least 1 second + if (is_int($timeout) && $timeout < 1) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::timeout', 'argument' => '$timeout'], + 'translate' => false + ]); + } + + $this->prepareForWriting(); + $this->timeout = $timeout; + + if (is_int($timeout)) { + $this->lastActivity = time(); + } else { + $this->lastActivity = null; + } + } elseif ($timeout !== null) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::timeout', 'argument' => '$timeout'], + 'translate' => false + ]); + } + + return $this->timeout; + } + + /** + * Gets or sets the renewable flag + * Automatically renews the session if renewing gets enabled + * + * @param bool $renewable Optional new renewable flag to set + * @return bool + */ + public function renewable(bool $renewable = null): bool + { + if (is_bool($renewable)) { + $this->prepareForWriting(); + $this->renewable = $renewable; + $this->autoRenew(); + } + + return $this->renewable; + } + + /** + * Returns the session data object + * + * @return \Kirby\Session\SessionData + */ + public function data() + { + return $this->data; + } + + /** + * Magic call method that proxies all calls to session data methods + * + * @param string $name Method name (one of set, increment, decrement, get, pull, remove, clear) + * @param array $arguments Method arguments + * @return mixed + */ + public function __call(string $name, array $arguments) + { + // validate that we can handle the called method + if (!in_array($name, ['set', 'increment', 'decrement', 'get', 'pull', 'remove', 'clear'])) { + throw new BadMethodCallException([ + 'data' => ['method' => 'Session::' . $name], + 'translate' => false + ]); + } + + return $this->data()->$name(...$arguments); + } + + /** + * Writes all changes to the session to the session store + * + * @return void + */ + public function commit() + { + // nothing to do if nothing changed or the session has been just created or destroyed + if ($this->writeMode !== true || $this->tokenExpiry === null || $this->destroyed === true) { + return; + } + + // collect all data + if ($this->newSession) { + // the token has changed + // we are writing to the old session: it only gets the reference to the new session + // and a shortened expiry time (30 second grace period) + $data = [ + 'startTime' => $this->startTime(), + 'expiryTime' => time() + 30, + 'newSession' => $this->newSession + ]; + } else { + $data = [ + 'startTime' => $this->startTime(), + 'expiryTime' => $this->expiryTime(), + 'duration' => $this->duration(), + 'timeout' => $this->timeout(), + 'lastActivity' => $this->lastActivity, + 'renewable' => $this->renewable(), + 'data' => $this->data()->get() + ]; + } + + // encode the data and attach an HMAC + $data = serialize($data); + $data = hash_hmac('sha256', $data, $this->tokenKey) . "\n" . $data; + + // store the data + $this->sessions->store()->set($this->tokenExpiry, $this->tokenId, $data); + $this->sessions->store()->unlock($this->tokenExpiry, $this->tokenId); + $this->writeMode = false; + } + + /** + * Entirely destroys the session + * + * @return void + */ + public function destroy() + { + // no need to destroy new or destroyed sessions + if ($this->tokenExpiry === null || $this->destroyed === true) { + return; + } + + // remove session file + $this->sessions->store()->destroy($this->tokenExpiry, $this->tokenId); + $this->destroyed = true; + $this->writeMode = false; + $this->needsRetransmission = false; + + // remove cookie + if ($this->mode === 'cookie') { + Cookie::remove($this->sessions->cookieName()); + } + } + + /** + * Renews the session with the same session duration + * Renewing also regenerates the session token + * + * @return void + */ + public function renew() + { + if ($this->renewable() !== true) { + throw new LogicException([ + 'key' => 'session.notRenewable', + 'fallback' => 'Cannot renew a session that is not renewable, call $session->renewable(true) first', + 'translate' => false, + ]); + } + + $this->prepareForWriting(); + $this->expiryTime = time() + $this->duration(); + $this->regenerateTokenIfNotNew(); + } + + /** + * Regenerates the session token + * The old token will keep its validity for a 30 second grace period + * + * @return void + */ + public function regenerateToken() + { + // don't do anything for destroyed sessions + if ($this->destroyed === true) { + return; + } + + $this->prepareForWriting(); + + // generate new token + $tokenExpiry = $this->expiryTime; + $tokenId = $this->sessions->store()->createId($tokenExpiry); + $tokenKey = bin2hex(random_bytes(32)); + + // mark the old session as moved if there is one + if ($this->tokenExpiry !== null) { + $this->newSession = $tokenExpiry . '.' . $tokenId; + $this->commit(); + + // we are now in the context of the new session + $this->newSession = null; + } + + // set new data as instance vars + $this->tokenExpiry = $tokenExpiry; + $this->tokenId = $tokenId; + $this->tokenKey = $tokenKey; + + // the new session needs to be written for the first time + $this->writeMode = true; + + // (re)transmit session token + if ($this->mode === 'cookie') { + Cookie::set($this->sessions->cookieName(), $this->token(), [ + 'lifetime' => $this->tokenExpiry, + 'path' => Url::index(['host' => null, 'trailingSlash' => true]), + 'secure' => Url::scheme() === 'https', + 'httpOnly' => true, + 'sameSite' => 'Lax' + ]); + } else { + $this->needsRetransmission = true; + } + + // update cache of the Sessions instance with the new token + $this->sessions->updateCache($this); + } + + /** + * Returns whether the session token needs to be retransmitted to the client + * Only relevant in header and manual modes + * + * @return bool + */ + public function needsRetransmission(): bool + { + return $this->needsRetransmission; + } + + /** + * Ensures that all pending changes are written to disk before the object is destructed + */ + public function __destruct() + { + $this->commit(); + } + + /** + * Initially generates the token for new sessions + * Used internally + * + * @return void + */ + public function ensureToken() + { + if ($this->tokenExpiry === null) { + $this->regenerateToken(); + } + } + + /** + * Puts the session into write mode by acquiring a lock and reloading the data + * Used internally + * + * @return void + */ + public function prepareForWriting() + { + // verify that we need to get into write mode: + // - new sessions are only written to if the token has explicitly been ensured + // using $session->ensureToken() -> lazy session creation + // - destroyed sessions are never written to + // - no need to lock and re-init if we are already in write mode + if ($this->tokenExpiry === null || $this->destroyed === true || $this->writeMode === true) { + return; + } + + // don't allow writing for read-only sessions + // (only the case for moved sessions) + if ($this->tokenKey === null) { + throw new LogicException([ + 'key' => 'session.readonly', + 'data' => ['token' => $this->token()], + 'fallback' => 'Session "' . $this->token() . '" is currently read-only because it was accessed via an old session token', + 'translate' => false + ]); + } + + $this->sessions->store()->lock($this->tokenExpiry, $this->tokenId); + $this->init(); + $this->writeMode = true; + } + + /** + * Parses a token string into its parts and sets them as instance vars + * + * @param string $token Session token + * @param bool $withoutKey If true, $token is passed without key + * @return void + */ + protected function parseToken(string $token, bool $withoutKey = false) + { + // split the token into its parts + $parts = explode('.', $token); + + // only continue if the token has exactly the right amount of parts + $expectedParts = ($withoutKey === true)? 2 : 3; + if (count($parts) !== $expectedParts) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::parseToken', 'argument' => '$token'], + 'translate' => false + ]); + } + + $tokenExpiry = (int)$parts[0]; + $tokenId = $parts[1]; + $tokenKey = ($withoutKey === true)? null : $parts[2]; + + // verify that all parts were parsed correctly using reassembly + $expectedToken = $tokenExpiry . '.' . $tokenId; + if ($withoutKey === false) { + $expectedToken .= '.' . $tokenKey; + } + if ($expectedToken !== $token) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::parseToken', 'argument' => '$token'], + 'translate' => false + ]); + } + + $this->tokenExpiry = $tokenExpiry; + $this->tokenId = $tokenId; + $this->tokenKey = $tokenKey; + } + + /** + * Makes sure that the given value is a valid timestamp + * + * @param string|int $time Timestamp or date string (must be supported by `strtotime()`) + * @param int $now Timestamp to use as a base for the calculation of relative dates + * @return int Timestamp value + */ + protected static function timeToTimestamp($time, int $now = null): int + { + // default to current time as $now + if (!is_int($now)) { + $now = time(); + } + + // convert date strings to a timestamp first + if (is_string($time)) { + $time = strtotime($time, $now); + } + + // now make sure that we have a valid timestamp + if (is_int($time)) { + return $time; + } else { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Session::timeToTimestamp', 'argument' => '$time'], + 'translate' => false + ]); + } + } + + /** + * Loads the session data from the session store + * + * @return void + */ + protected function init() + { + // sessions that are new, written to or that have been destroyed should never be initialized + if ($this->tokenExpiry === null || $this->writeMode === true || $this->destroyed === true) { + // unexpected error that shouldn't occur + throw new Exception(['translate' => false]); // @codeCoverageIgnore + } + + // make sure that the session exists + if ($this->sessions->store()->exists($this->tokenExpiry, $this->tokenId) !== true) { + throw new NotFoundException([ + 'key' => 'session.notFound', + 'data' => ['token' => $this->token()], + 'fallback' => 'Session "' . $this->token() . '" does not exist', + 'translate' => false, + 'httpCode' => 404 + ]); + } + + // get the session data from the store + $data = $this->sessions->store()->get($this->tokenExpiry, $this->tokenId); + + // verify HMAC + // skip if we don't have the key (only the case for moved sessions) + $hmac = Str::before($data, "\n"); + $data = trim(Str::after($data, "\n")); + if ($this->tokenKey !== null && hash_equals(hash_hmac('sha256', $data, $this->tokenKey), $hmac) !== true) { + throw new LogicException([ + 'key' => 'session.invalid', + 'data' => ['token' => $this->token()], + 'fallback' => 'Session "' . $this->token() . '" is invalid', + 'translate' => false, + 'httpCode' => 500 + ]); + } + + // decode the serialized data + try { + $data = unserialize($data); + } catch (Throwable $e) { + throw new LogicException([ + 'key' => 'session.invalid', + 'data' => ['token' => $this->token()], + 'fallback' => 'Session "' . $this->token() . '" is invalid', + 'translate' => false, + 'httpCode' => 500, + 'previous' => $e + ]); + } + + // verify start and expiry time + if (time() < $data['startTime'] || time() > $data['expiryTime']) { + throw new LogicException([ + 'key' => 'session.invalid', + 'data' => ['token' => $this->token()], + 'fallback' => 'Session "' . $this->token() . '" is invalid', + 'translate' => false, + 'httpCode' => 500 + ]); + } + + // follow to the new session if there is one + if (isset($data['newSession'])) { + $this->parseToken($data['newSession'], true); + $this->init(); + return; + } + + // verify timeout + if (is_int($data['timeout'])) { + if (time() - $data['lastActivity'] > $data['timeout']) { + throw new LogicException([ + 'key' => 'session.invalid', + 'data' => ['token' => $this->token()], + 'fallback' => 'Session "' . $this->token() . '" is invalid', + 'translate' => false, + 'httpCode' => 500 + ]); + } + + // set a new activity timestamp, but only every few minutes for better performance + // don't do this if another call to init() is already doing it to prevent endless loops; + // also don't do this for read-only sessions + if ($this->updatingLastActivity === false && $this->tokenKey !== null && time() - $data['lastActivity'] > $data['timeout'] / 15) { + $this->updatingLastActivity = true; + $this->prepareForWriting(); + + // the remaining init steps have been done by prepareForWriting() + $this->lastActivity = time(); + $this->updatingLastActivity = false; + return; + } + } + + // (re)initialize all instance variables + $this->startTime = $data['startTime']; + $this->expiryTime = $data['expiryTime']; + $this->duration = $data['duration']; + $this->timeout = $data['timeout']; + $this->lastActivity = $data['lastActivity']; + $this->renewable = $data['renewable']; + + // reload data into existing object to avoid breaking memory references + if (is_a($this->data, 'Kirby\Session\SessionData')) { + $this->data()->reload($data['data']); + } else { + $this->data = new SessionData($this, $data['data']); + } + } + + /** + * Regenerate session token, but only if there is already one + * + * @return void + */ + protected function regenerateTokenIfNotNew() + { + if ($this->tokenExpiry !== null) { + $this->regenerateToken(); + } + } + + /** + * Automatically renews the session if possible and necessary + * + * @return void + */ + protected function autoRenew() + { + // check if the session needs renewal at all + if ($this->needsRenewal() !== true) { + return; + } + + // re-load the session and check again to make sure that no other thread + // already renewed the session in the meantime + $this->prepareForWriting(); + if ($this->needsRenewal() === true) { + $this->renew(); + } + } + + /** + * Checks if the session can be renewed and if the last renewal + * was more than half a session duration ago + * + * @return bool + */ + protected function needsRenewal(): bool + { + return $this->renewable() === true && $this->expiryTime() - time() < $this->duration() / 2; + } +} diff --git a/kirby/src/Session/SessionData.php b/kirby/src/Session/SessionData.php new file mode 100644 index 0000000..a6aaf5e --- /dev/null +++ b/kirby/src/Session/SessionData.php @@ -0,0 +1,255 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class SessionData +{ + protected $session; + protected $data; + + /** + * Creates a new SessionData instance + * + * @codeCoverageIgnore + * @param \Kirby\Session\Session $session Session object this data belongs to + * @param array $data Currently stored session data + */ + public function __construct(Session $session, array $data) + { + $this->session = $session; + $this->data = $data; + } + + /** + * Sets one or multiple session values by key + * + * @param string|array $key The key to define or a key-value array with multiple values + * @param mixed $value The value for the passed key (only if one $key is passed) + * @return void + */ + public function set($key, $value = null) + { + $this->session->ensureToken(); + $this->session->prepareForWriting(); + + if (is_string($key)) { + $this->data[$key] = $value; + } elseif (is_array($key)) { + $this->data = array_merge($this->data, $key); + } else { + throw new InvalidArgumentException([ + 'data' => ['method' => 'SessionData::set', 'argument' => 'key'], + 'translate' => false + ]); + } + } + + /** + * Increments one or multiple session values by a specified amount + * + * @param string|array $key The key to increment or an array with multiple keys + * @param int $by Increment by which amount? + * @param int $max Maximum amount (value is not incremented further) + * @return void + */ + public function increment($key, int $by = 1, $max = null) + { + if ($max !== null && !is_int($max)) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'SessionData::increment', 'argument' => 'max'], + 'translate' => false + ]); + } + + if (is_string($key)) { + // make sure we have the correct values before getting + $this->session->prepareForWriting(); + + $value = $this->get($key, 0); + + if (!is_int($value)) { + throw new LogicException([ + 'key' => 'session.data.increment.nonInt', + 'data' => ['key' => $key], + 'fallback' => 'Session value "' . $key . '" is not an integer and cannot be incremented', + 'translate' => false + ]); + } + + // increment the value, but ensure $max constraint + if (is_int($max) && $value + $by > $max) { + // set the value to $max + // but not if the current $value is already larger than $max + $value = max($value, $max); + } else { + $value += $by; + } + + $this->set($key, $value); + } elseif (is_array($key)) { + foreach ($key as $k) { + $this->increment($k, $by, $max); + } + } else { + throw new InvalidArgumentException([ + 'data' => ['method' => 'SessionData::increment', 'argument' => 'key'], + 'translate' => false + ]); + } + } + + /** + * Decrements one or multiple session values by a specified amount + * + * @param string|array $key The key to decrement or an array with multiple keys + * @param int $by Decrement by which amount? + * @param int $min Minimum amount (value is not decremented further) + * @return void + */ + public function decrement($key, int $by = 1, $min = null) + { + if ($min !== null && !is_int($min)) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'SessionData::decrement', 'argument' => 'min'], + 'translate' => false + ]); + } + + if (is_string($key)) { + // make sure we have the correct values before getting + $this->session->prepareForWriting(); + + $value = $this->get($key, 0); + + if (!is_int($value)) { + throw new LogicException([ + 'key' => 'session.data.decrement.nonInt', + 'data' => ['key' => $key], + 'fallback' => 'Session value "' . $key . '" is not an integer and cannot be decremented', + 'translate' => false + ]); + } + + // decrement the value, but ensure $min constraint + if (is_int($min) && $value - $by < $min) { + // set the value to $min + // but not if the current $value is already smaller than $min + $value = min($value, $min); + } else { + $value -= $by; + } + + $this->set($key, $value); + } elseif (is_array($key)) { + foreach ($key as $k) { + $this->decrement($k, $by, $min); + } + } else { + throw new InvalidArgumentException([ + 'data' => ['method' => 'SessionData::decrement', 'argument' => 'key'], + 'translate' => false + ]); + } + } + + /** + * Returns one or all session values by key + * + * @param string|null $key The key to get or null for the entire data array + * @param mixed $default Optional default value to return if the key is not defined + * @return mixed + */ + public function get($key = null, $default = null) + { + if (is_string($key)) { + return $this->data[$key] ?? $default; + } elseif ($key === null) { + return $this->data; + } else { + throw new InvalidArgumentException([ + 'data' => ['method' => 'SessionData::get', 'argument' => 'key'], + 'translate' => false + ]); + } + } + + /** + * Retrieves a value and removes it afterwards + * + * @param string $key The key to get + * @param mixed $default Optional default value to return if the key is not defined + * @return mixed + */ + public function pull(string $key, $default = null) + { + // make sure we have the correct value before getting + // we do this here (but not in get) as we need to write anyway + $this->session->prepareForWriting(); + + $value = $this->get($key, $default); + $this->remove($key); + return $value; + } + + /** + * Removes one or multiple session values by key + * + * @param string|array $key The key to remove or an array with multiple keys + * @return void + */ + public function remove($key) + { + $this->session->prepareForWriting(); + + if (is_string($key)) { + unset($this->data[$key]); + } elseif (is_array($key)) { + foreach ($key as $k) { + unset($this->data[$k]); + } + } else { + throw new InvalidArgumentException([ + 'data' => ['method' => 'SessionData::remove', 'argument' => 'key'], + 'translate' => false + ]); + } + } + + /** + * Clears all session data + * + * @return void + */ + public function clear() + { + $this->session->prepareForWriting(); + + $this->data = []; + } + + /** + * Reloads the data array with the current session data + * Only used internally + * + * @param array $data Currently stored session data + * @return void + */ + public function reload(array $data) + { + $this->data = $data; + } +} diff --git a/kirby/src/Session/SessionStore.php b/kirby/src/Session/SessionStore.php new file mode 100644 index 0000000..a3a9611 --- /dev/null +++ b/kirby/src/Session/SessionStore.php @@ -0,0 +1,110 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +abstract class SessionStore +{ + /** + * Creates a new session ID with the given expiry time + * + * Needs to make sure that the session does not already exist + * and needs to reserve it by locking it exclusively. + * + * @param int $expiryTime Timestamp + * @return string Randomly generated session ID (without timestamp) + */ + abstract public function createId(int $expiryTime): string; + + /** + * Checks if the given session exists + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return bool true: session exists, + * false: session doesn't exist + */ + abstract public function exists(int $expiryTime, string $id): bool; + + /** + * Locks the given session exclusively + * + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return void + */ + abstract public function lock(int $expiryTime, string $id); + + /** + * Removes all locks on the given session + * + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return void + */ + abstract public function unlock(int $expiryTime, string $id); + + /** + * Returns the stored session data of the given session + * + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return string + */ + abstract public function get(int $expiryTime, string $id): string; + + /** + * Stores data to the given session + * + * Needs to make sure that the session exists. + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @param string $data Session data to write + * @return void + */ + abstract public function set(int $expiryTime, string $id, string $data); + + /** + * Deletes the given session + * + * Needs to throw an Exception on error. + * + * @param int $expiryTime Timestamp + * @param string $id Session ID + * @return void + */ + abstract public function destroy(int $expiryTime, string $id); + + /** + * Deletes all expired sessions + * + * Needs to throw an Exception on error. + * + * @return void + */ + abstract public function collectGarbage(); + + /** + * Securely generates a random session ID + * + * @return string Random hex string with 20 bytes + */ + protected static function generateId(): string + { + return bin2hex(random_bytes(10)); + } +} diff --git a/kirby/src/Session/Sessions.php b/kirby/src/Session/Sessions.php new file mode 100644 index 0000000..d947080 --- /dev/null +++ b/kirby/src/Session/Sessions.php @@ -0,0 +1,288 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Sessions +{ + protected $store; + protected $mode; + protected $cookieName; + + protected $cache = []; + + /** + * Creates a new Sessions instance + * + * @param \Kirby\Session\SessionStore|string $store SessionStore object or a path to the storage directory (uses the FileSessionStore) + * @param array $options Optional additional options: + * - `mode`: Default token transmission mode (cookie, header or manual); defaults to `cookie` + * - `cookieName`: Name to use for the session cookie; defaults to `kirby_session` + * - `gcInterval`: How often should the garbage collector be run?; integer or `false` for never; defaults to `100` + */ + public function __construct($store, array $options = []) + { + if (is_string($store)) { + $this->store = new FileSessionStore($store); + } elseif (is_a($store, 'Kirby\Session\SessionStore') === true) { + $this->store = $store; + } else { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Sessions::__construct', 'argument' => 'store'], + 'translate' => false + ]); + } + + $this->mode = $options['mode'] ?? 'cookie'; + $this->cookieName = $options['cookieName'] ?? 'kirby_session'; + $gcInterval = $options['gcInterval'] ?? 100; + + // validate options + if (!in_array($this->mode, ['cookie', 'header', 'manual'])) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Sessions::__construct', 'argument' => '$options[\'mode\']'], + 'translate' => false + ]); + } + if (!is_string($this->cookieName)) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Sessions::__construct', 'argument' => '$options[\'cookieName\']'], + 'translate' => false + ]); + } + + // trigger automatic garbage collection with the given probability + if (is_int($gcInterval) && $gcInterval > 0) { + // convert the interval into a probability between 0 and 1 + $gcProbability = 1 / $gcInterval; + + // generate a random number + $random = mt_rand(1, 10000); + + // $random will be below or equal $gcProbability * 10000 with a probability of $gcProbability + if ($random <= $gcProbability * 10000) { + $this->collectGarbage(); + } + } elseif ($gcInterval !== false) { + throw new InvalidArgumentException([ + 'data' => ['method' => 'Sessions::__construct', 'argument' => '$options[\'gcInterval\']'], + 'translate' => false + ]); + } + } + + /** + * Creates a new empty session + * + * @param array $options Optional additional options: + * - `mode`: Token transmission mode (cookie or manual); defaults to default mode of the Sessions instance + * - `startTime`: Time the session starts being valid (date string or timestamp); defaults to `now` + * - `expiryTime`: Time the session expires (date string or timestamp); defaults to `+ 2 hours` + * - `timeout`: Activity timeout in seconds (integer or false for none); defaults to `1800` (half an hour) + * - `renewable`: Should it be possible to extend the expiry date?; defaults to `true` + * @return \Kirby\Session\Session + */ + public function create(array $options = []) + { + // fall back to default mode + if (!isset($options['mode'])) { + $options['mode'] = $this->mode; + } + + return new Session($this, null, $options); + } + + /** + * Returns the specified Session object + * + * @param string $token Session token, either including or without the key + * @param string $mode Optional transmission mode override + * @return \Kirby\Session\Session + */ + public function get(string $token, string $mode = null) + { + if (isset($this->cache[$token])) { + return $this->cache[$token]; + } + + return $this->cache[$token] = new Session($this, $token, ['mode' => $mode ?? $this->mode]); + } + + /** + * Returns the current session based on the configured token transmission mode: + * - In `cookie` mode: Gets the session from the cookie + * - In `header` mode: Gets the session from the `Authorization` request header + * - In `manual` mode: Fails and throws an Exception + * + * @return \Kirby\Session\Session|null Either the current session or null in case there isn't one + */ + public function current() + { + $token = null; + switch ($this->mode) { + case 'cookie': + $token = $this->tokenFromCookie(); + break; + case 'header': + $token = $this->tokenFromHeader(); + break; + case 'manual': + throw new LogicException([ + 'key' => 'session.sessions.manualMode', + 'fallback' => 'Cannot automatically get current session in manual mode', + 'translate' => false, + 'httpCode' => 500 + ]); + break; + default: + // unexpected error that shouldn't occur + throw new Exception(['translate' => false]); // @codeCoverageIgnore + } + + // no token was found, no session + if (!is_string($token)) { + return null; + } + + // token was found, try to get the session + try { + return $this->get($token); + } catch (Throwable $e) { + return null; + } + } + + /** + * Returns the current session using the following detection order without using the configured mode: + * - Tries to get the session from the `Authorization` request header + * - Tries to get the session from the cookie + * - Otherwise returns null + * + * @return \Kirby\Session\Session|null Either the current session or null in case there isn't one + */ + public function currentDetected() + { + $tokenFromHeader = $this->tokenFromHeader(); + $tokenFromCookie = $this->tokenFromCookie(); + + // prefer header token over cookie token + $token = $tokenFromHeader ?? $tokenFromCookie; + + // no token was found, no session + if (!is_string($token)) { + return null; + } + + // token was found, try to get the session + try { + $mode = (is_string($tokenFromHeader))? 'header' : 'cookie'; + return $this->get($token, $mode); + } catch (Throwable $e) { + return null; + } + } + + /** + * Getter for the session store instance + * Used internally + * + * @return \Kirby\Session\SessionStore + */ + public function store() + { + return $this->store; + } + + /** + * Getter for the cookie name + * Used internally + * + * @return string + */ + public function cookieName(): string + { + return $this->cookieName; + } + + /** + * Deletes all expired sessions + * + * If the `gcInterval` is configured, this is done automatically + * on init of the Sessions object. + * + * @return void + */ + public function collectGarbage() + { + $this->store()->collectGarbage(); + } + + /** + * Updates the instance cache with a newly created + * session or a session with a regenerated token + * + * @internal + * @param \Kirby\Session\Session $session Session instance to push to the cache + */ + public function updateCache(Session $session) + { + $this->cache[$session->token()] = $session; + } + + /** + * Returns the auth token from the cookie + * + * @return string|null + */ + protected function tokenFromCookie() + { + $value = Cookie::get($this->cookieName()); + + if (is_string($value)) { + return $value; + } else { + return null; + } + } + + /** + * Returns the auth token from the Authorization header + * + * @return string|null + */ + protected function tokenFromHeader() + { + $request = new Request(); + $headers = $request->headers(); + + // check if the header exists at all + if (!isset($headers['Authorization'])) { + return null; + } + + // check if the header uses the "Session" scheme + $header = $headers['Authorization']; + if (Str::startsWith($header, 'Session ', true) !== true) { + return null; + } + + // return the part after the scheme + return substr($header, 8); + } +} diff --git a/kirby/src/Text/KirbyTag.php b/kirby/src/Text/KirbyTag.php new file mode 100644 index 0000000..495172c --- /dev/null +++ b/kirby/src/Text/KirbyTag.php @@ -0,0 +1,149 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class KirbyTag +{ + public static $aliases = []; + public static $types = []; + + public $attrs = []; + public $data = []; + public $options = []; + public $type = null; + public $value = null; + + public function __call(string $name, array $arguments = []) + { + return $this->data[$name] ?? $this->$name; + } + + public static function __callStatic(string $type, array $arguments = []) + { + return (new static($type, ...$arguments))->render(); + } + + public function __construct(string $type, string $value = null, array $attrs = [], array $data = [], array $options = []) + { + if (isset(static::$types[$type]) === false) { + if (isset(static::$aliases[$type]) === false) { + throw new InvalidArgumentException('Undefined tag type: ' . $type); + } + + $type = static::$aliases[$type]; + } + + foreach ($attrs as $attrName => $attrValue) { + $attrName = strtolower($attrName); + $this->$attrName = $attrValue; + } + + $this->attrs = $attrs; + $this->data = $data; + $this->options = $options; + $this->$type = $value; + $this->type = $type; + $this->value = $value; + } + + public function __get(string $attr) + { + $attr = strtolower($attr); + return $this->$attr ?? null; + } + + public function attr(string $name, $default = null) + { + $name = strtolower($name); + return $this->$name ?? $default; + } + + public static function factory(...$arguments) + { + return (new static(...$arguments))->render(); + } + + /** + * @param string $string + * @param array $data + * @param array $options + * @return self + */ + public static function parse(string $string, array $data = [], array $options = []) + { + // remove the brackets, extract the first attribute (the tag type) + $tag = trim(ltrim($string, '(')); + + // use substr instead of rtrim to keep non-tagged brackets + // (link: file.pdf text: Download (PDF)) + if (substr($tag, -1) === ')') { + $tag = substr($tag, 0, -1); + } + + $type = trim(substr($tag, 0, strpos($tag, ':'))); + $type = strtolower($type); + $attr = static::$types[$type]['attr'] ?? []; + + // the type should be parsed as an attribute, so we add it here + // to the list of possible attributes + array_unshift($attr, $type); + + // extract all attributes + $regex = sprintf('/(%s):/i', implode('|', $attr)); + $search = preg_split($regex, $tag, false, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + + // $search is now an array with alternating keys and values + // convert it to arrays of keys and values + $chunks = array_chunk($search, 2); + $keys = array_column($chunks, 0); + $values = array_map('trim', array_column($chunks, 1)); + + // ensure that there is a value for each key + // otherwise combining won't work + if (count($values) < count($keys)) { + $values[] = ''; + } + + // combine the two arrays to an associative array + $attributes = array_combine($keys, $values); + + // the first attribute is the type attribute + // extract and pass its value separately + $value = array_shift($attributes); + + return new static($type, $value, $attributes, $data, $options); + } + + public function option(string $key, $default = null) + { + return $this->options[$key] ?? $default; + } + + public function render(): string + { + $callback = static::$types[$this->type]['html'] ?? null; + + if (is_a($callback, 'Closure') === true) { + return (string)$callback($this); + } + + throw new BadMethodCallException('Invalid tag render function in tag: ' . $this->type); + } + + public function type(): string + { + return $this->type; + } +} diff --git a/kirby/src/Text/KirbyTags.php b/kirby/src/Text/KirbyTags.php new file mode 100644 index 0000000..76aa85d --- /dev/null +++ b/kirby/src/Text/KirbyTags.php @@ -0,0 +1,56 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class KirbyTags +{ + protected static $tagClass = 'Kirby\Text\KirbyTag'; + + public static function parse(string $text = null, array $data = [], array $options = []): string + { + $regex = '! + (?=[^\]]) # positive lookahead that matches a group after the main expression without including ] in the result + (?=\([a-z0-9_-]+:) # positive lookahead that requires starts with ( and lowercase ASCII letters, digits, underscores or hyphens followed with : immediately to the right of the current location + (\( # capturing group 1 + (?:[^()]+|(?1))*+ # repetitions of any chars other than ( and ) or the whole group 1 pattern (recursed) + \)) # end of capturing group 1 + !isx'; + + return preg_replace_callback($regex, function ($match) use ($data, $options) { + $debug = $options['debug'] ?? false; + + try { + return static::$tagClass::parse($match[0], $data, $options)->render(); + } catch (InvalidArgumentException $e) { + // stay silent in production and ignore non-existing tags + if ($debug !== true || Str::startsWith($e->getMessage(), 'Undefined tag type:') === true) { + return $match[0]; + } + + throw $e; + } catch (Exception $e) { + if ($debug === true) { + throw $e; + } + + return $match[0]; + } + }, $text); + } +} diff --git a/kirby/src/Text/Markdown.php b/kirby/src/Text/Markdown.php new file mode 100644 index 0000000..7b03050 --- /dev/null +++ b/kirby/src/Text/Markdown.php @@ -0,0 +1,79 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Markdown +{ + /** + * Array with all configured options + * for the parser + * + * @var array + */ + protected $options = []; + + /** + * Returns default values for all + * available parser options + * + * @return array + */ + public function defaults(): array + { + return [ + 'extra' => false, + 'breaks' => true + ]; + } + + /** + * Creates a new Markdown parser + * with the given options + * + * @param array $options + */ + public function __construct(array $options = []) + { + $this->options = array_merge($this->defaults(), $options); + } + + /** + * Parses the given text and returns the HTML + * + * @param string|null $text + * @param bool $inline + * @return string + */ + public function parse(string $text = null, bool $inline = false): string + { + if ($this->options['extra'] === true) { + $parser = new ParsedownExtra(); + } else { + $parser = new Parsedown(); + } + + $parser->setBreaksEnabled($this->options['breaks']); + + if ($inline === true) { + return @$parser->line($text); + } else { + return @$parser->text($text); + } + } +} diff --git a/kirby/src/Text/SmartyPants.php b/kirby/src/Text/SmartyPants.php new file mode 100644 index 0000000..9589a99 --- /dev/null +++ b/kirby/src/Text/SmartyPants.php @@ -0,0 +1,129 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class SmartyPants +{ + /** + * Array with all configured options + * for the parser + * + * @var array + */ + protected $options = []; + + /** + * Michelf's parser object + * + * @var SmartyPantsTypographer + */ + protected $parser; + + /** + * Returns default values for all + * available parser options + * + * @return array + */ + public function defaults(): array + { + return [ + 'attr' => 1, + 'doublequote.open' => '“', + 'doublequote.close' => '”', + 'doublequote.low' => '„', + 'singlequote.open' => '‘', + 'singlequote.close' => '’', + 'backtick.doublequote.open' => '“', + 'backtick.doublequote.close' => '”', + 'backtick.singlequote.open' => '‘', + 'backtick.singlequote.close' => '’', + 'emdash' => '—', + 'endash' => '–', + 'ellipsis' => '…', + 'space' => '(?: | | |�*160;|�*[aA]0;)', + 'space.emdash' => ' ', + 'space.endash' => ' ', + 'space.colon' => ' ', + 'space.semicolon' => ' ', + 'space.marks' => ' ', + 'space.frenchquote' => ' ', + 'space.thousand' => ' ', + 'space.unit' => ' ', + 'guillemet.leftpointing' => '«', + 'guillemet.rightpointing' => '»', + 'geresh' => '׳', + 'gershayim' => '״', + 'skip' => 'pre|code|kbd|script|style|math', + ]; + } + + /** + * Creates a new SmartyPants parser + * with the given options + * + * @param array $options + */ + public function __construct(array $options = []) + { + $this->options = array_merge($this->defaults(), $options); + $this->parser = new SmartyPantsTypographer($this->options['attr']); + + // configuration + $this->parser->smart_doublequote_open = $this->options['doublequote.open']; + $this->parser->smart_doublequote_close = $this->options['doublequote.close']; + $this->parser->smart_singlequote_open = $this->options['singlequote.open']; + $this->parser->smart_singlequote_close = $this->options['singlequote.close']; + $this->parser->backtick_doublequote_open = $this->options['backtick.doublequote.open']; + $this->parser->backtick_doublequote_close = $this->options['backtick.doublequote.close']; + $this->parser->backtick_singlequote_open = $this->options['backtick.singlequote.open']; + $this->parser->backtick_singlequote_close = $this->options['backtick.singlequote.close']; + $this->parser->em_dash = $this->options['emdash']; + $this->parser->en_dash = $this->options['endash']; + $this->parser->ellipsis = $this->options['ellipsis']; + $this->parser->tags_to_skip = $this->options['skip']; + $this->parser->space_emdash = $this->options['space.emdash']; + $this->parser->space_endash = $this->options['space.endash']; + $this->parser->space_colon = $this->options['space.colon']; + $this->parser->space_semicolon = $this->options['space.semicolon']; + $this->parser->space_marks = $this->options['space.marks']; + $this->parser->space_frenchquote = $this->options['space.frenchquote']; + $this->parser->space_thousand = $this->options['space.thousand']; + $this->parser->space_unit = $this->options['space.unit']; + $this->parser->doublequote_low = $this->options['doublequote.low']; + $this->parser->guillemet_leftpointing = $this->options['guillemet.leftpointing']; + $this->parser->guillemet_rightpointing = $this->options['guillemet.rightpointing']; + $this->parser->geresh = $this->options['geresh']; + $this->parser->gershayim = $this->options['gershayim']; + $this->parser->space = $this->options['space']; + } + + /** + * Parses the given text + * + * @param string|null $text + * @return string + */ + public function parse(string $text = null): string + { + // prepare the text + $text = str_replace('"', '"', $text); + + // parse the text + return $this->parser->transform($text); + } +} diff --git a/kirby/src/Toolkit/A.php b/kirby/src/Toolkit/A.php new file mode 100644 index 0000000..29d51d0 --- /dev/null +++ b/kirby/src/Toolkit/A.php @@ -0,0 +1,697 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class A +{ + /** + * Appends the given array + * + * @param array $array + * @param array $append + * @return array + */ + public static function append(array $array, array $append): array + { + return $array + $append; + } + + /** + * Gets an element of an array by key + * + * + * $array = [ + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ]; + * + * echo A::get($array, 'cat'); + * // output: 'miao' + * + * echo A::get($array, 'elephant', 'shut up'); + * // output: 'shut up' + * + * $catAndDog = A::get($array, ['cat', 'dog']); + * // result: ['cat' => 'miao', 'dog' => 'wuff']; + * + * + * @param array $array The source array + * @param mixed $key The key to look for + * @param mixed $default Optional default value, which should be + * returned if no element has been found + * @return mixed + */ + public static function get($array, $key, $default = null) + { + if (is_array($array) === false) { + return $array; + } + + // return the entire array if the key is null + if ($key === null) { + return $array; + } + + // get an array of keys + if (is_array($key) === true) { + $result = []; + foreach ($key as $k) { + $result[$k] = static::get($array, $k, $default); + } + return $result; + } + + if (isset($array[$key]) === true) { + return $array[$key]; + } + + // extract data from nested array structures using the dot notation + if (strpos($key, '.') !== false) { + $keys = explode('.', $key); + $firstKey = array_shift($keys); + + // if the input array also uses dot notation, try to find a subset of the $keys + if (isset($array[$firstKey]) === false) { + $currentKey = $firstKey; + + while ($innerKey = array_shift($keys)) { + $currentKey .= '.' . $innerKey; + + // the element needs to exist and also needs to be an array; otherwise + // we cannot find the remaining keys within it (invalid array structure) + if (isset($array[$currentKey]) === true && is_array($array[$currentKey]) === true) { + // $keys only holds the remaining keys that have not been shifted off yet + return static::get($array[$currentKey], implode('.', $keys), $default); + } + } + + // searching through the full chain of keys wasn't successful + return $default; + } + + // if the input array uses a completely nested structure, + // recursively progress layer by layer + if (is_array($array[$firstKey]) === true) { + return static::get($array[$firstKey], implode('.', $keys), $default); + } + + // the $firstKey element was found, but isn't an array, so we cannot + // find the remaining keys within it (invalid array structure) + return $default; + } + + return $default; + } + + /** + * @param mixed $value + * @param mixed $separator + * @return string + */ + public static function join($value, $separator = ', ') + { + if (is_string($value) === true) { + return $value; + } + return implode($separator, $value); + } + + const MERGE_OVERWRITE = 0; + const MERGE_APPEND = 1; + const MERGE_REPLACE = 2; + + /** + * Merges arrays recursively + * + * @param array $array1 + * @param array $array2 + * @param bool $mode Behavior for elements with numeric keys; + * A::MERGE_APPEND: elements are appended, keys are reset; + * A::MERGE_OVERWRITE: elements are overwritten, keys are preserved + * A::MERGE_REPLACE: non-associative arrays are completely replaced + * @return array + */ + public static function merge($array1, $array2, $mode = A::MERGE_APPEND) + { + $merged = $array1; + + if (static::isAssociative($array1) === false && $mode === static::MERGE_REPLACE) { + return $array2; + } + + foreach ($array2 as $key => $value) { + + // append to the merged array, don't overwrite numeric keys + if (is_int($key) === true && $mode == static::MERGE_APPEND) { + $merged[] = $value; + + // recursively merge the two array values + } elseif (is_array($value) === true && isset($merged[$key]) === true && is_array($merged[$key]) === true) { + $merged[$key] = static::merge($merged[$key], $value, $mode); + + // simply overwrite with the value from the second array + } else { + $merged[$key] = $value; + } + } + + if ($mode == static::MERGE_APPEND) { + // the keys don't make sense anymore, reset them + // array_merge() is the simplest way to renumber + // arrays that have both numeric and string keys; + // besides the keys, nothing changes here + $merged = array_merge($merged, []); + } + + return $merged; + } + + /** + * Plucks a single column from an array + * + * + * $array[] = [ + * 'id' => 1, + * 'username' => 'homer', + * ]; + * + * $array[] = [ + * 'id' => 2, + * 'username' => 'marge', + * ]; + * + * $array[] = [ + * 'id' => 3, + * 'username' => 'lisa', + * ]; + * + * var_dump(A::pluck($array, 'username')); + * // result: ['homer', 'marge', 'lisa']; + * + * + * @param array $array The source array + * @param string $key The key name of the column to extract + * @return array The result array with all values + * from that column. + */ + public static function pluck(array $array, string $key) + { + $output = []; + foreach ($array as $a) { + if (isset($a[$key]) === true) { + $output[] = $a[$key]; + } + } + + return $output; + } + + /** + * Prepends the given array + * + * @param array $array + * @param array $prepend + * @return array + */ + public static function prepend(array $array, array $prepend): array + { + return $prepend + $array; + } + + /** + * Shuffles an array and keeps the keys + * + * + * $array = [ + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ]; + * + * $shuffled = A::shuffle($array); + * // output: [ + * // 'dog' => 'wuff', + * // 'cat' => 'miao', + * // 'bird' => 'tweet' + * // ]; + * + * + * @param array $array The source array + * @return array The shuffled result array + */ + public static function shuffle(array $array): array + { + $keys = array_keys($array); + $new = []; + + shuffle($keys); + + // resort the array + foreach ($keys as $key) { + $new[$key] = $array[$key]; + } + + return $new; + } + + /** + * Returns the first element of an array + * + * + * $array = [ + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ]; + * + * $first = A::first($array); + * // first: 'miao' + * + * + * @param array $array The source array + * @return mixed The first element + */ + public static function first(array $array) + { + return array_shift($array); + } + + /** + * Returns the last element of an array + * + * + * $array = [ + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ]; + * + * $last = A::last($array); + * // last: 'tweet' + * + * + * @param array $array The source array + * @return mixed The last element + */ + public static function last(array $array) + { + return array_pop($array); + } + + /** + * Fills an array up with additional elements to certain amount. + * + * + * $array = [ + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ]; + * + * $result = A::fill($array, 5, 'elephant'); + * + * // result: [ + * // 'cat', + * // 'dog', + * // 'bird', + * // 'elephant', + * // 'elephant', + * // ]; + * + * + * @param array $array The source array + * @param int $limit The number of elements the array should + * contain after filling it up. + * @param mixed $fill The element, which should be used to + * fill the array + * @return array The filled-up result array + */ + public static function fill(array $array, int $limit, $fill = 'placeholder'): array + { + if (count($array) < $limit) { + $diff = $limit - count($array); + for ($x = 0; $x < $diff; $x++) { + $array[] = $fill; + } + } + return $array; + } + + /** + * Move an array item to a new index + * + * @param array $array + * @param int $from + * @param int $to + * @return array + */ + public static function move(array $array, int $from, int $to): array + { + $total = count($array); + + if ($from >= $total || $from < 0) { + throw new Exception('Invalid "from" index'); + } + + if ($to >= $total || $to < 0) { + throw new Exception('Invalid "to" index'); + } + + // remove the item from the array + $item = array_splice($array, $from, 1); + + // inject it at the new position + array_splice($array, $to, 0, $item); + + return $array; + } + + /** + * Checks for missing elements in an array + * + * This is very handy to check for missing + * user values in a request for example. + * + * + * $array = [ + * 'cat' => 'miao', + * 'dog' => 'wuff', + * 'bird' => 'tweet' + * ]; + * + * $required = ['cat', 'elephant']; + * + * $missng = A::missing($array, $required); + * // missing: [ + * // 'elephant' + * // ]; + * + * + * @param array $array The source array + * @param array $required An array of required keys + * @return array An array of missing fields. If this + * is empty, nothing is missing. + */ + public static function missing(array $array, array $required = []): array + { + $missing = []; + foreach ($required as $r) { + if (isset($array[$r]) === false) { + $missing[] = $r; + } + } + return $missing; + } + + /** + * Normalizes an array into a nested form by converting + * dot notation in keys to nested structures + * + * @param array $array + * @param array $ignore List of keys in dot notation that should + * not be converted to a nested structure + * @return array + */ + public static function nest(array $array, array $ignore = []): array + { + // convert a simple ignore list to a nested $key => true array + if (isset($ignore[0]) === true) { + $ignore = array_map(function () { + return true; + }, array_flip($ignore)); + + $ignore = A::nest($ignore); + } + + $result = []; + + foreach ($array as $fullKey => $value) { + // extract the first part of a multi-level key, keep the others + $subKeys = explode('.', $fullKey); + $key = array_shift($subKeys); + + // skip the magic for ignored keys + if (isset($ignore[$key]) === true && $ignore[$key] === true) { + $result[$fullKey] = $value; + continue; + } + + // untangle elements where the key uses dot notation + if (count($subKeys) > 0) { + $value = static::nestByKeys($value, $subKeys); + } + + // now recursively do the same for each array level if needed + if (is_array($value) === true) { + $value = static::nest($value, $ignore[$key] ?? []); + } + + // merge arrays with previous results if necessary + // (needed when the same keys are used both with and without dot notation) + if ( + isset($result[$key]) === true && + is_array($result[$key]) === true && + is_array($value) === true + ) { + $result[$key] = array_replace_recursive($result[$key], $value); + } else { + $result[$key] = $value; + } + } + + return $result; + } + + /** + * Recursively creates a nested array from a set of keys + * with a key on each level + * + * @param mixed $value Arbitrary value that will end up at the bottom of the tree + * @param array $keys List of keys to use sorted from the topmost level + * @return array|mixed Nested array or (if `$keys` is empty) the input `$value` + */ + public static function nestByKeys($value, array $keys) + { + // shift off the first key from the list + $firstKey = array_shift($keys); + + // stop further recursion if there are no more keys + if ($firstKey === null) { + return $value; + } + + // return one level of the output tree, recurse further + return [ + $firstKey => static::nestByKeys($value, $keys) + ]; + } + + /** + * Sorts a multi-dimensional array by a certain column + * + * + * $array[0] = [ + * 'id' => 1, + * 'username' => 'mike', + * ]; + * + * $array[1] = [ + * 'id' => 2, + * 'username' => 'peter', + * ]; + * + * $array[3] = [ + * 'id' => 3, + * 'username' => 'john', + * ]; + * + * $sorted = A::sort($array, 'username ASC'); + * // Array + * // ( + * // [0] => Array + * // ( + * // [id] => 3 + * // [username] => john + * // ) + * // [1] => Array + * // ( + * // [id] => 1 + * // [username] => mike + * // ) + * // [2] => Array + * // ( + * // [id] => 2 + * // [username] => peter + * // ) + * // ) + * + * + * + * @param array $array The source array + * @param string $field The name of the column + * @param string $direction desc (descending) or asc (ascending) + * @param int $method A PHP sort method flag or 'natural' for + * natural sorting, which is not supported in + * PHP by sort flags + * @return array The sorted array + */ + public static function sort(array $array, string $field, string $direction = 'desc', $method = SORT_REGULAR): array + { + $direction = strtolower($direction) == 'desc' ? SORT_DESC : SORT_ASC; + $helper = []; + $result = []; + + // build the helper array + foreach ($array as $key => $row) { + $helper[$key] = $row[$field]; + } + + // natural sorting + if ($direction === SORT_DESC) { + arsort($helper, $method); + } else { + asort($helper, $method); + } + + // rebuild the original array + foreach ($helper as $key => $val) { + $result[$key] = $array[$key]; + } + + return $result; + } + + /** + * Checks wether an array is associative or not + * + * + * $array = ['a', 'b', 'c']; + * + * A::isAssociative($array); + * // returns: false + * + * $array = ['a' => 'a', 'b' => 'b', 'c' => 'c']; + * + * A::isAssociative($array); + * // returns: true + * + * + * @param array $array The array to analyze + * @return bool true: The array is associative false: It's not + */ + public static function isAssociative(array $array): bool + { + return ctype_digit(implode(null, array_keys($array))) === false; + } + + /** + * Returns the average value of an array + * + * @param array $array The source array + * @param int $decimals The number of decimals to return + * @return float The average value + */ + public static function average(array $array, int $decimals = 0): float + { + return round((array_sum($array) / sizeof($array)), $decimals); + } + + /** + * Merges arrays recursively + * + * + * $defaults = [ + * 'username' => 'admin', + * 'password' => 'admin', + * ]; + * + * $options = A::extend($defaults, ['password' => 'super-secret']); + * // returns: [ + * // 'username' => 'admin', + * // 'password' => 'super-secret' + * // ]; + * + * + * @param array ...$arrays + * @return array + */ + public static function extend(...$arrays): array + { + return array_merge_recursive(...$arrays); + } + + /** + * Update an array with a second array + * The second array can contain callbacks as values, + * which will get the original values as argument + * + * + * $user = [ + * 'username' => 'homer', + * 'email' => 'homer@simpsons.com' + * ]; + * + * // simple updates + * A::update($user, [ + * 'username' => 'homer j. simpson' + * ]); + * + * // with callback + * A::update($user, [ + * 'username' => function ($username) { + * return $username . ' j. simpson' + * } + * ]); + * + * + * @param array $array + * @param array $update + * @return array + */ + public static function update(array $array, array $update): array + { + foreach ($update as $key => $value) { + if (is_a($value, 'Closure') === true) { + $array[$key] = call_user_func($value, static::get($array, $key)); + } else { + $array[$key] = $value; + } + } + + return $array; + } + + /** + * Wraps the given value in an array + * if it's not an array yet. + * + * @param mixed|null $array + * @return array + */ + public static function wrap($array = null): array + { + if ($array === null) { + return []; + } elseif (is_array($array) === false) { + return [$array]; + } else { + return $array; + } + } +} diff --git a/kirby/src/Toolkit/Collection.php b/kirby/src/Toolkit/Collection.php new file mode 100644 index 0000000..b7aeb37 --- /dev/null +++ b/kirby/src/Toolkit/Collection.php @@ -0,0 +1,1423 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Collection extends Iterator implements Countable +{ + /** + * All registered collection filters + * + * @var array + */ + public static $filters = []; + + /** + * Whether the collection keys should be + * treated as case-sensitive + * + * @var bool + */ + protected $caseSensitive = false; + + /** + * Pagination object + * @var \Kirby\Toolkit\Pagination + */ + protected $pagination; + + /** + * Magic getter function + * + * @param string $key + * @param mixed $arguments + * @return mixed + */ + public function __call(string $key, $arguments) + { + return $this->__get($key); + } + + /** + * Constructor + * + * @param array $data + * @param bool $caseSensitive Whether the collection keys should be + * treated as case-sensitive + */ + public function __construct(array $data = [], bool $caseSensitive = false) + { + $this->caseSensitive = $caseSensitive; + $this->set($data); + } + + /** + * Improve var_dump() output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->keys(); + } + + /** + * Low-level getter for elements + * + * @param mixed $key + * @return mixed + */ + public function __get($key) + { + if ($this->caseSensitive === true) { + return $this->data[$key] ?? null; + } + + return $this->data[$key] ?? $this->data[strtolower($key)] ?? null; + } + + /** + * Low-level setter for elements + * + * @param string $key string or array + * @param mixed $value + * @return \Kirby\Toolkit\Collection + */ + public function __set(string $key, $value) + { + if ($this->caseSensitive === true) { + $this->data[$key] = $value; + } else { + $this->data[strtolower($key)] = $value; + } + + return $this; + } + + /** + * Makes it possible to echo the entire object + * + * @return string + */ + public function __toString(): string + { + return $this->toString(); + } + + /** + * Low-level element remover + * + * @param mixed $key the name of the key + */ + public function __unset($key) + { + unset($this->data[$key]); + } + + /** + * Appends an element + * + * @param mixed $key + * @param mixed $item + * @param mixed ...$args + * @return \Kirby\Toolkit\Collection + */ + public function append(...$args) + { + if (count($args) === 1) { + $this->data[] = $args[0]; + } elseif (count($args) > 1) { + $this->set($args[0], $args[1]); + } + + return $this; + } + + /** + * Creates chunks of the same size. + * The last chunk may be smaller + * + * @param int $size Number of elements per chunk + * @return \Kirby\Toolkit\Collection A new collection with an element for each chunk and + * a sub collection in each chunk + */ + public function chunk(int $size) + { + // create a multidimensional array that is chunked with the given + // chunk size keep keys of the elements + $chunks = array_chunk($this->data, $size, true); + + // convert each chunk to a sub collection + $collection = []; + + foreach ($chunks as $items) { + // we clone $this instead of creating a new object because + // different objects may have different constructors + $clone = clone $this; + $clone->data = $items; + + $collection[] = $clone; + } + + // convert the array of chunks to a collection + $result = clone $this; + $result->data = $collection; + + return $result; + } + + /** + * Returns a cloned instance of the collection + * + * @return \Kirby\Toolkit\Collection + */ + public function clone() + { + return clone $this; + } + + /** + * Getter and setter for the data + * + * @param array|null $data + * @return array|\Kirby\Toolkit\Collection + */ + public function data(array $data = null) + { + if ($data === null) { + return $this->data; + } + + // clear all previous data + $this->data = []; + + // overwrite the data array + $this->data = $data; + + return $this; + } + + /** + * Clone and remove all elements from the collection + * + * @return \Kirby\Toolkit\Collection + */ + public function empty() + { + $collection = clone $this; + $collection->data = []; + + return $collection; + } + + /** + * Adds all elements to the collection + * + * @param mixed $items + * @return \Kirby\Toolkit\Collection + */ + public function extend($items) + { + $collection = clone $this; + return $collection->set($items); + } + + /** + * Filters elements by a custom + * filter function or an array of filters + * + * @param array|\Closure $filter + * @return self + * @throws \Exception if $filter is neither a closure nor an array + */ + public function filter($filter) + { + if (is_callable($filter) === true) { + $collection = clone $this; + $collection->data = array_filter($this->data, $filter); + + return $collection; + } elseif (is_array($filter) === true) { + $collection = $this; + + foreach ($filter as $arguments) { + $collection = $collection->filterBy(...$arguments); + } + + return $collection; + } + + throw new Exception('The filter method needs either an array of filterBy rules or a closure function to be passed as parameter.'); + } + + /** + * Filters elements by one of the + * predefined filter methods. + * + * @param string $field + * @param array ...$args + * @return \Kirby\Toolkit\Collection + */ + public function filterBy(string $field, ...$args) + { + $operator = '=='; + $test = $args[0] ?? null; + $split = $args[1] ?? false; + + if (is_string($test) === true && isset(static::$filters[$test]) === true) { + $operator = $test; + $test = $args[1] ?? null; + $split = $args[2] ?? false; + } + + if (is_object($test) === true && method_exists($test, '__toString') === true) { + $test = (string)$test; + } + + // get the filter from the filters array + $filter = static::$filters[$operator] ?? null; + + // return an unfiltered list if the filter does not exist + if ($filter === null) { + return $this; + } + + if (is_array($filter) === true) { + $collection = clone $this; + $validator = $filter['validator']; + $strict = $filter['strict'] ?? true; + $method = $strict ? 'filterMatchesAll' : 'filterMatchesAny'; + + foreach ($collection->data as $key => $item) { + $value = $collection->getAttribute($item, $field, $split); + + if ($split !== false) { + if ($this->$method($validator, $value, $test) === false) { + unset($collection->data[$key]); + } + } elseif ($validator($value, $test) === false) { + unset($collection->data[$key]); + } + } + + return $collection; + } + + return $filter(clone $this, $field, $test, $split); + } + + /** + * @param string $validator + * @param mixed $values + * @param mixed $test + * @return bool + */ + protected function filterMatchesAny($validator, $values, $test): bool + { + foreach ($values as $value) { + if ($validator($value, $test) !== false) { + return true; + } + } + + return false; + } + + /** + * @param string $validator + * @param array $values + * @param mixed $test + * @return bool + */ + protected function filterMatchesAll($validator, $values, $test): bool + { + foreach ($values as $value) { + if ($validator($value, $test) === false) { + return false; + } + } + + return true; + } + + /** + * @param string $validator + * @param array $values + * @param mixed $test + * @return bool + */ + protected function filterMatchesNone($validator, $values, $test): bool + { + $matches = 0; + + foreach ($values as $value) { + if ($validator($value, $test) !== false) { + $matches++; + } + } + + return $matches === 0; + } + + /** + * Find one or multiple elements by id + * + * @param string ...$keys + * @return mixed + */ + public function find(...$keys) + { + if (count($keys) === 1) { + if (is_array($keys[0]) === true) { + $keys = $keys[0]; + } else { + return $this->findByKey($keys[0]); + } + } + + $result = []; + + foreach ($keys as $key) { + if ($item = $this->findByKey($key)) { + if (is_object($item) && method_exists($item, 'id') === true) { + $key = $item->id(); + } + $result[$key] = $item; + } + } + + $collection = clone $this; + $collection->data = $result; + return $collection; + } + + /** + * Find a single element by an attribute and its value + * + * @param string $attribute + * @param mixed $value + * @return mixed|null + */ + public function findBy(string $attribute, $value) + { + foreach ($this->data as $item) { + if ($this->getAttribute($item, $attribute) == $value) { + return $item; + } + } + return null; + } + + /** + * Find a single element by key (id) + * + * @param string $key + * @return mixed + */ + public function findByKey(string $key) + { + return $this->get($key); + } + + /** + * Returns the first element + * + * @return mixed + */ + public function first() + { + $array = $this->data; + return array_shift($array); + } + + /** + * Returns the elements in reverse order + * + * @return \Kirby\Toolkit\Collection + */ + public function flip() + { + $collection = clone $this; + $collection->data = array_reverse($this->data, true); + return $collection; + } + + /** + * Getter + * + * @param mixed $key + * @param mixed $default + * @return mixed + */ + public function get($key, $default = null) + { + return $this->__get($key) ?? $default; + } + + /** + * Extracts an attribute value from the given element + * in the collection. This is useful if elements in the collection + * might be objects, arrays or anything else and you need to + * get the value independently from that. We use it for filterBy. + * + * @param array|object $item + * @param string $attribute + * @param bool $split + * @param mixed $related + * @return mixed + */ + public function getAttribute($item, string $attribute, $split = false, $related = null) + { + $value = $this->{'getAttributeFrom' . gettype($item)}($item, $attribute); + + if ($split !== false) { + return Str::split($value, $split === true ? ',' : $split); + } + + if ($related !== null) { + return Str::toType((string)$value, $related); + } + + return $value; + } + + /** + * @param array $array + * @param string $attribute + * @return mixed + */ + protected function getAttributeFromArray(array $array, string $attribute) + { + return $array[$attribute] ?? null; + } + + /** + * @param object $object + * @param string $attribute + * @return mixed + */ + protected function getAttributeFromObject($object, string $attribute) + { + return $object->{$attribute}(); + } + + /** + * Groups the elements by a given callback + * + * @param \Closure $callback + * @return \Kirby\Toolkit\Collection A new collection with an element for each group and a sub collection in each group + * @throws \Exception + */ + public function group(Closure $callback) + { + $groups = []; + + foreach ($this->data as $key => $item) { + + // get the value to group by + $value = $callback($item); + + // make sure that there's always a proper value to group by + if (!$value) { + throw new Exception('Invalid grouping value for key: ' . $key); + } + + // make sure we have a proper key for each group + if (is_array($value) === true) { + throw new Exception('You cannot group by arrays or objects'); + } elseif (is_object($value) === true) { + if (method_exists($value, '__toString') === false) { + throw new Exception('You cannot group by arrays or objects'); + } else { + $value = (string)$value; + } + } + + if (isset($groups[$value]) === false) { + // create a new entry for the group if it does not exist yet + $groups[$value] = new static([$key => $item]); + } else { + // add the element to an existing group + $groups[$value]->set($key, $item); + } + } + + return new Collection($groups); + } + + /** + * Groups the elements by a given field + * + * @param string $field + * @param bool $i + * @return \Kirby\Toolkit\Collection A new collection with an element for each group and a sub collection in each group + * @throws \Exception + */ + public function groupBy($field, bool $i = true) + { + if (is_string($field) === false) { + throw new Exception('Cannot group by non-string values. Did you mean to call group()?'); + } + + return $this->group(function ($item) use ($field, $i) { + $value = $this->getAttribute($item, $field); + + // ignore upper/lowercase for group names + return $i === true ? Str::lower($value) : $value; + }); + } + + /** + * Returns a Collection with the intersection of the given elements + * @since 3.3.0 + * + * @param \Kirby\Toolkit\Collection $other + * @return \Kirby\Toolkit\Collection + */ + public function intersection($other) + { + return $other->find($this->keys()); + } + + /** + * Checks if there is an intersection between the given collection and this collection + * @since 3.3.0 + * + * @param \Kirby\Toolkit\Collection $other + * @return bool + */ + public function intersects($other): bool + { + foreach ($this->keys() as $key) { + if ($other->has($key)) { + return true; + } + } + + return false; + } + + /** + * Checks if the number of elements is zero + * + * @return bool + */ + public function isEmpty(): bool + { + return $this->count() === 0; + } + + /** + * Checks if the number of elements is even + * + * @return bool + */ + public function isEven(): bool + { + return $this->count() % 2 === 0; + } + + /** + * Checks if the number of elements is more than zero + * + * @return bool + */ + public function isNotEmpty(): bool + { + return $this->count() > 0; + } + + /** + * Checks if the number of elements is odd + * + * @return bool + */ + public function isOdd(): bool + { + return $this->count() % 2 !== 0; + } + + /** + * Returns the last element + * + * @return mixed + */ + public function last() + { + $array = $this->data; + return array_pop($array); + } + + /** + * Returns a new object with a limited number of elements + * + * @param int $limit The number of elements to return + * @return \Kirby\Toolkit\Collection + */ + public function limit(int $limit) + { + return $this->slice(0, $limit); + } + + /** + * Map a function to each element + * + * @param callable $callback + * @return \Kirby\Toolkit\Collection + */ + public function map(callable $callback) + { + $this->data = array_map($callback, $this->data); + return $this; + } + + /** + * Returns the nth element from the collection + * + * @param int $n + * @return mixed + */ + public function nth(int $n) + { + return array_values($this->data)[$n] ?? null; + } + + /** + * Returns a Collection without the given element(s) + * + * @param string ...$keys any number of keys, passed as individual arguments + * @return \Kirby\Toolkit\Collection + */ + public function not(...$keys) + { + $collection = clone $this; + foreach ($keys as $key) { + unset($collection->data[$key]); + } + return $collection; + } + + /** + * Returns a new object starting from the given offset + * + * @param int $offset The index to start from + * @return \Kirby\Toolkit\Collection + */ + public function offset(int $offset) + { + return $this->slice($offset); + } + + /** + * Add pagination + * + * @param array ...$arguments + * @return \Kirby\Toolkit\Collection a sliced set of data + */ + public function paginate(...$arguments) + { + $this->pagination = Pagination::for($this, ...$arguments); + + // slice and clone the collection according to the pagination + return $this->slice($this->pagination->offset(), $this->pagination->limit()); + } + + /** + * Get the previously added pagination object + * + * @return \Kirby\Toolkit\Pagination|null + */ + public function pagination() + { + return $this->pagination; + } + + /** + * Extracts all values for a single field into + * a new array + * + * @param string $field + * @param string|null $split + * @param bool $unique + * @return array + */ + public function pluck(string $field, string $split = null, bool $unique = false): array + { + $result = []; + + foreach ($this->data as $item) { + $row = $this->getAttribute($item, $field); + + if ($split !== null) { + $result = array_merge($result, Str::split($row, $split)); + } else { + $result[] = $row; + } + } + + if ($unique === true) { + $result = array_unique($result); + } + + return array_values($result); + } + + /** + * Prepends an element to the data array + * + * @param mixed $key + * @param mixed $item + * @param mixed ...$args + * @return \Kirby\Toolkit\Collection + */ + public function prepend(...$args) + { + if (count($args) === 1) { + array_unshift($this->data, $args[0]); + } elseif (count($args) > 1) { + $data = $this->data; + $this->data = []; + $this->set($args[0], $args[1]); + $this->data += $data; + } + + return $this; + } + + /** + * Runs a combination of filterBy, sortBy, not + * offset, limit and paginate on the collection. + * Any part of the query is optional. + * + * @param array $arguments + * @return \Kirby\Toolkit\Collection + */ + public function query(array $arguments = []) + { + $result = clone $this; + + if (isset($arguments['not']) === true) { + $result = $result->not(...$arguments['not']); + } + + if (isset($arguments['filterBy']) === true) { + foreach ($arguments['filterBy'] as $filter) { + if (isset($filter['field']) === true && isset($filter['value']) === true) { + $result = $result->filterBy($filter['field'], $filter['operator'] ?? '==', $filter['value']); + } + } + } + + if (isset($arguments['offset']) === true) { + $result = $result->offset($arguments['offset']); + } + + if (isset($arguments['limit']) === true) { + $result = $result->limit($arguments['limit']); + } + + if (isset($arguments['sortBy']) === true) { + if (is_array($arguments['sortBy'])) { + $sort = explode(' ', implode(' ', $arguments['sortBy'])); + } else { + // if there are commas in the sortBy argument, removes it + if (Str::contains($arguments['sortBy'], ',') === true) { + $arguments['sortBy'] = Str::replace($arguments['sortBy'], ',', ''); + } + + $sort = explode(' ', $arguments['sortBy']); + } + $result = $result->sortBy(...$sort); + } + + if (isset($arguments['paginate']) === true) { + $result = $result->paginate($arguments['paginate']); + } + + return $result; + } + + /** + * Removes an element from the array by key + * + * @param mixed $key the name of the key + * @return \Kirby\Toolkit\Collection + */ + public function remove($key) + { + $this->__unset($key); + return $this; + } + + /** + * Adds a new element to the collection + * + * @param mixed $key string or array + * @param mixed $value + * @return \Kirby\Toolkit\Collection + */ + public function set($key, $value = null) + { + if (is_array($key)) { + foreach ($key as $k => $v) { + $this->__set($k, $v); + } + } else { + $this->__set($key, $value); + } + return $this; + } + + /** + * Shuffle all elements + * + * @return \Kirby\Toolkit\Collection + */ + public function shuffle() + { + $data = $this->data; + $keys = $this->keys(); + shuffle($keys); + + $collection = clone $this; + $collection->data = []; + + foreach ($keys as $key) { + $collection->data[$key] = $data[$key]; + } + + return $collection; + } + + /** + * Returns a slice of the object + * + * @param int $offset The optional index to start the slice from + * @param int|null $limit The optional number of elements to return + * @return \Kirby\Toolkit\Collection + */ + public function slice(int $offset = 0, int $limit = null) + { + if ($offset === 0 && $limit === null) { + return $this; + } + + $collection = clone $this; + $collection->data = array_slice($this->data, $offset, $limit); + return $collection; + } + + /** + * Get sort arguments from a string + * + * @param string $sortBy + * @return array + */ + public static function sortArgs(string $sortBy): array + { + // if there are commas in the sortBy argument, removes it + if (Str::contains($sortBy, ',') === true) { + $sortBy = Str::replace($sortBy, ',', ''); + } + + $sortArgs = Str::split($sortBy, ' '); + + // fill in PHP constants + array_walk($sortArgs, function (string &$value) { + if (Str::startsWith($value, 'SORT_') === true && defined($value) === true) { + $value = constant($value); + } + }); + + return $sortArgs; + } + + /** + * Sorts the elements by any number of fields + * + * @param string|callable $field Field name or value callback to sort by + * @param string $direction asc or desc + * @param int $method The sort flag, SORT_REGULAR, SORT_NUMERIC etc. + * @return \Kirby\Toolkit\Collection + */ + public function sortBy() + { + // there is no need to sort empty collections + if (empty($this->data) === true) { + return $this; + } + + $args = func_get_args(); + $array = $this->data; + $collection = $this->clone(); + + // loop through all method arguments and find sets of fields to sort by + $fields = []; + + foreach ($args as $arg) { + + // get the index of the latest field array inside the $fields array + $currentField = $fields ? count($fields) - 1 : 0; + + // detect the type of argument + // sorting direction + $argLower = is_string($arg) ? strtolower($arg) : null; + + if ($arg === SORT_ASC || $argLower === 'asc') { + $fields[$currentField]['direction'] = SORT_ASC; + } elseif ($arg === SORT_DESC || $argLower === 'desc') { + $fields[$currentField]['direction'] = SORT_DESC; + + // other string: the field name + } elseif (is_string($arg) === true) { + $values = []; + + foreach ($array as $key => $value) { + $value = $collection->getAttribute($value, $arg); + + // make sure that we return something sortable + // but don't convert other scalars (especially numbers) to strings! + $values[$key] = is_scalar($value) === true ? $value : (string)$value; + } + + $fields[] = ['field' => $arg, 'values' => $values]; + + // callable: custom field values + } elseif (is_callable($arg) === true) { + $values = []; + + foreach ($array as $key => $value) { + $value = $arg($value); + + // make sure that we return something sortable + // but don't convert other scalars (especially numbers) to strings! + $values[$key] = is_scalar($value) === true ? $value : (string)$value; + } + + $fields[] = ['field' => null, 'values' => $values]; + + // flags + } else { + $fields[$currentField]['flags'] = $arg; + } + } + + // build the multisort params in the right order + $params = []; + + foreach ($fields as $field) { + $params[] = $field['values'] ?? []; + $params[] = $field['direction'] ?? SORT_ASC; + $params[] = $field['flags'] ?? SORT_NATURAL | SORT_FLAG_CASE; + } + + // check what kind of collection items we have; only check for the first + // item for better performance (we assume that all collection items are + // of the same type) + $firstItem = $collection->first(); + if (is_object($firstItem) === true) { + // avoid the "Nesting level too deep - recursive dependency?" error + // when PHP tries to sort by the objects directly (in case all other + // fields are 100 % equal for some elements) + if (method_exists($firstItem, '__toString') === true) { + // PHP can easily convert the objects to strings, so it should + // compare them as strings instead of as objects to avoid the recursion + $params[] = &$array; + $params[] = SORT_STRING; + } else { + // we can't convert the objects to strings, so we need a fallback: + // custom fictional field that is guaranteed to have a unique value + // for each item; WARNING: may lead to slightly wrong sorting results + // and is therefore only used as a fallback if we don't have another way + $params[] = range(1, count($array)); + $params[] = SORT_ASC; + $params[] = SORT_NUMERIC; + + $params[] = &$array; + } + } else { + // collection items are scalar or array; no correction necessary + $params[] = &$array; + } + + // array_multisort receives $params as separate params + array_multisort(...$params); + + // $array has been overwritten by array_multisort + $collection->data = $array; + return $collection; + } + + /** + * Converts the object into an array + * + * @param \Closure|null $map + * @return array + */ + public function toArray(Closure $map = null): array + { + if ($map !== null) { + return array_map($map, $this->data); + } + + return $this->data; + } + + /** + * Converts the object into a JSON string + * + * @return string + */ + public function toJson(): string + { + return json_encode($this->toArray()); + } + + /** + * Converts the object to a string + * + * @return string + */ + public function toString(): string + { + return implode('
', $this->keys()); + } + + /** + * Returns an non-associative array + * with all values + * + * @return array + */ + public function values(): array + { + return array_values($this->data); + } + + /** + * The when method only executes the given Closure when the first parameter + * is true. If the first parameter is false, the Closure will not be executed. + * You may pass another Closure as the third parameter to the when method. + * This Closure will execute if the first parameter evaluates as false + * + * @since 3.3.0 + * @param mixed $condition + * @param \Closure $callback + * @param \Closure|null $fallback + * @return mixed + */ + public function when($condition, Closure $callback, Closure $fallback = null) + { + if ($condition) { + return $callback->call($this, $condition); + } + + if ($fallback !== null) { + return $fallback->call($this, $condition); + } + + return $this; + } + + /** + * Alias for $this->not() + * + * @param string ...$keys any number of keys, passed as individual arguments + * @return \Kirby\Toolkit\Collection + */ + public function without(...$keys) + { + return $this->not(...$keys); + } +} + +/** + * Equals Filter + * + * @param \Kirby\Toolkit\Collection $collection + * @param mixed $field + * @param mixed $test + * @param bool $split + * @return mixed + */ +Collection::$filters['=='] = function ($collection, $field, $test, $split = false) { + foreach ($collection->data as $key => $item) { + $value = $collection->getAttribute($item, $field, $split, $test); + + if ($split !== false) { + if (in_array($test, $value) === false) { + unset($collection->data[$key]); + } + } elseif ($value !== $test) { + unset($collection->data[$key]); + } + } + + return $collection; +}; + +/** + * Not Equals Filter + * + * @param \Kirby\Toolkit\Collection $collection + * @param mixed $field + * @param mixed $test + * @param bool $split + * @return mixed + */ +Collection::$filters['!='] = function ($collection, $field, $test, $split = false) { + foreach ($collection->data as $key => $item) { + $value = $collection->getAttribute($item, $field, $split, $test); + + if ($split !== false) { + if (in_array($test, $value) === true) { + unset($collection->data[$key]); + } + } elseif ((string)$value == $test) { + unset($collection->data[$key]); + } + } + + return $collection; +}; + +/** + * In Filter + */ +Collection::$filters['in'] = [ + 'validator' => function ($value, $test) { + return in_array($value, $test) === true; + }, + 'strict' => false +]; + +/** + * Not In Filter + */ +Collection::$filters['not in'] = [ + 'validator' => function ($value, $test) { + return in_array($value, $test) === false; + }, +]; + +/** + * Contains Filter + */ +Collection::$filters['*='] = [ + 'validator' => function ($value, $test) { + return strpos($value, $test) !== false; + }, + 'strict' => false +]; + +/** + * Not Contains Filter + */ +Collection::$filters['!*='] = [ + 'validator' => function ($value, $test) { + return strpos($value, $test) === false; + }, +]; + +/** + * More Filter + */ +Collection::$filters['>'] = [ + 'validator' => function ($value, $test) { + return $value > $test; + } +]; + +/** + * Min Filter + */ +Collection::$filters['>='] = [ + 'validator' => function ($value, $test) { + return $value >= $test; + } +]; + +/** + * Less Filter + */ +Collection::$filters['<'] = [ + 'validator' => function ($value, $test) { + return $value < $test; + } +]; + +/** + * Max Filter + */ +Collection::$filters['<='] = [ + 'validator' => function ($value, $test) { + return $value <= $test; + } +]; + +/** + * Ends With Filter + */ +Collection::$filters['$='] = [ + 'validator' => 'V::endsWith', + 'strict' => false, +]; + +/** + * Not Ends With Filter + */ +Collection::$filters['!$='] = [ + 'validator' => function ($value, $test) { + return V::endsWith($value, $test) === false; + } +]; + +/** + * Starts With Filter + */ +Collection::$filters['^='] = [ + 'validator' => 'V::startsWith', + 'strict' => false +]; + +/** + * Not Starts With Filter + */ +Collection::$filters['!^='] = [ + 'validator' => function ($value, $test) { + return V::startsWith($value, $test) === false; + } +]; + +/** + * Between Filter + */ +Collection::$filters['between'] = Collection::$filters['..'] = [ + 'validator' => function ($value, $test) { + return V::between($value, ...$test) === true; + }, + 'strict' => false +]; + +/** + * Match Filter + */ +Collection::$filters['*'] = [ + 'validator' => 'V::match', + 'strict' => false +]; + +/** + * Not Match Filter + */ +Collection::$filters['!*'] = [ + 'validator' => function ($value, $test) { + return V::match($value, $test) === false; + } +]; + +/** + * Max Length Filter + */ +Collection::$filters['maxlength'] = [ + 'validator' => 'V::maxLength', +]; + +/** + * Min Length Filter + */ +Collection::$filters['minlength'] = [ + 'validator' => 'V::minLength' +]; + +/** + * Max Words Filter + */ +Collection::$filters['maxwords'] = [ + 'validator' => 'V::maxWords', +]; + +/** + * Min Words Filter + */ +Collection::$filters['minwords'] = [ + 'validator' => 'V::minWords', +]; + +/** + * Date Equals Filter + */ +Collection::$filters['date =='] = [ + 'validator' => function ($value, $test) { + return V::date($value, '==', $test); + } +]; + +/** + * Date Not Equals Filter + */ +Collection::$filters['date !='] = [ + 'validator' => function ($value, $test) { + return V::date($value, '!=', $test); + } +]; + +/** + * Date More Filter + */ +Collection::$filters['date >'] = [ + 'validator' => function ($value, $test) { + return V::date($value, '>', $test); + } +]; + +/** + * Date Min Filter + */ +Collection::$filters['date >='] = [ + 'validator' => function ($value, $test) { + return V::date($value, '>=', $test); + } +]; + +/** + * Date Less Filter + */ +Collection::$filters['date <'] = [ + 'validator' => function ($value, $test) { + return V::date($value, '<', $test); + } +]; + +/** + * Date Max Filter + */ +Collection::$filters['date <='] = [ + 'validator' => function ($value, $test) { + return V::date($value, '<=', $test); + } +]; + +/** + * Date Between Filter + */ +Collection::$filters['date between'] = Collection::$filters['date ..'] = [ + 'validator' => function ($value, $test) { + return V::date($value, '>=', $test[0]) && + V::date($value, '<=', $test[1]); + } +]; diff --git a/kirby/src/Toolkit/Component.php b/kirby/src/Toolkit/Component.php new file mode 100644 index 0000000..537942a --- /dev/null +++ b/kirby/src/Toolkit/Component.php @@ -0,0 +1,290 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Component +{ + /** + * Registry for all component mixins + * + * @var array + */ + public static $mixins = []; + + /** + * Registry for all component types + * + * @var array + */ + public static $types = []; + + /** + * An array of all passed attributes + * + * @var array + */ + protected $attrs = []; + + /** + * An array of all computed properties + * + * @var array + */ + protected $computed = []; + + /** + * An array of all registered methods + * + * @var array + */ + protected $methods = []; + + /** + * An array of all component options + * from the component definition + * + * @var array + */ + protected $options = []; + + /** + * An array of all resolved props + * + * @var array + */ + protected $props = []; + + /** + * The component type + * + * @var string + */ + protected $type; + + /** + * Magic caller for defined methods and properties + * + * @param string $name + * @param array $arguments + * @return mixed + */ + public function __call(string $name, array $arguments = []) + { + if (array_key_exists($name, $this->computed) === true) { + return $this->computed[$name]; + } + + if (array_key_exists($name, $this->props) === true) { + return $this->props[$name]; + } + + if (array_key_exists($name, $this->methods) === true) { + return $this->methods[$name]->call($this, ...$arguments); + } + + return $this->$name; + } + + /** + * Creates a new component for the given type + * + * @param string $type + * @param array $attrs + */ + public function __construct(string $type, array $attrs = []) + { + if (isset(static::$types[$type]) === false) { + throw new InvalidArgumentException('Undefined component type: ' . $type); + } + + $this->attrs = $attrs; + $this->options = $options = $this->setup($type); + $this->methods = $methods = $options['methods'] ?? []; + + foreach ($attrs as $attrName => $attrValue) { + $this->$attrName = $attrValue; + } + + if (isset($options['props']) === true) { + $this->applyProps($options['props']); + } + + if (isset($options['computed']) === true) { + $this->applyComputed($options['computed']); + } + + $this->attrs = $attrs; + $this->methods = $methods; + $this->options = $options; + $this->type = $type; + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + /** + * Fallback for missing properties to return + * null instead of an error + * + * @param string $attr + * @return null + */ + public function __get(string $attr) + { + return null; + } + + /** + * A set of default options for each component. + * This can be overwritten by extended classes + * to define basic options that should always + * be applied. + * + * @return array + */ + public static function defaults(): array + { + return []; + } + + /** + * Register all defined props and apply the + * passed values. + * + * @param array $props + * @return void + */ + protected function applyProps(array $props): void + { + foreach ($props as $propName => $propFunction) { + if (is_callable($propFunction) === true) { + if (isset($this->attrs[$propName]) === true) { + try { + $this->$propName = $this->props[$propName] = $propFunction->call($this, $this->attrs[$propName]); + } catch (TypeError $e) { + throw new TypeError('Invalid value for "' . $propName . '"'); + } + } else { + try { + $this->$propName = $this->props[$propName] = $propFunction->call($this); + } catch (ArgumentCountError $e) { + throw new ArgumentCountError('Please provide a value for "' . $propName . '"'); + } + } + } else { + $this->$propName = $this->props[$propName] = $propFunction; + } + } + } + + /** + * Register all computed properties and calculate their values. + * This must happen after all props are registered. + * + * @param array $computed + * @return void + */ + protected function applyComputed(array $computed): void + { + foreach ($computed as $computedName => $computedFunction) { + if (is_callable($computedFunction) === true) { + $this->$computedName = $this->computed[$computedName] = $computedFunction->call($this); + } + } + } + + /** + * Load a component definition by type + * + * @param string $type + * @return array + */ + public static function load(string $type): array + { + $definition = static::$types[$type]; + + // load definitions from string + if (is_string($definition) === true) { + if (is_file($definition) !== true) { + throw new Exception('Component definition ' . $definition . ' does not exist'); + } + + static::$types[$type] = $definition = F::load($definition); + } + + return $definition; + } + + /** + * Loads all options from the component definition + * mixes in the defaults from the defaults method and + * then injects all additional mixins, defined in the + * component options. + * + * @param string $type + * @return array + */ + public static function setup(string $type): array + { + // load component definition + $definition = static::load($type); + + if (isset($definition['extends']) === true) { + // extend other definitions + $options = array_replace_recursive(static::defaults(), static::load($definition['extends']), $definition); + } else { + // inject defaults + $options = array_replace_recursive(static::defaults(), $definition); + } + + // inject mixins + if (isset($options['mixins']) === true) { + foreach ($options['mixins'] as $mixin) { + if (isset(static::$mixins[$mixin]) === true) { + $options = array_replace_recursive(static::$mixins[$mixin], $options); + } + } + } + + return $options; + } + + /** + * Converts all props and computed props to an array + * + * @return array + */ + public function toArray(): array + { + if (is_a($this->options['toArray'] ?? null, 'Closure') === true) { + return $this->options['toArray']->call($this); + } + + $array = array_merge($this->attrs, $this->props, $this->computed); + + ksort($array); + + return $array; + } +} diff --git a/kirby/src/Toolkit/Config.php b/kirby/src/Toolkit/Config.php new file mode 100644 index 0000000..59cabf3 --- /dev/null +++ b/kirby/src/Toolkit/Config.php @@ -0,0 +1,21 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Config extends Silo +{ + /** + * @var array + */ + public static $data = []; +} diff --git a/kirby/src/Toolkit/Controller.php b/kirby/src/Toolkit/Controller.php new file mode 100644 index 0000000..ee18435 --- /dev/null +++ b/kirby/src/Toolkit/Controller.php @@ -0,0 +1,66 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Controller +{ + protected $function; + + public function __construct(Closure $function) + { + $this->function = $function; + } + + public function arguments(array $data = []): array + { + $info = new ReflectionFunction($this->function); + $args = []; + + foreach ($info->getParameters() as $parameter) { + $name = $parameter->getName(); + $args[] = $data[$name] ?? null; + } + + return $args; + } + + public function call($bind = null, $data = []) + { + $args = $this->arguments($data); + + if ($bind === null) { + return call_user_func($this->function, ...$args); + } + + return $this->function->call($bind, ...$args); + } + + public static function load(string $file) + { + if (is_file($file) === false) { + return null; + } + + $function = F::load($file); + + if (is_a($function, 'Closure') === false) { + return null; + } + + return new static($function); + } +} diff --git a/kirby/src/Toolkit/Dir.php b/kirby/src/Toolkit/Dir.php new file mode 100644 index 0000000..7b57c62 --- /dev/null +++ b/kirby/src/Toolkit/Dir.php @@ -0,0 +1,439 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Dir +{ + /** + * Ignore when scanning directories + * + * @var array + */ + public static $ignore = [ + '.', + '..', + '.DS_Store', + '.gitignore', + '.git', + '.svn', + '.htaccess', + 'Thumb.db', + '@eaDir' + ]; + + /** + * Copy the directory to a new destination + * + * @param string $dir + * @param string $target + * @param bool $recursive + * @param array $ignore + * @return bool + */ + public static function copy(string $dir, string $target, bool $recursive = true, array $ignore = []): bool + { + if (is_dir($dir) === false) { + throw new Exception('The directory "' . $dir . '" does not exist'); + } + + if (is_dir($target) === true) { + throw new Exception('The target directory "' . $target . '" exists'); + } + + if (static::make($target) !== true) { + throw new Exception('The target directory "' . $target . '" could not be created'); + } + + foreach (static::read($dir) as $name) { + $root = $dir . '/' . $name; + + if (in_array($root, $ignore) === true) { + continue; + } + + if (is_dir($root) === true) { + if ($recursive === true) { + static::copy($root, $target . '/' . $name); + } + } else { + F::copy($root, $target . '/' . $name); + } + } + + return true; + } + + /** + * Get all subdirectories + * + * @param string $dir + * @param array $ignore + * @param bool $absolute + * @return array + */ + public static function dirs(string $dir, array $ignore = null, bool $absolute = false): array + { + $result = array_values(array_filter(static::read($dir, $ignore, true), 'is_dir')); + + if ($absolute !== true) { + $result = array_map('basename', $result); + } + + return $result; + } + + /** + * Checks if the directory exists on disk + * + * @param string $dir + * @return bool + */ + public static function exists(string $dir): bool + { + return is_dir($dir) === true; + } + + /** + * Get all files + * + * @param string $dir + * @param array $ignore + * @param bool $absolute + * @return array + */ + public static function files(string $dir, array $ignore = null, bool $absolute = false): array + { + $result = array_values(array_filter(static::read($dir, $ignore, true), 'is_file')); + + if ($absolute !== true) { + $result = array_map('basename', $result); + } + + return $result; + } + + /** + * Read the directory and all subdirectories + * + * @param string $dir + * @param bool $recursive + * @param array $ignore + * @param string $path + * @return array + */ + public static function index(string $dir, bool $recursive = false, array $ignore = null, string $path = null) + { + $result = []; + $dir = realpath($dir); + $items = static::read($dir); + + foreach ($items as $item) { + $root = $dir . '/' . $item; + $entry = $path !== null ? $path . '/' . $item: $item; + $result[] = $entry; + + if ($recursive === true && is_dir($root) === true) { + $result = array_merge($result, static::index($root, true, $ignore, $entry)); + } + } + + return $result; + } + + /** + * Checks if the folder has any contents + * + * @param string $dir + * @return bool + */ + public static function isEmpty(string $dir): bool + { + return count(static::read($dir)) === 0; + } + + /** + * Checks if the directory is readable + * + * @param string $dir + * @return bool + */ + public static function isReadable(string $dir): bool + { + return is_readable($dir); + } + + /** + * Checks if the directory is writable + * + * @param string $dir + * @return bool + */ + public static function isWritable(string $dir): bool + { + return is_writable($dir); + } + + /** + * Create a (symbolic) link to a directory + * + * @param string $source + * @param string $link + * @return bool + */ + public static function link(string $source, string $link): bool + { + Dir::make(dirname($link), true); + + if (is_dir($link) === true) { + return true; + } + + if (is_dir($source) === false) { + throw new Exception(sprintf('The directory "%s" does not exist and cannot be linked', $source)); + } + + try { + return symlink($source, $link) === true; + } catch (Throwable $e) { + return false; + } + } + + /** + * Creates a new directory + * + * @param string $dir The path for the new directory + * @param bool $recursive Create all parent directories, which don't exist + * @return bool True: the dir has been created, false: creating failed + */ + public static function make(string $dir, bool $recursive = true): bool + { + if (empty($dir) === true) { + return false; + } + + if (is_dir($dir) === true) { + return true; + } + + $parent = dirname($dir); + + if ($recursive === true) { + if (is_dir($parent) === false) { + static::make($parent, true); + } + } + + if (is_writable($parent) === false) { + throw new Exception(sprintf('The directory "%s" cannot be created', $dir)); + } + + return mkdir($dir); + } + + /** + * Recursively check when the dir and all + * subfolders have been modified for the last time. + * + * @param string $dir The path of the directory + * @param string $format + * @param string $handler + * @return int + */ + public static function modified(string $dir, string $format = null, string $handler = 'date') + { + $modified = filemtime($dir); + $items = static::read($dir); + + foreach ($items as $item) { + if (is_file($dir . '/' . $item) === true) { + $newModified = filemtime($dir . '/' . $item); + } else { + $newModified = static::modified($dir . '/' . $item); + } + + $modified = ($newModified > $modified) ? $newModified : $modified; + } + + return $format !== null ? $handler($format, $modified) : $modified; + } + + /** + * Moves a directory to a new location + * + * @param string $old The current path of the directory + * @param string $new The desired path where the dir should be moved to + * @return bool true: the directory has been moved, false: moving failed + */ + public static function move(string $old, string $new): bool + { + if ($old === $new) { + return true; + } + + if (is_dir($old) === false || is_dir($new) === true) { + return false; + } + + if (static::make(dirname($new), true) !== true) { + throw new Exception('The parent directory cannot be created'); + } + + return rename($old, $new); + } + + /** + * Returns a nicely formatted size of all the contents of the folder + * + * @param string $dir The path of the directory + * @return mixed + */ + public static function niceSize(string $dir) + { + return F::niceSize(static::size($dir)); + } + + /** + * Reads all files from a directory and returns them as an array. + * It skips unwanted invisible stuff. + * + * @param string $dir The path of directory + * @param array $ignore Optional array with filenames, which should be ignored + * @param bool $absolute If true, the full path for each item will be returned + * @return array An array of filenames + */ + public static function read(string $dir, array $ignore = null, bool $absolute = false): array + { + if (is_dir($dir) === false) { + return []; + } + + // create the ignore pattern + $ignore = $ignore ?? static::$ignore; + $ignore = array_merge($ignore, ['.', '..']); + + // scan for all files and dirs + $result = array_values((array)array_diff(scandir($dir), $ignore)); + + // add absolute paths + if ($absolute === true) { + $result = array_map(function ($item) use ($dir) { + return $dir . '/' . $item; + }, $result); + } + + return $result; + } + + /** + * Removes a folder including all containing files and folders + * + * @param string $dir + * @return bool + */ + public static function remove(string $dir): bool + { + $dir = realpath($dir); + + if (is_dir($dir) === false) { + return true; + } + + if (is_link($dir) === true) { + return unlink($dir); + } + + foreach (scandir($dir) as $childName) { + if (in_array($childName, ['.', '..']) === true) { + continue; + } + + $child = $dir . '/' . $childName; + + if (is_link($child) === true) { + unlink($child); + } elseif (is_dir($child) === true) { + static::remove($child); + } else { + F::remove($child); + } + } + + return rmdir($dir); + } + + /** + * Gets the size of the directory and all subfolders and files + * + * @param string $dir The path of the directory + * @return mixed + */ + public static function size(string $dir) + { + if (is_dir($dir) === false) { + return false; + } + + $size = 0; + $items = static::read($dir); + + foreach ($items as $item) { + $root = $dir . '/' . $item; + + if (is_dir($root) === true) { + $size += static::size($root); + } elseif (is_file($root) === true) { + $size += F::size($root); + } + } + + return $size; + } + + /** + * Checks if the directory or any subdirectory has been + * modified after the given timestamp + * + * @param string $dir + * @param int $time + * @return bool + */ + public static function wasModifiedAfter(string $dir, int $time): bool + { + if (filemtime($dir) > $time) { + return true; + } + + $content = static::read($dir); + + foreach ($content as $item) { + $subdir = $dir . '/' . $item; + + if (filemtime($subdir) > $time) { + return true; + } + + if (is_dir($subdir) === true && static::wasModifiedAfter($subdir, $time) === true) { + return true; + } + } + + return false; + } +} diff --git a/kirby/src/Toolkit/Escape.php b/kirby/src/Toolkit/Escape.php new file mode 100644 index 0000000..5b6ebcd --- /dev/null +++ b/kirby/src/Toolkit/Escape.php @@ -0,0 +1,145 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Escape +{ + /** + * Escape common HTML attributes data + * + * This can be used to put untrusted data into typical attribute values + * like width, name, value, etc. + * + * This should not be used for complex attributes like href, src, style, + * or any of the event handlers like onmouseover. + * Use esc($string, 'js') for event handler attributes, esc($string, 'url') + * for src attributes and esc($string, 'css') for style attributes. + * + *
content
+ *
content
+ *
content
+ * + * @param string $string + * @return string + */ + public static function attr($string) + { + return (new Escaper('utf-8'))->escapeHtmlAttr($string); + } + + /** + * Escape HTML style property values + * + * This can be used to put untrusted data into a stylesheet or a style tag. + * + * Stay away from putting untrusted data into complex properties like url, + * behavior, and custom (-moz-binding). You should also not put untrusted data + * into IE’s expression property value which allows JavaScript. + * + * + * + * text + * + * @param string $string + * @return string + */ + public static function css($string) + { + return (new Escaper('utf-8'))->escapeCss($string); + } + + /** + * Escape HTML element content + * + * This can be used to put untrusted data directly into the HTML body somewhere. + * This includes inside normal tags like div, p, b, td, etc. + * + * Escapes &, <, >, ", and ' with HTML entity encoding to prevent switching + * into any execution context, such as script, style, or event handlers. + * + * ...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE... + *
...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...
+ * + * @param string $string + * @return string + */ + public static function html($string) + { + return (new Escaper('utf-8'))->escapeHtml($string); + } + + /** + * Escape JavaScript data values + * + * This can be used to put dynamically generated JavaScript code + * into both script blocks and event-handler attributes. + * + * + * + *
+ * + * @param string $string + * @return string + */ + public static function js($string) + { + return (new Escaper('utf-8'))->escapeJs($string); + } + + /** + * Escape URL parameter values + * + * This can be used to put untrusted data into HTTP GET parameter values. + * This should not be used to escape an entire URI. + * + * link + * + * @param string $string + * @return string + */ + public static function url($string) + { + return rawurlencode($string); + } + + /** + * Escape XML element content + * + * Removes offending characters that could be wrongfully interpreted as XML markup. + * + * The following characters are reserved in XML and will be replaced with their + * corresponding XML entities: + * + * ' is replaced with ' + * " is replaced with " + * & is replaced with & + * < is replaced with < + * > is replaced with > + * + * @param string $string + * @return string + */ + public static function xml($string) + { + return htmlspecialchars($string, ENT_QUOTES | ENT_XML1, 'UTF-8'); + } +} diff --git a/kirby/src/Toolkit/F.php b/kirby/src/Toolkit/F.php new file mode 100644 index 0000000..209c611 --- /dev/null +++ b/kirby/src/Toolkit/F.php @@ -0,0 +1,835 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class F +{ + public static $types = [ + 'archive' => [ + 'gz', + 'gzip', + 'tar', + 'tgz', + 'zip', + ], + 'audio' => [ + 'aif', + 'aiff', + 'm4a', + 'midi', + 'mp3', + 'wav', + ], + 'code' => [ + 'css', + 'js', + 'json', + 'java', + 'htm', + 'html', + 'php', + 'rb', + 'py', + 'scss', + 'xml', + 'yaml', + 'yml', + ], + 'document' => [ + 'csv', + 'doc', + 'docx', + 'dotx', + 'indd', + 'md', + 'mdown', + 'pdf', + 'ppt', + 'pptx', + 'rtf', + 'txt', + 'xl', + 'xls', + 'xlsx', + 'xltx', + ], + 'image' => [ + 'ai', + 'avif', + 'bmp', + 'gif', + 'eps', + 'ico', + 'j2k', + 'jp2', + 'jpeg', + 'jpg', + 'jpe', + 'jp2', + 'png', + 'ps', + 'psd', + 'svg', + 'tif', + 'tiff', + 'webp' + ], + 'video' => [ + 'avi', + 'flv', + 'm4v', + 'mov', + 'movie', + 'mpe', + 'mpg', + 'mp4', + 'ogg', + 'ogv', + 'swf', + 'webm', + ], + ]; + + public static $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + /** + * Appends new content to an existing file + * + * @param string $file The path for the file + * @param mixed $content Either a string or an array. Arrays will be converted to JSON. + * @return bool + */ + public static function append(string $file, $content): bool + { + return static::write($file, $content, true); + } + + /** + * Returns the file content as base64 encoded string + * + * @param string $file The path for the file + * @return string + */ + public static function base64(string $file): string + { + return base64_encode(static::read($file)); + } + + /** + * Copy a file to a new location. + * + * @param string $source + * @param string $target + * @param bool $force + * @return bool + */ + public static function copy(string $source, string $target, bool $force = false): bool + { + if (file_exists($source) === false || (file_exists($target) === true && $force === false)) { + return false; + } + + $directory = dirname($target); + + // create the parent directory if it does not exist + if (is_dir($directory) === false) { + Dir::make($directory, true); + } + + return copy($source, $target); + } + + /** + * Just an alternative for dirname() to stay consistent + * + * + * + * $dirname = F::dirname('/var/www/test.txt'); + * // dirname is /var/www + * + * + * + * @param string $file The path + * @return string + */ + public static function dirname(string $file): string + { + return dirname($file); + } + + /** + * Checks if the file exists on disk + * + * @param string $file + * @param string $in + * @return bool + */ + public static function exists(string $file, string $in = null): bool + { + try { + static::realpath($file, $in); + return true; + } catch (Exception $e) { + return false; + } + } + + /** + * Gets the extension of a file + * + * @param string $file The filename or path + * @param string $extension Set an optional extension to overwrite the current one + * @return string + */ + public static function extension(string $file = null, string $extension = null): string + { + // overwrite the current extension + if ($extension !== null) { + return static::name($file) . '.' . $extension; + } + + // return the current extension + return Str::lower(pathinfo($file, PATHINFO_EXTENSION)); + } + + /** + * Converts a file extension to a mime type + * + * @param string $extension + * @return string|false + */ + public static function extensionToMime(string $extension) + { + return Mime::fromExtension($extension); + } + + /** + * Returns the file type for a passed extension + * + * @param string $extension + * @return string|false + */ + public static function extensionToType(string $extension) + { + foreach (static::$types as $type => $extensions) { + if (in_array($extension, $extensions) === true) { + return $type; + } + } + + return false; + } + + /** + * Returns all extensions for a certain file type + * + * @param string $type + * @return array + */ + public static function extensions(string $type = null) + { + if ($type === null) { + return array_keys(Mime::types()); + } + + return static::$types[$type] ?? []; + } + + /** + * Extracts the filename from a file path + * + * + * + * $filename = F::filename('/var/www/test.txt'); + * // filename is test.txt + * + * + * + * @param string $name The path + * @return string + */ + public static function filename(string $name): string + { + return pathinfo($name, PATHINFO_BASENAME); + } + + /** + * Invalidate opcode cache for file. + * + * @param string $file The path of the file + * @return bool + */ + public static function invalidateOpcodeCache(string $file): bool + { + if (function_exists('opcache_invalidate') && strlen(ini_get('opcache.restrict_api')) === 0) { + return opcache_invalidate($file, true); + } else { + return false; + } + } + + /** + * Checks if a file is of a certain type + * + * @param string $file Full path to the file + * @param string $value An extension or mime type + * @return bool + */ + public static function is(string $file, string $value): bool + { + // check for the extension + if (in_array($value, static::extensions()) === true) { + return static::extension($file) === $value; + } + + // check for the mime type + if (strpos($value, '/') !== false) { + return static::mime($file) === $value; + } + + return false; + } + + /** + * Checks if the file is readable + * + * @param string $file + * @return bool + */ + public static function isReadable(string $file): bool + { + return is_readable($file); + } + + /** + * Checks if the file is writable + * + * @param string $file + * @return bool + */ + public static function isWritable(string $file): bool + { + if (file_exists($file) === false) { + return is_writable(dirname($file)); + } + + return is_writable($file); + } + + /** + * Create a (symbolic) link to a file + * + * @param string $source + * @param string $link + * @param string $method + * @return bool + */ + public static function link(string $source, string $link, string $method = 'link'): bool + { + Dir::make(dirname($link), true); + + if (is_file($link) === true) { + return true; + } + + if (is_file($source) === false) { + throw new Exception(sprintf('The file "%s" does not exist and cannot be linked', $source)); + } + + try { + return $method($source, $link) === true; + } catch (Throwable $e) { + return false; + } + } + + /** + * Loads a file and returns the result or `false` if the + * file to load does not exist + * + * @param string $file + * @param mixed $fallback + * @param array $data Optional array of variables to extract in the variable scope + * @return mixed + */ + public static function load(string $file, $fallback = null, array $data = []) + { + if (is_file($file) === false) { + return $fallback; + } + + // we use the loadIsolated() method here to prevent the included + // file from overwriting our $fallback in this variable scope; see + // https://www.php.net/manual/en/function.include.php#example-124 + $result = static::loadIsolated($file, $data); + + if ($fallback !== null && gettype($result) !== gettype($fallback)) { + return $fallback; + } + + return $result; + } + + /** + * Loads a file with as little as possible in the variable scope + * + * @param string $file + * @param array $data Optional array of variables to extract in the variable scope + * @return mixed + */ + protected static function loadIsolated(string $file, array $data = []) + { + // extract the $data variables in this scope to be accessed by the included file; + // protect $file against being overwritten by a $data variable + $___file___ = $file; + extract($data); + + return include $___file___; + } + + /** + * Loads a file using `include_once()` and returns whether loading was successful + * + * @param string $file + * @return bool + */ + public static function loadOnce(string $file): bool + { + if (is_file($file) === false) { + return false; + } + + include_once $file; + return true; + } + + /** + * Returns the mime type of a file + * + * @param string $file + * @return string|false + */ + public static function mime(string $file) + { + return Mime::type($file); + } + + /** + * Converts a mime type to a file extension + * + * @param string $mime + * @return string|false + */ + public static function mimeToExtension(string $mime = null) + { + return Mime::toExtension($mime); + } + + /** + * Returns the type for a given mime + * + * @param string $mime + * @return string|false + */ + public static function mimeToType(string $mime) + { + return static::extensionToType(Mime::toExtension($mime)); + } + + /** + * Get the file's last modification time. + * + * @param string $file + * @param string $format + * @param string $handler date or strftime + * @return mixed + */ + public static function modified(string $file, string $format = null, string $handler = 'date') + { + if (file_exists($file) !== true) { + return false; + } + + $stat = stat($file); + $mtime = $stat['mtime'] ?? 0; + $ctime = $stat['ctime'] ?? 0; + $modified = max([$mtime, $ctime]); + + if (is_null($format) === true) { + return $modified; + } + + return $handler($format, $modified); + } + + /** + * Moves a file to a new location + * + * @param string $oldRoot The current path for the file + * @param string $newRoot The path to the new location + * @param bool $force Force move if the target file exists + * @return bool + */ + public static function move(string $oldRoot, string $newRoot, bool $force = false): bool + { + // check if the file exists + if (file_exists($oldRoot) === false) { + return false; + } + + if (file_exists($newRoot) === true) { + if ($force === false) { + return false; + } + + // delete the existing file + static::remove($newRoot); + } + + // actually move the file if it exists + if (rename($oldRoot, $newRoot) !== true) { + return false; + } + + return true; + } + + /** + * Extracts the name from a file path or filename without extension + * + * @param string $name The path or filename + * @return string + */ + public static function name(string $name): string + { + return pathinfo($name, PATHINFO_FILENAME); + } + + /** + * Converts an integer size into a human readable format + * + * @param mixed $size The file size or a file path + * @return string|int + */ + public static function niceSize($size): string + { + // file mode + if (is_string($size) === true && file_exists($size) === true) { + $size = static::size($size); + } + + // make sure it's an int + $size = (int)$size; + + // avoid errors for invalid sizes + if ($size <= 0) { + return '0 KB'; + } + + // the math magic + return round($size / pow(1024, ($i = floor(log($size, 1024)))), 2) . ' ' . static::$units[$i]; + } + + /** + * Reads the content of a file + * + * @param string $file The path for the file + * @return string|false + */ + public static function read(string $file) + { + return @file_get_contents($file); + } + + /** + * Changes the name of the file without + * touching the extension + * + * @param string $file + * @param string $newName + * @param bool $overwrite Force overwrite existing files + * @return string|false + */ + public static function rename(string $file, string $newName, bool $overwrite = false) + { + // create the new name + $name = static::safeName(basename($newName)); + + // overwrite the root + $newRoot = rtrim(dirname($file) . '/' . $name . '.' . F::extension($file), '.'); + + // nothing has changed + if ($newRoot === $file) { + return $newRoot; + } + + if (F::move($file, $newRoot) !== true) { + return false; + } + + return $newRoot; + } + + /** + * Returns the absolute path to the file if the file can be found. + * + * @param string $file + * @param string $in + * @return string|null + */ + public static function realpath(string $file, string $in = null) + { + $realpath = realpath($file); + + if ($realpath === false || is_file($realpath) === false) { + throw new Exception(sprintf('The file does not exist at the given path: "%s"', $file)); + } + + if ($in !== null) { + $parent = realpath($in); + + if ($parent === false || is_dir($parent) === false) { + throw new Exception(sprintf('The parent directory does not exist: "%s"', $in)); + } + + if (substr($realpath, 0, strlen($parent)) !== $parent) { + throw new Exception('The file is not within the parent directory'); + } + } + + return $realpath; + } + + /** + * Returns the relative path of the file + * starting after $in + * + * @param string $file + * @param string $in + * @return string + */ + public static function relativepath(string $file, string $in = null): string + { + if (empty($in) === true) { + return basename($file); + } + + // windows + $file = str_replace('\\', '/', $file); + $in = str_replace('\\', '/', $in); + + if (Str::contains($file, $in) === false) { + return basename($file); + } + + return Str::after($file, $in); + } + + /** + * Deletes a file + * + * + * + * $remove = F::remove('test.txt'); + * if($remove) echo 'The file has been removed'; + * + * + * + * @param string $file The path for the file + * @return bool + */ + public static function remove(string $file): bool + { + if (strpos($file, '*') !== false) { + foreach (glob($file) as $f) { + static::remove($f); + } + + return true; + } + + $file = realpath($file); + + if (file_exists($file) === false) { + return true; + } + + return unlink($file); + } + + /** + * Sanitize a filename to strip unwanted special characters + * + * + * + * $safe = f::safeName('über genious.txt'); + * // safe will be ueber-genious.txt + * + * + * + * @param string $string The file name + * @return string + */ + public static function safeName(string $string): string + { + $name = static::name($string); + $extension = static::extension($string); + $safeName = Str::slug($name, '-', 'a-z0-9@._-'); + $safeExtension = empty($extension) === false ? '.' . Str::slug($extension) : ''; + + return $safeName . $safeExtension; + } + + /** + * Tries to find similar or the same file by + * building a glob based on the path + * + * @param string $path + * @param string $pattern + * @return array + */ + public static function similar(string $path, string $pattern = '*'): array + { + $dir = dirname($path); + $name = static::name($path); + $extension = static::extension($path); + $glob = $dir . '/' . $name . $pattern . '.' . $extension; + return glob($glob); + } + + /** + * Returns the size of a file. + * + * @param mixed $file The path + * @return int + */ + public static function size(string $file): int + { + try { + return filesize($file); + } catch (Throwable $e) { + return 0; + } + } + + /** + * Categorize the file + * + * @param string $file Either the file path or extension + * @return string|null + */ + public static function type(string $file) + { + $length = strlen($file); + + if ($length >= 2 && $length <= 4) { + // use the file name as extension + $extension = $file; + } else { + // get the extension from the filename + $extension = pathinfo($file, PATHINFO_EXTENSION); + } + + if (empty($extension) === true) { + // detect the mime type first to get the most reliable extension + $mime = static::mime($file); + $extension = static::mimeToExtension($mime); + } + + // sanitize extension + $extension = strtolower($extension); + + foreach (static::$types as $type => $extensions) { + if (in_array($extension, $extensions) === true) { + return $type; + } + } + + return null; + } + + /** + * Unzips a zip file + * + * @param string $file + * @param string $to + * @return bool + */ + public static function unzip(string $file, string $to): bool + { + if (class_exists('ZipArchive') === false) { + throw new Exception('The ZipArchive class is not available'); + } + + $zip = new ZipArchive(); + + if ($zip->open($file) === true) { + $zip->extractTo($to); + $zip->close(); + return true; + } + + return false; + } + + /** + * Returns the file as data uri + * + * @param string $file The path for the file + * @return string|false + */ + public static function uri(string $file) + { + if ($mime = static::mime($file)) { + return 'data:' . $mime . ';base64,' . static::base64($file); + } + + return false; + } + + /** + * Creates a new file + * + * @param string $file The path for the new file + * @param mixed $content Either a string, an object or an array. Arrays and objects will be serialized. + * @param bool $append true: append the content to an exisiting file if available. false: overwrite. + * @return bool + */ + public static function write(string $file, $content, bool $append = false): bool + { + if (is_array($content) === true || is_object($content) === true) { + $content = serialize($content); + } + + $mode = $append === true ? FILE_APPEND | LOCK_EX : LOCK_EX; + + // if the parent directory does not exist, create it + if (is_dir(dirname($file)) === false) { + if (Dir::make(dirname($file)) === false) { + return false; + } + } + + if (static::isWritable($file) === false) { + throw new Exception('The file "' . $file . '" is not writable'); + } + + return file_put_contents($file, $content, $mode) !== false; + } +} diff --git a/kirby/src/Toolkit/Facade.php b/kirby/src/Toolkit/Facade.php new file mode 100644 index 0000000..3387a97 --- /dev/null +++ b/kirby/src/Toolkit/Facade.php @@ -0,0 +1,36 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +abstract class Facade +{ + /** + * Returns the instance that should be + * available statically + * + * @return mixed + */ + abstract public static function instance(); + + /** + * Proxy for all public instance calls + * + * @param string $method + * @param array $args + * @return mixed + */ + public static function __callStatic(string $method, array $args = null) + { + return static::instance()->$method(...$args); + } +} diff --git a/kirby/src/Toolkit/File.php b/kirby/src/Toolkit/File.php new file mode 100644 index 0000000..7a43a5c --- /dev/null +++ b/kirby/src/Toolkit/File.php @@ -0,0 +1,340 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class File +{ + /** + * Absolute file path + * + * @var string + */ + protected $root; + + /** + * Constructs a new File object by absolute path + * + * @param string $root Absolute file path + */ + public function __construct(string $root = null) + { + $this->root = $root; + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + /** + * Returns the file content as base64 encoded string + * + * @return string + */ + public function base64(): string + { + return base64_encode($this->read()); + } + + /** + * Copy a file to a new location. + * + * @param string $target + * @param bool $force + * @return self + */ + public function copy(string $target, bool $force = false) + { + if (F::copy($this->root, $target, $force) !== true) { + throw new Exception('The file "' . $this->root . '" could not be copied'); + } + + return new static($target); + } + + /** + * Returns the file as data uri + * + * @param bool $base64 Whether the data should be base64 encoded or not + * @return string + */ + public function dataUri(bool $base64 = true): string + { + if ($base64 === true) { + return 'data:' . $this->mime() . ';base64,' . $this->base64(); + } + + return 'data:' . $this->mime() . ',' . Escape::url($this->read()); + } + + /** + * Deletes the file + * + * @return bool + */ + public function delete(): bool + { + if (F::remove($this->root) !== true) { + throw new Exception('The file "' . $this->root . '" could not be deleted'); + } + + return true; + } + + /** + * Checks if the file actually exists + * + * @return bool + */ + public function exists(): bool + { + return file_exists($this->root) === true; + } + + /** + * Returns the current lowercase extension (without .) + * + * @return string + */ + public function extension(): string + { + return F::extension($this->root); + } + + /** + * Returns the filename + * + * @return string + */ + public function filename(): string + { + return basename($this->root); + } + + /** + * Returns a md5 hash of the root + * + * @return string + */ + public function hash(): string + { + return md5($this->root); + } + + /** + * Checks if a file is of a certain type + * + * @param string $value An extension or mime type + * @return bool + */ + public function is(string $value): bool + { + return F::is($this->root, $value); + } + + /** + * Checks if the file is readable + * + * @return bool + */ + public function isReadable(): bool + { + return is_readable($this->root) === true; + } + + /** + * Checks if the file is writable + * + * @return bool + */ + public function isWritable(): bool + { + return F::isWritable($this->root); + } + + /** + * Detects the mime type of the file + * + * @return string|null + */ + public function mime() + { + return Mime::type($this->root); + } + + /** + * Get the file's last modification time. + * + * @param string $format + * @param string $handler date or strftime + * @return mixed + */ + public function modified(string $format = null, string $handler = 'date') + { + return F::modified($this->root, $format, $handler); + } + + /** + * Move the file to a new location + * + * @param string $newRoot + * @param bool $overwrite Force overwriting any existing files + * @return self + */ + public function move(string $newRoot, bool $overwrite = false) + { + if (F::move($this->root, $newRoot, $overwrite) !== true) { + throw new Exception('The file: "' . $this->root . '" could not be moved to: "' . $newRoot . '"'); + } + + return new static($newRoot); + } + + /** + * Getter for the name of the file + * without the extension + * + * @return string + */ + public function name(): string + { + return pathinfo($this->root, PATHINFO_FILENAME); + } + + /** + * Returns the file size in a + * human-readable format + * + * @return string + */ + public function niceSize(): string + { + return F::niceSize($this->root); + } + + /** + * Reads the file content and returns it. + * + * @return string + */ + public function read() + { + return F::read($this->root); + } + + /** + * Returns the absolute path to the file + * + * @return string + */ + public function realpath(): string + { + return realpath($this->root); + } + + /** + * Changes the name of the file without + * touching the extension + * + * @param string $newName + * @param bool $overwrite Force overwrite existing files + * @return self + */ + public function rename(string $newName, bool $overwrite = false) + { + $newRoot = F::rename($this->root, $newName, $overwrite); + + if ($newRoot === false) { + throw new Exception('The file: "' . $this->root . '" could not be renamed to: "' . $newName . '"'); + } + + return new static($newRoot); + } + + /** + * Returns the given file path + * + * @return string|null + */ + public function root(): ?string + { + return $this->root; + } + + /** + * Returns the raw size of the file + * + * @return int + */ + public function size(): int + { + return F::size($this->root); + } + + /** + * Converts the media object to a + * plain PHP array + * + * @return array + */ + public function toArray(): array + { + return [ + 'root' => $this->root(), + 'hash' => $this->hash(), + 'filename' => $this->filename(), + 'name' => $this->name(), + 'safeName' => F::safeName($this->name()), + 'extension' => $this->extension(), + 'size' => $this->size(), + 'niceSize' => $this->niceSize(), + 'modified' => $this->modified('c'), + 'mime' => $this->mime(), + 'type' => $this->type(), + 'isWritable' => $this->isWritable(), + 'isReadable' => $this->isReadable(), + ]; + } + + /** + * Returns the file type. + * + * @return string|false + */ + public function type() + { + return F::type($this->root); + } + + /** + * Writes content to the file + * + * @param string $content + * @return bool + */ + public function write($content): bool + { + if (F::write($this->root, $content) !== true) { + throw new Exception('The file "' . $this->root . '" could not be written'); + } + + return true; + } +} diff --git a/kirby/src/Toolkit/Html.php b/kirby/src/Toolkit/Html.php new file mode 100644 index 0000000..5fb505d --- /dev/null +++ b/kirby/src/Toolkit/Html.php @@ -0,0 +1,571 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier GmbH + * @license https://opensource.org/licenses/MIT + */ +class Html extends Xml +{ + /** + * An internal store for an HTML entities translation table + * + * @var array + */ + public static $entities; + + /** + * Closing string for void tags; + * can be used to switch to trailing slashes if required + * + * ```php + * Html::$void = ' />' + * ``` + * + * @var string + */ + public static $void = '>'; + + /** + * List of HTML tags that are considered to be self-closing + * + * @var array + */ + public static $voidList = [ + 'area', + 'base', + 'br', + 'col', + 'command', + 'embed', + 'hr', + 'img', + 'input', + 'keygen', + 'link', + 'meta', + 'param', + 'source', + 'track', + 'wbr' + ]; + + /** + * Generic HTML tag generator + * Can be called like `Html::p('A paragraph', ['class' => 'text'])` + * + * @param string $tag Tag name + * @param array $arguments Further arguments for the Html::tag() method + * @return string + */ + public static function __callStatic(string $tag, array $arguments = []): string + { + if (static::isVoid($tag) === true) { + return static::tag($tag, null, ...$arguments); + } + + return static::tag($tag, ...$arguments); + } + + /** + * Generates an `` tag; automatically supports mailto: and tel: links + * + * @param string $href The URL for the `` tag + * @param string|array|null $text The optional text; if `null`, the URL will be used as text + * @param array $attr Additional attributes for the tag + * @return string The generated HTML + */ + public static function a(string $href, $text = null, array $attr = []): string + { + if (Str::startsWith($href, 'mailto:')) { + return static::email(substr($href, 7), $text, $attr); + } + + if (Str::startsWith($href, 'tel:')) { + return static::tel(substr($href, 4), $text, $attr); + } + + return static::link($href, $text, $attr); + } + + /** + * Generates a single attribute or a list of attributes + * + * @param string|array $name String: A single attribute with that name will be generated. + * Key-value array: A list of attributes will be generated. Don't pass a second argument in that case. + * @param mixed $value If used with a `$name` string, pass the value of the attribute here. + * If used with a `$name` array, this can be set to `false` to disable attribute sorting. + * @return string|null The generated HTML attributes string + */ + public static function attr($name, $value = null): ?string + { + // HTML supports boolean attributes without values + if (is_array($name) === false && is_bool($value) === true) { + return $value === true ? strtolower($name) : null; + } + + // all other cases can share the XML variant + $attr = parent::attr($name, $value); + + // HTML supports named entities + $entities = parent::entities(); + $html = array_keys($entities); + $xml = array_values($entities); + return str_replace($xml, $html, $attr); + } + + /** + * Converts lines in a string into HTML breaks + * + * @param string $string + * @return string + */ + public static function breaks(string $string): string + { + return nl2br($string); + } + + /** + * Generates an `` tag with `mailto:` + * + * @param string $email The email address + * @param string|array|null $text The optional text; if `null`, the email address will be used as text + * @param array $attr Additional attributes for the tag + * @return string The generated HTML + */ + public static function email(string $email, $text = null, array $attr = []): string + { + if (empty($email) === true) { + return ''; + } + + if (empty($text) === true) { + // show only the email address without additional parameters + $address = Str::contains($email, '?') ? Str::before($email, '?') : $email; + + $text = [Str::encode($address)]; + } + + $email = Str::encode($email); + $attr = array_merge([ + 'href' => [ + 'value' => 'mailto:' . $email, + 'escape' => false + ] + ], $attr); + + // add rel=noopener to target blank links to improve security + $attr['rel'] = static::rel($attr['rel'] ?? null, $attr['target'] ?? null); + + return static::tag('a', $text, $attr); + } + + /** + * Converts a string to an HTML-safe string + * + * @param string|null $string + * @param bool $keepTags If true, existing tags won't be escaped + * @return string The HTML string + */ + public static function encode(?string $string, bool $keepTags = false): string + { + if ($string === null) { + return ''; + } + + if ($keepTags === true) { + $list = static::entities(); + unset($list['"'], $list['<'], $list['>'], $list['&']); + + $search = array_keys($list); + $values = array_values($list); + + return str_replace($search, $values, $string); + } + + return htmlentities($string, ENT_COMPAT, 'utf-8'); + } + + /** + * Returns the entity translation table + * + * @return array + */ + public static function entities(): array + { + return self::$entities = self::$entities ?? get_html_translation_table(HTML_ENTITIES); + } + + /** + * Creates a `
` tag with optional caption + * + * @param string|array $content Contents of the `
` tag + * @param string|array $caption Optional `
` text to use + * @param array $attr Additional attributes for the `
` tag + * @return string The generated HTML + */ + public static function figure($content, $caption = '', array $attr = []): string + { + if ($caption) { + $figcaption = static::tag('figcaption', $caption); + + if (is_string($content) === true) { + $content = [static::encode($content, false)]; + } + + $content[] = $figcaption; + } + + return static::tag('figure', $content, $attr); + } + + /** + * Embeds a GitHub Gist + * + * @param string $url Gist URL + * @param string|null $file Optional specific file to embed + * @param array $attr Additional attributes for the ` + + + + + + + + + + diff --git a/kirby/views/php.php b/kirby/views/php.php new file mode 100644 index 0000000..35a4757 --- /dev/null +++ b/kirby/views/php.php @@ -0,0 +1,11 @@ + + +

+ This page is currently offline. We are very sorry for the inconvenience and will fix it as soon as possible. +

+

+ Advice for developers and administrators:
+ Change the PHP version to 7.2, 7.3 or 7.4 (PHP 7.3 or 7.4 are recommended) +

+ + diff --git a/kirby/views/snippets/footer.php b/kirby/views/snippets/footer.php new file mode 100644 index 0000000..308b1d0 --- /dev/null +++ b/kirby/views/snippets/footer.php @@ -0,0 +1,2 @@ + + diff --git a/kirby/views/snippets/header.php b/kirby/views/snippets/header.php new file mode 100644 index 0000000..558624e --- /dev/null +++ b/kirby/views/snippets/header.php @@ -0,0 +1,39 @@ + + + + + + + Error + + + + +