├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── attachment.png ├── captcha.png ├── checkbox.png ├── custom.png ├── custom_form_block_data.png ├── error_fields.png ├── form_fields.png ├── grid-form-block.gif ├── input.png ├── license.png ├── mailview.png ├── named.png ├── radio.png ├── screenshot-grid-form-block.gif ├── select.png └── textarea.png ├── .gitignore ├── LICENSE.md ├── README.md ├── assets ├── formblock.css └── formblock.js ├── blueprints ├── blocks │ ├── customfields.yml │ └── formfields │ │ ├── 01_input.yml │ │ ├── 02_textarea.yml │ │ ├── 03_checkbox.yml │ │ ├── 04_radio.yml │ │ ├── 05_select.yml │ │ ├── 06_file.yml │ │ └── 07_captcha.yml ├── files │ └── formfile.yml ├── pages │ ├── formcontainer.yml │ └── formrequest.yml └── snippets │ ├── form_confirm.yml │ ├── form_notify.yml │ └── form_options.yml ├── classes ├── Blueprint.php ├── Field.php ├── Fields.php ├── Form.php └── Request.php ├── composer.json ├── composer.lock ├── config ├── api │ └── routes.php ├── blockModels.php ├── blueprints.php ├── fields.php ├── options.php ├── routes.php └── snippets.php ├── i18n ├── de.json ├── en.json ├── fr.json ├── hu.json └── lt.json ├── index.css ├── index.js ├── index.php ├── lib └── defaults │ ├── formblock_default_de.json │ ├── formblock_default_en.json │ ├── formblock_default_fr.json │ └── formblock_default_lt.json ├── package-lock.json ├── package.json ├── snippets └── blocks │ ├── form.php │ ├── formcore │ ├── hidden.php │ ├── script.php │ ├── styles.php │ └── validation.php │ ├── formfields │ ├── captcha.php │ ├── checkbox.php │ ├── file.php │ ├── input.php │ ├── radio.php │ ├── select.php │ └── textarea.php │ └── formtemplates │ ├── field_error.php │ ├── fields.php │ ├── form_error.php │ ├── form_success.php │ └── submit.php ├── src ├── components │ ├── MailList.vue │ ├── blocks │ │ └── Form.vue │ ├── dialog │ │ └── Form.vue │ └── fields │ │ └── MailView.vue └── index.js └── utils ├── .gitignore ├── Autoloader.php ├── License.php ├── PlainLicense.vue ├── Plugin.php ├── README.md └── load.php /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:vue/recommended", 9 | "prettier" 10 | ], 11 | "parserOptions": { 12 | "ecmaVersion": 12, 13 | "sourceType": "module" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | I confirm that the Troubleshooting chapter does not solve my problem and that installation does not contain any custom form templates. 11 | -------------------------------------------------------------------------------- /.github/attachment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plain-solutions-gmbh/kirby-form-block-suite/db22b8e1295f87567abb57867cb160064271778d/.github/attachment.png -------------------------------------------------------------------------------- /.github/captcha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plain-solutions-gmbh/kirby-form-block-suite/db22b8e1295f87567abb57867cb160064271778d/.github/captcha.png -------------------------------------------------------------------------------- /.github/checkbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plain-solutions-gmbh/kirby-form-block-suite/db22b8e1295f87567abb57867cb160064271778d/.github/checkbox.png -------------------------------------------------------------------------------- /.github/custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plain-solutions-gmbh/kirby-form-block-suite/db22b8e1295f87567abb57867cb160064271778d/.github/custom.png -------------------------------------------------------------------------------- /.github/custom_form_block_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plain-solutions-gmbh/kirby-form-block-suite/db22b8e1295f87567abb57867cb160064271778d/.github/custom_form_block_data.png -------------------------------------------------------------------------------- /.github/error_fields.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plain-solutions-gmbh/kirby-form-block-suite/db22b8e1295f87567abb57867cb160064271778d/.github/error_fields.png -------------------------------------------------------------------------------- /.github/form_fields.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plain-solutions-gmbh/kirby-form-block-suite/db22b8e1295f87567abb57867cb160064271778d/.github/form_fields.png -------------------------------------------------------------------------------- /.github/grid-form-block.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plain-solutions-gmbh/kirby-form-block-suite/db22b8e1295f87567abb57867cb160064271778d/.github/grid-form-block.gif -------------------------------------------------------------------------------- /.github/input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plain-solutions-gmbh/kirby-form-block-suite/db22b8e1295f87567abb57867cb160064271778d/.github/input.png -------------------------------------------------------------------------------- /.github/license.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plain-solutions-gmbh/kirby-form-block-suite/db22b8e1295f87567abb57867cb160064271778d/.github/license.png -------------------------------------------------------------------------------- /.github/mailview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plain-solutions-gmbh/kirby-form-block-suite/db22b8e1295f87567abb57867cb160064271778d/.github/mailview.png -------------------------------------------------------------------------------- /.github/named.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plain-solutions-gmbh/kirby-form-block-suite/db22b8e1295f87567abb57867cb160064271778d/.github/named.png -------------------------------------------------------------------------------- /.github/radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plain-solutions-gmbh/kirby-form-block-suite/db22b8e1295f87567abb57867cb160064271778d/.github/radio.png -------------------------------------------------------------------------------- /.github/screenshot-grid-form-block.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plain-solutions-gmbh/kirby-form-block-suite/db22b8e1295f87567abb57867cb160064271778d/.github/screenshot-grid-form-block.gif -------------------------------------------------------------------------------- /.github/select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plain-solutions-gmbh/kirby-form-block-suite/db22b8e1295f87567abb57867cb160064271778d/.github/select.png -------------------------------------------------------------------------------- /.github/textarea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plain-solutions-gmbh/kirby-form-block-suite/db22b8e1295f87567abb57867cb160064271778d/.github/textarea.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS files 2 | .DS_Store 3 | 4 | # npm modules 5 | /node_modules 6 | 7 | #Symbolic Links 8 | *.lnk 9 | *.symlink 10 | 11 | # migration file 12 | /migrated -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Plain Solutions Software License 2 | Version 1.1, February 2025 3 | 4 | Copyright (c) 2025 Plain Solutions GmbH 5 | All rights reserved. 6 | 7 | ## 1. Grant of License 8 | This software is provided under GPL-3.0-only license. You are granted a non-exclusive, non-transferable, and revocable right to download, install, and use the software **for non-commercial purposes** only. 9 | 10 | ## 2. Permitted Use 11 | - You may download and use the software for personal, educational, or research purposes. 12 | - You may modify the software **for personal use only** but may not distribute modified versions. 13 | 14 | ## 3. Restrictions 15 | - Commercial use, resale, or redistribution of the software in any form is **strictly prohibited**. 16 | - You may not sublicense, rent, lease, or sell copies of this software. 17 | - Decompiling, reverse engineering, or modifying the software for redistribution is not allowed. 18 | 19 | ## 4. No Warranty & Limitation of Liability 20 | This software is provided "as is" without any warranties. Plain Solutions GmbH is not responsible for any damages, data loss, or other issues arising from the use of this software. 21 | 22 | ## 5. Support & Updates 23 | Support and updates may be provided at the sole discretion of Plain Solutions GmbH. There is no guarantee of future updates or bug fixes. 24 | 25 | ## 6. Termination 26 | Plain Solutions GmbH reserves the right to revoke this license if the terms are violated. Upon termination, you must stop using the software and delete all copies. 27 | 28 | ## 7. Governing Law 29 | This license is governed by Swiss law. Any disputes shall be settled in the jurisdiction of Plain Solutions GmbH’s registered office in Raron, Switzerland. 30 | 31 | For inquiries, please contact: 32 | Plain Solutions GmbH 33 | Theaterstrasse 2, 3942 Raron, Switzerland 34 | Email: support@plain-solutions.net 35 | -------------------------------------------------------------------------------- /assets/formblock.css: -------------------------------------------------------------------------------- 1 | /* Colors */ 2 | 3 | :root { 4 | --formblock-color: #0d47a1; 5 | --formblock-color-invert: #fff; 6 | --formblock-color-error: #CC0000; 7 | --formblock-color-success: #007E33; 8 | } 9 | 10 | 11 | /* Form */ 12 | 13 | .formblock { 14 | display: grid; 15 | gap: 30px 50px; 16 | margin: 40px auto; 17 | max-width: 500px; 18 | } 19 | 20 | .formfield__container { 21 | width: 100%; 22 | } 23 | 24 | .formfield__container[data-type=hidden] { 25 | visibility:hidden; 26 | position: absolute; 27 | width: 0; 28 | height:0; 29 | } 30 | 31 | /* Label */ 32 | 33 | .formblock_field__label { 34 | display: block; 35 | margin-bottom: 10px; 36 | font-weight: 700; 37 | } 38 | 39 | 40 | /* Messages */ 41 | 42 | .formblock__message--error, 43 | .formblock__message--success { 44 | margin-top: 10px; 45 | } 46 | 47 | .formblock__message--hidden { 48 | display: none; 49 | } 50 | 51 | .formblock__message--error { 52 | color: var(--formblock-color-error); 53 | width: 100%; 54 | } 55 | 56 | .formblock__message--success { 57 | color: var(--formblock-color-success); 58 | } 59 | 60 | .formblock__message--error ul>li, 61 | .formblock__message--success ul>li { 62 | list-style-position: inside; 63 | } 64 | 65 | .formblock__message--error[data-form="form_error"], 66 | .formblock__message--success[data-form="form_success"] { 67 | padding: 20px; 68 | color: var(--formblock-color-invert); 69 | } 70 | 71 | .formblock__message--error[data-form="form_error"] { 72 | background-color: var(--formblock-color-error); 73 | } 74 | 75 | .formblock__message--success[data-form="form_success"] { 76 | background-color: var(--formblock-color-success); 77 | } 78 | 79 | 80 | /* Fields */ 81 | 82 | .formfield__input, 83 | .formfield__select, 84 | .formfield__radio, 85 | .formfield__textarea { 86 | display: block; 87 | width: 100%; 88 | box-sizing: border-box; 89 | padding: 11px 20px; 90 | border: 1px solid #ebebeb; 91 | outline: none; 92 | font-size: .9em; 93 | font-weight: 500; 94 | appearance: unset!important; 95 | -moz-appearance: unset!important; 96 | -webkit-appearance: unset!important; 97 | -o-appearance: unset!important; 98 | -ms-appearance: unset!important; 99 | } 100 | 101 | .formfield__container[data-valid="false"] .formfield__input, 102 | .formfield__container[data-valid="false"] .formfield__select, 103 | .formfield__container[data-valid="false"] .formfield__radio, 104 | .formfield__container[data-valid="false"] .formfield__file, 105 | .formfield__container[data-valid="false"] .formblock__option__container, 106 | .formfield__container[data-valid="false"] .formfield__textarea { 107 | border: 1px solid var(--formblock-color-error); 108 | } 109 | 110 | .formfield__container[data-valid="true"] .formfield__input, 111 | .formfield__container[data-valid="true"] .formfield__select, 112 | .formfield__container[data-valid="true"] .formfield__radio, 113 | .formfield__container[data-valid="true"] .formfield__file, 114 | .formfield__container[data-valid="true"] .formblock__option__container, 115 | .formfield__container[data-valid="true"] .formfield__textarea { 116 | border: 1px solid var(--formblock-color-success); 117 | } 118 | 119 | .formfield__container[data-valid] .formblock__option__container { 120 | padding: 0px 15px 15px; 121 | } 122 | 123 | .formfield__container[data-valid] .formfield__file { 124 | padding: 15px; 125 | } 126 | 127 | .formfield__input::-webkit-outer-spin-button, 128 | .formfield__input::-webkit-inner-spin-button { 129 | margin: 0; 130 | appearance: none!important; 131 | -moz-appearance: none!important; 132 | -webkit-appearance: none!important; 133 | -o-appearance: none!important; 134 | -ms-appearance: none!important; 135 | } 136 | 137 | .formfield__input:focus, 138 | .formfield__select:focus, 139 | .formfield__textarea:focus { 140 | border: 1px solid var(--formblock-color); 141 | outline: none; 142 | box-shadow: none!important; 143 | -moz-box-shadow: none!important; 144 | -webkit-box-shadow: none!important; 145 | -o-box-shadow: none!important; 146 | -ms-box-shadow: none!important; 147 | } 148 | 149 | 150 | /* Input Fields */ 151 | 152 | .formfield__input[type=number] { 153 | box-shadow: 0 0 0 30px transparent inset; 154 | -moz-box-shadow: 0 0 0 30px transparent inset; 155 | -webkit-box-shadow: 0 0 0 30px transparent inset; 156 | -o-box-shadow: 0 0 0 30px transparent inset; 157 | -ms-box-shadow: 0 0 0 30px transparent inset; 158 | -moz-appearance: textfield!important; 159 | appearance: none!important; 160 | -webkit-appearance: none!important; 161 | } 162 | 163 | .formfield__textarea { 164 | resize: vertical; 165 | font-family: inherit; 166 | } 167 | 168 | 169 | /* Optionfields Fields */ 170 | 171 | .formblock__option__container { 172 | display: flex; 173 | display: -webkit-flex; 174 | justify-content: space-between; 175 | flex-wrap: wrap; 176 | border: none; 177 | } 178 | 179 | .formfield__checkbox { 180 | appearance: checkbox!important; 181 | -moz-appearance: checkbox!important; 182 | -webkit-appearance: checkbox!important; 183 | -o-appearance: checkbox!important; 184 | -ms-appearance: checkbox!important 185 | } 186 | 187 | .formfield__radio { 188 | appearance: radio!important; 189 | -moz-appearance: radio!important; 190 | -webkit-appearance: radio!important; 191 | -o-appearance: radio!important; 192 | -ms-appearance: radio!important 193 | } 194 | 195 | .formfield__radio, 196 | .formfield__checkbox { 197 | position: absolute; 198 | visibility: hidden; 199 | } 200 | 201 | .formfield__option__label { 202 | position: relative; 203 | display: block; 204 | padding-left: 25px; 205 | font-weight: 500; 206 | line-height: 24px; 207 | cursor: pointer; 208 | z-index: 9; 209 | } 210 | 211 | .formfield__radio__check, 212 | .formfield__checkbox__check { 213 | display: inline-block; 214 | position: absolute; 215 | border: 1px solid #ebebeb; 216 | height: 13px; 217 | width: 13px; 218 | top: 4px; 219 | left: 0; 220 | z-index: 5; 221 | transition: border .25s linear; 222 | -webkit-transition: border .25s linear; 223 | } 224 | 225 | .formfield__radio__check:before, 226 | .formfield__checkbox__check:before { 227 | content: ''; 228 | position: absolute; 229 | display: block; 230 | margin: auto; 231 | color: #fff; 232 | } 233 | 234 | .formfield__radio__check, 235 | .formfield__radio__check:before { 236 | border-radius: 50%; 237 | transition: background .25s linear; 238 | -webkit-transition: background .25s linear; 239 | } 240 | 241 | .formfield__radio__check:before { 242 | width: 9px; 243 | height: 9px; 244 | top: 2px; 245 | left: 2px; 246 | } 247 | 248 | .formfield__checkbox__check::before { 249 | content: '\2713'; 250 | width: 13px; 251 | line-height: 13px; 252 | font-size: .8em; 253 | text-align: center; 254 | transition: color .25s linear; 255 | -webkit-transition: color .25s linear; 256 | } 257 | 258 | .formfield__radio:checked~.formfield__radio__check, 259 | .formfield__radio:hover~.formfield__radio__check, 260 | .formfield__checkbox:checked~.formfield__checkbox__check, 261 | .formfield__checkbox:hover~.formfield__checkbox__check { 262 | border: 1px solid var(--formblock-color); 263 | } 264 | 265 | .formfield__radio:checked~.formfield__radio__check::before { 266 | background: var(--formblock-color); 267 | } 268 | 269 | .formfield__checkbox:checked~.formfield__checkbox__check::before { 270 | color: var(--formblock-color); 271 | } 272 | 273 | .formfield__option { 274 | position: relative; 275 | margin-right: 45px; 276 | } 277 | 278 | 279 | /* Select Field */ 280 | 281 | .formfield__select__wrapper { 282 | position: relative; 283 | width: 100%; 284 | } 285 | 286 | .formfield__select { 287 | position: relative; 288 | background: 0 0; 289 | appearance: none!important; 290 | -moz-appearance: none!important; 291 | -webkit-appearance: none!important; 292 | -o-appearance: none!important; 293 | -ms-appearance: none!important; 294 | z-index: 10; 295 | cursor: pointer; 296 | } 297 | 298 | .formfield__select__chevron { 299 | position: absolute; 300 | top: 0; 301 | right: 0; 302 | bottom: 0; 303 | z-index: 0; 304 | } 305 | 306 | .formfield__select__chevron:before { 307 | content: '\2304'; 308 | display: block; 309 | height: 40px; 310 | width: 40px; 311 | text-align: center; 312 | line-height: 30px; 313 | color: #999; 314 | transform: scaleX(1.5); 315 | } 316 | 317 | .formfield__select:hover~.formfield__select__chevron:before { 318 | color: var(--formblock-color); 319 | } 320 | 321 | .formfield__select:hover { 322 | border: 1px solid var(--formblock-color); 323 | } 324 | 325 | 326 | /* Buttons */ 327 | 328 | .formblock__submit { 329 | position: relative; 330 | width: 140px; 331 | text-align: right; 332 | margin-left: auto; 333 | } 334 | 335 | .formblock__submit>input { 336 | width: 100%; 337 | } 338 | 339 | .formblock__submit>input, 340 | .formfield__file::file-selector-button { 341 | display: inline-block; 342 | border: none; 343 | background: var(--formblock-color); 344 | color: var(--formblock-color-invert); 345 | line-height: 40px; 346 | font-weight: 400; 347 | font-size: .9em; 348 | cursor: pointer; 349 | } 350 | 351 | .formfield__file::file-selector-button { 352 | line-height: 25px; 353 | } 354 | 355 | .formblock__submit>input:hover, 356 | .formfield__file::file-selector-button:hover { 357 | filter: brightness(85%); 358 | } 359 | 360 | .formblock__submit__bar { 361 | position: absolute; 362 | top: 0; 363 | bottom: 0; 364 | display: block; 365 | background-color: #000; 366 | mix-blend-mode: overlay; 367 | } 368 | 369 | @media screen and (max-width:480px) { 370 | .formblock__option__container { 371 | flex-direction: column; 372 | } 373 | } -------------------------------------------------------------------------------- /assets/formblock.js: -------------------------------------------------------------------------------- 1 | // Export the "multiply" function: 2 | export function FormBlock(config, formElement) { 3 | 4 | let $this = this; 5 | this.config = config; 6 | this.formElement = formElement; 7 | this.state = "new"; 8 | this.data = {}; 9 | 10 | //this.formElement = document.getElementById(this.config.form_id) 11 | 12 | this.submitInput = this.formElement.querySelector('[data-form="submit"]'); 13 | this.submitBar = this.formElement.querySelector('[data-form="bar"]'); 14 | 15 | this.onsubmit = (e) => { 16 | 17 | $this.data = JSON.parse(e.target.response); 18 | 19 | if (e.target.status === 200) { 20 | 21 | $this.state = $this.data.state ?? $this.data.status; 22 | 23 | $this.formElement.dataset.process = $this.state; 24 | $this.submitBar.style.width = 0; 25 | $this.submitInput.value = $this.config.messages.send; 26 | 27 | switch ($this.state) { 28 | case "invalid": 29 | $this.onfielderror($this.data); 30 | break; 31 | 32 | case "fatal": 33 | $this.onerror($this.data.error_message); 34 | break; 35 | 36 | case "success": 37 | $this.onsuccess($this.data.success_message); 38 | break; 39 | 40 | default: 41 | break; 42 | } 43 | 44 | } else { 45 | 46 | //Show error in Console 47 | console.error( $this.data ); 48 | 49 | //Show Error message in Form 50 | $this.onerror($this.config.messages.fatal); 51 | 52 | } 53 | 54 | } 55 | 56 | this.onvalidate = function(e) { 57 | 58 | $this.data = JSON.parse(e.target.response); 59 | $this.state = $this.data.state; 60 | $this.onfieldvalidate($this.data.fields, false); 61 | 62 | } 63 | 64 | this.onfielderror = (data) => { 65 | 66 | $this.formElement.querySelector('[data-form="form_error"]').outerHTML = $this.data.error_message; 67 | $this.onfieldvalidate($this.data.fields, true); 68 | 69 | } 70 | 71 | this.onfieldvalidate = (field_data) => { 72 | 73 | $this.formElement.querySelector('[data-form="form_error"]').outerHTML = $this.data.error_message; 74 | 75 | field_data.forEach((field) => { 76 | 77 | let fieldElement = $this.formElement.querySelector('[data-id="' + field.slug + '"]'); 78 | 79 | if (fieldElement !== null) { 80 | fieldElement.dataset.valid = field.is_valid; 81 | fieldElement.querySelector('[aria-describedby]')?.toggleAttribute('invalid', !field.is_valid); 82 | 83 | let errorfield = fieldElement.querySelector('[data-form="fields_error"]'); 84 | if (errorfield) { 85 | errorfield.outerHTML = field.message; 86 | } 87 | } 88 | 89 | }); 90 | 91 | 92 | } 93 | 94 | this.onerror = (msg) => { 95 | 96 | $this.formElement.innerHTML = msg; 97 | $this.centerform(); 98 | 99 | } 100 | 101 | this.onsuccess = (msg) => { 102 | 103 | if ($this.data.redirect != "") { 104 | window.location.href = $this.data.redirect; 105 | } else { 106 | $this.formElement.innerHTML = msg; 107 | $this.centerform(); 108 | } 109 | 110 | 111 | } 112 | 113 | this.centerform = () => { 114 | 115 | $this.formElement.scrollIntoView({ 116 | behavior: 'auto', 117 | block: 'center', 118 | inline: 'center' 119 | }); 120 | 121 | } 122 | 123 | this.onprogress = (event) => { 124 | 125 | let percent = parseInt((event.loaded / event.total) * 100) + "%"; 126 | 127 | $this.submitBar.style.width = percent; 128 | $this.submitInput.value = $this.config.messages.loading.replace("{{percent}}", percent); 129 | 130 | } 131 | 132 | this.validate = (field_name) => { 133 | 134 | $this.formdata = new FormData($this.formElement); 135 | $this.formdata.append("page", config.page_id); 136 | $this.formdata.append("lang", config.language); 137 | $this.formdata.append("field_validation", field_name); 138 | 139 | $this.formElement.querySelectorAll('[data-form="files"]').forEach(function(field) { 140 | if (field.dataset.form == "files") { 141 | $this.formdata.delete(field.name) 142 | } 143 | }) 144 | 145 | const xhr_validate = new XMLHttpRequest(); 146 | 147 | xhr_validate.open("POST", $this.config.endpoint, true); 148 | xhr_validate.addEventListener('load', $this.onvalidate); 149 | xhr_validate.send($this.formdata); 150 | 151 | } 152 | 153 | this.submit = (e) => { 154 | 155 | e.preventDefault() 156 | 157 | if ($this.formElement.dataset.process != "loading") { 158 | 159 | $this.formElement.dataset.process = "loading"; 160 | 161 | $this.formdata = new FormData($this.formElement); 162 | $this.formdata.append("page", config.page_id); 163 | $this.formdata.append("lang", config.language); 164 | 165 | const xhr_submit = new XMLHttpRequest(); 166 | 167 | xhr_submit.open("POST", $this.config.endpoint, true); 168 | 169 | xhr_submit.addEventListener('load', $this.onsubmit); 170 | xhr_submit.addEventListener('error', $this.onerror); 171 | xhr_submit.upload.addEventListener('progress', $this.onprogress); 172 | 173 | xhr_submit.send($this.formdata); 174 | 175 | } 176 | 177 | } 178 | 179 | this.formElement.querySelectorAll("[data-form='field'").forEach(function(el) { 180 | 181 | el.addEventListener("change", function(e) { 182 | $this.validate(e.target.closest("[data-id]").dataset.id) 183 | }); 184 | 185 | }); 186 | 187 | this.formElement.addEventListener("submit", this.submit); 188 | }; -------------------------------------------------------------------------------- /blueprints/blocks/customfields.yml: -------------------------------------------------------------------------------- 1 | 2 | label: 3 | label: form.block.fromfields.label 4 | type: text 5 | width: 1/2 6 | 7 | slug: 8 | label: form.block.fromfields.slug 9 | type: slug 10 | required: true 11 | wizard: 12 | text: " " 13 | field: label 14 | width: 1/2 15 | 16 | autofill: 17 | label: form.block.fromfields.autofill 18 | type: select 19 | options: 20 | name: 21 | de: "Vollständiger Name" 22 | en: "full name" 23 | honorific-prefix: 24 | de: "Anrede" 25 | en: "honorific" 26 | given-name: 27 | de: "Vorname" 28 | en: "first-name" 29 | family-name: 30 | de: "Nachname" 31 | en: "family-name" 32 | email: 33 | de: "E-Mail" 34 | en: "email" 35 | tel: 36 | de: "Telefonnummer" 37 | en: "telephone number" 38 | street-address: 39 | de: "Adresse" 40 | en: "address" 41 | postal-code: 42 | de: "Postleitzahl" 43 | en: "postal-code" 44 | address-line2: 45 | de: "Ort" 46 | en: "Location" 47 | address-line1: 48 | de: "Region" 49 | en: "area" 50 | country-name: 51 | de: "Landesname" 52 | en: "country-name" 53 | organization: 54 | de: "Firma" 55 | en: "company" 56 | organization-title: 57 | de: "Funktion" 58 | en: "function" 59 | url: 60 | de: "Webseite" 61 | en: "website" 62 | language: 63 | de: "Sprache" 64 | en: "language" 65 | bday: 66 | de: "Geburtstag" 67 | en: "birthday" 68 | bday-day: 69 | de: "Geburtstag (Tag)" 70 | en: "birthday (day)" 71 | bday-month: 72 | de: "Geburtstag (Monat)" 73 | en: "birthday (month)" 74 | bday-year: 75 | de: "Geburtstag (Jahr)" 76 | en: "birthday (year)" 77 | nickname: 78 | de: "Spitzname" 79 | en: "nickname" 80 | additional-name: 81 | de: "Zweiter Vorname" 82 | en: "middle name" 83 | username: 84 | de: "Benutzername" 85 | en: "username" 86 | new-password: 87 | de: "Neues Passwort" 88 | en: "new password" 89 | current-password: 90 | de: "Aktuelle Passwort" 91 | en: "current password" 92 | width: 1/2 93 | 94 | required: 95 | label: form.block.fromfields.required 96 | type: toggle 97 | width: 1/2 98 | 99 | required_fail: 100 | label: form.block.fromfields.required_fail 101 | type: text 102 | help: form.block.default.help 103 | when: 104 | required: true 105 | -------------------------------------------------------------------------------- /blueprints/blocks/formfields/01_input.yml: -------------------------------------------------------------------------------- 1 | name: form.block.fromfields.input 2 | icon: headline 3 | 4 | fields: 5 | inputtype: 6 | label: form.block.fromfields.input.inputtype 7 | type: select 8 | default: text 9 | width: 1/3 10 | options: 11 | text: text 12 | number: number 13 | email: email 14 | tel: tel 15 | url: url 16 | password: password 17 | hidden: hidden 18 | 19 | placeholder: 20 | label: form.block.fromfields.input.placeholder 21 | type: text 22 | width: 1/3 23 | 24 | default: 25 | label: form.block.fromfields.input.default 26 | type: text 27 | width: 1/3 28 | 29 | validate: 30 | label: form.block.fromfields.input.validate 31 | type: structure 32 | columns: 33 | validate: 34 | width: 1/4 35 | label: form.block.fromfields.input.validate 36 | msg: 37 | width: 3/4 38 | label: form.block.fromfields.input.validate.msg 39 | 40 | fields: 41 | validate: 42 | label: form.block.fromfields.input.fields 43 | type: select 44 | width: 1/2 45 | options: 46 | alpha: alpha 47 | num: num 48 | minLength: minLength 49 | maxLength: maxLength 50 | min: min 51 | max: max 52 | email: email 53 | url: url 54 | match: match 55 | 56 | alpha: 57 | label: form.block.fromfields.input.fields.alpha 58 | text: form.block.fromfields.input.fields.alpha.text 59 | type: info 60 | width: 1/2 61 | when: 62 | validate: alpha 63 | 64 | match: 65 | label: form.block.fromfields.input.fields.match 66 | type: text 67 | help: form.block.fromfields.input.fields.match.help 68 | width: 1/2 69 | when: 70 | validate: match 71 | 72 | minlength: 73 | label: form.block.fromfields.input.fields.minLength 74 | type: number 75 | default: 0 76 | width: 1/2 77 | when: 78 | validate: minLength 79 | 80 | maxlength: 81 | label: form.block.fromfields.input.fields.maxLength 82 | type: number 83 | default: 100 84 | width: 1/2 85 | when: 86 | validate: maxLength 87 | 88 | min: 89 | label: form.block.fromfields.input.fields.min 90 | type: number 91 | default: 0 92 | width: 1/2 93 | when: 94 | validate: min 95 | 96 | max: 97 | label: form.block.fromfields.input.fields.max 98 | type: number 99 | default: 100 100 | width: 1/2 101 | when: 102 | validate: max 103 | 104 | msg: 105 | label: form.block.fromfields.input.fields.msg 106 | type: text 107 | help: form.block.default.help 108 | -------------------------------------------------------------------------------- /blueprints/blocks/formfields/02_textarea.yml: -------------------------------------------------------------------------------- 1 | name: form.block.fromfields.textarea 2 | icon: text 3 | 4 | fields: 5 | placeholder: 6 | label: form.block.fromfields.textarea.placeholder 7 | type: text 8 | width: 2/4 9 | row: 10 | label: form.block.fromfields.textarea.row 11 | type: number 12 | default: 5 13 | width: 1/4 14 | man: 15 | label: form.block.fromfields.textarea.man 16 | type: number 17 | default: 255 18 | width: 1/4 -------------------------------------------------------------------------------- /blueprints/blocks/formfields/03_checkbox.yml: -------------------------------------------------------------------------------- 1 | name: form.block.fromfields.checkbox 2 | icon: check 3 | 4 | fields: 5 | options: 6 | label: form.block.fromfields.checkbox.options 7 | type: structure 8 | required: true 9 | columns: 10 | label: 11 | type: text 12 | selected: 13 | type: bool 14 | 15 | fields: 16 | label: 17 | label: form.block.fromfields.checkbox.options.label 18 | type: text 19 | required: true 20 | width: 1/3 21 | 22 | slug: 23 | label: form.block.fromfields.checkbox.options.slug 24 | type: slug 25 | wizard: 26 | text: " " 27 | field: label 28 | required: true 29 | width: 1/3 30 | 31 | selected: 32 | label: form.block.fromfields.checkbox.options.selected 33 | type: toggle 34 | width: 1/3 35 | 36 | -------------------------------------------------------------------------------- /blueprints/blocks/formfields/04_radio.yml: -------------------------------------------------------------------------------- 1 | name: form.block.fromfields.radio 2 | icon: circle-filled 3 | 4 | fields: 5 | 6 | options: 7 | label: form.block.fromfields.radio.options 8 | type: structure 9 | required: true 10 | columns: 11 | label: 12 | type: text 13 | selected: 14 | type: bool 15 | 16 | fields: 17 | label: 18 | label: form.block.fromfields.radio.options.label 19 | type: text 20 | required: true 21 | width: 1/3 22 | 23 | slug: 24 | label: form.block.fromfields.radio.options.slug 25 | type: slug 26 | wizard: 27 | text: " " 28 | field: label 29 | required: true 30 | width: 1/3 31 | 32 | selected: 33 | label: form.block.fromfields.checkbox.options.selected 34 | type: toggle 35 | width: 1/3 -------------------------------------------------------------------------------- /blueprints/blocks/formfields/05_select.yml: -------------------------------------------------------------------------------- 1 | name: form.block.fromfields.select 2 | icon: angle-down 3 | 4 | fields: 5 | placeholder: 6 | label: form.block.fromfields.select.placeholder 7 | type: text 8 | 9 | options: 10 | label: form.block.fromfields.select.options 11 | type: structure 12 | required: true 13 | columns: 14 | label: 15 | type: text 16 | selected: 17 | type: bool 18 | 19 | fields: 20 | label: 21 | label: form.block.fromfields.select.options.label 22 | type: text 23 | required: true 24 | width: 1/3 25 | 26 | slug: 27 | label: form.block.fromfields.select.options.slug 28 | type: slug 29 | wizard: 30 | text: " " 31 | field: label 32 | required: true 33 | width: 1/3 34 | 35 | selected: 36 | label: form.block.fromfields.checkbox.options.selected 37 | type: toggle 38 | width: 1/3 -------------------------------------------------------------------------------- /blueprints/blocks/formfields/06_file.yml: -------------------------------------------------------------------------------- 1 | name: form.block.fromfields.file 2 | icon: file 3 | 4 | fields: 5 | accept: 6 | label: form.block.fromfields.file.accept 7 | type: tags 8 | width: 2/4 9 | help: form.block.fromfields.file.accept.help 10 | maxsize: 11 | label: form.block.fromfields.file.maxsize 12 | type: number 13 | width: 1/4 14 | help: form.block.fromfields.file.maxsize.help 15 | after: MB 16 | default: 10 17 | maxnumber: 18 | label: form.block.fromfields.file.maxnumber 19 | type: number 20 | width: 1/4 21 | min: 1 22 | default: 1 23 | file_accept: 24 | label: form.block.fromfields.file.accept.fail 25 | type: text 26 | help: form.block.default.help 27 | info: 28 | label: form.block.fromfields.file.warning.label 29 | type: info 30 | text: form.block.fromfields.file.warning.text 31 | theme: negative -------------------------------------------------------------------------------- /blueprints/blocks/formfields/07_captcha.yml: -------------------------------------------------------------------------------- 1 | name: form.block.fromfields.captcha 2 | icon: bug 3 | 4 | fields: 5 | slug: 6 | type: hidden 7 | default: captcha-input 8 | placeholder: 9 | type: hidden 10 | required: 11 | default: true 12 | type: hidden 13 | ask: 14 | label: form.block.fromfields.captcha.ask 15 | type: text 16 | help: form.block.default.help 17 | fail: 18 | label: form.block.fromfields.captcha.fail 19 | type: text 20 | help: form.block.default.help 21 | autofill: 22 | type: hidden -------------------------------------------------------------------------------- /blueprints/files/formfile.yml: -------------------------------------------------------------------------------- 1 | options: 2 | read: false 3 | preview: false 4 | -------------------------------------------------------------------------------- /blueprints/pages/formcontainer.yml: -------------------------------------------------------------------------------- 1 | options: 2 | read: false 3 | preview: false 4 | 5 | -------------------------------------------------------------------------------- /blueprints/pages/formrequest.yml: -------------------------------------------------------------------------------- 1 | title: Formdata 2 | options: 3 | read: false 4 | preview: false -------------------------------------------------------------------------------- /blueprints/snippets/form_confirm.yml: -------------------------------------------------------------------------------- 1 | enable_confirm: 2 | type: toggle 3 | label: form.block.options.enable_confirm 4 | 5 | confirm_email: 6 | label: form.block.options.confirm_email 7 | type: text 8 | help: form.block.options.email.help 9 | width: 1/2 10 | when: 11 | enable_confirm: true 12 | 13 | confirm_subject: 14 | label: form.block.options.confirm_subject 15 | type: text 16 | help: form.block.default.help 17 | width: 1/2 18 | when: 19 | enable_confirm: true 20 | 21 | confirm_body: 22 | label: form.block.options.confirm_body 23 | type: textarea 24 | help: form.block.default.help 25 | buttons: false 26 | when: 27 | enable_confirm: true 28 | 29 | line2: 30 | type: line 31 | when: 32 | enable_confirm: true -------------------------------------------------------------------------------- /blueprints/snippets/form_notify.yml: -------------------------------------------------------------------------------- 1 | enable_notify: 2 | type: toggle 3 | label: form.block.options.enable_notify 4 | 5 | notify_email: 6 | label: form.block.options.notify_email 7 | type: text 8 | placeholder: form.block.options.notify_placeholder 9 | help: form.block.options.email.help 10 | width: 1/2 11 | when: 12 | enable_notify: true 13 | 14 | notify_subject: 15 | label: form.block.options.notify_subject 16 | type: text 17 | help: form.block.default.help 18 | width: 1/2 19 | when: 20 | enable_notify: true 21 | 22 | notify_body: 23 | label: form.block.options.notify_body 24 | type: textarea 25 | help: form.block.default.help 26 | buttons: false 27 | when: 28 | enable_notify: true 29 | 30 | line1: 31 | type: line 32 | when: 33 | enable_notify: true -------------------------------------------------------------------------------- /blueprints/snippets/form_options.yml: -------------------------------------------------------------------------------- 1 | redirect: 2 | type: toggle 3 | label: form.block.options.redirect 4 | width: 1/3 5 | text: 6 | - form.block.options.redirect.on 7 | - form.block.options.redirect.off 8 | 9 | success_message: 10 | label: form.block.options.success_text 11 | type: writer 12 | help: form.block.default.help 13 | width: 2/3 14 | when: 15 | redirect: false 16 | 17 | success_url: 18 | label: form.block.options.success_url 19 | type: pages 20 | min: 1 21 | width: 2/3 22 | when: 23 | redirect: true 24 | -------------------------------------------------------------------------------- /classes/Blueprint.php: -------------------------------------------------------------------------------- 1 | 8 | * @link https://plain-solutions.net/ 9 | * @copyright Roman Gsponer 10 | * @license https://plain-solutions.net/terms/ 11 | */ 12 | 13 | use Kirby\Filesystem\Dir; 14 | use Kirby\Filesystem\F; 15 | use Kirby\Data\Yaml; 16 | 17 | class Blueprint 18 | { 19 | 20 | /** 21 | * Get Blueprint as array 22 | * 23 | * @param Array $path Filename or path of Blueprint 24 | * 25 | * @return array 26 | */ 27 | public static function getBlueprint(String $path, Bool $merge = false): array 28 | { 29 | 30 | $plugindata = Yaml::read(__DIR__ . "/../blueprints/$path.yml"); 31 | $userfile = kirby()->root('blueprints') . "/$path.yml"; 32 | if (F::exists($userfile)) { 33 | return $merge ? array_merge($plugindata, Yaml::read($userfile)) : Yaml::read($userfile); 34 | } 35 | 36 | return $plugindata; 37 | } 38 | 39 | 40 | 41 | /** 42 | * Get inbox tab 43 | * 44 | * @return array|bool 45 | */ 46 | public static function getInbox() 47 | { 48 | if (!static::isEnabled('inbox')) { 49 | return false; 50 | }; 51 | 52 | return [ 53 | 'label' => 'form.block.inbox', 54 | 'fields' => [ 55 | 'formid' => ['type' => 'hidden'], 56 | 'mailview' => [ 57 | 'type' => 'mailview' 58 | ] 59 | ] 60 | ]; 61 | } 62 | 63 | /** 64 | * Get form tab 65 | * 66 | * @return array 67 | */ 68 | public static function getForm(): array 69 | { 70 | return [ 71 | 'label' => 'form.block.fromfields', 72 | 'fields' => static::getFormfields() 73 | ]; 74 | } 75 | 76 | /** 77 | * Get option tab 78 | * 79 | * @return array 80 | */ 81 | public static function getOptions(): array 82 | { 83 | return [ 84 | 'label' => 'form.block.options', 85 | 'fields' => static::mergeField( 86 | [ 87 | 'name' => [ 88 | 'type' => 'hidden' 89 | ], 90 | 'info' => static::getInfoText() 91 | ], 92 | (static::isEnabled('notify')) ? static::getBlueprint('snippets/form_notify') : [], 93 | (static::isEnabled('confirm')) ? static::getBlueprint('snippets/form_confirm') : [], 94 | static::getBlueprint('snippets/form_options') 95 | ) 96 | ]; 97 | } 98 | 99 | /** 100 | * Merge field in formfield 101 | * 102 | * @param array $fields Formfields to merge 103 | * 104 | * @return array 105 | */ 106 | public static function mergeField(array ...$fields): array 107 | { 108 | $out = []; 109 | foreach ($fields as $collection) { 110 | foreach ($collection as $key => $value) { 111 | $out[$key] = $value; 112 | } 113 | } 114 | return $out; 115 | } 116 | 117 | /** 118 | * Get formfields from user/plugin blueprints 119 | * 120 | * @return array 121 | */ 122 | private static function getFormfields(): array 123 | { 124 | 125 | $fieldsets = []; 126 | 127 | $out = []; 128 | 129 | $customfields = static::getBlueprint('blocks/customfields', true); 130 | 131 | $fieldsets = static::mergeFormfields(__DIR__ . '/../blueprints/blocks/formfields', $fieldsets, $customfields); 132 | 133 | if (Dir::exists($userlocation = kirby()->root('blueprints') . '/blocks/formfields')) { 134 | $fieldsets = static::mergeFormfields($userlocation, $fieldsets, $customfields); 135 | } 136 | 137 | return [ 138 | 'formfields' => [ 139 | 'type' => 'blocks', 140 | 'fieldsets' => $fieldsets 141 | ], 142 | 'display' => [ 143 | 'type' => 'text', 144 | 'label' => 'form.block.fromfields.display', 145 | 'help' => 'form.block.fromfields.display.help' 146 | ] 147 | ]; 148 | } 149 | 150 | /** 151 | * Merge field in formfield 152 | * 153 | * @param string $formblockfolder Path to blueprint 154 | * @param array $out Previous blueprint 155 | * @param array $customfields Fields to add on each formfield 156 | * 157 | * @return array 158 | */ 159 | private static function mergeFormfields(string $formblockfolder, array $out, array $customfields): array 160 | { 161 | foreach (Dir::read($formblockfolder, null, true) as $f) { 162 | 163 | //Convert formblock to array 164 | $this_block = Yaml::read($f); 165 | $identifier = 'formfields_' . pathinfo($f)['filename']; 166 | 167 | if (count($this_block) == 0) { 168 | 169 | //Users formblock is empty -> delete formblock from plugin 170 | unset($out[$identifier]); 171 | } else { 172 | //Merge custom- and user-fields and add it to fieldset-array 173 | $this_block['fields'] = array_merge($customfields, $this_block['fields']); 174 | $this_block['label'] = "{{ label }}"; 175 | $out[$identifier] = $this_block; 176 | } 177 | }; 178 | return $out; 179 | } 180 | 181 | /** 182 | * Get info text from placeholderfields 183 | * 184 | * @return array|bool 185 | */ 186 | private static function getInfoText() 187 | { 188 | 189 | if (!static::isEnabled('placeholder_hint')) { 190 | return false; 191 | }; 192 | 193 | $text = '**With *\{\{ \}\}* you can insert incoming values using placeholder.**'; 194 | foreach (static::getPlaceholders() as $key => $value) { 195 | $text .= "\n**\{\{ $key \}\}**: ".$value['label']; 196 | } 197 | return [ 198 | 'text' => $text 199 | ]; 200 | 201 | } 202 | 203 | /** 204 | * Users/plugin placeholders 205 | * 206 | * @return array 207 | */ 208 | public static function getPlaceholders(): array 209 | { 210 | return array_merge([ 211 | 'summary' => [ 212 | 'label' => "Summary", 213 | 'value' => function ($fields) { 214 | 215 | $table = ""; 216 | 217 | foreach ($fields as $field) { 218 | $table .= ""; 219 | } 220 | 221 | $table .= "
" . $field->label() . "" . nl2br($field->value()) . "
"; 222 | return $table; 223 | } 224 | ] 225 | ], kirby()->option('plain.formblock.placeholders') ?? []); 226 | } 227 | 228 | /** 229 | * Check if tab/function is enabled in config 230 | * 231 | * @param string $fnc 232 | * 233 | * @return bool 234 | */ 235 | private static function isEnabled($fnc): bool 236 | { 237 | $option_value = ".formblock.disable_$fnc"; 238 | return empty(kirby()->option("plain.$option_value")); 239 | } 240 | 241 | } -------------------------------------------------------------------------------- /classes/Field.php: -------------------------------------------------------------------------------- 1 | 8 | * @link https://plain-solutions.net/ 9 | * @copyright Roman Gsponer 10 | * @license https://plain-solutions.net/terms/ 11 | */ 12 | 13 | use Kirby\Cms\Block as KirbyBlock; 14 | use Kirby\Filesystem\F; 15 | use Kirby\Toolkit\A; 16 | use Kirby\Toolkit\V; 17 | use Kirby\Toolkit\Str; 18 | use Kirby\Toolkit\Escape; 19 | use Kirby\Http\Request\Files; 20 | use Kirby\Filesystem\Mime; 21 | 22 | class Field extends KirbyBlock 23 | { 24 | 25 | /** 26 | * Visitor send some values 27 | * 28 | * @var Bool 29 | */ 30 | protected $isFilled; 31 | 32 | 33 | /** 34 | * Visitor send some values 35 | * 36 | * @var Array 37 | */ 38 | protected $errors; 39 | 40 | /** 41 | * Fileobject if it's Field 42 | * 43 | * @var Array 44 | */ 45 | protected $files; 46 | 47 | 48 | /** 49 | * Creates a field 50 | * 51 | * @param array $params Fieldsdata 52 | * @param object $parent 53 | * 54 | * @return null 55 | */ 56 | public function __construct(array $params, object $parent) 57 | { 58 | 59 | $this->parent = $parent; 60 | 61 | if (array_key_exists('options', $params['content'])) { 62 | $params['content']['opt'] = $this->setOptions($params); 63 | } else { 64 | $params['content']['value'] = $this->setValue($params); 65 | } 66 | 67 | $this->isFilled = $params['isFilled']; 68 | parent::__construct($params); 69 | 70 | if ($this->type(true) == "file") { 71 | $file_obj = new Files(); 72 | $this->files = $file_obj->get($this->slug()); 73 | } 74 | 75 | $this->errors = $this->getErrorMessages(); 76 | 77 | } 78 | 79 | /** 80 | * Get request value by parameter or array of all if $slug is empty 81 | * 82 | * @param string $slug 83 | * 84 | * @return array|string 85 | */ 86 | private function request($slug = null) 87 | { 88 | if (is_null($slug)) { 89 | return get(); 90 | } 91 | 92 | return get(is_string($slug) ? $slug : $slug->toString()) ?: ""; 93 | } 94 | 95 | /** 96 | * Prepare the options to work with them 97 | * 98 | * @param array $field 99 | * 100 | * @return array 101 | */ 102 | private function setOptions(array $field): array 103 | { 104 | if (!$field['isFilled']) { 105 | return $field['content']['options']; 106 | } 107 | 108 | return array_map(function ($option) use ($field) { 109 | 110 | if ($field['type'] == 'formfields/checkbox') { 111 | $option['selected'] = in_array($option['slug'], array_keys($this->request())); 112 | } else { 113 | $option['selected'] = $this->request($field['content']['slug']) == $option['slug']; 114 | } 115 | 116 | return $option; 117 | 118 | }, $field['content']['options']); 119 | 120 | } 121 | 122 | /** 123 | * Prepare the value to work with them 124 | * 125 | * @param array $field 126 | * 127 | * @return string 128 | */ 129 | private function setValue(array $field): string 130 | { 131 | if ($field['isFilled']) { 132 | return $this->request($field['content']['slug']); 133 | } 134 | 135 | if (isset($field['content']['default'])) { 136 | return $field['content']['default']; 137 | } 138 | 139 | return ""; 140 | } 141 | 142 | /** 143 | * Retruns the value of the field 144 | * 145 | * @param bool $raw return value without parsing 146 | * 147 | * @return string 148 | */ 149 | public function value($raw = false): string 150 | { 151 | if ($this->hasOptions()) { 152 | return $this->isFilled() ? A::join($this->selectedOptions($raw ? 'slug' : 'label'), ', ') : ""; 153 | } 154 | 155 | 156 | if (!is_null($this->files)) { 157 | return implode(', ', array_map(fn($f) => F::safeName($f['name']), $this->files)); 158 | } 159 | 160 | // Prefill value from query string variable matching slug 161 | $slug = (string) $this->content()->slug(); 162 | if(isset($_GET[$slug]) && ($value = $_GET[$slug])){ 163 | return Escape::html($value); 164 | } 165 | 166 | return $raw ? $this->content()->value() : Escape::html($this->content()->value()); 167 | } 168 | 169 | /** 170 | * Get Autofill 171 | * 172 | * @param bool return with attribute 173 | * 174 | * @return string|null 175 | */ 176 | public function autofill($html = false) 177 | { 178 | $val = $this->content()->autofill(); 179 | 180 | if (!$html) return $val; 181 | if (!$val->isEmpty()) return ' autocomplete="' . $val . '"'; 182 | 183 | return ""; 184 | } 185 | 186 | /** 187 | * Get Aria Error Atribute 188 | * 189 | * 190 | * @return string|null 191 | */ 192 | public function ariaAttr() 193 | { 194 | return 'aria-labelledby="label-' . $this->id() . '" aria-describedby="' . $this->id() . '-error-message"'; 195 | } 196 | 197 | /** 198 | * Get required 199 | * 200 | * @param bool|string return with attribute 201 | * 202 | * @return string|bool 203 | */ 204 | public function required($html = false) 205 | { 206 | if (!$html) { 207 | return $this->content()->required()->isTrue(); 208 | } 209 | 210 | if ($this->content()->required()->isTrue()) { 211 | if ($html === 'asterisk') return '*'; 212 | if ($html === 'attr') return ' required'; 213 | } 214 | return ""; 215 | } 216 | 217 | /** 218 | * Get Tag 219 | * 220 | * @param string 221 | * 222 | * @return string 223 | */ 224 | public function getTag($kind = "container") 225 | { 226 | 227 | if ($kind == "label") 228 | return ($this->hasOptions()) ? 'legend' : 'label'; 229 | 230 | return ($this->hasOptions()) ? 'fieldset' : 'div'; 231 | 232 | } 233 | 234 | 235 | /** 236 | * Convert type 237 | * 238 | * @param bool $onlyName 239 | * 240 | * @return string 241 | */ 242 | public function type($onlyName = false): string 243 | { 244 | if ($onlyName) { 245 | return A::last(Str::split($this->type, '/')); 246 | } 247 | return $this->type; 248 | } 249 | 250 | /*********************/ 251 | /** Options Methods **/ 252 | /*********************/ 253 | 254 | 255 | /** 256 | * Check if this this field is an option field 257 | * 258 | * @return bool 259 | */ 260 | public function hasOptions(): bool 261 | { 262 | return !$this->options()->isEmpty(); 263 | } 264 | 265 | /** 266 | * Returns option fields as structure 267 | * 268 | * @return Kirby\Cms\Structure 269 | */ 270 | 271 | public function options() 272 | { 273 | return $this->opt()->toStructure(); 274 | } 275 | 276 | /** 277 | * Get Selected options as Array or by $prop 278 | * 279 | * @param array $prop 280 | * @return array|null 281 | */ 282 | public function selectedOptions($prop = 'label') 283 | { 284 | $out = []; 285 | foreach ($this->options()->toArray() as $value) { 286 | if ($value['selected']) 287 | array_push($out,$value[$prop]); 288 | } 289 | return $out; 290 | return $this->options()->filterBy('selected', true)->pluck($prop, true); 291 | } 292 | 293 | /************************/ 294 | /** Validation Methods **/ 295 | /************************/ 296 | 297 | /** 298 | * Check if form is filled 299 | * 300 | * @return bool 301 | */ 302 | public function isFilled(): bool 303 | { 304 | return $this->isFilled; 305 | } 306 | 307 | /** 308 | * Get Messages 309 | * 310 | * @param string $key 311 | * @param array $replaceArray Additional array for replacing 312 | * 313 | * @return string 314 | */ 315 | public function message($key, $replaceArray = []): string 316 | { 317 | 318 | return Form::translate($key, $this->__call($key), $replaceArray); 319 | 320 | } 321 | 322 | /** 323 | * Get array of all validators (with errors if occur) 324 | * 325 | * @return array 326 | */ 327 | public function getErrorMessages(): array 328 | { 329 | $rules = []; 330 | $messages = []; 331 | 332 | if (!$this->isFilled) 333 | return []; 334 | 335 | //Validate File 336 | if (!is_null($this->files) ) 337 | return $this->validateFile(); 338 | 339 | 340 | 341 | if ($this->required() && empty($this->value())) { 342 | 343 | return ['required' => $this->message('required_fail')]; 344 | 345 | } 346 | 347 | //Validate Requirement 348 | $validator = $this->validate()->toStructure()->toArray(); 349 | 350 | //Validate spam protection 351 | if ($this->type(true) === "captcha" ) { 352 | 353 | $calc = array_sum(explode('_', get('captcha-id'))); 354 | 355 | array_push($validator, [ 356 | 'validate' => 'same', 357 | 'same' => strval($calc), 358 | 'msg' => $this->fail()->or($this->message('captcha_fail')) 359 | ]); 360 | 361 | } 362 | 363 | 364 | foreach ($validator as $v) { 365 | $rule = Str::lower($v['validate']); 366 | $rules[$rule] = [isset($v[$rule]) ? $v[$rule] : "" ]; 367 | $messages[$rule] = empty($v['msg']) ? t('error.validation.' . $v['validate']) : $v['msg']; 368 | } 369 | 370 | $errors = V::errors($this->value(), $rules, $messages); 371 | 372 | return kirby()->apply('formblock.validation:before', [ 373 | 'type' => $this->type(true), 374 | 'value' => $this->value(), 375 | 'errors' => $errors, 376 | 'field' => $this, 377 | 'slug' => $this->slug() 378 | 379 | ], 'errors'); 380 | 381 | } 382 | 383 | /** 384 | * Validate Attachment 385 | * 386 | * @return array 387 | */ 388 | private function validateFile(): array 389 | { 390 | $errors = []; 391 | 392 | $maxsize = min( 393 | Str::toBytes(ini_get('post_max_size')), 394 | Str::toBytes(ini_get('upload_max_filesize')), 395 | Str::toBytes(ini_get('memory_limit')), 396 | Str::toBytes($this->maxsize()."M") 397 | ); 398 | 399 | 400 | //Max Number of files 401 | if (count($this->files) > $this->maxnumber()->value()) { 402 | $errors['maxnumber'] = $this->message('file_maxnumber', ['maxnumber' => $this->maxnumber()->value()]); 403 | } 404 | 405 | foreach ($this->files as $f) { 406 | 407 | //No files 408 | if ($f['error'] == 4) { 409 | if ($this->required()){ 410 | $errors['require'] = $this->message('file_required'); 411 | } 412 | return []; 413 | } 414 | 415 | //Check file size 416 | if ( $f['size'] > $maxsize) 417 | $errors["filesize"] = $this->message('file_maxsize', ['maxsize' => ($maxsize / 1024 / 1024 )]); 418 | 419 | //Check MIME Types 420 | $mime = Mime::fromMimeContentType($f['tmp_name']); 421 | $accept = $this->accept()->value(); 422 | 423 | if(!Mime::isAccepted($mime, $accept) and $this->accept()->isNotEmpty()) 424 | $errors["mime"] = $this->message('file_accept', ['accept' => $accept]); 425 | 426 | if ($f['error'] > 0) { 427 | $errors["fatal"] = $this->message('file_fatal', ['error' => $f['error']]); 428 | } 429 | 430 | } 431 | 432 | return $errors; 433 | 434 | } 435 | 436 | 437 | 438 | /** 439 | * Get first failed fields message 440 | * 441 | * @return string 442 | */ 443 | public function errorMessage(): string 444 | { 445 | return A::first($this->errors) ?: ""; 446 | } 447 | 448 | /** 449 | * Get true if everything filled right 450 | * 451 | * @return bool 452 | */ 453 | public function isValid(): bool 454 | { 455 | return count($this->errors) === 0; 456 | } 457 | 458 | /** 459 | * Controller for the blockfield snippet 460 | * 461 | * @return array 462 | */ 463 | public function controller(): array 464 | { 465 | return [ 466 | 'formfield' => $this, 467 | 'content' => $this->content(), 468 | 'id' => $this->id(), 469 | 'prev' => $this->prev(), 470 | 'next' => $this->next() 471 | ]; 472 | } 473 | } -------------------------------------------------------------------------------- /classes/Fields.php: -------------------------------------------------------------------------------- 1 | 8 | * @link https://plain-solutions.net/ 9 | * @copyright Roman Gsponer 10 | * @license https://plain-solutions.net/terms/ 11 | */ 12 | 13 | use Kirby\Cms\Blocks as KirbyBlock; 14 | use Kirby\Http\Environment; 15 | 16 | class Fields extends KirbyBlock 17 | { 18 | 19 | /** 20 | * Visitor send some values 21 | * 22 | * @var Bool 23 | */ 24 | protected $isFilled; 25 | 26 | 27 | /** 28 | * Set all attachment for email 29 | * 30 | * @var array 31 | */ 32 | public $attachments = []; 33 | 34 | /** 35 | * Magic getter function 36 | * 37 | * @param array $params 38 | * @param object $parent 39 | * @param string $formid 40 | * 41 | * @return mixed 42 | */ 43 | public function __construct(array $params, object $parent, string $formid) 44 | { 45 | $this->parent = $parent; 46 | 47 | //Main check if Form is filled. 48 | $this->isFilled = Environment::getGlobally('REQUEST_METHOD') === 'POST' && get('hash'); 49 | 50 | //Add field object to class 51 | foreach ($params as $formfield) { 52 | 53 | $this->add( 54 | new Field( 55 | [ 56 | "content" => $formfield['content'], 57 | 'id' => $formfield['id'], 58 | //Escape the prefixes form formfield type 59 | 'type' => preg_replace('/_[0-9]+_|\_/', '/', $formfield['type']), 60 | 'isFilled' => $this->isFilled() 61 | ], $this->parent() 62 | ) 63 | ); 64 | 65 | } 66 | } 67 | 68 | /** 69 | * Returns method or field 70 | * 71 | * @param string $key 72 | * @param mixed $arguments 73 | * 74 | * @return mixed 75 | */ 76 | public function __call(string $key, $arguments) 77 | { 78 | // Return method 79 | if ($this->hasMethod($key) === true) { 80 | return $this->callMethod($key, $arguments); 81 | } 82 | 83 | //Return field 84 | if ($field = $this->findBy('slug', str_replace('_', '-', $key))) 85 | return $field; 86 | 87 | return null; 88 | } 89 | 90 | /** 91 | * Download Files in the Form 92 | * 93 | * @return mixed 94 | */ 95 | public function hasAttachment() 96 | { 97 | 98 | //Walker through fields looking for file 99 | foreach ($this as $f) { 100 | if ($f->type(true) == "file") { 101 | return true; 102 | } 103 | } 104 | 105 | return false; 106 | 107 | } 108 | 109 | /** 110 | * Returns a list of fields 111 | * 112 | * @param string $attr What value to return 113 | * @return string|array 114 | */ 115 | public function errorFields($attr = null) 116 | { 117 | $errors = []; 118 | 119 | //Walker through failed fields 120 | foreach ($this as $f) { 121 | if (!$f->isValid()) { 122 | array_push($errors, ($attr) ? $f->$attr()->toString() : $f); 123 | } 124 | } 125 | 126 | return $errors; 127 | } 128 | 129 | /** 130 | * Check if all field filled properly 131 | * 132 | * @return bool 133 | */ 134 | public function isValid(): bool 135 | { 136 | return count($this->errorFields()) == 0 and $this->isFilled(); 137 | } 138 | 139 | /** 140 | * Check if form is filled 141 | * 142 | * @return bool 143 | */ 144 | public function isFilled(): bool 145 | { 146 | return $this->isFilled; 147 | } 148 | 149 | /** 150 | * Check if the bear grabs into the honeypot 151 | * 152 | * @param string HoneypotID 153 | * 154 | * @return bool 155 | */ 156 | public function checkHoneypot($hpId): bool 157 | { 158 | 159 | if ((get($hpId) === null || get($hpId) !== "") && $this->isFilled()) { 160 | $this->isFilled = false; 161 | return false; 162 | }; 163 | return true; 164 | 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /classes/Request.php: -------------------------------------------------------------------------------- 1 | 8 | * @link https://plain-solutions.net/ 9 | * @copyright Roman Gsponer 10 | * @license https://plain-solutions.net/terms/ 11 | */ 12 | 13 | use Kirby\Cms\App; 14 | use Kirby\Toolkit\I18n; 15 | use Kirby\Http\Request\Files; 16 | use Kirby\Filesystem\F; 17 | use Kirby\Toolkit\A; 18 | use Kirby\Toolkit\Str; 19 | use Kirby\Uuid\Uuid; 20 | 21 | class Request 22 | { 23 | 24 | 25 | /** 26 | * Page with form 27 | * 28 | * @var \Kirby\Cms\Page 29 | */ 30 | protected $page; 31 | 32 | 33 | protected $page_id; 34 | 35 | /** 36 | * Request container 37 | * 38 | * @var \Kirby\Cms\Page 39 | */ 40 | protected $container; 41 | 42 | /** 43 | * Current Request 44 | * 45 | * @var \Kirby\Cms\Page 46 | */ 47 | protected $request; 48 | 49 | /** 50 | * Current Request 51 | * 52 | * @var \Kirby\Cms\Pages 53 | */ 54 | protected $forms; 55 | 56 | /** 57 | * Magic getter function 58 | * 59 | * @param Array $props 60 | * 61 | * @return mixed 62 | */ 63 | public function __construct($props) 64 | { 65 | 66 | $this->page_id = $props['page_id'] ?? 'site'; 67 | 68 | //Get Page 69 | if ($this->page_id === 'site') { 70 | $this->page = site(); 71 | } else { 72 | $this->page = site()->index(true)->find($this->page_id); 73 | } 74 | 75 | $this->forms = $this->page->index(true)->filterBy('intendedTemplate', 'formcontainer'); 76 | 77 | //Get container 78 | if ($props['form_id'] ?? false) { 79 | 80 | //Set Container 81 | $this->container = $this->forms->findBy('slug', $props['form_id']); 82 | 83 | } 84 | 85 | //Create if not exists 86 | if (is_null($this->container) && ($props['allowCreate'] ?? false)) { 87 | $this->createContainer($props); 88 | } 89 | 90 | //Set current request 91 | if ($props['request_id'] ?? false) { 92 | $this->request($props['request_id']); 93 | } 94 | 95 | } 96 | 97 | 98 | /** 99 | * Create Formcontainer 100 | * 101 | * @param array $props 102 | * 103 | * @return null 104 | */ 105 | private function createContainer($props) { 106 | 107 | site()->kirby()->impersonate('kirby'); 108 | 109 | $this->container = $this->page->createChild([ 110 | 'slug' => $props['form_id'], 111 | 'template' => 'formcontainer', 112 | 'content' => [ 113 | 'name' => $props['form_name'] ?? $props['form_id'], 114 | ] 115 | ]); 116 | 117 | } 118 | 119 | /** 120 | * Set/Get Request 121 | * 122 | * @param string $request_id 123 | * 124 | * @return \Kirby\Cms\Page 125 | */ 126 | private function request($request_id = null) { 127 | 128 | if (!is_null($request_id) && is_null($this->container) === false) { 129 | 130 | $this->request = $this->container->draft($request_id); 131 | 132 | } 133 | 134 | return $this->request; 135 | 136 | } 137 | 138 | /** 139 | * Create a new request 140 | * 141 | * @param array $content 142 | * @param string $requestid 143 | * 144 | * @return \Kirby\Cms\Page|null 145 | */ 146 | public function create($content, $requestid): \Kirby\Cms\Page|null 147 | { 148 | 149 | 150 | if (is_null($this->request($requestid))) { 151 | 152 | site()->kirby()->impersonate('kirby'); 153 | 154 | $this->request = $this->container->createChild([ 155 | 'slug' => $requestid, 156 | 'template' => 'formrequest', 157 | 'content' => $content 158 | ]); 159 | 160 | return $this->request; 161 | } 162 | 163 | return null; 164 | 165 | } 166 | 167 | /** 168 | * Update the request as page 169 | * 170 | * @param array $input Changes 171 | * 172 | * @return \Kirby\Cms\Page|null 173 | * 174 | */ 175 | public function update(array $input = []) 176 | { 177 | 178 | if (!is_null($this->request)) { 179 | 180 | site()->kirby()->impersonate('kirby'); 181 | return $this->request = $this->request->update($input); 182 | 183 | } 184 | 185 | return null; 186 | } 187 | 188 | /** 189 | * Update the formdata of the request 190 | * 191 | * @param array $input Changes 192 | * 193 | * @return \Kirby\Cms\Page|null 194 | * 195 | */ 196 | public function updateFormdata(array $input = []) 197 | { 198 | 199 | $fd = json_decode($this->request->content()->formdata()->value(), true); 200 | $fd = array_merge($fd, $input); 201 | return $this->update(['formdata' => json_encode($fd)]); 202 | 203 | } 204 | 205 | 206 | /** 207 | * Delete request 208 | * 209 | * @return \Kirby\Cms\Page|null 210 | * 211 | */ 212 | public function delete () { 213 | 214 | if (!is_null($this->request)) { 215 | 216 | site()->kirby()->impersonate('kirby'); 217 | $this->request->delete(); 218 | 219 | if($this->container->hasDrafts() === false) { 220 | $this->container->delete(); 221 | } 222 | 223 | return true; 224 | } 225 | 226 | return false; 227 | } 228 | 229 | 230 | /** 231 | * Update container 232 | * 233 | * @param array $input Changes 234 | * 235 | * @return \Kirby\Cms\Page 236 | * 237 | */ 238 | 239 | public function updateContainer(array $input = []) 240 | { 241 | 242 | site()->kirby()->impersonate('kirby'); 243 | return $this->container = $this->container->update($input); 244 | 245 | } 246 | 247 | 248 | private function verifyName($form_name) { 249 | 250 | if (is_null($this->container) === false && $this->container->name()->value() !== $form_name) { 251 | $this->updateContainer([ 252 | 'name' => $form_name 253 | ]); 254 | } 255 | 256 | } 257 | 258 | /** 259 | * Get infos of requests 260 | * 261 | * @param array $kind Kind of infos 262 | * @param \Kirby\Cms\Page $container Container to init 263 | * 264 | * @return array|string 265 | * 266 | */ 267 | public function info($params = []) 268 | { 269 | 270 | if (array_key_exists('form_name', $params)) { 271 | $this->verifyName($params['form_name']); 272 | } 273 | 274 | return $this->infoPart('array'); 275 | 276 | 277 | } 278 | 279 | public function infoPart($kind = "count", $container = null) 280 | { 281 | 282 | $container ??= $this->container; 283 | $counter = [0,0,0]; 284 | 285 | if (is_null($container) === false && $container->hasDrafts()) { 286 | $counter = [ 287 | $container->drafts()->count(), 288 | $container->drafts()->filterBy('read', '')->count(), 289 | $container->drafts()->filterBy([['read', ''],['error', '!=', '']])->count() 290 | ]; 291 | } 292 | 293 | 294 | 295 | switch ($kind) { 296 | case 'count': 297 | return $counter[0]; 298 | break; 299 | 300 | case 'read': 301 | return $counter[1]; 302 | break; 303 | 304 | case 'fail': 305 | return $counter[2]; 306 | break; 307 | 308 | case 'state': 309 | if ($this->infoPart('read', $container) > 0) 310 | return 'positive'; 311 | 312 | if ($this->infoPart('fail', $container) > 0) 313 | return 'negative'; 314 | 315 | return 'info'; 316 | break; 317 | 318 | case 'theme': 319 | if ($this->infoPart('read', $container) > 0) 320 | return 'positive'; 321 | 322 | if ($this->infoPart('fail', $container) > 0) 323 | return 'negative'; 324 | 325 | return 'gray'; 326 | break; 327 | 328 | case 'text': 329 | $text = $this->infoPart('read', $container) . "/" . $this->infoPart('count', $container) . " " . I18n::translate('form.block.inbox.new'); 330 | 331 | if ($this->infoPart('fail', $container) > 0) 332 | $text .= " & " . $this->infoPart('fail', $container) . " " . I18n::translate('form.block.inbox.failed'); 333 | 334 | return $text; 335 | break; 336 | 337 | default: 338 | 339 | return [ 340 | "count" => $this->infoPart('count', $container), 341 | "read" => $this->infoPart('read', $container), 342 | "fail" => $this->infoPart('fail', $container), 343 | "state" => $this->infoPart('state', $container), 344 | "theme" => $this->infoPart('theme', $container), 345 | "text" => $this->infoPart('text', $container) 346 | ]; 347 | } 348 | 349 | 350 | } 351 | 352 | private function downloadLink($form_id, $title) { 353 | return A::join([ 354 | kirby()->url(), 355 | 'form', 356 | 'download', 357 | csrf(), 358 | $this->page_id, 359 | $form_id, 360 | Str::slug($title) 361 | ], '/') . '.csv'; 362 | } 363 | 364 | public function download() 365 | { 366 | 367 | $output = null; 368 | 369 | function parseField($field) { 370 | $array = json_decode($field->value(), true); 371 | unset($array['summary']); 372 | return array_values($array); 373 | } 374 | 375 | foreach ($this->container->drafts()->sortBy('received', 'desc') as $b) { 376 | 377 | $content = $b->content(); 378 | $received = $content->received()->toValue(); 379 | $id = $content->slug(); 380 | 381 | $output ??= A::join(['ID', ...parseField($content->formfields()), 'Received'], ';') . "\n"; 382 | $output .= A::join([$id, ...parseField($content->formdata()), $received], ';') . "\n"; 383 | 384 | } 385 | 386 | return $output; 387 | } 388 | 389 | /** 390 | * Get cinfos about request 391 | * 392 | * @param array $props Properies to add 393 | * 394 | * @return array|string 395 | * 396 | */ 397 | public function requestsArray($props = []) { 398 | 399 | $out = array(); 400 | 401 | foreach ($this->forms as $a) { 402 | 403 | $content = []; 404 | $read = 0; 405 | $filter = $props['filter']; 406 | 407 | if (count($filter) > 0 && in_array($a->slug(), $filter) === false) { 408 | continue; 409 | } 410 | 411 | foreach ($a->drafts()->sortBy('received', 'desc') as $b) { 412 | if ($b->read()) 413 | $read ++; 414 | array_push($content, array_merge($b->content()->toArray(), $b->toArray())); 415 | } 416 | 417 | $pagetitle = ($a->parent()) ? $a->parent()->title()->value() : site()->title()->value(); 418 | $formtitle = $pagetitle . " - " . $a->name()->value(); 419 | 420 | $out[$a->slug()] = [ 421 | "content" => $content, 422 | "id" => $a->slug(), 423 | "page" => $this->page_id, 424 | "uuid" => $a->content()->uuid()->value(), 425 | "header" => [ 426 | "page" => $pagetitle, 427 | "name" => $formtitle, 428 | "state" => $this->infoPart('array', $a), 429 | "download" => $this->downloadLink($a->slug(), $formtitle) 430 | ] 431 | ] ; 432 | 433 | } 434 | 435 | return $out; 436 | } 437 | 438 | /** 439 | * Download Files in the Form 440 | * 441 | * @return mixed 442 | */ 443 | public function uploadFiles($fields) 444 | { 445 | 446 | $file_obj = new Files(); 447 | 448 | //Become almighty 449 | kirby()->impersonate('kirby'); 450 | 451 | //Prepare array for mail attachment 452 | $attachments = array(); 453 | 454 | //Prepare array for request 455 | $fileinfos = []; 456 | 457 | //Prepare array for filename colision 458 | $filenames = array(); 459 | 460 | //Increser for filename colision 461 | $index = 1; 462 | 463 | //Walker through file fields 464 | foreach ($fields as $field_slug) { 465 | 466 | //Prepare Field 467 | $filearray = []; 468 | 469 | //Walker through files in fields 470 | foreach ($file_obj->get($field_slug) as $f) { 471 | 472 | if ($f['error'] > 0) { 473 | break; 474 | } 475 | 476 | $name = F::safeName($f['name']); 477 | 478 | //Check for filename colision 479 | if ( in_array($name, $filenames ) ){ 480 | 481 | $name = explode('.', $name); 482 | $name[0] .= "_" . $index; 483 | $name = implode('.', $name); 484 | $index ++; 485 | } 486 | 487 | //Insert name to filename colision 488 | array_push($filenames, $name); 489 | 490 | $tmpName = pathinfo($f['tmp_name']); 491 | 492 | $filename = $tmpName['dirname']. '/'. $name; 493 | 494 | rename($f['tmp_name'], $filename); 495 | 496 | //Push File for email upload 497 | array_push($attachments, $filename); 498 | 499 | //Save file 500 | $localfile = $this->request->createFile([ 501 | 'source' => $filename, 502 | 'template' => 'formfile', 503 | 'filename' => $name, 504 | 'content' => [ 505 | 'filename' => $f['name'], 506 | 'field' => $field_slug 507 | ] 508 | ]); 509 | 510 | //Push fileinfos 511 | array_push($filearray, [ 512 | 'name' => F::safeName($f['name']), 513 | 'tmp_name' => $name, 514 | 'location' => $localfile->url(), 515 | 'size' => $f['size'] 516 | ]); 517 | 518 | } 519 | 520 | $fileinfos[$field_slug] = $filearray; 521 | 522 | 523 | } 524 | 525 | $this->update(['attachment' => json_encode($fileinfos)]); 526 | 527 | return $attachments; 528 | 529 | } 530 | 531 | /** 532 | * API from Panel 533 | * 534 | * @param array $res Responding data 535 | * 536 | * @return mixed 537 | * 538 | */ 539 | public function api($res) { 540 | 541 | $params = json_decode($res['params'] ?? "", true); 542 | return $this->{$res['action']}($params); 543 | } 544 | 545 | } 546 | 547 | ?> -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plain/kirby-form-block-suite", 3 | "description": "Contactform based on Kirby blocks.", 4 | "type": "kirby-plugin", 5 | "version": "5.1.1", 6 | "license": "GPL-3.0-only", 7 | "homepage": "https://plain-solutions.net/801346", 8 | "authors": [ 9 | { 10 | "name": "Roman Gsponer", 11 | "email": "kirby@plain-solutions.net" 12 | } 13 | ], 14 | "require": { 15 | "getkirby/composer-installer": "^1.1" 16 | }, 17 | "extra": { 18 | "title": "Form Block Suite" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "304fbffcda38799a42598ef7d0bc6d3c", 8 | "packages": [ 9 | { 10 | "name": "getkirby/composer-installer", 11 | "version": "1.2.1", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/getkirby/composer-installer.git", 15 | "reference": "c98ece30bfba45be7ce457e1102d1b169d922f3d" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/getkirby/composer-installer/zipball/c98ece30bfba45be7ce457e1102d1b169d922f3d", 20 | "reference": "c98ece30bfba45be7ce457e1102d1b169d922f3d", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "composer-plugin-api": "^1.0 || ^2.0" 25 | }, 26 | "require-dev": { 27 | "composer/composer": "^1.8 || ^2.0" 28 | }, 29 | "type": "composer-plugin", 30 | "extra": { 31 | "class": "Kirby\\ComposerInstaller\\Plugin" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Kirby\\": "src/" 36 | } 37 | }, 38 | "notification-url": "https://packagist.org/downloads/", 39 | "license": [ 40 | "MIT" 41 | ], 42 | "description": "Kirby's custom Composer installer for the Kirby CMS and for Kirby plugins", 43 | "homepage": "https://getkirby.com", 44 | "support": { 45 | "issues": "https://github.com/getkirby/composer-installer/issues", 46 | "source": "https://github.com/getkirby/composer-installer/tree/1.2.1" 47 | }, 48 | "funding": [ 49 | { 50 | "url": "https://getkirby.com/buy", 51 | "type": "custom" 52 | } 53 | ], 54 | "time": "2020-12-28T12:54:39+00:00" 55 | } 56 | ], 57 | "packages-dev": [], 58 | "aliases": [], 59 | "minimum-stability": "stable", 60 | "stability-flags": [], 61 | "prefer-stable": false, 62 | "prefer-lowest": false, 63 | "platform": [], 64 | "platform-dev": [], 65 | "plugin-api-version": "2.3.0" 66 | } 67 | -------------------------------------------------------------------------------- /config/api/routes.php: -------------------------------------------------------------------------------- 1 | 'formblock', 8 | 'action' => function() { 9 | $formRequest = new Request($this->requestQuery()); 10 | return $formRequest->api($this->requestQuery()); 11 | } 12 | ] 13 | ]; -------------------------------------------------------------------------------- /config/blockModels.php: -------------------------------------------------------------------------------- 1 | Form::class ]; -------------------------------------------------------------------------------- /config/blueprints.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'name' => 'form.block.name', 8 | 'icon' => 'email', 9 | 'tabs' => [ 10 | 'inbox' => Blueprint::getInbox(), 11 | 'form' => Blueprint::getForm(), 12 | 'options' => Blueprint::getOptions() 13 | ] 14 | ], 15 | 'pages/formrequest' => Blueprint::getBlueprint('pages/formrequest'), 16 | 'pages/formcontainer' => Blueprint::getBlueprint('pages/formcontainer'), 17 | ]; -------------------------------------------------------------------------------- /config/fields.php: -------------------------------------------------------------------------------- 1 | []]; -------------------------------------------------------------------------------- /config/options.php: -------------------------------------------------------------------------------- 1 | 'no-reply@' . App::instance()->environment()->host(), 8 | 'placeholders' => Blueprint::getPlaceholders(), 9 | 'honeypot_variants' => ["email", "name", "url", "tel", "given-name", "family-name", "street-address", "postal-code", "address-line2", "address-line1", "country-name", "language", "bday"], 10 | 'default_language' => 'en', 11 | 'disable_confirm' => false, 12 | 'disable_notify' => false, 13 | 'disable_html' => false, 14 | 'email_field' => 'email', 15 | 'dynamic_validation' => true 16 | ] 17 | 18 | ?> -------------------------------------------------------------------------------- /config/routes.php: -------------------------------------------------------------------------------- 1 | 'form/validator', 9 | 'method' => "POST", 10 | 'action' => function () { 11 | 12 | //Get Page 13 | if ((get('page') ?? "site") === 'site') { 14 | $page = site(); 15 | } else { 16 | $page = site()->index(true)->find(get('page')); 17 | } 18 | site()->visit($page, get('lang')); 19 | $rendered_page = page()->render(); 20 | preg_match('/\<\!--\[Startvalidation:' . get('id') . '\]--\>(.*?)\<\!--\[Endvalidation\]--\>/s', $rendered_page, $out); 21 | 22 | if (empty($out)) { 23 | return json_encode([ 24 | 'state' => "fatal", 25 | 'error_message' => t('form.block.message.fatal_message'), 26 | 'success_message' => "", 27 | 'redirect' => "", 28 | 'fields' => [], 29 | ]); 30 | } 31 | 32 | return end($out); 33 | 34 | } 35 | ], 36 | [ 37 | 'pattern' => 'form/download/(:all)', 38 | 'action' => function ($params) { 39 | 40 | [$csrf, $page_id, $form_id, $filename] = Str::split($params, '/'); 41 | 42 | if (csrf($csrf) === false) { 43 | return go('error'); 44 | } 45 | 46 | header('Content-Type: text/csv'); 47 | header("Content-Disposition: attachment;filename={$filename}"); 48 | 49 | $formRequest = new Request(compact('page_id', 'form_id')); 50 | return $formRequest->download(); 51 | 52 | } 53 | ] 54 | ]; -------------------------------------------------------------------------------- /config/snippets.php: -------------------------------------------------------------------------------- 1 | match und geben Sie als Regulärer Ausdruck /^[a-zA-Z ]*$/ ein.", 47 | "form.block.fromfields.input.fields.num": "Nur Zahlen", 48 | "form.block.fromfields.input.fields.minLength": "Min. Anzahl Zeichen", 49 | "form.block.fromfields.input.fields.maxLength": "Max. Anzahl Zeichen", 50 | "form.block.fromfields.input.fields.min": "Minimaler Wert", 51 | "form.block.fromfields.input.fields.max": "Maximaler Wert", 52 | "form.block.fromfields.input.fields.email": "Email", 53 | "form.block.fromfields.input.fields.url": "Webseite", 54 | "form.block.fromfields.input.fields.match": "Regulärer Ausdruck", 55 | "form.block.fromfields.input.fields.match.help": "Mehr Informationen", 56 | "form.block.fromfields.input.fields.msg": "Fehlermeldung", 57 | "form.block.fromfields.textarea": "Textbereich", 58 | "form.block.fromfields.textarea.placeholder": "Platzhalter", 59 | "form.block.fromfields.textarea.row": "Anzahl Zeilen", 60 | "form.block.fromfields.textarea.man": "Max. Zeichen", 61 | "form.block.fromfields.checkbox": "Mehrfachauswahl", 62 | "form.block.fromfields.checkbox.columns": "Anzahl Spalten", 63 | "form.block.fromfields.checkbox.options": "Auswahl", 64 | "form.block.fromfields.checkbox.options.label": "Anzeigename", 65 | "form.block.fromfields.checkbox.options.slug": "Eindeutiger Bezeichner", 66 | "form.block.fromfields.checkbox.options.selected": "Ausgewählt", 67 | "form.block.fromfields.radio": "Auswahl", 68 | "form.block.fromfields.radio.columns": "Anzahl Spalten", 69 | "form.block.fromfields.radio.default": "Ausgewählt", 70 | "form.block.fromfields.radio.default.help": "Dialog erneut öffnen, wenn nicht aktuell.", 71 | "form.block.fromfields.radio.options": "Auswahl", 72 | "form.block.fromfields.radio.options.label": "Anzeigename", 73 | "form.block.fromfields.radio.options.slug": "Eindeutiger Bezeichner", 74 | "form.block.fromfields.select": "Dropdown", 75 | "form.block.fromfields.select.placeholder": "Platzhalter", 76 | "form.block.fromfields.select.default": "Ausgewählt", 77 | "form.block.fromfields.select.default.help": "Dialog erneut öffnen, wenn nicht aktuell.", 78 | "form.block.fromfields.select.options": "Auswahl", 79 | "form.block.fromfields.select.options.label": "Anzeigename", 80 | "form.block.fromfields.select.options.slug": "Eindeutiger Bezeichner", 81 | "form.block.fromfields.file": "Anhang", 82 | "form.block.fromfields.file.accept": "Erlaubte MIME-Type", 83 | "form.block.fromfields.file.accept.help": "Es können auch Platzhalter verwendet werden (z.B. image/* für alle Arten von Bildern). Weitere Infos", 84 | "form.block.fromfields.file.accept.fail": "Fehlermeldung für fehlerhafte MIME types", 85 | "form.block.fromfields.file.maxsize": "Max. Dateigrösse", 86 | "form.block.fromfields.file.maxsize.help": "Die maximale Dateigrösse kann vom Server begrenzt werden.", 87 | "form.block.fromfields.file.maxnumber": "Max. Dateimengen", 88 | "form.block.fromfields.file.warning.label": "Warnung!", 89 | "form.block.fromfields.file.warning.text": "Vorsicht bei ausführbaren Dateimimes (z.B. application/zip, application/msword). Diese können Schadprogramme enthalten.", 90 | "form.block.options": "Sendeoptionen", 91 | "form.block.options.email.help": "Mehrere Empfänger möglich. Getrennt mit `;`", 92 | "form.block.options.info": "**Mit *{{ }}* können Sie eingehende Werte mittels Bezeichner einfügen.**n", 93 | "form.block.options.enable_notify": "Benachrichtigung senden", 94 | "form.block.options.notify_email": "Empfängeradresse", 95 | "form.block.options.notify_subject": "Betreffzeile", 96 | "form.block.options.notify_body": "Nachricht", 97 | "form.block.options.enable_confirm": "Bestätigungsmail senden", 98 | "form.block.options.confirm_email": "Absenderadresse", 99 | "form.block.options.confirm_subject": "Betreffzeile", 100 | "form.block.options.confirm_body": "Nachricht", 101 | "form.block.options.redirect": "Bei Erfolg", 102 | "form.block.options.redirect.on": "Text einblenden", 103 | "form.block.options.redirect.off": "Besucher weiterleiten", 104 | "form.block.options.success_text": "Bestätigungstext", 105 | "form.block.options.success_url": "Weiterleitung", 106 | "form.block.placeholdes.summary": "Zusammenfassung", 107 | "form.block.message.confirm_body": "Danke {{ name }}. Wir werden uns schnellst möglich bei dir melden.", 108 | "form.block.message.confirm_subject": "Deine Anfrage", 109 | "form.block.message.exists_message": "Das Formular wurde bereits ausgefüllt.", 110 | "form.block.message.fatal_message": "Es ist etwas schief gelaufen. Kontaktieren Sie den Administrator oder versuchen Sie es später noch einmal.", 111 | "form.block.message.required_fail": "Dieses Feld ist erforderlich.", 112 | "form.block.message.file_accept": "Nur folgende Dateitypen werden akzeptiert: {{ accept }}.", 113 | "form.block.message.file_maxsize": "Dateien dürfen nicht grösser als {{ maxsize }}MB sein.", 114 | "form.block.message.file_maxnumber": "Es dürfen nicht mehr als {{maxnumber}} hochgeladen werden.", 115 | "form.block.message.file_required": "Wähle mindestens eine Datei zum Hochladen aus.", 116 | "form.block.message.file_fatal": "Mit dem Upload ist etwas schiefgelaufen. Fehler Nr. {{ error }}.", 117 | "form.block.message.invalid_message": "

Bitte überprüfen Sie diese Felder:

", 118 | "form.block.message.notify_body": "

{{ name }} hat eine Anfrage gesendet:

{{ summary }}

", 119 | "form.block.message.notify_subject": "Anfrage aus der Webseite.", 120 | "form.block.message.send_button": "Senden", 121 | "form.block.message.loading": "Hochladen ({{percent}})", 122 | "form.block.message.success_message": "Danke {{ name }}. Wir werden uns schnellst möglich bei dir melden.", 123 | "form.block.license.info.standalone": "Die Lizenz für das Kirby Form Block Suite ist noch nicht aktiviert.", 124 | "form.block.license.info.premium": "Kirby Form Block Suite ist ein Premium Feature", 125 | "form.block.license.info.link": "Bitte aktiviere deine Lizenz", 126 | "form.block.license.error.common": "Etwas ist schiefgelaufen. Versuche es später oder kontaktiere den Support.", 127 | "form.block.license.error.connection": "Keine Verbindung zum Lizenzserver. Versuche es später oder kontaktiere den Support.", 128 | "form.block.license.error.support": "Kontaktiere: {{support}}", 129 | "form.block.license.error.origin": "Der Lizenz ist nur in Kombination mit {{origin}} gültig.", 130 | "form.block.license.error.invalid": "Dieser Lizenzschlüssel ist ungültig", 131 | "form.block.license.error.blocked": "Die Lizenz wurde blockiert", 132 | "form.block.license.error.limit": "Diese Lizenz wurde bereits den maximal verfügbaren Domains zugewiesen.", 133 | "form.block.license.error.test": "Das Lizenztool ist derzeit im Test-Modus. Versuche es später nochmals.", 134 | "form.block.message.captcha_ask": "Bitte löse diese Rechenaufgabe:", 135 | "form.block.message.captcha_fail": "Das Ergebnis ist nicht korrekt.", 136 | "form.block.fromfields.captcha": "Spamschutz", 137 | "form.block.fromfields.captcha.info": "Nur einmal pro Formular verwenden!", 138 | "form.block.fromfields.captcha.fail": "Meldung bei falscher Eingabe", 139 | "form.block.fromfields.captcha.ask": "Aufgabendefinition", 140 | "form.block.options.notify_placeholder": "Name " 141 | } 142 | -------------------------------------------------------------------------------- /i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "form.block.name": "Form", 3 | "form.block.default.help": "Leave blank for default text", 4 | "form.block.inbox": "Inbox", 5 | "form.block.inbox.empty": "No emails to display", 6 | "form.block.inbox.asread": "Mark as read", 7 | "form.block.inbox.asunread": "Mark as unread", 8 | "form.block.inbox.delete": "Delete", 9 | "form.block.inbox.new": "New", 10 | "form.block.inbox.loading": "Loading…", 11 | "form.block.inbox.show": "Show requests", 12 | "form.block.inbox.failed": "Failed", 13 | "form.block.inbox.error": "Unable to load statistics", 14 | "form.block.inbox.tooltip.read": "This request has already been read", 15 | "form.block.inbox.tooltip.unread": "This request has not been read yet", 16 | "form.block.inbox.notinblock": "The mail view field can only be used in form blocks", 17 | "form.block.migrate.success": "Your form data has just been migrated. Please save the page to continue working.", 18 | "form.block.fromfields": "Form fields", 19 | "form.block.fromfields.label": "Display name", 20 | "form.block.fromfields.width": "Width", 21 | "form.block.fromfields.width1": "Full width", 22 | "form.block.fromfields.width2": "Half", 23 | "form.block.fromfields.width3": "Third", 24 | "form.block.fromfields.width4": "Quarter", 25 | "form.block.fromfields.slug": "Unique identifier", 26 | "form.block.fromfields.autofill": "Context", 27 | "form.block.fromfields.required": "Required", 28 | "form.block.fromfields.required_fail": "Message in case of missing input", 29 | "form.block.fromfields.display": "Display title", 30 | "form.block.fromfields.display.help": "Needed to display emails in the panel. You can use the unique identifier of the form fields as placeholders (e.g., {{name}})", 31 | "form.block.fromfields.input": "Text field", 32 | "form.block.fromfields.input.inputtype": "Input type", 33 | "form.block.fromfields.input.inputtype.text": "Text", 34 | "form.block.fromfields.input.inputtype.number": "Number", 35 | "form.block.fromfields.input.inputtype.email": "Email", 36 | "form.block.fromfields.input.inputtype.tel": "Phone", 37 | "form.block.fromfields.input.inputtype.url": "URL", 38 | "form.block.fromfields.input.inputtype.password": "Password", 39 | "form.block.fromfields.input.inputtype.hidden": "Hidden", 40 | "form.block.fromfields.input.placeholder": "Placeholder", 41 | "form.block.fromfields.input.default": "Default", 42 | "form.block.fromfields.input.validate": "Validation", 43 | "form.block.fromfields.input.validate.msg": "Error message", 44 | "form.block.fromfields.input.fields": "Validation type", 45 | "form.block.fromfields.input.fields.alpha": "Text only", 46 | "form.block.fromfields.input.fields.alpha.text": "To allow spaces, select match and enter as regular expression /^[a-zA-Z ]*$/.", 47 | "form.block.fromfields.input.fields.num": "Numbers only", 48 | "form.block.fromfields.input.fields.minLength": "Min. number of characters", 49 | "form.block.fromfields.input.fields.maxLength": "Max. number of characters", 50 | "form.block.fromfields.input.fields.min": "Minimum value", 51 | "form.block.fromfields.input.fields.max": "Maximum value", 52 | "form.block.fromfields.input.fields.email": "Email", 53 | "form.block.fromfields.input.fields.url": "URL", 54 | "form.block.fromfields.input.fields.match": "Regular expression", 55 | "form.block.fromfields.input.fields.match.help": "More Information", 56 | "form.block.fromfields.input.fields.msg": "Error message", 57 | "form.block.fromfields.textarea": "Textarea", 58 | "form.block.fromfields.textarea.placeholder": "Placeholder", 59 | "form.block.fromfields.textarea.row": "Number of rows", 60 | "form.block.fromfields.textarea.man": "Max. characters", 61 | "form.block.fromfields.checkbox": "Checkbox inputs", 62 | "form.block.fromfields.checkbox.columns": "Number of columns", 63 | "form.block.fromfields.checkbox.options": "Options", 64 | "form.block.fromfields.checkbox.options.label": "Display name", 65 | "form.block.fromfields.checkbox.options.slug": "Unique identifier", 66 | "form.block.fromfields.checkbox.options.selected": "Selected", 67 | "form.block.fromfields.captcha": "Spam protection", 68 | "form.block.fromfields.captcha.ask": "Task definition", 69 | "form.block.fromfields.captcha.fail": "Message in case of incorrect input", 70 | "form.block.fromfields.radio": "Radio group", 71 | "form.block.fromfields.radio.columns": "Number of columns", 72 | "form.block.fromfields.radio.default": "Selected", 73 | "form.block.fromfields.radio.default.help": "Reopen dialog if not current.", 74 | "form.block.fromfields.radio.options": "Options", 75 | "form.block.fromfields.radio.options.label": "Display name", 76 | "form.block.fromfields.radio.options.slug": "Unique identifier", 77 | "form.block.fromfields.select": "Dropdown", 78 | "form.block.fromfields.select.placeholder": "Placeholder", 79 | "form.block.fromfields.select.default": "Selected", 80 | "form.block.fromfields.select.default.help": "Reopen dialog if not current.", 81 | "form.block.fromfields.select.options": "Options", 82 | "form.block.fromfields.select.options.label": "Display name", 83 | "form.block.fromfields.select.options.slug": "Unique identifier", 84 | "form.block.fromfields.file": "Attachment", 85 | "form.block.fromfields.file.accept": "Accepted MIME type", 86 | "form.block.fromfields.file.accept.help": "Placeholders can be used (like image/* for all types of images). More Info", 87 | "form.block.fromfields.file.accept.fail": "Error message for wrong MIME types", 88 | "form.block.fromfields.file.maxsize": "Max. filesize per file", 89 | "form.block.fromfields.file.maxsize.help": "The maximum file size may be limited by the server.", 90 | "form.block.fromfields.file.maxnumber": "Max. number of files", 91 | "form.block.fromfields.file.warning.label": "Warning!", 92 | "form.block.fromfields.file.warning.text": "Be careful with executable file types (e.g., application/zip, application/msword). These can contain malware.", 93 | "form.block.options": "Options", 94 | "form.block.options.email.help": "Multiple recipients possible, separated by `;`", 95 | "form.block.options.info": "**With *{{ }}* you can insert incoming values by using identifiers.**n", 96 | "form.block.options.enable_notify": "Send notification", 97 | "form.block.options.notify_email": "Recipient's address", 98 | "form.block.options.notify_placeholder": "Name ", 99 | "form.block.options.notify_subject": "Subject", 100 | "form.block.options.notify_body": "Message", 101 | "form.block.options.enable_confirm": "Send confirmation", 102 | "form.block.options.confirm_email": "Sender's address", 103 | "form.block.options.confirm_subject": "Subject", 104 | "form.block.options.confirm_body": "Message", 105 | "form.block.options.redirect": "On success", 106 | "form.block.options.redirect.on": "Display text", 107 | "form.block.options.redirect.off": "Redirect visitor", 108 | "form.block.options.success_text": "Confirmation text", 109 | "form.block.options.success_url": "Redirect URL", 110 | "form.block.placeholdes.summary": "Summary", 111 | "form.block.message.confirm_body": "

Thank you {{name}}. We will get back to you as soon as possible.

", 112 | "form.block.message.confirm_subject": "Your request", 113 | "form.block.message.exists_message": "

The form has already been filled out.

", 114 | "form.block.message.fatal_message": "

Something went wrong. Contact the administrator or try again later.

", 115 | "form.block.message.required_fail": "This field is required.", 116 | "form.block.message.file_accept": "Only the following file types are accepted: {{accept}}.", 117 | "form.block.message.file_maxsize": "Files must not be larger than {{maxsize}}MB", 118 | "form.block.message.file_maxnumber": "No more than {{maxnumber}} files may be uploaded.", 119 | "form.block.message.file_required": "Please choose at least one file to upload.", 120 | "form.block.message.file_fatal": "Something went wrong with the upload. Error no. {{error}}.", 121 | "form.block.message.captcha_ask": "Please solve this calculation problem:", 122 | "form.block.message.captcha_fail": "The result is not correct.", 123 | "form.block.message.invalid_message": "

Please check these fields:

", 124 | "form.block.message.notify_body": "

{{name}} has sent a request:

{{summary}}

", 125 | "form.block.message.notify_subject": "Request from website.", 126 | "form.block.message.send_button": "Send", 127 | "form.block.message.loading": "Uploading ({{percent}})", 128 | "form.block.message.success_message": "

Thank you {{name}}. We will get back to you as soon as possible.

", 129 | "form.block.license.info.standalone": "The license for the Kirby Form Block Suite has not yet been activated", 130 | "form.block.license.info.premium": "Kirby Form Block Suite is a premium feature", 131 | "form.block.license.info.link": "Please activate your license", 132 | "form.block.license.error.common": "Something went wrong. \nTry later or contact support.", 133 | "form.block.license.error.connection": "No connection to the license server. Try again later or contact support.", 134 | "form.block.license.error.support": "Contact: {{support}}", 135 | "form.block.license.error.origin": "The license is only valid in combination with {{origin}}", 136 | "form.block.license.error.invalid": "This license key is invalid", 137 | "form.block.license.error.blocked": "The license has been blocked", 138 | "form.block.license.error.limit": "This license has already been assigned to the maximum available domains", 139 | "form.block.license.error.test": "The license tool is currently in test mode. Please try again later.", 140 | "form.block.fromfields.captcha.info": "Use only once per form!" 141 | } 142 | -------------------------------------------------------------------------------- /i18n/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "form.block.name": "Formulaire", 3 | "form.block.default.help": "Laissez vide pour le texte par défaut", 4 | "form.block.inbox": "Boîte de réception", 5 | "form.block.inbox.empty": "Aucun e-mail à afficher", 6 | "form.block.inbox.asread": "Marquer comme lu", 7 | "form.block.inbox.asunread": "Marquer comme non lu", 8 | "form.block.inbox.delete": "Supprimer", 9 | "form.block.inbox.new": "Nouveau", 10 | "form.block.inbox.loading": "Chargement…", 11 | "form.block.inbox.show": "Afficher les demandes", 12 | "form.block.inbox.failed": "Échec", 13 | "form.block.inbox.error": "Impossible de charger les statistiques", 14 | "form.block.inbox.tooltip.read": "Cette demande a déjà été lue", 15 | "form.block.inbox.tooltip.unread": "Cette demande n’a pas encore été lue", 16 | "form.block.inbox.notinblock": "Le champ d’affichage de mails peut uniquement être utilisé dans les blocs de formulaire", 17 | "form.block.migrate.success": "Vos données de formulaire ont été migrées avec succès. Veuillez enregistrer la page pour continuer.", 18 | "form.block.fromfields": "Champs du formulaire", 19 | "form.block.fromfields.label": "Nom d’affichage", 20 | "form.block.fromfields.width": "Largeur", 21 | "form.block.fromfields.width1": "Pleine largeur", 22 | "form.block.fromfields.width2": "Demi", 23 | "form.block.fromfields.width3": "Tiers", 24 | "form.block.fromfields.width4": "Quart", 25 | "form.block.fromfields.slug": "Identifiant unique", 26 | "form.block.fromfields.autofill": "Contexte", 27 | "form.block.fromfields.required": "Requis", 28 | "form.block.fromfields.required_fail": "Message en cas de saisie manquante", 29 | "form.block.fromfields.display": "Titre d’affichage", 30 | "form.block.fromfields.display.help": "Nécessaire pour l’affichage des e-mails dans le panneau. Vous pouvez utiliser l’identifiant unique des champs comme Texte de substitution (ex. {{name}})", 31 | "form.block.fromfields.input": "Champ de texte", 32 | "form.block.fromfields.input.inputtype": "Type d’entrée", 33 | "form.block.fromfields.input.inputtype.text": "Texte", 34 | "form.block.fromfields.input.inputtype.number": "Nombre", 35 | "form.block.fromfields.input.inputtype.email": "E-mail", 36 | "form.block.fromfields.input.inputtype.tel": "Téléphone", 37 | "form.block.fromfields.input.inputtype.url": "URL", 38 | "form.block.fromfields.input.inputtype.password": "Mot de passe", 39 | "form.block.fromfields.input.inputtype.hidden": "Caché", 40 | "form.block.fromfields.input.placeholder": "Texte de substitution", 41 | "form.block.fromfields.input.default": "Par défaut", 42 | "form.block.fromfields.input.validate": "Validation", 43 | "form.block.fromfields.input.validate.msg": "Message d’erreur", 44 | "form.block.fromfields.input.fields": "Type de validation", 45 | "form.block.fromfields.input.fields.alpha": "Texte seulement", 46 | "form.block.fromfields.input.fields.alpha.text": "Pour permettre les espaces, sélectionnez correspondance et entrez en tant qu’expression régulière /^[a-zA-Z ]*$/.", 47 | "form.block.fromfields.input.fields.num": "Chiffres seulement", 48 | "form.block.fromfields.input.fields.minLength": "Nombre min. de caractères", 49 | "form.block.fromfields.input.fields.maxLength": "Nombre max. de caractères", 50 | "form.block.fromfields.input.fields.min": "Valeur minimale", 51 | "form.block.fromfields.input.fields.max": "Valeur maximale", 52 | "form.block.fromfields.input.fields.email": "E-mail", 53 | "form.block.fromfields.input.fields.url": "URL", 54 | "form.block.fromfields.input.fields.match": "Expression régulière", 55 | "form.block.fromfields.input.fields.match.help": "Plus d’informations", 56 | "form.block.fromfields.input.fields.msg": "Message d'erreur", 57 | "form.block.fromfields.textarea": "Zone de texte", 58 | "form.block.fromfields.textarea.placeholder": "Texte de substitution", 59 | "form.block.fromfields.textarea.row": "Nombre de lignes", 60 | "form.block.fromfields.textarea.man": "Nb max. de caractères", 61 | "form.block.fromfields.checkbox": "Cases à cocher", 62 | "form.block.fromfields.checkbox.columns": "Nombre de colonnes", 63 | "form.block.fromfields.checkbox.options": "Options", 64 | "form.block.fromfields.checkbox.options.label": "Nom d’affichage", 65 | "form.block.fromfields.checkbox.options.slug": "Identifiant unique", 66 | "form.block.fromfields.checkbox.options.selected": "Sélectionné", 67 | "form.block.fromfields.radio": "Boutons radio", 68 | "form.block.fromfields.radio.columns": "Nombre de colonnes", 69 | "form.block.fromfields.radio.default": "Sélection par défaut", 70 | "form.block.fromfields.radio.default.help": "Réouvrir le dialogue si non actuel.", 71 | "form.block.fromfields.radio.options": "Options", 72 | "form.block.fromfields.radio.options.label": "Nom d’affichage", 73 | "form.block.fromfields.radio.options.slug": "Identifiant unique", 74 | "form.block.fromfields.select": "Menu déroulant", 75 | "form.block.fromfields.select.placeholder": "Texte de substitution", 76 | "form.block.fromfields.select.default": "Sélection par défaut", 77 | "form.block.fromfields.select.default.help": "Réouvrir le dialogue si non actuel.", 78 | "form.block.fromfields.select.options": "Options", 79 | "form.block.fromfields.select.options.label": "Nom d’affichage", 80 | "form.block.fromfields.select.options.slug": "Identifiant unique", 81 | "form.block.fromfields.file": "Pièce jointe", 82 | "form.block.fromfields.file.accept": "Type MIME accepté", 83 | "form.block.fromfields.file.accept.help": "Des espaces réservés peuvent être utilisés (comme image/* pour tout type d’images). Plus d’infos", 84 | "form.block.fromfields.file.accept.fail": "Message d’erreur pour les types MIME incorrects", 85 | "form.block.fromfields.file.maxsize": "Taille max. par fichier", 86 | "form.block.fromfields.file.maxsize.help": "La taille maximale du fichier peut être limitée par le serveur.", 87 | "form.block.fromfields.file.maxnumber": "Nombre max. de fichiers", 88 | "form.block.fromfields.file.warning.label": "Attention !", 89 | "form.block.fromfields.file.warning.text": "Soyez prudent avec les types de fichiers exécutables (par exemple, application/zip, application/msword). Ils peuvent contenir des logiciels malveillants.", 90 | "form.block.options": "Options d’envoi", 91 | "form.block.options.email.help": "Plusieurs destinataires possibles, séparés par `;`", 92 | "form.block.options.info": "**Avec *{{ }}* vous pouvez insérer des valeurs entrantes grâce aux identifiants.**n", 93 | "form.block.options.enable_notify": "Activer les notifications", 94 | "form.block.options.notify_email": "Adresse e-mail du destinataire", 95 | "form.block.options.notify_subject": "Objet", 96 | "form.block.options.notify_body": "Message", 97 | "form.block.options.enable_confirm": "Envoyer un e-mail de confirmation", 98 | "form.block.options.confirm_email": "Adresse e-mail de l'expéditeur", 99 | "form.block.options.confirm_subject": "Objet", 100 | "form.block.options.confirm_body": "Message", 101 | "form.block.options.redirect": "En cas de succès", 102 | "form.block.options.redirect.on": "Afficher un texte", 103 | "form.block.options.redirect.off": "Rediriger le visiteur", 104 | "form.block.options.success_text": "Texte de confirmation", 105 | "form.block.options.success_url": "URL de redirection", 106 | "form.block.placeholdes.summary": "Résumé", 107 | "form.block.message.confirm_body": "

Merci {{name}}. Nous vous contacterons dès que possible.

", 108 | "form.block.message.confirm_subject": "Votre demande", 109 | "form.block.message.exists_message": "

Le formulaire a déjà été rempli.

", 110 | "form.block.message.fatal_message": "

Un problème est survenu. Contactez l’administrateur ou réessayez plus tard.

", 111 | "form.block.message.required_fail": "Ce champ est requis.", 112 | "form.block.message.file_accept": "Seuls les types de fichiers suivants sont acceptés : {{accept}}.", 113 | "form.block.message.file_maxsize": "Les fichiers ne doivent pas dépasser {{maxsize}}Mo", 114 | "form.block.message.file_maxnumber": "Vous ne pouvez pas télécharger plus de {{maxnumber}} fichiers.", 115 | "form.block.message.file_required": "Veuillez choisir au moins un fichier à télécharger.", 116 | "form.block.message.file_fatal": "Un problème est survenu lors du téléchargement. Erreur n° {{error}}.", 117 | "form.block.message.invalid_message": "

Veuillez vérifier ces champs :

", 118 | "form.block.message.notify_body": "

{{name}} a envoyé une demande :

{{summary}}

", 119 | "form.block.message.notify_subject": "Demande reçue depuis le site web.", 120 | "form.block.message.send_button": "Envoyer", 121 | "form.block.message.loading": "Chargement ({{percent}})", 122 | "form.block.message.success_message": "

Merci {{name}}. Nous reviendrons vers vous dans les plus brefs délais.

", 123 | "form.block.license.info.standalone": "La license pour le Kirby Form Block Suite n’est pas encore activée", 124 | "form.block.license.info.premium": "Kirby Form Block Suite est une fonctionnalité premium", 125 | "form.block.license.info.link": "Veuillez activer votre license", 126 | "form.block.license.error.common": "Quelque chose s'est mal passé. \nEssayez plus tard ou contactez l'assistance.", 127 | "form.block.license.error.connection": "Aucune connexion avec le serveur de license. Essayez plus tard ou contactez le support.", 128 | "form.block.license.error.support": "Contactez : {{support}}", 129 | "form.block.license.error.origin": "La license est uniquement valide en combinaison avec {{origin}}", 130 | "form.block.license.error.invalid": "Cette clé de license est invalide", 131 | "form.block.license.error.blocked": "La license a été bloquée", 132 | "form.block.license.error.limit": "Cette license a déjà été assignée au nombre maximal de domaines disponibles", 133 | "form.block.license.error.test": "L’outil de license est actuellement en mode test. Réessayez plus tard.", 134 | "form.block.message.captcha_fail": "Le résultat n'est pas correct.", 135 | "form.block.message.captcha_ask": "Veuillez résoudre ce problème de calcul :", 136 | "form.block.fromfields.captcha": "Protection contre les spams", 137 | "form.block.fromfields.captcha.info": "À utiliser une seule fois par formulaire !", 138 | "form.block.fromfields.captcha.fail": "Message en cas de saisie incorrecte", 139 | "form.block.fromfields.captcha.ask": "Définition de la tâche", 140 | "form.block.options.notify_placeholder": "Nom " 141 | } 142 | -------------------------------------------------------------------------------- /i18n/hu.json: -------------------------------------------------------------------------------- 1 | { 2 | "form.block.name": "Űrlap", 3 | "form.block.default.help": "Az alapértelmezett szöveghez hagyja üresen", 4 | "form.block.inbox": "Beérkezett üzenetek", 5 | "form.block.inbox.empty": "Nincsenek megjeleníthető e-mailek", 6 | "form.block.inbox.asread": "Megjelölés olvasottnak", 7 | "form.block.inbox.asunread": "Megjelölés olvasatlannak", 8 | "form.block.inbox.delete": "Törlés", 9 | "form.block.inbox.new": "Új", 10 | "form.block.inbox.loading": "Betöltés...", 11 | "form.block.inbox.show": "Kérések megjelenítése", 12 | "form.block.inbox.failed": "sikertelen", 13 | "form.block.inbox.error": "Rekordok nem elérhetőek", 14 | "form.block.inbox.tooltip.read": "Ezt a kérést már elolvasták", 15 | "form.block.inbox.tooltip.unread": "Ezt a kérést még nem olvasták el", 16 | "form.block.inbox.notinblock": "A levélnézet mező csak űrlapblokkokban használható", 17 | "form.block.migrate.success": "Az űrlap adatai most kerültek átvitelre. Kérjük, mentse el az oldalt a munka folytatásához.", 18 | "form.block.fromfields": "Űrlapmezők", 19 | "form.block.fromfields.label": "Megjelenítendő név", 20 | "form.block.fromfields.width": "Szélesség", 21 | "form.block.fromfields.width1": "Szélesség - teljes", 22 | "form.block.fromfields.width2": "Szélesség - felez", 23 | "form.block.fromfields.width3": "Szélesség - kisebb mint fél", 24 | "form.block.fromfields.width4": "Szélesség - negyed", 25 | "form.block.fromfields.slug": "Egyedi azonosító", 26 | "form.block.fromfields.autofill": "Kontextus", 27 | "form.block.fromfields.required": "Kötelező", 28 | "form.block.fromfields.required_fail": "Üzenet hiányzó bevitel esetén", 29 | "form.block.fromfields.display": "Cím megjelenítése", 30 | "form.block.fromfields.display.help": "Needed to display the emails in the panel. You can use the unique identifier of the fields as placeholderAz e-mailek panelen való megjelenítéséhez szükséges. Helyőrzőként használhatja a mezők egyedi azonosítóját (pl.: {{name}})", 31 | "form.block.fromfields.input": "Szövegmező", 32 | "form.block.fromfields.input.inputtype": "Mező típusa", 33 | "form.block.fromfields.input.inputtype.text": "Szöveg", 34 | "form.block.fromfields.input.inputtype.number": "Szám", 35 | "form.block.fromfields.input.inputtype.email": "E-mail", 36 | "form.block.fromfields.input.inputtype.tel": "Telefonszám", 37 | "form.block.fromfields.input.inputtype.url": "Weboldal", 38 | "form.block.fromfields.input.inputtype.password": "Jelszó", 39 | "form.block.fromfields.input.inputtype.hidden": "Rejtett", 40 | "form.block.fromfields.input.placeholder": "Helykitöltő", 41 | "form.block.fromfields.input.default": "Alapértelmezett", 42 | "form.block.fromfields.input.validate": "Validáció", 43 | "form.block.fromfields.input.validate.msg": "Hiba üzenet", 44 | "form.block.fromfields.input.fields": "Validáció típusa", 45 | "form.block.fromfields.input.fields.alpha": "Csak szöveg", 46 | "form.block.fromfields.input.fields.alpha.text": "Ha szóközt szeretne engedélyezni. Válassza az match lehetőséget, és írja be a reguláris kifejezést /^[a-zA-Z ]*$/.", 47 | "form.block.fromfields.input.fields.num": "Csak számok", 48 | "form.block.fromfields.input.fields.minLength": "Min. karakterek száma", 49 | "form.block.fromfields.input.fields.maxLength": "Max. karakterek száma", 50 | "form.block.fromfields.input.fields.min": "Minimum érték", 51 | "form.block.fromfields.input.fields.max": "Maximum érték", 52 | "form.block.fromfields.input.fields.email": "E-mail", 53 | "form.block.fromfields.input.fields.url": "Weboldal", 54 | "form.block.fromfields.input.fields.match": "Reguláris kifejezés", 55 | "form.block.fromfields.input.fields.match.help": "További információ", 56 | "form.block.fromfields.input.fields.msg": "Hiba üzenet", 57 | "form.block.fromfields.textarea": "Szövegmező - Hosszú", 58 | "form.block.fromfields.textarea.placeholder": "Helykitöltő", 59 | "form.block.fromfields.textarea.row": "Sorok száma", 60 | "form.block.fromfields.textarea.man": "Max. karakterek száma", 61 | "form.block.fromfields.checkbox": "Többszörös kiválasztás", 62 | "form.block.fromfields.checkbox.columns": "Oszlopok száma", 63 | "form.block.fromfields.checkbox.options": "Kiválasztás", 64 | "form.block.fromfields.checkbox.options.label": "Megjelenítendő név", 65 | "form.block.fromfields.checkbox.options.slug": "Egyedi azonosító", 66 | "form.block.fromfields.checkbox.options.selected": "Kiválasztott", 67 | "form.block.fromfields.radio": "Kiválasztás", 68 | "form.block.fromfields.radio.columns": "Oszlopok száma", 69 | "form.block.fromfields.radio.default": "Kiválasztott", 70 | "form.block.fromfields.radio.default.help": "Reopen dialog if not current.", 71 | "form.block.fromfields.radio.options": "Kiválasztás", 72 | "form.block.fromfields.radio.options.label": "Megjelenítendő név", 73 | "form.block.fromfields.radio.options.slug": "Egyedi azonosító", 74 | "form.block.fromfields.select": "Legördülő", 75 | "form.block.fromfields.select.placeholder": "Helykitöltő", 76 | "form.block.fromfields.select.default": "Kiválasztott", 77 | "form.block.fromfields.select.default.help": "Reopen dialog if not current.", 78 | "form.block.fromfields.select.options": "Kiválasztás", 79 | "form.block.fromfields.select.options.label": "Megjelenítendő név", 80 | "form.block.fromfields.select.options.slug": "Egyedi azonosító", 81 | "form.block.fromfields.file": "Csatolmány", 82 | "form.block.fromfields.file.accept": "Elfogadott MIME típus", 83 | "form.block.fromfields.file.accept.help": "Helyőrzőt is használhat (mint az image/* bármilyen típusú képhez). További Info", 84 | "form.block.fromfields.file.accept.fail": "Hibaüzenet rossz MIME-típusokhoz", 85 | "form.block.fromfields.file.maxsize": "Max. fájl méret/fájl", 86 | "form.block.fromfields.file.maxsize.help": "A maximális fájlméretet a szerver korlátozhatja.", 87 | "form.block.fromfields.file.maxnumber": "Max. fájlok száma", 88 | "form.block.fromfields.file.warning.label": "Figyelem!", 89 | "form.block.fromfields.file.warning.text": "Legyen óvatos a futtatható fájltípusokkal (például. alkalmazás/zip, alkalmazás/msword). Ezek rosszindulatú programokat tartalmazhatnak.", 90 | "form.block.options": "Beállítások", 91 | "form.block.options.email.help": "Több címzett is lehetséges. \";\" karakterrel elválasztva", 92 | "form.block.options.info": "**A *{{ }}* segítségével beszúrhat bejövő értékeket azonosítók segítségével.**n", 93 | "form.block.options.enable_notify": "Értesítés küldése", 94 | "form.block.options.notify_email": "E-mail", 95 | "form.block.options.notify_subject": "Cím", 96 | "form.block.options.notify_body": "Üzenet", 97 | "form.block.options.enable_confirm": "Visszaigazolás küldése", 98 | "form.block.options.confirm_email": "E-mail", 99 | "form.block.options.confirm_subject": "Cím", 100 | "form.block.options.confirm_body": "Üzenet", 101 | "form.block.options.redirect": "Átirányítás", 102 | "form.block.options.redirect.on": "Aktív", 103 | "form.block.options.redirect.off": "Inaktív", 104 | "form.block.options.success_text": "Sikeres végrehajtás - Üzenet", 105 | "form.block.options.success_url": "Sikeres végrehajtás - URL", 106 | "form.block.placeholdes.summary": "Összefoglaló", 107 | "form.block.message.confirm_body": "

Köszönjük kérését, a lehető leghamarabb keresni fogunk.

", 108 | "form.block.message.confirm_subject": "Kérés", 109 | "form.block.message.exists_message": "

Az űrlapot már kitöltötték.

", 110 | "form.block.message.fatal_message": "

Hiba történt.
Vegye fel a kapcsolatot a rendszergazdával, vagy próbálja újra később.

", 111 | "form.block.message.required_fail": "Ez a mező kötelező.", 112 | "form.block.message.file_accept": "Csak a következő fájltípusok fogadhatók el: {{accept}}.", 113 | "form.block.message.file_maxsize": "A fájl(ok) nem lehetnek nagyobbak, mint {{maxsize}}MB", 114 | "form.block.message.file_maxnumber": "Nem több, mint {{maxnumber}} lehet feltölteni.", 115 | "form.block.message.file_required": "Válasszon legalább egy feltöltendő fájlt.", 116 | "form.block.message.file_fatal": "Valami hiba történt a feltöltéssel. Hiba száma: {{error}}.", 117 | "form.block.message.invalid_message": "

Kérjük, ellenőrizze ezeket a mezőket:

", 118 | "form.block.message.notify_body": "

{{name}} kérést küldeni:

{{summary}}

", 119 | "form.block.message.notify_subject": "Kérés a weboldalról.", 120 | "form.block.message.send_button": "Küldés", 121 | "form.block.message.loading": "Feltöltés ({{percent}})", 122 | "form.block.message.success_message": "

Köszönjük kérését, a lehető leghamarabb keresni fogunk.

", 123 | "form.block.license.info.standalone": "A Kirby Form Block Suite licencét még nem aktiválták", 124 | "form.block.license.info.premium": "A Kirby Form Block Suite egy prémium szolgáltatás", 125 | "form.block.license.info.link": "Kérjük, aktiválja licencét", 126 | "form.block.license.error.common": "Valami elromlott. \nPróbálkozzon később, vagy lépjen kapcsolatba az ügyfélszolgálattal.", 127 | "form.block.license.error.connection": "Nincs kapcsolat a licenckiszolgálóval. Próbálja meg később, vagy lépjen kapcsolatba az ügyfélszolgálattal", 128 | "form.block.license.error.support": "Kapcsolat: {{support}}", 129 | "form.block.license.error.origin": "A licenc csak a {{eredet}}-vel együtt érvényes", 130 | "form.block.license.error.invalid": "Ez a licenckulcs érvénytelen", 131 | "form.block.license.error.blocked": "A licenc letiltásra került", 132 | "form.block.license.error.limit": "Ezt a licenset már hozzárendelték a maximálisan elérhető domainekhez", 133 | "form.block.license.error.test": "A licenc eszköz jelenleg teszt üzemmódban van. Kérjük, próbálja meg később újra.", 134 | "form.block.message.captcha_fail": "Az eredmény nem megfelelő.", 135 | "form.block.message.captcha_ask": "Kérjük, oldja meg ezt a számítási feladatot:", 136 | "form.block.fromfields.captcha": "Spam-védelem", 137 | "form.block.fromfields.captcha.info": "Űrlaponként csak egyszer használja!", 138 | "form.block.fromfields.captcha.fail": "Üzenet hibás bevitel esetén", 139 | "form.block.fromfields.captcha.ask": "Feladat meghatározása", 140 | "form.block.options.notify_placeholder": "Név " 141 | } 142 | -------------------------------------------------------------------------------- /i18n/lt.json: -------------------------------------------------------------------------------- 1 | { 2 | "form.block.name": "Forma", 3 | "form.block.default.help": "Leave blank for default text", 4 | "form.block.inbox": "Užpildymai", 5 | "form.block.inbox.empty": "Nėra laiškų", 6 | "form.block.inbox.asread": "Pažymėti kaip perskaitytą", 7 | "form.block.inbox.asunread": "Pažymėti kaip neperskaitytą", 8 | "form.block.inbox.delete": "Ištrinti", 9 | "form.block.inbox.new": "Naujas", 10 | "form.block.inbox.loading": "Kraunasi...", 11 | "form.block.inbox.show": "Parodyti užpildymus", 12 | "form.block.inbox.failed": "nepavyko", 13 | "form.block.inbox.error": "Nepavyko gauti statistikos", 14 | "form.block.inbox.tooltip.read": "Šis užpildymas jau perskaitytas", 15 | "form.block.inbox.tooltip.unread": "Šis užpildymas dar neperskaitytas", 16 | "form.block.inbox.notinblock": "The mail view field can only used in form blocks", 17 | "form.block.migrate.success": "Jūsų formos duomenys ką tik perkelti. Norėdami tęsti darbą, išsaugokite puslapį.", 18 | "form.block.fromfields": "Formos laukeliai", 19 | "form.block.fromfields.label": "Rodomas pavadinimas", 20 | "form.block.fromfields.width": "Plotis", 21 | "form.block.fromfields.width1": "Per visą plotį", 22 | "form.block.fromfields.width2": "Pusė", 23 | "form.block.fromfields.width3": "Trečdalis", 24 | "form.block.fromfields.width4": "Ketvirtadalis", 25 | "form.block.fromfields.slug": "Unikalus ID", 26 | "form.block.fromfields.autofill": "Kontekstas", 27 | "form.block.fromfields.required": "Būtinas", 28 | "form.block.fromfields.required_fail": "Pranešimas, jei trūksta įvesties", 29 | "form.block.fromfields.display": "Rodomas pavadinimas", 30 | "form.block.fromfields.display.help": "Needed to display the emails in the panel. You can use the unique identifier of the fields as placeholder (e.g. {{name}})", 31 | "form.block.fromfields.input": "Textfield", 32 | "form.block.fromfields.input.inputtype": "Input type", 33 | "form.block.fromfields.input.inputtype.text": "Tekstas", 34 | "form.block.fromfields.input.inputtype.number": "Numeris", 35 | "form.block.fromfields.input.inputtype.email": "El. paštas", 36 | "form.block.fromfields.input.inputtype.tel": "Telefonas", 37 | "form.block.fromfields.input.inputtype.url": "Svetainė", 38 | "form.block.fromfields.input.inputtype.password": "Slaptažodis", 39 | "form.block.fromfields.input.inputtype.hidden": "Paslėptas", 40 | "form.block.fromfields.input.placeholder": "Placeholder", 41 | "form.block.fromfields.input.default": "Default", 42 | "form.block.fromfields.input.validate": "Validation", 43 | "form.block.fromfields.input.validate.msg": "Error message", 44 | "form.block.fromfields.input.fields": "Validation type", 45 | "form.block.fromfields.input.fields.alpha": "Text only", 46 | "form.block.fromfields.input.fields.alpha.text": "If you want to allow spaces. Select match and enter as regular expression /^[a-zA-Z ]*$/.", 47 | "form.block.fromfields.input.fields.num": "Numbers only", 48 | "form.block.fromfields.input.fields.minLength": "Min. simbolių kiekis", 49 | "form.block.fromfields.input.fields.maxLength": "Maks. simbolių kiekis", 50 | "form.block.fromfields.input.fields.min": "Minimum value", 51 | "form.block.fromfields.input.fields.max": "Maximum value", 52 | "form.block.fromfields.input.fields.email": "Email", 53 | "form.block.fromfields.input.fields.url": "Website", 54 | "form.block.fromfields.input.fields.match": "Regular expression", 55 | "form.block.fromfields.input.fields.match.help": "More Information", 56 | "form.block.fromfields.input.fields.msg": "Error message", 57 | "form.block.fromfields.textarea": "Textarea", 58 | "form.block.fromfields.textarea.placeholder": "Placeholder", 59 | "form.block.fromfields.textarea.row": "Number of rows", 60 | "form.block.fromfields.textarea.man": "Max. Characters", 61 | "form.block.fromfields.checkbox": "Multiple selection", 62 | "form.block.fromfields.checkbox.columns": "Number of columns", 63 | "form.block.fromfields.checkbox.options": "Selection", 64 | "form.block.fromfields.checkbox.options.label": "Display name", 65 | "form.block.fromfields.checkbox.options.slug": "Unique identifier", 66 | "form.block.fromfields.checkbox.options.selected": "Selected", 67 | "form.block.fromfields.radio": "Selection", 68 | "form.block.fromfields.radio.columns": "Number of columns", 69 | "form.block.fromfields.radio.default": "Selected", 70 | "form.block.fromfields.radio.default.help": "Reopen dialog if not current.", 71 | "form.block.fromfields.radio.options": "Selection", 72 | "form.block.fromfields.radio.options.label": "Display name", 73 | "form.block.fromfields.radio.options.slug": "Unique identifier", 74 | "form.block.fromfields.select": "Dropdown", 75 | "form.block.fromfields.select.placeholder": "Placeholder", 76 | "form.block.fromfields.select.default": "Selected", 77 | "form.block.fromfields.select.default.help": "Reopen dialog if not current.", 78 | "form.block.fromfields.select.options": "Selection", 79 | "form.block.fromfields.select.options.label": "Display name", 80 | "form.block.fromfields.select.options.slug": "Unique identifier", 81 | "form.block.fromfields.file": "Attachment", 82 | "form.block.fromfields.file.accept": "Accepted MIME type", 83 | "form.block.fromfields.file.accept.help": "You can also use placeholder (like image/* for any kind of images). More Info", 84 | "form.block.fromfields.file.accept.fail": "Error message for wrong MIME types", 85 | "form.block.fromfields.file.maxsize": "Max. filesize/file", 86 | "form.block.fromfields.file.maxsize.help": "The maximum file size can be limited by the server.", 87 | "form.block.fromfields.file.maxnumber": "Max. number of files", 88 | "form.block.fromfields.file.warning.label": "Warning!", 89 | "form.block.fromfields.file.warning.text": "Be careful with executable file types (e.g. application/zip, application/msword). These may contain malware.", 90 | "form.block.options": "Options", 91 | "form.block.options.email.help": "Multiple recipients possible. Separated with `;`", 92 | "form.block.options.enable_notify": "Send notification", 93 | "form.block.options.notify_email": "Recipient address", 94 | "form.block.options.notify_subject": "Subject", 95 | "form.block.options.notify_body": "Message", 96 | "form.block.options.enable_confirm": "Send confirmation", 97 | "form.block.options.confirm_email": "From address", 98 | "form.block.options.confirm_subject": "Subject", 99 | "form.block.options.confirm_body": "Message", 100 | "form.block.options.redirect": "On success", 101 | "form.block.options.redirect.on": "Show text", 102 | "form.block.options.redirect.off": "Redirect visitor", 103 | "form.block.options.success_text": "Confirmation text", 104 | "form.block.options.success_url": "Redirect", 105 | "form.block.placeholdes.summary": "Summary", 106 | "form.block.message.confirm_body": "

Thank you for your request, we will get back to you as soon as possible.

", 107 | "form.block.message.confirm_subject": "Your request", 108 | "form.block.message.exists_message": "

The form has already been filled in.

", 109 | "form.block.message.fatal_message": "

Something went wrong.
Contact the administrator or try again later.

", 110 | "form.block.message.required_fail": "This field is required.", 111 | "form.block.message.file_accept": "Only following file types are accepted: {{accept}}.", 112 | "form.block.message.file_maxsize": "File(s) must not be larger than {{ maxsize }}MB", 113 | "form.block.message.file_maxnumber": "No more than {{maxnumber}} may be uploaded.", 114 | "form.block.message.file_required": "Choose at least one File to upload.", 115 | "form.block.message.file_fatal": "Something went wrong with the upload. Error no. {{ error }}.", 116 | "form.block.message.invalid_message": "

Please check these fields:

", 117 | "form.block.message.notify_body": "

{{ name }} send a request:

{{ summary }}

", 118 | "form.block.message.notify_subject": "Request from website.", 119 | "form.block.message.send_button": "Siųsti", 120 | "form.block.message.loading": "Uploading ({{percent}})", 121 | "form.block.message.success_message": "

Thank you {{ name }}. We will get back to you as soon as possible.

", 122 | "form.block.license.info.standalone": "Kirby Form Block Suite licencija dar nėra aktyvuota", 123 | "form.block.license.info.link": "Prašome aktyvuoti jūsų licenciją", 124 | "form.block.license.info.premium": "Kirby Form Block Suite is a premium feature", 125 | "form.block.license.error.common": "Kažkas nutiko ne taip. Pabandykite vėliau arba kreipkitės į palaikymo tarnybą.", 126 | "form.block.license.error.connection": "Nėra ryšio su licencijų serveriu. Pabandykite vėliau arba kreipkitės į palaikymo tarnybą", 127 | "form.block.license.error.support": "Susisiekite su: {{support}}", 128 | "form.block.license.error.origin": "Licencija galioja tik kartu su {{origin}}", 129 | "form.block.license.error.invalid": "Šis licencijos raktas negalioja", 130 | "form.block.license.error.blocked": "Licencija užblokuota", 131 | "form.block.license.error.limit": "Ši licencija jau priskirta maksimaliam galimų domenų skaičiui", 132 | "form.block.license.error.test": "Šiuo metu licencijos įrankis veikia bandomuoju režimu. Prašome pabandyti dar kartą vėliau.", 133 | "form.block.options.info": "**Naudodami *{{ }}* galite įterpti gaunamas reikšmes naudodami identifikatorius.**n", 134 | "form.block.message.captcha_fail": "Rezultatas neteisingas.", 135 | "form.block.message.captcha_ask": "Išspręskite šią skaičiavimo problemą:", 136 | "form.block.fromfields.captcha": "Apsauga nuo šiukšlių", 137 | "form.block.fromfields.captcha.info": "Naudokite tik vieną kartą vienoje formoje!", 138 | "form.block.fromfields.captcha.fail": "Pranešimas neteisingai įvedus", 139 | "form.block.fromfields.captcha.ask": "Užduoties apibrėžimas", 140 | "form.block.options.notify_placeholder": "Vardas " 141 | } 142 | -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | .k-field-type-page-dialog-table{width:100%;background:var(--item-color-back, white);padding:var(--spacing-3)}.k-field-type-page-dialog-table td,.k-field-type-page-dialog-table th{vertical-align:top;padding:var(--spacing-2);overflow:hidden;text-overflow:ellipsis}.k-field-type-page-dialog-table .field_summary{display:none}.k-field-type-page-dialog-table .k-field-type-page-change-display{padding-top:3px}.k-field-type-page-dialog-table .k-field-type-page-dialog-link{display:flex;font-size:.9em;line-height:1.75em}.k-field-type-page-dialog-table .k-field-type-page-dialog-link span.k-icon{--size: .99em;margin-right:6px}.k-mailview-list-header{position:relative}.k-mailview-list-header>.k-button{position:absolute;top:50%;transform:translateY(-50%);right:var(--spacing-1)}.k-plain-license{cursor:pointer;display:flex;justify-content:end;padding:7px 0;color:var(--color-pink-500);pointer-events:none}.k-plain-license>.k-icon{width:25px;height:25px;pointer-events:all}.k-plain-license>.k-text{padding-right:7px;line-height:1;text-align:right;pointer-events:all} 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | (function(){"use strict";var g=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",[n("k-grid",{staticStyle:{gap:"0.25rem","--columns":"12"}},[n("k-input",t._b({staticStyle:{"--width":"1/3"},attrs:{type:"text"},on:{input:t.onInput},model:{value:t.content.name,callback:function(i){t.$set(t.content,"name",i)},expression:"content.name"}},"k-input",t.field("name"),!1)),t.loading?n("k-box",{staticStyle:{"--width":"2/3"},attrs:{theme:"info",icon:"loader",text:t.$t("form.block.inbox.loading")}}):n("k-box",{staticStyle:{"--width":"2/3"},attrs:{icon:"email",theme:t.status.theme,text:t.$t("form.block.inbox.show")+" ("+t.status.text+")"},nativeOn:{click:function(i){return t.open.apply(null,arguments)}}})],1)],1)},$=[];function d(t,e,n,i,a,r,_,W){var s=typeof t=="function"?t.options:t;e&&(s.render=e,s.staticRenderFns=n,s._compiled=!0),i&&(s.functional=!0),r&&(s._scopeId="data-v-"+r);var o;if(_?(o=function(l){l=l||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,!l&&typeof __VUE_SSR_CONTEXT__!="undefined"&&(l=__VUE_SSR_CONTEXT__),a&&a.call(this,l),l&&l._registeredComponents&&l._registeredComponents.add(_)},s._ssrRegister=o):a&&(o=W?function(){a.call(this,(s.functional?this.parent:this).$root.$options.shadowRoot)}:a),o)if(s.functional){s._injectStyles=o;var G=s.render;s.render=function(K,v){return o.call(v),G(K,v)}}else{var m=s.beforeCreate;s.beforeCreate=m?[].concat(m,o):[o]}return{exports:t,options:s}}const y={data(){return{migrate:!1,loading:!0,status:{type:Object,default:{count:"-",read:"-",fail:"-",state:"wait"}}}},destroyed(){window.panel.events.off("form.update",this.updateCount)},created(){window.panel.events.on("content/STATUS",function(t){t.type=="content/STATUS"&&window.panel.events.emit("form.update")}),this.content.formid=this.id,this.updateCount(),window.panel.events.on("form.update",this.updateCount)},methods:{updateCount(){const t=this;this.$api.get("formblock",{action:"info",form_id:this.id,params:JSON.stringify({form_name:this.content.name})}).then(e=>{t.status=e,this.loading=!1})},onInput(t){this.$emit("update",t)}}},u={};var k=d(y,g,$,!1,b,null,null,null);function b(t){for(let e in u)this[e]=u[e]}var w=function(){return k.exports}(),x=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",t._b({ref:"dialog",staticClass:"k-field-type-page-dialog",on:{cancel:function(i){return t.$emit("cancel")},submit:function(i){return t.$emit("submit")}}},"k-dialog",t.$props,!1),[n("k-headline",[t._v(t._s(t.current.title))]),t.current.formfields?n("div",[n("table",{staticClass:"k-field-type-page-dialog-table"},t._l(t.current.formfields,function(i,a){return n("tr",{key:a,class:"field_"+a},[n("td",[t._v(t._s(i))]),t.current.attachment[a]?n("td",[n("ul",{staticClass:"k-field-type-page-dialog-linklist"},t._l(t.current.attachment[a],function(r){return n("li",{key:r.tmp_name},[n("a",{staticClass:"k-field-type-page-dialog-link",attrs:{href:r.location,download:r.name}},[n("k-icon",{attrs:{type:"attachment"}}),t._v(" "+t._s(r.name)+" ")],1)])}),0)]):n("td",[t._v(" "+t._s(t.current.formdata[a])+" ")])])}),0)]):n("div",{staticClass:"k-field-type-page-dialog-table"},[t._v(" "+t._s(t.current.formdata.summary)+" ")]),t.current.error?n("k-box",{attrs:{text:t.current.error,theme:"negative"}}):t._e()],1)},S=[],Q="";const O={extends:"k-dialog",props:{current:{type:Object,default(){}}}},c={};var C=d(O,x,S,!1,M,null,null,null);function M(t){for(let e in c)this[e]=c[e]}var R=function(){return C.exports}(),D=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-mailview-list"},[t.hideheader?t._e():n("div",{staticClass:"k-mailview-list-header"},[n("k-button",{attrs:{icon:"download",variant:"filled",link:t.value.header.download,theme:t.value.header.state.theme,download:!0}}),n("k-box",{attrs:{theme:t.value.header.state.theme,icon:t.isOpen?"angle-up":"angle-down",text:t.headerText},nativeOn:{click:function(i){return t.toggleOpen()}}})],1),t.isOpen||t.hideheader?n("k-items",{attrs:{items:t.items}}):t._e()],1)},T=[],Z="";const Y={props:{value:{type:Array,required:!0},showuuid:Boolean,hideheader:Boolean},data(){return{isOpen:!1}},computed:{items(){return this.value.content.length===0?[{text:this.$t("form.block.inbox.empty"),theme:"disabled"}]:this.value.content},headerText(){return this.showuuid?this.value.header.name+" ("+this.value.uuid+")":this.value.header.name}},created(){this.isOpen=sessionStorage.getItem(`plain.form.showOpen.${this.value.page}.${this.value.uuid}`)==="on"},methods:{toggleOpen(){this.isOpen=!this.isOpen,sessionStorage.setItem(`plain.form.showOpen.${this.value.page}.${this.value.uuid}`,this.isOpen?"on":"off")}}},f={};var B=d(Y,D,T,!1,j,null,null,null);function j(t){for(let e in f)this[e]=f[e]}var L=function(){return B.exports}(),F=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-field-type-mail-view"},[t.loading?n("k-box",{attrs:{theme:"info",icon:"loader",text:t.$t("form.block.inbox.loading")}}):n("k-grid",{style:{gap:"var(--spacing-2)"},attrs:{variant:"fields"}},[t._l(t.data,function(i){return n("k-mail-list",{key:i.slug,staticClass:"k-table k-field-type-mail-table",staticStyle:{"--width":"1/1"},attrs:{hideheader:t.hideheader,value:i,showuuid:t.isUnique(i)},on:{setRead:t.setRead,deleteMail:t.deleteMail}})}),t.data.length===0?n("k-box",{staticStyle:{"--width":"1/1"},attrs:{theme:"info",text:t.$t("form.block.inbox.empty")}}):t._e()],2),n("k-plain-license",{attrs:{prefix:"formblock"}})],1)},N=[];const E={props:{value:{type:String,default:""},dateformat:{type:String,default:"DD.MM.YYYY HH:mm"},forms:{type:Array,default:()=>[]},formData:{type:Object,default:()=>{}}},data(){return{data:[],filter:[],loading:!0,hideheader:!1}},computed:{thispage(){return this.$attrs.endpoints.model.replace("/pages/","").replace(/\+/g,"/")}},created(){this.formData.formid?(this.filter=[this.formData.formid],this.hideheader=!0):this.filter=this.forms,this.updateList(),window.panel.events.on("form.update",this.updateList)},destroyed(){window.panel.events.off("form.update",this.updateList)},methods:{send(t,e,n){var i,a;this.$api.get("formblock",{action:t,page_id:this.thispage,request_id:(i=e==null?void 0:e.request)!=null?i:"",form_id:(a=e==null?void 0:e.form)!=null?a:"",params:JSON.stringify(e)}).then(r=>{this.loading=!1,n(r)})},isUnique(t){return this.data.filter(e=>t.header.page===e.header.page&&t.header.name===e.header.name).length>1},updateList(){let t=this;this.send("requestsArray",{filter:this.filter},e=>{this.data=Object.keys(e).map(function(n){return e[n].content=e[n].content.map(i=>{i.formid=n,i.attachment="attachment"in i?JSON.parse(i.attachment):!1,i.formdata=JSON.parse(i.formdata),i.formfields="formfields"in i?JSON.parse(i.formfields):!1;let a=t.$library.dayjs(i.received,"YYYY-MM-DD HH:mm:ss");return i.info=a.isValid()?a.format(t.dateformat):"",i.text=t.getLabel(i),i.image=t.getImage(i),i.buttons=[t.getButton("info",i)],i.options=[i.read===""?t.getButton("unread",i):t.getButton("read",i),t.getButton("delete",i)],i}),e[n]})})},setRead(t,e){this.send("update",{form:e.formid,request:e.slug,read:t==!1?"":this.$library.dayjs().format("YYYY-MM-DD HH:mm:ss")},()=>{window.panel.events.emit("form.update"),this.$panel.dialog.close()})},getLabel(t){return t.display?t.display:this.value?this.$helper.string.template(this.value,t.formdata):t.id},getButton(t,e){return t==="delete"?{icon:"trash",text:this.$t("form.block.inbox.delete"),click:()=>this.send("delete",{form:e.formid,request:e.slug},()=>{window.panel.events.emit("form.update")})}:t==="unread"?{icon:"preview",text:this.$t("form.block.inbox.asread"),click:()=>this.setRead(!0,e)}:t==="read"?{icon:"hidden",text:this.$t("form.block.inbox.asunread"),click:()=>this.setRead(!1,e)}:{icon:"info",click:()=>this.$panel.dialog.open({component:"k-mail-dialog",props:{current:e,size:"medium",submitButton:e.read?{}:this.getButton("unread",e),cancelButton:e.read?this.getButton("read",e):{}}})}},getImage(t){return t.read?{icon:"circle",color:"yellow",back:"transparent"}:t.error?{icon:"cancel",color:"red",back:"transparent"}:{icon:"circle-filled",color:"green",back:"transparent"}}}},h={};var H=d(E,F,N,!1,I,null,null,null);function I(t){for(let e in h)this[e]=h[e]}var U=function(){return H.exports}(),A=function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.license!==null?n("div",{staticClass:"k-plain-license",style:t.containerStyle,on:{click:t.showDialog}},[n("k-text",{style:t.textStyle,attrs:{size:"tiny",html:t.licenseText}}),n("k-icon",{style:t.iconStyle,attrs:{type:"alert"}})],1):t._e()},J=[],q="";const V={props:{prefix:{type:String,default(){return null}},styling:{type:Object,default(){return{}}}},data(){return{license:null}},computed:{licenseText(){return this.license?`${this.license.title}
${this.license.cta}`:""},containerStyle(){return this.styling&&this.styling.container?this.styling.container:this.styling},textStyle(){var t,e;return(e=(t=this.styling)==null?void 0:t.text)!=null?e:{}},iconStyle(){var t,e;return(e=(t=this.styling)==null?void 0:t.icon)!=null?e:{}}},created(){this.license=window.panel.translation.data&&window.panel.translation.data["plain.licenses."+this.prefix]?window.panel.translation.data["plain.licenses."+this.prefix]:null,window.panel.translation.data["plain.licenses."+this.prefix]=null},methods:{showDialog(){this.license&&this.license.dialog&&this.$dialog(this.license.dialog)}}},p={};var z=d(V,A,J,!1,P,null,null,null);function P(t){for(let e in p)this[e]=p[e]}var X=function(){return z.exports}();window.panel.plugin("plain/formblock",{fields:{mailview:U},components:{"k-mail-list":L,"k-mail-dialog":R,"k-plain-license":X},blocks:{form:w}})})(); 2 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 17 | 18 | showForm()): ?> 19 | 20 |
21 | 22 | template('fields') ?> 23 | 24 | template('form_error') ?> 25 | 26 | template('hidden') ?> 27 | template('submit') ?> 28 | 29 |
30 | 31 | template('script') ?> 32 | 33 | 34 | 35 | template('validation') ?> 36 | 37 | -------------------------------------------------------------------------------- /snippets/blocks/formcore/hidden.php: -------------------------------------------------------------------------------- 1 | honeypotId(); ?> 2 | 3 | 16 | 17 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /snippets/blocks/formcore/script.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /snippets/blocks/formcore/styles.php: -------------------------------------------------------------------------------- 1 | url('media') . '/plugins/plain/formblock/formblock.css') ?> -------------------------------------------------------------------------------- /snippets/blocks/formcore/validation.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | isValid()) 11 | $state = "invalid"; 12 | 13 | if ($form->isSuccess()) 14 | $state = "success"; 15 | 16 | $fields = []; 17 | 18 | $toValidate = get('field_validation') ? [$form->form_field(get('field_validation'))] : $form->fields(); 19 | 20 | foreach ($toValidate as $field) { 21 | 22 | array_push($fields, [ 23 | 'slug' => $field->slug()->toString(), 24 | 'is_valid' => $field->isValid(), 25 | 'message' => $form->template('field_error', ['field' => $field], $field->isValid()) 26 | ]); 27 | } 28 | 29 | echo json_encode([ 30 | 'state' => $state , 31 | 'error_message' => $form->template('form_error', [], (!$form->isFatal() and $form->isValid())), 32 | 'success_message' => $form->template('form_success', [], !$form->isSuccess()), 33 | 'redirect' => ($form->redirect()->isTrue() && $form->isSuccess()) ? $form->success_url()->toPage()->url() : "", 34 | 'fields' => $fields 35 | ]); 36 | 37 | ?> 38 | 39 | 40 | 41 | template('form_error', [], false) ?> 42 | 43 | 44 | -------------------------------------------------------------------------------- /snippets/blocks/formfields/captcha.php: -------------------------------------------------------------------------------- 1 | ask()->or($formfield->message('captcha_ask')); 8 | 9 | ?> 10 | 11 |

12 | 13 | "> 14 | 15 | required('attr') ?> 25 | ariaAttr() ?> 26 | /> 27 | -------------------------------------------------------------------------------- /snippets/blocks/formfields/checkbox.php: -------------------------------------------------------------------------------- 1 | options() as $option) : ?> 2 | 3 |
4 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /snippets/blocks/formfields/file.php: -------------------------------------------------------------------------------- 1 | 2 | maxnumber()->value() > 1; ?> 3 | autofill(true) ?> 11 | required('attr') ?> 12 | ariaAttr() ?> 13 | 14 | 15 | /> 16 | -------------------------------------------------------------------------------- /snippets/blocks/formfields/input.php: -------------------------------------------------------------------------------- 1 | 2 | autofill(true) ?> 11 | required('attr') ?> 12 | ariaAttr() ?> 13 | /> 14 | -------------------------------------------------------------------------------- /snippets/blocks/formfields/radio.php: -------------------------------------------------------------------------------- 1 | options() as $option) : ?> 2 | 3 |
4 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /snippets/blocks/formfields/select.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 29 | 30 |
-------------------------------------------------------------------------------- /snippets/blocks/formfields/textarea.php: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /snippets/blocks/formtemplates/field_error.php: -------------------------------------------------------------------------------- 1 |
2 | 3 |
    4 | 5 | getErrorMessages() as $errorfield): ?> 6 |
  • 7 | 8 | 9 |
10 | 11 |
-------------------------------------------------------------------------------- /snippets/blocks/formtemplates/fields.php: -------------------------------------------------------------------------------- 1 | 2 | fields() as $field) : ?> 3 | 4 |
5 | 6 | hasOptions() && $field->type(true) != "select"): ?> 7 |
8 | 9 | 10 | 11 | label() ?> 12 | 13 | 14 | 15 | 16 | toHtml() ?> 17 | 18 | template('field_error', ['field' => $field]) ?> 19 | 20 |
21 | 22 | 23 | 24 | 30 | 31 | toHtml() ?> 32 | 33 | template('field_error', ['field' => $field]) ?> 34 | 35 | 36 | 37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /snippets/blocks/formtemplates/form_error.php: -------------------------------------------------------------------------------- 1 |
2 | 3 |
errorMessage() ?>
4 | 5 | isValid()): ?> 6 | 7 |
    8 | fields()->errorFields('label') as $errorfield): ?> 9 |
  • 10 | 11 |
12 | 13 | 14 | 15 |
-------------------------------------------------------------------------------- /snippets/blocks/formtemplates/form_success.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
successMessage() ?>
4 |
5 | -------------------------------------------------------------------------------- /snippets/blocks/formtemplates/submit.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
-------------------------------------------------------------------------------- /src/components/MailList.vue: -------------------------------------------------------------------------------- 1 | 2 | 24 | 25 | 79 | 80 | 92 | -------------------------------------------------------------------------------- /src/components/blocks/Form.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 84 | -------------------------------------------------------------------------------- /src/components/dialog/Form.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 59 | 60 | 91 | -------------------------------------------------------------------------------- /src/components/fields/MailView.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 241 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Form from "./components/blocks/Form.vue"; 2 | import MailDialog from "./components/dialog/Form.vue"; 3 | import MailList from "./components/MailList.vue"; 4 | import MailView from "./components/fields/MailView.vue"; 5 | import PlainLicense from "../utils/PlainLicense.vue"; 6 | 7 | window.panel.plugin("plain/formblock", { 8 | fields: { 9 | mailview: MailView, 10 | }, 11 | components: { 12 | "k-mail-list": MailList, 13 | "k-mail-dialog": MailDialog, 14 | "k-plain-license": PlainLicense, 15 | }, 16 | blocks: { 17 | form: Form, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /utils/.gitignore: -------------------------------------------------------------------------------- 1 | # OS files 2 | .DS_Store 3 | 4 | # npm modules 5 | /node_modules 6 | 7 | 8 | #Symbolic Links 9 | *.lnk 10 | *.symlink 11 | 12 | # migration file 13 | /migrated -------------------------------------------------------------------------------- /utils/Autoloader.php: -------------------------------------------------------------------------------- 1 | [ 37 | 'method' => 'loadCache', 38 | 'path' => './' 39 | ], 40 | /** 41 | * Inject translations right at the begining. 42 | * So it could be used already in classes 43 | */ 44 | 'translations' => [ 45 | 'method' => 'loadTranslations', 46 | 'path' => './i18n' 47 | ], 48 | 'classes' => [ 49 | 'method' => 'loadClasses', 50 | 'path' => './classes', 51 | 'namespace' => null 52 | ], 53 | 'config' => [ 54 | 'method' => 'deepWalker', 55 | 'path' => './config' 56 | ], 57 | 'fields' => [ 58 | 'method' => 'flatWalker', 59 | 'path' => './fields', 60 | 'rootkey' => 'fields', 61 | 'read' => true 62 | ], 63 | 'sections' => [ 64 | 'method' => 'flatWalker', 65 | 'path' => './sections', 66 | 'rootkey' => 'sections', 67 | 'read' => true 68 | ], 69 | 'blueprints' => [ 70 | 'method' => 'flatWalker', 71 | 'path' => './blueprints', 72 | 'rootkey' => 'blueprints', 73 | 'read' => true 74 | ], 75 | 'snippets' => [ 76 | 'method' => 'flatWalker', 77 | 'path' => './snippets', 78 | 'rootkey' => 'snippets', 79 | 'read' => false 80 | ], 81 | 'templates' => [ 82 | 'method' => 'flatWalker', 83 | 'path' => './templates', 84 | 'rootkey' => 'templates', 85 | 'read' => true 86 | ] 87 | ]; 88 | 89 | /** 90 | * @param string $name Plugin name 91 | * @param string $root The root path of the plugin 92 | * @param array $data Predefined extension data to extend 93 | * @param array|bool $tasks A list of tasks to autoload (see $tasks property) 94 | * @return void|$this 95 | */ 96 | public function __construct( 97 | private string $name, 98 | private string $root, 99 | private array $data = [], 100 | array|bool $tasks = true 101 | ) { 102 | 103 | //Skip Autoloader 104 | if ($tasks === false) { 105 | return; 106 | } 107 | 108 | $this->setUserTasks($tasks); 109 | 110 | foreach ($this->tasks as $task) { 111 | $method = $task['method']; 112 | 113 | //Call given user function 114 | if ($method instanceof Closure) { 115 | $method($this); 116 | continue; 117 | } 118 | 119 | 120 | $reflection = new ReflectionMethod($this, $method); 121 | 122 | //Only pubilc methods could be loadet 123 | if ($reflection->isPublic() === false) { 124 | throw new InvalidArgumentException("Cannot use '{$method}' for autoloader tasks."); 125 | } 126 | 127 | //Remove methodname from array and call task 128 | unset($task['method']); 129 | call_user_func([$this, $method], ...$task); 130 | 131 | //Cache is lodaet -> escape 132 | if ($this->cache === true) { 133 | return $this; 134 | } 135 | } 136 | 137 | $this->saveCache(); 138 | 139 | return $this; 140 | } 141 | 142 | /** 143 | * Modify the default tasks with an array 144 | * 145 | * @param bool|array $tasks 146 | * @return void 147 | */ 148 | private function setUserTasks(bool|array $user_tasks): void 149 | { 150 | //Enable all 151 | if (is_array($user_tasks) === false) { 152 | $user_tasks = array_keys($this->tasks); 153 | } 154 | 155 | //Cache and classes needs to be first 156 | $user_tasks = A::merge([ 157 | 'cache' => true, 158 | 'classes' => false 159 | ], $user_tasks); 160 | 161 | foreach ($user_tasks as $key => &$task) { 162 | 163 | //Activate task without key 164 | if (is_numeric($key) && is_string($task)) { 165 | $key = $task; 166 | $task = true; 167 | } 168 | 169 | //Take from default 170 | if ($task === null || $task === true) { 171 | $task = []; 172 | } 173 | 174 | //Disable item 175 | if ($task === false) { 176 | unset($user_tasks[$key]); 177 | continue; 178 | } 179 | 180 | //Pass new path 181 | if (is_string($task)) { 182 | $task['path'] = $task; 183 | } 184 | 185 | //Merge array with default 186 | if (is_array($task)) { 187 | $task = A::merge($this->tasks[$key] ?? [], $task); 188 | } 189 | 190 | //Make absolute 191 | if (array_key_exists('path', $task) && Str::startsWith($task['path'], '.')) { 192 | $task['path'] = $this->root . Str::ltrim($task['path'], '.'); 193 | } 194 | } 195 | 196 | //Unset the reference 197 | unset($task); 198 | $this->tasks = $user_tasks; 199 | 200 | } 201 | 202 | /** 203 | * Run autoloader and return the results 204 | * 205 | * @param mixed ...$params 206 | * @return array 207 | */ 208 | public static function load(...$params): array 209 | { 210 | $self = new self(...$params); 211 | return $self->toArray(); 212 | } 213 | 214 | /** 215 | * Load the extension from the cache file. 216 | * 217 | * @param string $path Path to check the modified time 218 | * @param string $cache_folder By default 'site/cache/autoloader/plugin_vendor/plugin_name/' 219 | * @return void 220 | */ 221 | public function loadCache( 222 | string $path, 223 | ?string $cache_folder = null 224 | ): void 225 | { 226 | 227 | $cache_folder ??= App::instance()->root('cache') . "/autoloader/{$this->name}"; 228 | $this->cache_file = $cache_folder . '/' . Dir::modified($path) . '.php'; 229 | 230 | //No cache -> continue autoload 231 | if (F::exists($this->cache_file) === false) { 232 | //Enable Caching 233 | $this->cache = []; 234 | return; 235 | } 236 | 237 | try { 238 | $cache = Data::read($this->cache_file); 239 | } catch (\Throwable $th) { 240 | $error = $th->getMessage(); 241 | throw new Exception("Error reading autoload cache: {$error}"); 242 | }; 243 | 244 | //Load classes 245 | $this->registerClasses($cache['classes']); 246 | 247 | //Load classes 248 | $this->registerTranslations($cache['translations']); 249 | 250 | //Merge cache to data 251 | $this->merge($cache['data']); 252 | 253 | //Load resistant cache items to data 254 | if ($this->cache_resistance = $cache['resistance'] ?? null) { 255 | foreach ($this->cache_resistance as $file => $keychain) { 256 | $this->setValueFromKeyChain($this->data, $keychain, Data::read($file)); 257 | }; 258 | }; 259 | 260 | //Indicate that cache is loadet 261 | $this->cache = true; 262 | } 263 | 264 | /** 265 | * Set value to data (and cache) with a chain of keys 266 | * 267 | * @param string|array $key_chain 268 | * @param string $file 269 | * @param bool $read True sets the content of the file otherwise the path to the file 270 | * @return void 271 | */ 272 | private function setValue(string|array $key_chain, string $file, bool $read = true): void 273 | { 274 | 275 | //Make shure $key_chain is an array 276 | $key_chain = A::wrap($key_chain); 277 | 278 | //Sets file or filename to value 279 | $value = ($read) ? Data::read($file) : $file; 280 | 281 | //Value may be a closure 282 | if (is_array($value) && count($value) === 0) { 283 | $chainstring = A::join($key_chain, ' -> '); 284 | throw new Exception("The value for $chainstring is empty and cannot be resolved by the autoloader"); 285 | } 286 | 287 | //Skip unwanted keys 288 | if (in_array($key_chain[0] ?? null, static::ALLOWED_IDS) === false) { 289 | throw new Exception("'$key_chain[0]' is not a valid extension type", 1); 290 | } 291 | 292 | //Add value to data 293 | $this->setValueFromKeyChain($this->data, $key_chain, $value); 294 | 295 | //Cache is disabled 296 | if ($this->cache === false) { 297 | return; 298 | } 299 | 300 | if (is_string($value) || $this->isCacheable($value)) { 301 | //Value can be stored in cache 302 | $this->setValueFromKeyChain($this->cache, $key_chain, $value); 303 | } else { 304 | //Add filename to the non-cachable array 305 | $this->cache_resistance[$file] = $key_chain; 306 | } 307 | } 308 | 309 | /** 310 | * Walk through the key chain an sets the value to the given array 311 | * 312 | * @param array &$array 313 | * @param array $key_chain 314 | * @param mixed $value 315 | * @return void 316 | */ 317 | private function setValueFromKeyChain(array &$array, array $key_chain, $value) 318 | { 319 | $array ??= $this->cache; 320 | 321 | //Reference to the array 322 | $temp = &$array; 323 | 324 | foreach ($key_chain as $key) { 325 | 326 | //Set pseudo value if array not exists 327 | if (is_array($temp[$key] ?? null) === false) { 328 | $temp[$key] = null; 329 | } 330 | 331 | $temp = &$temp[$key]; 332 | } 333 | 334 | //End of chain -> set the value 335 | $temp ??= $value; 336 | } 337 | 338 | 339 | /** 340 | * Check if the file/folder name not starts with '_' 341 | * 342 | * @param string $path 343 | * @return null|string 344 | */ 345 | private function isActive(string $path): ?string 346 | { 347 | return Str::startsWith(pathinfo($path, PATHINFO_FILENAME), '_') === false; 348 | } 349 | 350 | /** 351 | * Get absolute path and returns a string or an array of the subfolders 352 | * 353 | * @param string $root 354 | * @param string $path 355 | * @param null|string $separator If set the subfolder will be glued with this value 356 | * @return array|string 357 | */ 358 | private function keyFromPath(string $root, string $path, ?string $separator = null): array|string 359 | { 360 | $diff = Str::ltrim($path, $root . '/'); 361 | $key = substr($diff, 0, strrpos($diff, '.')); 362 | if ($separator) { 363 | return Str::replace($key, '/', $separator); 364 | } 365 | return Str::split($key, '/'); 366 | } 367 | 368 | /** 369 | * Returns the extension data 370 | * 371 | * @return array */ 372 | public function toArray(): array 373 | { 374 | return $this->data; 375 | } 376 | 377 | /** 378 | * Merge aray to the extension data 379 | * 380 | * @param null|array $array 381 | * @return void 382 | */ 383 | public function merge(?array $array) 384 | { 385 | $this->data = A::merge($this->data, $array); 386 | } 387 | 388 | /** 389 | * Walk throw the given directory recursively and pass the filename to the callback 390 | * Check if files doesn't starts with '_' 391 | * 392 | * @param null|string $path 393 | * @param null|Closure $callback 394 | * @param bool $read 395 | * @return void 396 | */ 397 | public function filesWalker( 398 | ?string $path = null, 399 | ?Closure $callback = null, 400 | bool $read = true 401 | ): void { 402 | 403 | $callback ??= fn($file) => $this->setValue(pathinfo($file, PATHINFO_FILENAME), $file, $read); 404 | 405 | A::map( 406 | Dir::index($path, true), 407 | function ($item) use ($callback, $path) { 408 | $file = "{$path}/{$item}"; 409 | 410 | //Check if file active 411 | if ($this->isActive($item) && is_file($file)) { 412 | $callback($file); 413 | } 414 | } 415 | ); 416 | } 417 | 418 | /** 419 | * Walk through folders recursively and set values nested in the array 420 | * 421 | * @param string $path 422 | * @param bool $read 423 | * @return void 424 | */ 425 | public function deepWalker(string $path, bool $read = true): void 426 | { 427 | $this->filesWalker($path, function ($file) use ($path, $read) { 428 | $keychain = $this->keyFromPath($path, $file); 429 | $this->setValue($keychain, $file, $read); 430 | }); 431 | } 432 | 433 | /** 434 | * Walk through folders recursively and set values flat in the array 435 | * 436 | * @param string $path 437 | * @param null|string $rootkey 438 | * @param bool $read 439 | * @param string $separator 440 | * @return void 441 | */ 442 | public function flatWalker( 443 | string $path, 444 | ?string $rootkey = null, 445 | bool $read = true, 446 | string $separator = '/' 447 | ): void { 448 | 449 | if ($this->isActive($path) === false) { 450 | return; 451 | } 452 | 453 | $this->filesWalker( 454 | path: $path, 455 | callback: function ($file) use ($path, $rootkey, $read, $separator) { 456 | $key = $this->keyFromPath($path, $file, $separator); 457 | if ($rootkey) { 458 | $key = [$rootkey, $key]; 459 | } 460 | $this->setValue($key, $file, $read); 461 | } 462 | ); 463 | } 464 | 465 | /** 466 | * Load translations from folder I18n 467 | * 468 | * @param string $path 469 | * @return void 470 | */ 471 | public function loadTranslations( 472 | string $path 473 | ): void { 474 | $this->filesWalker($path, function ($file) use ($path) { 475 | $key = $this->keyFromPath($path, $file, '/'); 476 | $this->translations[$key] = Data::read($file); 477 | }); 478 | 479 | $this->registerTranslations(); 480 | } 481 | 482 | /** 483 | * Register translations 484 | * 485 | * @param null|array $translations 486 | * @return void 487 | */ 488 | private function registerTranslations(?array $translations = null): void 489 | { 490 | $translations ??= $this->translations; 491 | App::instance()->extend(compact('translations')); 492 | } 493 | 494 | /** 495 | * Load classes from folder 496 | * PluginVendor\PluginName\Folder\Class 497 | * 498 | * @param string $path 499 | * @param null|string $namespace A custom namespace for classes 500 | * @return void 501 | */ 502 | public function loadClasses( 503 | string $path, 504 | ?string $namespace = null 505 | ): void { 506 | 507 | $namespace ??= A::join( 508 | A::map( 509 | array: Str::split($this->name, '/'), 510 | map: fn($item) => Str::ucfirst(Str::camel($item)) 511 | ), 512 | '\\' 513 | ); 514 | 515 | $this->filesWalker($path, function ($file) use ($path, $namespace) { 516 | $key = $namespace . '\\' . $this->keyFromPath($path, $file, '\\'); 517 | $this->classes[$key] = $file; 518 | }); 519 | 520 | $this->registerClasses(); 521 | } 522 | 523 | /** 524 | * Register classes 525 | * 526 | * @param null|array $classes 527 | * @return void 528 | */ 529 | private function registerClasses(?array $classes = null): void 530 | { 531 | $classes ??= $this->classes; 532 | 533 | if (count($classes) > 0) { 534 | F::loadClasses($classes); 535 | }; 536 | } 537 | 538 | /** 539 | * Check if the array doesn't contains any closure which are not storable 540 | * 541 | * @param mixed $data 542 | * @return bool 543 | */ 544 | private function isCacheable($data): bool 545 | { 546 | if (!is_array($data)) { 547 | return !($data instanceof Closure); 548 | } 549 | 550 | foreach ($data as $value) { 551 | if ($value instanceof Closure || !$this->isCacheable($value)) { 552 | return false; 553 | } 554 | } 555 | 556 | return true; 557 | } 558 | 559 | 560 | /** 561 | * Save data, classes and to a file in the cachefolder 562 | * 563 | * @return void 564 | */ 565 | private function saveCache(): void 566 | { 567 | 568 | //Caching disabled 569 | if ($this->cache === false) { 570 | return; 571 | } 572 | 573 | //Clean from old caches 574 | $cache_folder = pathinfo($this->cache_file, PATHINFO_DIRNAME); 575 | try { 576 | Dir::remove($cache_folder); 577 | } catch (\Throwable $th) {}; 578 | 579 | Data::write($this->cache_file, [ 580 | 'classes' => $this->classes, 581 | 'translations' => $this->translations, 582 | 'data' => $this->cache, 583 | 'resistance' => $this->cache_resistance 584 | ]); 585 | } 586 | } 587 | -------------------------------------------------------------------------------- /utils/License.php: -------------------------------------------------------------------------------- 1 | 8 | * @link https://plain-solutions.net/ 9 | * @copyright Roman Gsponer 10 | * @license https://plain-solutions.net/terms/ 11 | * 12 | * If you're reading this, you're probably up to skip the license validation. 13 | * 14 | * Keep in mind, that i spent a lot of time developing this. 15 | * You will also save a lot of time with this extension. 16 | * 17 | */ 18 | 19 | use Kirby\Cms\App; 20 | use Kirby\Data\Json; 21 | use Kirby\Panel\Field; 22 | use Kirby\Http\Remote; 23 | use Kirby\Toolkit\V; 24 | use Kirby\Toolkit\Str; 25 | use Kirby\Toolkit\I18n; 26 | use Kirby\Exception\Exception; 27 | use Kirby\Toolkit\A; 28 | 29 | class License 30 | { 31 | 32 | private const PROXY = 'https://plain-solutions.net/proxy'; 33 | 34 | public string $title; 35 | public string $link; 36 | private string $prefix; 37 | private string $licensefile; 38 | private array $licensedata = []; 39 | 40 | private static $cache = []; 41 | public static array $licenses = []; 42 | 43 | public function __construct( 44 | public string $name, 45 | public ?array $info = null, 46 | private ?bool $isValid = null 47 | ) { 48 | 49 | if ($info['license'] === 'MIT') { 50 | return null; 51 | } 52 | 53 | $this->prefix = Str::after($this->name, '/'); 54 | $this->licensefile = App::instance()->root("config") . "/.{$this->prefix}_license"; 55 | 56 | $this->title = $info['extra']['title'] ?? $this->name; 57 | $this->link = $info['homepage']; 58 | 59 | if (file_exists($this->licensefile)) { 60 | $this->licensedata = Json::read($this->licensefile, 'json', false); 61 | } 62 | 63 | static::$cache[$name] = $this; 64 | } 65 | 66 | public static function factory($name, ?array $info = null): self 67 | { 68 | if (array_key_exists($name, static::$cache)) { 69 | return static::$cache[$name]; 70 | } 71 | 72 | return new self($name, $info); 73 | } 74 | 75 | public function getLicenseObject(string $locale = null): ?array 76 | { 77 | if (static::isValid()) { 78 | return null; 79 | } 80 | return [ 81 | 'title' => $this->title, 82 | 'cta' => I18n::translate('license.activate.label'), 83 | 'dialog' => $this->prefix . "/register" 84 | ]; 85 | } 86 | 87 | public function licenseArray(): ?array 88 | { 89 | if ($this->isValid()) { 90 | return null; 91 | } 92 | 93 | return [ 94 | 'value' => 'missing', 95 | 'theme' => 'negative', 96 | 'label' => App::instance()->translation()->get('license.unregistered.label'), 97 | 'icon' => 'alert', 98 | 'dialog' => "{$this->prefix}/register" 99 | ]; 100 | } 101 | 102 | private function isValid(): bool 103 | { 104 | 105 | if ($this->isValid !== null) { 106 | return $this->isValid; 107 | } 108 | 109 | $license = $this->licensedata; 110 | 111 | if ( 112 | isset($license["key"], $license["email"], $license["signature"]) !== true && 113 | count($license) === 0 114 | ) { 115 | return $this->isValid = false; 116 | } 117 | 118 | $licensedata = $this->generateLicensedata($license["key"], $license["email"]); 119 | 120 | if ($license["signature"] !== md5(json_encode($licensedata))) { 121 | return $this->isValid = false; 122 | } 123 | 124 | return $this->isValid = true; 125 | } 126 | 127 | public function extends($extends) { 128 | if ($this->isValid()) { 129 | return $extends; 130 | } 131 | 132 | $prefix = $this->prefix; 133 | $lang = App::instance()->user()?->language() ?? App::instance()->currentLanguage()?->code() ?? 'en';; 134 | 135 | return A::merge($extends, [ 136 | 'api' => [ 137 | 'routes' => [ 138 | [ 139 | "pattern" => "plain/licenses/validate", 140 | "action" => function () { 141 | return License::factory(get('name'))->register(get("key"), get("email")); 142 | }, 143 | ], 144 | ] 145 | ], 146 | 'areas' => [ 147 | $prefix => [ 148 | 'dialogs' => [ 149 | "$prefix/register" => $this->dialog() 150 | ] 151 | ] 152 | ], 153 | 'translations' => [ 154 | $lang => [ 155 | "plain.licenses.$prefix" => $this->getLicenseObject($lang) 156 | ] 157 | ], 158 | ]); 159 | } 160 | 161 | public function dialog(): array 162 | { 163 | 164 | $license_obj = $this; 165 | 166 | return [ 167 | 'load' => function () use ($license_obj) { 168 | 169 | $system = App::instance()->system(); 170 | $local = $system->isLocal(); 171 | $instance = $system->indexUrl(); 172 | $text_key = 'license.activate.' . ($local ? 'local' : 'domain'); 173 | $text = I18n::template($text_key, ['host' => $instance]); 174 | 175 | return [ 176 | 'component' => 'k-form-dialog', 177 | 'props' => [ 178 | 'fields' => [ 179 | 'headline' => [ 180 | 'label' => $license_obj->title, 181 | 'type' => 'headline' 182 | ], 183 | 'domain' => [ 184 | 'label' => I18n::translate('license.activate.label'), 185 | 'type' => 'info', 186 | 'theme' => $local ? 'warning' : 'info', 187 | 'text' => Str::replace($text, 'Kirby', $license_obj->title) 188 | ], 189 | 'license' => [ 190 | 'label' => I18n::translate('license.code.label'), 191 | 'type' => 'text', 192 | 'required' => true, 193 | 'counter' => false, 194 | 'placeholder' => '', 195 | 'help' => I18n::translate('license.code.help') . ' ' . '' . I18n::translate('license.buy') . ' →' 196 | ], 197 | 'email' => Field::email(['required' => true]), 198 | 'license_id' => Field::hidden() 199 | ], 200 | 'submitButton' => [ 201 | 'icon' => 'key', 202 | 'text' => I18n::translate('activate'), 203 | 'theme' => 'love', 204 | ], 205 | 'value' => [ 206 | 'license' => null, 207 | 'email' => null, 208 | 'name' => $license_obj->name 209 | ] 210 | ] 211 | ]; 212 | }, 213 | 'submit' => function () { 214 | 215 | $request = App::instance()->request(); 216 | 217 | License::factory($request->get('name'))->register ( 218 | $request->get('license'), 219 | $request->get('email') 220 | ); 221 | 222 | return [ 223 | 'message' => I18n::translate('license.success') 224 | ]; 225 | 226 | } 227 | ]; 228 | } 229 | 230 | public function register(string $key, string $email): void 231 | { 232 | 233 | if (V::email($email) === false) { 234 | throw new Exception("error.validation.email"); 235 | } 236 | 237 | $licensedata = $this->generateLicensedata($key, $email); 238 | 239 | try { 240 | $request = new Remote($this->link, [ 241 | "method" => "POST", 242 | "data" => $licensedata, 243 | "timeout" => 5, 244 | ]); 245 | 246 | $response = $request->json(); 247 | 248 | } catch (\Throwable $e) { 249 | throw new Exception("No connection to the license server. Visit: " . static::PROXY); 250 | } 251 | 252 | if ($response === null || $response["error"] ?? false === 1) { 253 | throw new Exception($response["text"] ??= 'An error has occurred!'); 254 | } 255 | 256 | $this->writeLicensedata($licensedata); 257 | 258 | } 259 | 260 | private function generateLicensedata(string $key, string $email): array 261 | { 262 | return [ 263 | "product" => Str::ltrim(parse_url($this->link, PHP_URL_PATH), '/'), 264 | "key" => $key, 265 | "email" => Str::lower(trim($email)), 266 | "site" => App::instance() 267 | ->system() 268 | ->indexUrl(), 269 | ]; 270 | } 271 | 272 | private function writeLicensedata(array $licensedata): void 273 | { 274 | $licensedata["signature"] = md5(json_encode($licensedata)); 275 | $this->licensedata = $licensedata; 276 | Json::write($this->licensefile, $licensedata); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /utils/PlainLicense.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 70 | 71 | 92 | -------------------------------------------------------------------------------- /utils/Plugin.php: -------------------------------------------------------------------------------- 1 | 8 | * @link https://plain-solutions.net/ 9 | * @copyright Roman Gsponer 10 | * @license https://plain-solutions.net/terms/ 11 | */ 12 | 13 | use Kirby\Cms\App; 14 | use Kirby\Data\Data; 15 | 16 | class Plugin 17 | { 18 | 19 | public static function load( 20 | string $name, 21 | ?array $extends = null, 22 | ?array $info = null, 23 | string|null $root = null, 24 | Autoloader|array|bool $autoloader = false 25 | ): void { 26 | 27 | 28 | $root ??= dirname(debug_backtrace()[0]['file']); 29 | $info ??= Data::read($root . '/composer.json'); 30 | 31 | //Needs to be loadet before autoload! 32 | $license_obj = ($info['license'] === 'MIT') ? null : new License($name, $info); 33 | 34 | $extends = Autoloader::load( 35 | name: $name, 36 | root: $root, 37 | data: $extends ?? [], 38 | tasks: $autoloader 39 | ); 40 | 41 | $params = [ 42 | 'name' => $name, 43 | 'info' => $info, 44 | 'root' => $root 45 | ]; 46 | 47 | //Kirby > 5.0.0 allows license status 48 | if (version_compare(App::version() ?? '0.0.0', '4.9.9', '>')) { 49 | 50 | if ($license_obj === null) { 51 | $status = static::licenseArray($info); 52 | } 53 | 54 | $params['license'] = [ 55 | 'name' => $info['license'], 56 | 'status' => $status ?? $license_obj?->licenseArray($info) 57 | ]; 58 | } 59 | 60 | $params['extends'] = $license_obj?->extends($extends) ?? $extends; 61 | 62 | App::plugin(...$params); 63 | } 64 | 65 | private static function licenseArray($info): ?array 66 | { 67 | if ($donate = $info['funding'][0]['url'] ?? null) { 68 | return [ 69 | 'value' => 'active', 70 | 'link' => $donate, 71 | 'theme' => 'pink', 72 | 'label' => 'Donate', 73 | 'icon' => 'heart', 74 | ]; 75 | } 76 | return null; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /utils/README.md: -------------------------------------------------------------------------------- 1 | This is a submodule that is loaded in every Kirby plugin from Plain Solutions GmbH. 2 | 3 | It contains helpers for autoloading the plugin and managing license validation. -------------------------------------------------------------------------------- /utils/load.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/Plugin.php', 6 | 'Plain\Helpers\Autoloader' => __DIR__ . '/Autoloader.php', 7 | 'Plain\Helpers\License' => __DIR__ . '/License.php' 8 | ]); --------------------------------------------------------------------------------