Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
87.74% |
415 / 473 |
|
70.27% |
26 / 37 |
CRAP | |
0.00% |
0 / 1 |
Form | |
87.74% |
415 / 473 |
|
70.27% |
26 / 37 |
173.11 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
setFormId | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getDisplayString | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
isEnabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
useCaptcha | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
reportReferrer | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
reportUserAgent | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
showOnlyForLoggedUsers | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFormElementConfig | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRecipient | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
getTitle | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getHelp | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getEmailSubject | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
4 | |||
getSubmitResponse | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getEmailFromAddress | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getEmailFromName | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
formatEmailMessage | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
mapRequestParamsToFieldValues | |
55.26% |
21 / 38 |
|
0.00% |
0 / 1 |
35.15 | |||
getInputFilter | |
100.00% |
64 / 64 |
|
100.00% |
1 / 1 |
11 | |||
getFormConfig | |
70.00% |
7 / 10 |
|
0.00% |
0 / 1 |
3.24 | |||
mergeLocalConfig | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
parseConfig | |
88.78% |
87 / 98 |
|
0.00% |
0 / 1 |
30.19 | |||
getElementOptions | |
69.57% |
16 / 23 |
|
0.00% |
0 / 1 |
5.70 | |||
getElementOptionGroups | |
90.48% |
19 / 21 |
|
0.00% |
0 / 1 |
5.02 | |||
getFormSettingFields | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
1 | |||
getFormElementSettingFields | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
1 | |||
getProtectedFieldNames | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
buildForm | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
getFormElement | |
92.86% |
78 / 84 |
|
0.00% |
0 / 1 |
24.21 | |||
getFormElementClass | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getValidationMessage | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getFormElements | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
getElementId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPrimaryHandler | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getSecondaryHandlers | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getFormId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
sanitizePrefill | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | /** |
4 | * Configurable form. |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) The National Library of Finland 2018-2021. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Form |
25 | * @author Samuli Sillanpää <samuli.sillanpaa@helsinki.fi> |
26 | * @author Ere Maijala <ere.maijala@helsinki.fi> |
27 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
28 | * @link https://vufind.org/wiki/development:plugins:controllers Wiki |
29 | */ |
30 | |
31 | namespace VuFind\Form; |
32 | |
33 | use Laminas\InputFilter\InputFilter; |
34 | use Laminas\InputFilter\InputFilterInterface; |
35 | use Laminas\Validator\Callback; |
36 | use Laminas\Validator\EmailAddress; |
37 | use Laminas\Validator\Identical; |
38 | use Laminas\Validator\NotEmpty; |
39 | use Laminas\View\HelperPluginManager; |
40 | use VuFind\Config\YamlReader; |
41 | use VuFind\Form\Handler\HandlerInterface; |
42 | use VuFind\Form\Handler\PluginManager as HandlerManager; |
43 | |
44 | use function count; |
45 | use function in_array; |
46 | use function is_array; |
47 | |
48 | /** |
49 | * Configurable form. |
50 | * |
51 | * @category VuFind |
52 | * @package Form |
53 | * @author Samuli Sillanpää <samuli.sillanpaa@helsinki.fi> |
54 | * @author Ere Maijala <ere.maijala@helsinki.fi> |
55 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
56 | * @link https://vufind.org/wiki/development:plugins:controllers Wiki |
57 | */ |
58 | class Form extends \Laminas\Form\Form implements |
59 | \VuFind\I18n\Translator\TranslatorAwareInterface |
60 | { |
61 | use \VuFind\I18n\Translator\TranslatorAwareTrait; |
62 | |
63 | /** |
64 | * Input filter |
65 | * |
66 | * @var InputFilter |
67 | */ |
68 | protected $inputFilter; |
69 | |
70 | /** |
71 | * Default, untranslated validation messages |
72 | * |
73 | * @var array |
74 | */ |
75 | protected $messages = [ |
76 | 'empty' => 'This field is required', |
77 | 'invalid_email' => 'Email address is invalid', |
78 | ]; |
79 | |
80 | /** |
81 | * VuFind main configuration |
82 | * |
83 | * @var array |
84 | */ |
85 | protected $vufindConfig; |
86 | |
87 | /** |
88 | * Default form configuration (from config.ini > Feedback) |
89 | * |
90 | * @var array |
91 | */ |
92 | protected $defaultFormConfig; |
93 | |
94 | /** |
95 | * Form element configuration |
96 | * |
97 | * @var array |
98 | */ |
99 | protected $formElementConfig = []; |
100 | |
101 | /** |
102 | * Form configuration |
103 | * |
104 | * @var array |
105 | */ |
106 | protected $formConfig; |
107 | |
108 | /** |
109 | * YAML reader |
110 | * |
111 | * @var YamlReader |
112 | */ |
113 | protected $yamlReader; |
114 | |
115 | /** |
116 | * View helper manager. |
117 | * |
118 | * @var HelperPluginManager |
119 | */ |
120 | protected $viewHelperManager; |
121 | |
122 | /** |
123 | * Handler plugin manager |
124 | * |
125 | * @var HandlerManager |
126 | */ |
127 | protected $handlerManager; |
128 | |
129 | /** |
130 | * Constructor |
131 | * |
132 | * @param YamlReader $yamlReader YAML reader |
133 | * @param HelperPluginManager $viewHelperManager View helper manager |
134 | * @param HandlerManager $handlerManager Handler plugin manager |
135 | * @param array $config VuFind main configuration |
136 | * (optional) |
137 | * |
138 | * @throws \Exception |
139 | */ |
140 | public function __construct( |
141 | YamlReader $yamlReader, |
142 | HelperPluginManager $viewHelperManager, |
143 | HandlerManager $handlerManager, |
144 | array $config = null |
145 | ) { |
146 | parent::__construct(); |
147 | |
148 | $this->vufindConfig = $config; |
149 | $this->defaultFormConfig = $config['Feedback'] ?? null; |
150 | $this->yamlReader = $yamlReader; |
151 | $this->viewHelperManager = $viewHelperManager; |
152 | $this->handlerManager = $handlerManager; |
153 | } |
154 | |
155 | /** |
156 | * Set form id |
157 | * |
158 | * @param string $formId Form id |
159 | * @param array $params Additional form parameters. |
160 | * @param array $prefill Prefill form with these values. |
161 | * |
162 | * @return void |
163 | * @throws \Exception |
164 | */ |
165 | public function setFormId($formId, $params = [], $prefill = []) |
166 | { |
167 | if (!$config = $this->getFormConfig($formId)) { |
168 | throw new \VuFind\Exception\RecordMissing("Form '$formId' not found"); |
169 | } |
170 | |
171 | $this->formElementConfig |
172 | = $this->parseConfig($formId, $config, $params, $prefill); |
173 | |
174 | $this->buildForm(); |
175 | } |
176 | |
177 | /** |
178 | * Get display string. |
179 | * |
180 | * @param string $translationKey Translation key |
181 | * @param bool $escape Whether to escape the output. |
182 | * Default behaviour is to escape when the translation key does |
183 | * not end with '_html'. |
184 | * |
185 | * @return string |
186 | */ |
187 | public function getDisplayString($translationKey, $escape = null) |
188 | { |
189 | $escape ??= !str_ends_with($translationKey, '_html'); |
190 | $helper = $this->viewHelperManager->get($escape ? 'transEsc' : 'translate'); |
191 | return $helper($translationKey); |
192 | } |
193 | |
194 | /** |
195 | * Check if form enabled. |
196 | * |
197 | * @return bool |
198 | */ |
199 | public function isEnabled() |
200 | { |
201 | // Enabled unless explicitly disabled |
202 | return ($this->formConfig['enabled'] ?? true) === true; |
203 | } |
204 | |
205 | /** |
206 | * Check if the form should use Captcha validation (if supported) |
207 | * |
208 | * @return bool |
209 | */ |
210 | public function useCaptcha() |
211 | { |
212 | return (bool)($this->formConfig['useCaptcha'] ?? true); |
213 | } |
214 | |
215 | /** |
216 | * Check if the form should report referrer url |
217 | * |
218 | * @return bool |
219 | */ |
220 | public function reportReferrer() |
221 | { |
222 | return (bool)($this->formConfig['reportReferrer'] ?? false); |
223 | } |
224 | |
225 | /** |
226 | * Check if the form should report browser's user agent |
227 | * |
228 | * @return bool |
229 | */ |
230 | public function reportUserAgent() |
231 | { |
232 | return (bool)($this->formConfig['reportUserAgent'] ?? false); |
233 | } |
234 | |
235 | /** |
236 | * Check if form is available only for logged users. |
237 | * |
238 | * @return bool |
239 | */ |
240 | public function showOnlyForLoggedUsers() |
241 | { |
242 | return !empty($this->formConfig['onlyForLoggedUsers']); |
243 | } |
244 | |
245 | /** |
246 | * Return form element configuration. |
247 | * |
248 | * @return array |
249 | */ |
250 | public function getFormElementConfig(): array |
251 | { |
252 | return $this->formElementConfig; |
253 | } |
254 | |
255 | /** |
256 | * Return form recipient(s). |
257 | * |
258 | * @param array $postParams Posted form data |
259 | * |
260 | * @return array of recipients, each consisting of an array with |
261 | * name, email or null if not configured |
262 | * |
263 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
264 | */ |
265 | public function getRecipient($postParams = null) |
266 | { |
267 | $recipient = $this->formConfig['recipient'] ?? [null]; |
268 | $recipients = isset($recipient['email']) || isset($recipient['name']) |
269 | ? [$recipient] : $recipient; |
270 | |
271 | foreach ($recipients as &$recipient) { |
272 | $recipient['email'] ??= $this->defaultFormConfig['recipient_email'] ?? null; |
273 | $recipient['name'] ??= $this->defaultFormConfig['recipient_name'] ?? null; |
274 | } |
275 | |
276 | return $recipients; |
277 | } |
278 | |
279 | /** |
280 | * Return form title. |
281 | * |
282 | * @return string |
283 | */ |
284 | public function getTitle() |
285 | { |
286 | return $this->formConfig['title'] ?? null; |
287 | } |
288 | |
289 | /** |
290 | * Return form help texts. |
291 | * |
292 | * @return array|null |
293 | */ |
294 | public function getHelp() |
295 | { |
296 | return $this->formConfig['help'] ?? null; |
297 | } |
298 | |
299 | /** |
300 | * Return form email message subject. |
301 | * |
302 | * Replaces any placeholders for form field values or labels with the submitted |
303 | * values. |
304 | * |
305 | * @param array $postParams Posted form data |
306 | * |
307 | * @return string |
308 | */ |
309 | public function getEmailSubject($postParams) |
310 | { |
311 | $subject = 'VuFind Feedback'; |
312 | |
313 | if (!empty($this->formConfig['emailSubject'])) { |
314 | $subject = $this->formConfig['emailSubject']; |
315 | } elseif (!empty($this->defaultFormConfig['email_subject'])) { |
316 | $subject = $this->defaultFormConfig['email_subject']; |
317 | } |
318 | |
319 | $mappings = []; |
320 | foreach ($this->mapRequestParamsToFieldValues($postParams) as $field) { |
321 | // Use translated value as default for backward compatibility: |
322 | $mappings["%%{$field['name']}%%"] |
323 | = $mappings["%%translatedValue:{$field['name']}%%"] |
324 | = implode( |
325 | ', ', |
326 | array_map( |
327 | [$this, 'translate'], |
328 | (array)($field['value'] ?? []) |
329 | ) |
330 | ); |
331 | $mappings["%%value:{$field['name']}%%"] |
332 | = implode(', ', (array)($field['value'] ?? [])); |
333 | $mappings["%%label:{$field['name']}%%"] |
334 | = implode(', ', (array)($field['valueLabel'] ?? [])); |
335 | $mappings["%%translatedLabel:{$field['name']}%%"] = implode( |
336 | ', ', |
337 | array_map( |
338 | [$this, 'translate'], |
339 | (array)($field['valueLabel'] ?? []) |
340 | ) |
341 | ); |
342 | } |
343 | |
344 | return trim($this->translate($subject, $mappings)); |
345 | } |
346 | |
347 | /** |
348 | * Return response that is shown after successful form submit. |
349 | * |
350 | * @return string |
351 | */ |
352 | public function getSubmitResponse() |
353 | { |
354 | return !empty($this->formConfig['response']) |
355 | ? $this->formConfig['response'] |
356 | : 'feedback_response'; |
357 | } |
358 | |
359 | /** |
360 | * Return email from address |
361 | * |
362 | * @return string |
363 | */ |
364 | public function getEmailFromAddress(): string |
365 | { |
366 | return !empty($this->formConfig['emailFrom']['email']) |
367 | ? $this->formConfig['emailFrom']['email'] |
368 | : ''; |
369 | } |
370 | |
371 | /** |
372 | * Return email from name |
373 | * |
374 | * @return string |
375 | */ |
376 | public function getEmailFromName(): string |
377 | { |
378 | return !empty($this->formConfig['emailFrom']['name']) |
379 | ? $this->formConfig['emailFrom']['name'] |
380 | : ''; |
381 | } |
382 | |
383 | /** |
384 | * Format email message. |
385 | * |
386 | * @param array $requestParams Request parameters |
387 | * |
388 | * @return array Array with template parameters and template name. |
389 | * |
390 | * @deprecated Use mapRequestParamsToFieldValues |
391 | */ |
392 | public function formatEmailMessage(array $requestParams = []) |
393 | { |
394 | return [ |
395 | $this->mapRequestParamsToFieldValues($requestParams), |
396 | 'Email/form.phtml', |
397 | ]; |
398 | } |
399 | |
400 | /** |
401 | * Map request parameters to field values |
402 | * |
403 | * @param array $requestParams Request parameters |
404 | * |
405 | * @return array |
406 | */ |
407 | public function mapRequestParamsToFieldValues(array $requestParams): array |
408 | { |
409 | $params = []; |
410 | foreach ($this->getFormElementConfig() as $el) { |
411 | $type = $el['type']; |
412 | if ($type === 'submit') { |
413 | continue; |
414 | } |
415 | $value = $requestParams[$el['name']] ?? null; |
416 | $valueLabel = null; |
417 | |
418 | if (in_array($type, ['radio', 'select'])) { |
419 | $option = null; |
420 | if (isset($el['options'])) { |
421 | $option = $el['options'][$value] ?? null; |
422 | } elseif (isset($el['optionGroups'])) { |
423 | foreach ($el['optionGroups'] as $group) { |
424 | if (isset($group['options'][$value])) { |
425 | $option = $group['options'][$value]; |
426 | break; |
427 | } |
428 | } |
429 | } |
430 | if (null === $option) { |
431 | $value = null; |
432 | $valueLabel = null; |
433 | } else { |
434 | $value = $option['value']; |
435 | $valueLabel = $option['label']; |
436 | } |
437 | } elseif ($type === 'checkbox' && !empty($value)) { |
438 | $labels = []; |
439 | $values = []; |
440 | foreach ($value as $val) { |
441 | $option = $el['options'][$val] ?? null; |
442 | if (null === $option) { |
443 | continue; |
444 | } |
445 | $labels[] = $option['label']; |
446 | $values[] = $option['value']; |
447 | } |
448 | $value = $values; |
449 | $valueLabel = $labels; |
450 | } elseif ($type === 'date' && !empty($value)) { |
451 | $format = $el['format'] |
452 | ?? $this->vufindConfig['Site']['displayDateFormat'] ?? 'Y-m-d'; |
453 | $date = strtotime($value); |
454 | $value = date($format, $date); |
455 | } |
456 | $params[] = $el + compact('value', 'valueLabel'); |
457 | } |
458 | |
459 | return $params; |
460 | } |
461 | |
462 | /** |
463 | * Retrieve input filter used by this form |
464 | * |
465 | * @return InputFilterInterface |
466 | */ |
467 | public function getInputFilter(): InputFilterInterface |
468 | { |
469 | if ($this->inputFilter) { |
470 | return $this->inputFilter; |
471 | } |
472 | |
473 | $inputFilter = new InputFilter(); |
474 | |
475 | $validators = [ |
476 | 'email' => [ |
477 | 'name' => EmailAddress::class, |
478 | 'options' => [ |
479 | 'message' => $this->getValidationMessage('invalid_email'), |
480 | ], |
481 | ], |
482 | 'notEmpty' => [ |
483 | 'name' => NotEmpty::class, |
484 | 'options' => [ |
485 | 'message' => [ |
486 | NotEmpty::IS_EMPTY => $this->getValidationMessage('empty'), |
487 | ], |
488 | ], |
489 | ], |
490 | ]; |
491 | |
492 | $elementObjects = $this->getElements(); |
493 | foreach ($this->getFormElementConfig() as $el) { |
494 | $isCheckbox = $el['type'] === 'checkbox'; |
495 | $requireOne = $isCheckbox && ($el['requireOne'] ?? false); |
496 | $required = $el['required'] ?? $requireOne; |
497 | |
498 | $fieldValidators = []; |
499 | if ($required || $requireOne) { |
500 | $fieldValidators[] = $validators['notEmpty']; |
501 | } |
502 | if ($isCheckbox) { |
503 | if ($requireOne) { |
504 | $fieldValidators[] = [ |
505 | 'name' => Callback::class, |
506 | 'options' => [ |
507 | 'callback' => function ($value, $context) use ($el) { |
508 | return |
509 | !empty( |
510 | array_intersect( |
511 | array_keys($el['options']), |
512 | $value |
513 | ) |
514 | ); |
515 | }, |
516 | ], |
517 | ]; |
518 | } elseif ($required) { |
519 | $fieldValidators[] = [ |
520 | 'name' => Identical::class, |
521 | 'options' => [ |
522 | 'message' => [ |
523 | Identical::MISSING_TOKEN |
524 | => $this->getValidationMessage('empty'), |
525 | ], |
526 | 'strict' => true, |
527 | 'token' => array_keys($el['options']), |
528 | ], |
529 | ]; |
530 | } |
531 | } |
532 | |
533 | if ($el['type'] === 'email') { |
534 | $fieldValidators[] = $validators['email']; |
535 | } |
536 | |
537 | if (in_array($el['type'], ['checkbox', 'radio', 'select'])) { |
538 | // Add InArray validator from element object instance |
539 | $elementObject = $elementObjects[$el['name']]; |
540 | $elementSpec = $elementObject->getInputSpecification(); |
541 | $fieldValidators |
542 | = array_merge($fieldValidators, $elementSpec['validators']); |
543 | } |
544 | |
545 | $inputFilter->add( |
546 | [ |
547 | 'name' => $el['name'], |
548 | 'required' => $required, |
549 | 'validators' => $fieldValidators, |
550 | ] |
551 | ); |
552 | } |
553 | |
554 | $this->inputFilter = $inputFilter; |
555 | return $this->inputFilter; |
556 | } |
557 | |
558 | /** |
559 | * Get form configuration |
560 | * |
561 | * @param string $formId Form id |
562 | * |
563 | * @return mixed null|array |
564 | * @throws \Exception |
565 | */ |
566 | protected function getFormConfig($formId = null) |
567 | { |
568 | $confName = 'FeedbackForms.yaml'; |
569 | $config = $this->yamlReader->get($confName, false, true); |
570 | $localConfig = $this->yamlReader->get($confName, true, true); |
571 | |
572 | if (!$formId) { |
573 | $formId = $localConfig['default'] ?? $config['default'] ?? null; |
574 | if (!$formId) { |
575 | return null; |
576 | } |
577 | } |
578 | |
579 | $config = $config['forms'][$formId] ?? null; |
580 | $localConfig = $localConfig['forms'][$formId] ?? null; |
581 | |
582 | return $this->mergeLocalConfig($config, $localConfig); |
583 | } |
584 | |
585 | /** |
586 | * Merge local configuration into default configuration. |
587 | * |
588 | * @param array $config Default configuration |
589 | * @param ?array $localConfig Local configuration |
590 | * |
591 | * @return array |
592 | */ |
593 | protected function mergeLocalConfig($config, $localConfig = null) |
594 | { |
595 | return $localConfig ?? $config; |
596 | } |
597 | |
598 | /** |
599 | * Parse form configuration. |
600 | * |
601 | * @param string $formId Form id |
602 | * @param array $config Configuration |
603 | * @param array $params Additional form parameters. |
604 | * @param array $prefill Prefill form with these values. |
605 | * |
606 | * @return array |
607 | */ |
608 | protected function parseConfig($formId, $config, $params, $prefill) |
609 | { |
610 | $formConfig = [ |
611 | 'id' => $formId, |
612 | 'title' => !empty($config['name']) ?: $formId, |
613 | ]; |
614 | |
615 | foreach ($this->getFormSettingFields() as $key) { |
616 | if (isset($config[$key])) { |
617 | $formConfig[$key] = $config[$key]; |
618 | } |
619 | } |
620 | |
621 | $this->formConfig = $formConfig; |
622 | |
623 | $prefill = $this->sanitizePrefill($prefill); |
624 | |
625 | $elements = []; |
626 | $configuredElements = $this->getFormElements($config); |
627 | |
628 | // Defaults for sender contact name & email fields: |
629 | $senderName = [ |
630 | 'name' => 'name', |
631 | 'type' => 'text', |
632 | 'label' => $this->translate('feedback_name'), |
633 | 'group' => '__sender__', |
634 | ]; |
635 | $senderEmail = [ |
636 | 'name' => 'email', |
637 | 'type' => 'email', |
638 | 'label' => $this->translate('feedback_email'), |
639 | 'group' => '__sender__', |
640 | ]; |
641 | if ($formConfig['senderInfoRequired'] ?? false) { |
642 | $senderEmail['required'] = $senderName['required'] = true; |
643 | } |
644 | if ($formConfig['senderNameRequired'] ?? false) { |
645 | $senderName['required'] = true; |
646 | } |
647 | if ($formConfig['senderEmailRequired'] ?? false) { |
648 | $senderEmail['required'] = true; |
649 | } |
650 | |
651 | foreach ($configuredElements as $el) { |
652 | $element = []; |
653 | |
654 | $required = ['type', 'name']; |
655 | $optional = $this->getFormElementSettingFields(); |
656 | foreach ( |
657 | array_merge($required, $optional) as $field |
658 | ) { |
659 | if (!isset($el[$field])) { |
660 | continue; |
661 | } |
662 | $value = $el[$field]; |
663 | $element[$field] = $value; |
664 | } |
665 | |
666 | if ( |
667 | in_array($element['type'], ['checkbox', 'radio']) |
668 | && !isset($element['group']) |
669 | ) { |
670 | $element['group'] = $element['name']; |
671 | } |
672 | |
673 | $element['label'] = $el['label'] ?? ''; |
674 | |
675 | $elementType = $element['type']; |
676 | if (in_array($elementType, ['checkbox', 'radio', 'select'])) { |
677 | if ($options = $this->getElementOptions($el)) { |
678 | $element['options'] = $options; |
679 | } elseif ($optionGroups = $this->getElementOptionGroups($el)) { |
680 | $element['optionGroups'] = $optionGroups; |
681 | } |
682 | } |
683 | |
684 | $settings = []; |
685 | foreach ($el['settings'] ?? [] as $setting) { |
686 | if (!is_array($setting)) { |
687 | continue; |
688 | } |
689 | // Allow both [key => value] and [key, value]: |
690 | if (count($setting) !== 2) { |
691 | reset($setting); |
692 | $settingId = trim(key($setting)); |
693 | $settingVal = trim(current($setting)); |
694 | } else { |
695 | $settingId = trim($setting[0]); |
696 | $settingVal = trim($setting[1]); |
697 | } |
698 | $settings[$settingId] = $settingVal; |
699 | } |
700 | $element['settings'] = $settings; |
701 | |
702 | // Merge sender fields with any existing field definitions: |
703 | if ('name' === $element['name']) { |
704 | $element = array_replace_recursive($senderName, $element); |
705 | $senderName = null; |
706 | } elseif ('email' === $element['name']) { |
707 | $element = array_replace_recursive($senderEmail, $element); |
708 | $senderEmail = null; |
709 | } |
710 | |
711 | if ($elementType == 'textarea') { |
712 | if (!isset($element['settings']['rows'])) { |
713 | $element['settings']['rows'] = 8; |
714 | } |
715 | } |
716 | |
717 | if (!empty($prefill[$element['name']])) { |
718 | $element['settings']['value'] = $prefill[$element['name']]; |
719 | } |
720 | |
721 | $elements[] = $element; |
722 | } |
723 | |
724 | // Add sender fields if they were not merged in the loop above: |
725 | if ($senderName) { |
726 | $elements[] = $senderName; |
727 | } |
728 | if ($senderEmail) { |
729 | $elements[] = $senderEmail; |
730 | } |
731 | |
732 | if ($this->reportReferrer()) { |
733 | if ($referrer = ($params['referrer'] ?? false)) { |
734 | $elements[] = [ |
735 | 'type' => 'hidden', |
736 | 'name' => 'referrer', |
737 | 'settings' => ['value' => $referrer], |
738 | 'label' => 'Referrer', |
739 | ]; |
740 | } |
741 | } |
742 | |
743 | if ($this->reportUserAgent()) { |
744 | if ($userAgent = ($params['userAgent'] ?? false)) { |
745 | $elements[] = [ |
746 | 'type' => 'hidden', |
747 | 'name' => 'useragent', |
748 | 'settings' => ['value' => $userAgent], |
749 | 'label' => 'User Agent', |
750 | ]; |
751 | } |
752 | } |
753 | |
754 | $elements[] = [ |
755 | 'type' => 'submit', |
756 | 'name' => 'submitButton', |
757 | 'label' => 'Send', |
758 | ]; |
759 | |
760 | return $elements; |
761 | } |
762 | |
763 | /** |
764 | * Get options for an element |
765 | * |
766 | * @param array $element Element configuration |
767 | * |
768 | * @return array |
769 | */ |
770 | protected function getElementOptions(array $element): array |
771 | { |
772 | if (!isset($element['options'])) { |
773 | return []; |
774 | } |
775 | |
776 | $options = []; |
777 | $isSelect = $element['type'] === 'select'; |
778 | $placeholder = $element['placeholder'] ?? null; |
779 | |
780 | if ($isSelect && $placeholder) { |
781 | // Add placeholder option (without value) for |
782 | // select element. |
783 | $options['o0'] = [ |
784 | 'value' => '', |
785 | 'label' => $placeholder, |
786 | 'attributes' => [ |
787 | 'selected' => 'selected', 'disabled' => 'disabled', |
788 | ], |
789 | ]; |
790 | } |
791 | $idx = 0; |
792 | foreach ($element['options'] as $option) { |
793 | ++$idx; |
794 | $value = $option['value'] ?? $option; |
795 | $label = $option['label'] ?? $option; |
796 | $options["o$idx"] = [ |
797 | 'value' => $value, |
798 | 'label' => $label, |
799 | ]; |
800 | } |
801 | return $options; |
802 | } |
803 | |
804 | /** |
805 | * Get option groups for an element |
806 | * |
807 | * @param array $element Element configuration |
808 | * |
809 | * @return array |
810 | */ |
811 | protected function getElementOptionGroups(array $element): array |
812 | { |
813 | if (!isset($element['optionGroups'])) { |
814 | return []; |
815 | } |
816 | $groups = []; |
817 | $idx = 0; |
818 | foreach ($element['optionGroups'] as $group) { |
819 | if (empty($group['options'])) { |
820 | continue; |
821 | } |
822 | $options = []; |
823 | foreach ($group['options'] as $option) { |
824 | ++$idx; |
825 | $value = $option['value'] ?? $option; |
826 | $label = $option['label'] ?? $option; |
827 | |
828 | $options["o$idx"] = [ |
829 | 'value' => $value, |
830 | 'label' => $label, |
831 | ]; |
832 | } |
833 | $groups[$group['label']] = [ |
834 | 'label' => $group['label'], |
835 | 'options' => $options, |
836 | ]; |
837 | } |
838 | return $groups; |
839 | } |
840 | |
841 | /** |
842 | * Return a list of field names to read from settings file. |
843 | * |
844 | * @return array |
845 | */ |
846 | protected function getFormSettingFields() |
847 | { |
848 | return [ |
849 | 'emailFrom', |
850 | 'emailSubject', |
851 | 'enabled', |
852 | 'help', |
853 | 'onlyForLoggedUsers', |
854 | 'recipient', |
855 | 'reportReferrer', |
856 | 'reportUserAgent', |
857 | 'response', |
858 | 'senderEmailRequired', |
859 | 'senderInfoRequired', |
860 | 'senderNameRequired', |
861 | 'submit', |
862 | 'title', |
863 | 'useCaptcha', |
864 | 'primaryHandler', |
865 | 'secondaryHandlers', |
866 | 'prefillFields', |
867 | ]; |
868 | } |
869 | |
870 | /** |
871 | * Return a list of field names to read from form element settings. |
872 | * |
873 | * @return array |
874 | */ |
875 | protected function getFormElementSettingFields() |
876 | { |
877 | return [ |
878 | 'format', |
879 | 'group', |
880 | 'help', |
881 | 'inputType', |
882 | 'maxValue', |
883 | 'minValue', |
884 | 'placeholder', |
885 | 'required', |
886 | 'requireOne', |
887 | 'value', |
888 | ]; |
889 | } |
890 | |
891 | /** |
892 | * Return field names that should not be prefilled. |
893 | * |
894 | * @return array |
895 | */ |
896 | protected function getProtectedFieldNames(): array |
897 | { |
898 | return [ |
899 | 'referrer', |
900 | 'submit', |
901 | 'userAgent', |
902 | ]; |
903 | } |
904 | |
905 | /** |
906 | * Build form. |
907 | * |
908 | * @return void |
909 | */ |
910 | protected function buildForm() |
911 | { |
912 | foreach ($this->formElementConfig as $el) { |
913 | if ($element = $this->getFormElement($el)) { |
914 | $this->add($element); |
915 | } |
916 | } |
917 | } |
918 | |
919 | /** |
920 | * Get configuration for a Laminas form element |
921 | * |
922 | * @param array $el Element configuration |
923 | * |
924 | * @return array |
925 | */ |
926 | protected function getFormElement($el) |
927 | { |
928 | $type = $el['type']; |
929 | if (!($class = $this->getFormElementClass($type))) { |
930 | return null; |
931 | } |
932 | |
933 | $conf = []; |
934 | $conf['name'] = $el['name']; |
935 | |
936 | $conf['type'] = $class; |
937 | $conf['options'] = []; |
938 | |
939 | $attributes = [ |
940 | 'id' => $this->getElementId($el['name']), |
941 | 'class' => [$el['settings']['class'] ?? null], |
942 | ]; |
943 | |
944 | if ($type !== 'submit') { |
945 | $attributes['class'][] = 'form-control'; |
946 | } |
947 | |
948 | if (!empty($el['required'])) { |
949 | $attributes['required'] = true; |
950 | } |
951 | if (!empty($el['settings'])) { |
952 | $attributes += $el['settings']; |
953 | } |
954 | // Add aria-label only if not a hidden field and no aria-label specified: |
955 | if ( |
956 | !empty($el['label']) && 'hidden' !== $type |
957 | && !isset($attributes['aria-label']) |
958 | ) { |
959 | $attributes['aria-label'] = $el['label']; |
960 | } |
961 | |
962 | switch ($type) { |
963 | case 'checkbox': |
964 | $options = []; |
965 | if (isset($el['options'])) { |
966 | $options = $el['options']; |
967 | } |
968 | $optionElements = []; |
969 | foreach ($options as $key => $item) { |
970 | $optionElements[] = [ |
971 | 'label' => $this->translate($item['label']), |
972 | 'value' => $key, |
973 | 'attributes' => [ |
974 | 'id' => $this->getElementId($el['name'] . '_' . $key), |
975 | ], |
976 | ]; |
977 | } |
978 | $conf['options'] = ['value_options' => $optionElements]; |
979 | break; |
980 | case 'date': |
981 | if (isset($el['minValue'])) { |
982 | $attributes['min'] = date('Y-m-d', strtotime($el['minValue'])); |
983 | } |
984 | if (isset($el['maxValue'])) { |
985 | $attributes['max'] = date('Y-m-d', strtotime($el['maxValue'])); |
986 | } |
987 | break; |
988 | case 'radio': |
989 | $options = []; |
990 | if (isset($el['options'])) { |
991 | $options = $el['options']; |
992 | } |
993 | $optionElements = []; |
994 | $first = true; |
995 | foreach ($options as $key => $option) { |
996 | $elemId = $this->getElementId($el['name'] . '_' . $key); |
997 | $optionElements[] = [ |
998 | 'label' => $this->translate($option['label']), |
999 | 'value' => $key, |
1000 | 'label_attributes' => ['for' => $elemId], |
1001 | 'attributes' => [ |
1002 | 'id' => $elemId, |
1003 | ], |
1004 | 'selected' => $first, |
1005 | ]; |
1006 | $first = false; |
1007 | } |
1008 | $conf['options'] = ['value_options' => $optionElements]; |
1009 | break; |
1010 | case 'select': |
1011 | if (isset($el['options'])) { |
1012 | $options = $el['options']; |
1013 | foreach ($options as $key => &$option) { |
1014 | $option['value'] = $key; |
1015 | } |
1016 | // Unset reference: |
1017 | unset($option); |
1018 | $conf['options'] = ['value_options' => $options]; |
1019 | } elseif (isset($el['optionGroups'])) { |
1020 | $groups = $el['optionGroups']; |
1021 | foreach ($groups as &$group) { |
1022 | foreach ($group['options'] as $key => &$option) { |
1023 | $option['value'] = $key; |
1024 | } |
1025 | // Unset reference: |
1026 | unset($key); |
1027 | } |
1028 | // Unset reference: |
1029 | unset($group); |
1030 | $conf['options'] = ['value_options' => $groups]; |
1031 | } |
1032 | break; |
1033 | case 'submit': |
1034 | $attributes['value'] = $el['label']; |
1035 | $attributes['class'][] = 'btn'; |
1036 | $attributes['class'][] = 'btn-primary'; |
1037 | break; |
1038 | } |
1039 | |
1040 | $attributes['class'] = trim(implode(' ', $attributes['class'])); |
1041 | $conf['attributes'] = $attributes; |
1042 | |
1043 | return $conf; |
1044 | } |
1045 | |
1046 | /** |
1047 | * Get form element class. |
1048 | * |
1049 | * @param string $type Element type |
1050 | * |
1051 | * @return string|null |
1052 | */ |
1053 | protected function getFormElementClass($type) |
1054 | { |
1055 | $map = [ |
1056 | 'checkbox' => '\Laminas\Form\Element\MultiCheckbox', |
1057 | 'date' => '\Laminas\Form\Element\Date', |
1058 | 'email' => '\Laminas\Form\Element\Email', |
1059 | 'hidden' => '\Laminas\Form\Element\Hidden', |
1060 | 'radio' => '\Laminas\Form\Element\Radio', |
1061 | 'select' => '\Laminas\Form\Element\Select', |
1062 | 'submit' => '\Laminas\Form\Element\Submit', |
1063 | 'text' => '\Laminas\Form\Element\Text', |
1064 | 'textarea' => '\Laminas\Form\Element\Textarea', |
1065 | 'url' => '\Laminas\Form\Element\Url', |
1066 | ]; |
1067 | |
1068 | return $map[$type] ?? null; |
1069 | } |
1070 | |
1071 | /** |
1072 | * Get translated validation message. |
1073 | * |
1074 | * @param string $messageId Message identifier |
1075 | * |
1076 | * @return string |
1077 | */ |
1078 | protected function getValidationMessage($messageId) |
1079 | { |
1080 | return $this->translate( |
1081 | $this->messages[$messageId] ?? $messageId |
1082 | ); |
1083 | } |
1084 | |
1085 | /** |
1086 | * Get form elements |
1087 | * |
1088 | * @param array $config Form configuration |
1089 | * |
1090 | * @return array |
1091 | */ |
1092 | protected function getFormElements($config) |
1093 | { |
1094 | $elements = []; |
1095 | foreach ($config['fields'] as $field) { |
1096 | if (!isset($field['type'])) { |
1097 | continue; |
1098 | } |
1099 | $elements[] = $field; |
1100 | } |
1101 | return $elements; |
1102 | } |
1103 | |
1104 | /** |
1105 | * Get a complete id for an element |
1106 | * |
1107 | * @param string $id Element ID |
1108 | * |
1109 | * @return string |
1110 | */ |
1111 | protected function getElementId(string $id): string |
1112 | { |
1113 | return 'form_' . $this->getFormId() . '_' . $id; |
1114 | } |
1115 | |
1116 | /** |
1117 | * Get primary form handler |
1118 | * |
1119 | * @return HandlerInterface |
1120 | */ |
1121 | public function getPrimaryHandler(): HandlerInterface |
1122 | { |
1123 | $handlerName = ($this->formConfig['primaryHandler'] ?? 'email'); |
1124 | return $this->handlerManager->get($handlerName); |
1125 | } |
1126 | |
1127 | /** |
1128 | * Get secondary form handlers |
1129 | * |
1130 | * @return HandlerInterface[] |
1131 | */ |
1132 | public function getSecondaryHandlers(): array |
1133 | { |
1134 | $handlerNames = (array)($this->formConfig['secondaryHandlers'] ?? []); |
1135 | return array_map([$this->handlerManager, 'get'], $handlerNames); |
1136 | } |
1137 | |
1138 | /** |
1139 | * Get current form id/name |
1140 | * |
1141 | * @return string |
1142 | */ |
1143 | public function getFormId(): string |
1144 | { |
1145 | return $this->formConfig['id'] ?? ''; |
1146 | } |
1147 | |
1148 | /** |
1149 | * Validates prefill data and returns only the prefill values for enabled fields |
1150 | * |
1151 | * @param array $prefill Prefill data |
1152 | * |
1153 | * @return array |
1154 | */ |
1155 | protected function sanitizePrefill(array $prefill): array |
1156 | { |
1157 | $prefillFields = $this->formConfig['prefillFields'] ?? []; |
1158 | $prefill = array_filter( |
1159 | $prefill, |
1160 | function ($key) use ($prefillFields) { |
1161 | return in_array($key, $prefillFields) |
1162 | && !in_array($key, $this->getProtectedFieldNames()); |
1163 | }, |
1164 | ARRAY_FILTER_USE_KEY |
1165 | ); |
1166 | return $prefill; |
1167 | } |
1168 | } |