├── .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 .= "" . $field->label() . " " . nl2br($field->value()) . " ";
219 | }
220 |
221 | $table .= "
";
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 |
30 |
31 | = $block->template('script') ?>
32 |
33 |
34 |
35 | = $block->template('validation') ?>
36 |
37 |
--------------------------------------------------------------------------------
/snippets/blocks/formcore/hidden.php:
--------------------------------------------------------------------------------
1 | honeypotId(); ?>
2 |
3 |
4 | = ucfirst($hpot) ?>
5 |
15 |
16 |
17 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/snippets/blocks/formcore/script.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/snippets/blocks/formcore/styles.php:
--------------------------------------------------------------------------------
1 | = css(kirby()->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 | = $form->template('form_error', [], false) ?>
42 |
43 |
44 |
--------------------------------------------------------------------------------
/snippets/blocks/formfields/captcha.php:
--------------------------------------------------------------------------------
1 | ask()->or($formfield->message('captcha_ask'));
8 |
9 | ?>
10 |
11 | = "{$text} {$add1} + {$add2}" ?>
12 |
13 | ">
14 |
15 | required('attr') ?>
25 | = $formfield->ariaAttr() ?>
26 | />
27 |
--------------------------------------------------------------------------------
/snippets/blocks/formfields/checkbox.php:
--------------------------------------------------------------------------------
1 | options() as $option) : ?>
2 |
3 |
4 |
5 | autofill(true) ?>
13 | = e($option->selected()->isTrue(), " checked") ?>
14 | = $formfield->required('attr') ?>
15 | = $formfield->ariaAttr() ?>
16 | >
17 | = $option->label() ?>
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/snippets/blocks/formfields/file.php:
--------------------------------------------------------------------------------
1 |
2 | maxnumber()->value() > 1; ?>
3 | autofill(true) ?>
11 | = $formfield->required('attr') ?>
12 | = $formfield->ariaAttr() ?>
13 | = ($isMultiple) ? "multiple" : "" ?>
14 |
15 | />
16 |
--------------------------------------------------------------------------------
/snippets/blocks/formfields/input.php:
--------------------------------------------------------------------------------
1 |
2 | autofill(true) ?>
11 | = $formfield->required('attr') ?>
12 | = $formfield->ariaAttr() ?>
13 | />
14 |
--------------------------------------------------------------------------------
/snippets/blocks/formfields/radio.php:
--------------------------------------------------------------------------------
1 | options() as $option) : ?>
2 |
3 |
4 |
5 | autofill(true) ?>
13 | = e($option->selected()->isTrue(), " checked") ?>
14 | = $formfield->required('attr') ?>
15 | = $formfield->ariaAttr() ?>
16 | >
17 | = $option->label() ?>
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/snippets/blocks/formfields/select.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | required('attr') ?>
11 | = $formfield->ariaAttr() ?>
12 | = $formfield->autofill(true) ?>
13 | >
14 |
15 | value() == "" ? "selected" : ""?>
16 |
17 | >= $formfield->placeholder() ?>
18 |
19 | options() as $option) : ?>
20 |
21 | selected()->isTrue() ? "selected" : ""?>
22 |
23 | >
24 | = $option->label() ?>
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/snippets/blocks/formfields/textarea.php:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/snippets/blocks/formtemplates/field_error.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/snippets/blocks/formtemplates/fields.php:
--------------------------------------------------------------------------------
1 |
2 | fields() as $field) : ?>
3 |
4 |
5 |
6 | hasOptions() && $field->type(true) != "select"): ?>
7 |
8 |
9 |
10 |
11 | = $field->label() ?>
12 | = $field->required('asterisk') ?>
13 |
14 |
15 |
16 | = $field->toHtml() ?>
17 |
18 | = $form->template('field_error', ['field' => $field]) ?>
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | = $field->label() ?>
27 | = $field->required('asterisk') ?>
28 |
29 |
30 |
31 | = $field->toHtml() ?>
32 |
33 | = $form->template('field_error', ['field' => $field]) ?>
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/snippets/blocks/formtemplates/form_error.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/snippets/blocks/formtemplates/form_success.php:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/snippets/blocks/formtemplates/submit.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/components/MailList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
20 |
21 |
22 |
23 |
24 |
25 |
79 |
80 |
92 |
--------------------------------------------------------------------------------
/src/components/blocks/Form.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
19 |
20 |
28 |
29 |
30 |
31 |
32 |
84 |
--------------------------------------------------------------------------------
/src/components/dialog/Form.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 | {{ current.title }}
10 |
11 |
12 |
13 |
18 | {{ label }}
19 |
20 |
32 |
33 |
34 | {{ current.formdata[key] }}
35 |
36 |
37 |
38 |
39 |
40 |
41 | {{ current.formdata.summary }}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
59 |
60 |
91 |
--------------------------------------------------------------------------------
/src/components/fields/MailView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
22 |
23 |
29 |
30 |
31 |
32 |
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 |
2 |
8 |
9 |
10 |
11 |
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 | ]);
--------------------------------------------------------------------------------