PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.1.0
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.1.0
11.1.1 11.1.0 11.0.5 11.0.4 11.0.3 11.0.2 11.0.1 11.0.0 10.11.1 10.11.0 10.10.2 10.10.1 10.10.0 10.9.1 trunk 10.0.0 10.0.1 10.1.0 10.1.1 10.1.2 10.1.3 10.1.4 10.2.0 10.2.1 10.2.2 All 62 releases
← All changes | back/Classes/FieldClass.php +306 -165 10.11.1 → 11.1.0 View file →
@@ -2,8 +2,9 @@
2 2
3 3 namespace AcyMailing\Classes;
4 4
5 5 use AcyMailing\Core\AcymClass;
6 +use AcyMailing\Helpers\SecurityHelper;
6 7
7 8 class FieldClass extends AcymClass
8 9 {
9 10 const LANGUAGE_FIELD_ID_KEY = 'language_field_id';
@@ -141,9 +142,11 @@
141 142 }
142 143
143 144 public function store(int $userID, array $fields, bool $ajax = false): void
144 145 {
145 - if (!empty($_FILES['customField'])) {
146 + $uploadedFileFields = [];
147 + $customField = acym_getVar('array', 'customField', [], 'FILES');
148 + if (!empty($customField['tmp_name'])) {
146 149 $uploadFolder = trim(acym_cleanPath(html_entity_decode(acym_getFilesFolder(true))), DS.' ').DS;
147 150 $uploadPath = acym_cleanPath(ACYM_ROOT.$uploadFolder.'userfiles'.DS.$userID.DS);
148 151 if (!file_exists($uploadPath)) {
149 152 acym_createDir($uploadPath);
@@ -149,23 +152,25 @@
149 152 acym_createDir($uploadPath);
150 153 }
151 154 $allowedExtensions = explode(',', $this->config->get('allowed_files'));
152 155
153 - foreach ($_FILES['customField']['tmp_name'] as $key => $value) {
156 + foreach ($customField['tmp_name'] as $key => $value) {
154 157 if (is_array($value) && isset($value[0])) $value = $value[0];
155 158 if (empty($value)) continue;
156 159
157 - $fileName = $_FILES['customField']['name'][$key];
160 + // Get the uploaded file name
161 + $fileName = $customField['name'][$key];
158 162 while (is_array($fileName) && isset($fileName[0])) {
159 163 $fileName = $fileName[0];
160 164 }
165 + $fileName = basename($fileName);
161 166
162 - if (!preg_match('#\.('.implode('|', $allowedExtensions).')$#Ui', $fileName)) {
167 + if (!acym_fileNameValid($fileName) || !preg_match('#\.('.implode('|', array_map('preg_quote', $allowedExtensions)).')$#i', $fileName)) {
163 168 $ext = substr($fileName, strrpos($fileName, '.') + 1);
164 169 if ($ajax) {
165 170 $this->errors[] = acym_translationSprintf(
166 171 'ACYM_ACCEPTED_TYPE',
167 - acym_escape($ext),
172 + esc_html($ext),
168 173 implode(', ', $allowedExtensions)
169 174 );
170 175 } else {
171 176 acym_enqueueMessage(
@@ -170,9 +175,9 @@
170 175 } else {
171 176 acym_enqueueMessage(
172 177 acym_translationSprintf(
173 178 'ACYM_ACCEPTED_TYPE',
174 - acym_escape($ext),
179 + esc_html($ext),
175 180 implode(', ', $allowedExtensions)
176 181 ),
177 182 'error',
178 183 5000
@@ -181,9 +186,9 @@
181 186
182 187 continue;
183 188 }
184 189
185 - if (!acym_uploadFile($value, $uploadPath.$fileName)) {
190 + if (!acym_isPathWithinDirectory($uploadPath, $uploadPath.$fileName) || !acym_uploadFile($value, $uploadPath.$fileName)) {
186 191 if ($ajax) {
187 192 $this->errors[] = acym_translation('ACYM_ERROR_SAVING');
188 193 } else {
189 194 acym_enqueueMessage(acym_translation('ACYM_ERROR_SAVING'), 'error');
@@ -190,9 +195,10 @@
190 195 }
191 196
192 197 continue;
193 198 }
194 - $fields[$key] = $_FILES['customField']['name'][$key];
199 + $fields[$key] = $fileName;
200 + $uploadedFileFields[$key] = true;
195 201 }
196 202 }
197 203
198 204 if (empty($fields)) return;
@@ -245,8 +251,14 @@
245 251 if (in_array($field->type, ['text', 'textarea']) && !empty($fieldOptions->max_characters)) {
246 252 $value = substr($value, 0, $fieldOptions->max_characters);
247 253 }
248 254
255 + // A file field value can only be set through an actual upload, never through a plain posted string
256 + if ($field->type === 'file' && strlen($value) !== 0 && empty($uploadedFileFields[$id])) {
257 + continue;
258 + }
259 +
260 + // If deleting a file field, also delete the physical file
249 261 if ($field->type === 'file' && strlen($value) === 0) {
250 262 $oldValue = acym_loadResult(
251 263 'SELECT `value` FROM #__acym_user_has_field WHERE `user_id` = '.intval($userID).' AND `field_id` = '.intval($id)
252 264 );
@@ -254,17 +266,25 @@
254 266 $fileName = @json_decode($oldValue, true);
255 267 if (is_array($fileName)) $fileName = $fileName[0];
256 268 if (empty($fileName)) $fileName = $oldValue;
257 269
258 - $uploadFolder = trim(acym_cleanPath(html_entity_decode(acym_getFilesFolder(true))), DS.' ').DS;
259 - $filePath = acym_cleanPath(ACYM_ROOT.$uploadFolder.'userfiles'.DS.$userID.DS.$fileName);
260 - if (!file_exists($filePath)) {
261 - $filePath = acym_cleanPath(ACYM_ROOT.$uploadFolder.'userfiles'.DS.$fileName);
270 + $fileName = basename($fileName);
271 + if (acym_fileNameValid($fileName)) {
272 + $uploadFolder = trim(acym_cleanPath(html_entity_decode(acym_getFilesFolder(true))), DS.' ').DS;
273 + $userfilesBase = acym_cleanPath(ACYM_ROOT.$uploadFolder.'userfiles'.DS);
274 + // Try new path (with user folder) first, then legacy path (without user folder)
275 + $filePath = acym_cleanPath($userfilesBase.$userID.DS.$fileName);
276 + if (!file_exists($filePath)) {
277 + $filePath = acym_cleanPath($userfilesBase.$fileName);
278 + }
279 + if (acym_isPathWithinDirectory($userfilesBase, $filePath)) {
280 + acym_deleteFile($filePath);
281 + }
262 282 }
263 - acym_deleteFile($filePath, false);
264 283 }
265 284 }
266 285
286 + // The user removed a value, don't add an empty line and remove any previous value from the bd
267 287 if (strlen($value) === 0) {
268 288 $query = 'DELETE FROM `#__acym_user_has_field`
269 289 WHERE `user_id` = '.intval($userID).'
270 290 AND `field_id` = '.intval($id);
@@ -353,8 +373,9 @@
353 373 return !empty($fieldOption['value']) || !empty($fieldOption['title']);
354 374 });
355 375
356 376 if (empty($defaultValues)) {
377 + // Options from database
357 378 if (!empty($one->field_value)) {
358 379 $fieldOptions = json_decode($one->option, true);
359 380 if (!empty($fieldOptions['fieldDB'])) {
360 381 $dbOptions = json_decode($fieldOptions['fieldDB'], true);
@@ -366,8 +387,9 @@
366 387 }
367 388 }
368 389 }
369 390 } else {
391 + // Classic options
370 392 foreach ($defaultValues as $oneValue) {
371 393 if (!in_array($oneValue['value'], explode(',', $one->field_value))) continue;
372 394 $fieldValues[$key][] = $oneValue['title'];
373 395 }
@@ -429,19 +451,22 @@
429 451
430 452 return parent::delete($elements);
431 453 }
432 454
455 + /**
456 + * @param mixed $defaultValue
457 + */
433 458 public function displayField(
434 - object $field,
435 - $defaultValue,
436 - array $valuesArray,
437 - bool $displayOutside = true,
438 - bool $displayFront = false,
459 + object $field,
460 + $defaultValue,
461 + array $valuesArray,
462 + bool $displayOutside = true,
463 + bool $displayFront = false,
439 464 ?object $user = null,
440 - bool $showField = true
441 - ): string {
465 + bool $showField = true
466 + ): void {
442 467 if (intval($field->active) === 0 && intval($field->core) !== 1) {
443 - return '';
468 + return;
444 469 }
445 470
446 471 $extraErrors = $this->config->get('extra_errors', '0');
447 472 $isCoreField = $field->core == 1;
@@ -446,22 +471,22 @@
446 471 $extraErrors = $this->config->get('extra_errors', '0');
447 472 $isCoreField = $field->core == 1;
448 473
449 474 if (!$isCoreField && !acym_level(ACYM_ENTERPRISE)) {
450 - return '';
475 + return;
451 476 }
452 477
453 478 if (!$showField) {
454 479 if (!acym_isAdmin()) {
455 - return '';
480 + return;
456 481 }
457 482
458 483 if (acym_isAdmin() && acym_level(ACYM_ENTERPRISE) && !$isCoreField) {
459 - return '';
484 + return;
460 485 }
461 486 }
462 487
463 - $return = '';
488 + $data = [];
464 489
465 490 $cmsUser = false;
466 491 if ($displayFront && !empty($user->id)) {
467 492 $cmsUser = !empty($user->cms_id);
@@ -497,8 +522,9 @@
497 522 }
498 523 $valuesArray = $valuesArrayTmp;
499 524 }
500 525
526 + // Translate text of the options
501 527 if (is_array($valuesArray)) {
502 528 foreach ($valuesArray as $key => $oneValue) {
503 529 if (is_object($oneValue) && !empty($valuesArray[$key]->text)) {
504 530 $valuesArray[$key]->text = acym_translation($valuesArray[$key]->text);
@@ -516,49 +542,78 @@
516 542 }
517 543
518 544 $field->name = acym_translation($field->name);
519 545
520 - if (empty((array)$field->option) || !isset($field->option->authorized_content)) {
521 - $authorizedContent = '';
522 - } else {
546 + if (!empty((array)$field->option) && isset($field->option->authorized_content)) {
523 547 if (empty($field->option->error_message_invalid)) {
524 548 $field->option->authorized_content->message = acym_translationSprintf('ACYM_INCORRECT_FIELD_VALUE', $field->name);
525 549 } else {
526 550 $field->option->authorized_content->message = $field->option->error_message_invalid;
527 551 }
528 - $authorizedContent = ' data-authorized-content="'.acym_escape($field->option->authorized_content, false).'"';
552 + $data['authorizedContent'] = json_encode($field->option->authorized_content);
529 553 }
530 554
531 555 $attributesSelectField = [];
532 - $maxCharacters = empty($field->option->max_characters) ? '' : ' maxlength="'.$field->option->max_characters.'"';
533 - $style = '';
556 + if (!empty($field->option->max_characters)) {
557 + $data['maxCharacters'] = $field->option->max_characters;
558 + }
534 559 if (!empty($field->option->size)) {
535 560 $attributesSelectField['style'] = 'width:'.$field->option->size.'px';
536 - $style = ' style="width:'.$field->option->size.'px"';
561 + $data['style'] = 'width:'.$field->option->size.'px';
537 562 }
538 563
539 - $placeholder = '';
540 564 if (!$displayOutside) {
541 - $placeholder = ' placeholder="'.acym_escape($field->name).'" aria-label="'.acym_escape($field->name).'"';
565 + $data['placeholder'] = $field->name;
542 566 }
543 567
544 568 $name = 'customField['.intval($field->id).']';
545 - $nameAttribute = ' name="'.$name.'"';
546 569 if ($field->type === 'text') {
547 - $value = ' value="'.acym_escape($defaultValue).'"';
570 + $data['value'] = empty($defaultValue)
571 + ? ''
572 + : (
573 + is_scalar($defaultValue)
574 + ? $defaultValue
575 + : json_encode($defaultValue)
576 + );
548 577 }
549 578
550 - if (
551 - $displayOutside
552 - && (
553 - in_array($field->id, [1, 2])
554 - || in_array($field->type, ['text', 'textarea', 'single_dropdown', 'multiple_dropdown', 'custom_text', 'file', 'language'])
555 - )
556 - ) {
557 - $return .= '<label class="cell margin-top-1"><span class="acym__users__creation__fields__title">'.$field->name.'</span>';
579 + $allowedFieldNameTags = array_merge(SecurityHelper::ALLOWED_HTML_FIELD_NAME, [
580 + 'a' => [
581 + 'href' => true,
582 + 'target' => true,
583 + 'title' => true,
584 + 'class' => true,
585 + 'rel' => true,
586 + 'aria-label' => true,
587 + ],
588 + ]);
589 +
590 + $labelTypes = ['text', 'textarea', 'single_dropdown', 'multiple_dropdown', 'custom_text', 'language'];
591 + $titleTypes = ['date', 'radio', 'checkbox', 'file'];
592 +
593 + $useLabel = $displayOutside && (in_array($field->id, [1, 2]) || in_array($field->type, $labelTypes));
594 + $useTitle = $displayOutside && in_array($field->type, $titleTypes);
595 +
596 + if ($useLabel) {
597 + echo '<label class="cell margin-top-1"><span class="acym__users__creation__fields__title">'.wp_kses(
598 + $field->name,
599 + SecurityHelper::ALLOWED_HTML_FIELD_NAME
600 + ).'</span>';
558 601 }
559 - if ($displayOutside && in_array($field->type, ['date', 'radio', 'checkbox'])) {
560 - $return .= '<div class="cell margin-top-1"><div class="acym__users__creation__fields__title">'.$field->name.'</div>';
602 + if ($useTitle) {
603 + if (in_array($field->type, ['date', 'radio', 'checkbox'])) {
604 + $groupLabelId = 'acym__field__label__'.intval($field->id).'_'.acym_rand(1000, 9999);
605 + echo '<div class="cell margin-top-1" role="group" aria-labelledby="'.esc_attr($groupLabelId).'"'.($field->required ? ' aria-required="true"' : '').'>';
606 + echo '<div id="'.esc_attr($groupLabelId).'" class="acym__users__creation__fields__title">'.wp_kses(
607 + $field->name,
608 + $allowedFieldNameTags
609 + ).'</div>';
610 + } else {
611 + echo '<div class="cell margin-top-1"><div class="acym__users__creation__fields__title">'.wp_kses(
612 + $field->name,
613 + $allowedFieldNameTags
614 + ).'</div>';
615 + }
561 616 }
562 617
563 618 $messageRequired = empty($field->option->error_message) ? '' : acym_translation($field->option->error_message);
564 619 $requiredJson = json_encode(['type' => $field->type, 'message' => $messageRequired]);
@@ -563,10 +618,13 @@
563 618 $messageRequired = empty($field->option->error_message) ? '' : acym_translation($field->option->error_message);
564 619 $requiredJson = json_encode(['type' => $field->type, 'message' => $messageRequired]);
565 620 $required = '';
566 621 if ($field->required) {
567 - $required = ' data-required="'.acym_escape($requiredJson).'"';
622 + $data['data-required'] = $requiredJson;
623 + $data['aria-required'] = 'true';
624 + $required = ' data-required="'.esc_attr($requiredJson).'"';
568 625 $attributesSelectField['data-required'] = $requiredJson;
626 + $attributesSelectField['aria-required'] = 'true';
569 627 }
570 628
571 629 $readonly = '';
572 630 if (!$showField && acym_isAdmin() && acym_level(ACYM_ENTERPRISE) && $isCoreField) {
@@ -573,81 +631,117 @@
573 631 $attributesSelectField['disabled'] = true;
574 632 unset($attributesSelectField['data-required']);
575 633 $readonly = ' disabled ';
576 634 $required = '';
635 + $data['disabled'] = true;
636 + unset($data['data-required']);
577 637 }
578 638
579 639 $defaultValue = $this->prepareDefaultValue($defaultValue, $field->type, $user);
580 640
581 641 if ($field->id == 1) {
582 - $nameAttribute = ' name="user[name]"';
583 - $inputTmp = '<input autocomplete="name" '.$nameAttribute.$placeholder.$required.$value.$authorizedContent.$style.$maxCharacters.$readonly.' type="text" class="cell">';
584 - if (!empty($readonly)) {
585 - $inputTmp = acym_tooltip(
642 + $data['type'] = 'text';
643 + $data['name'] = 'user[name]';
644 + $data['autocomplete'] = 'name';
645 + $data['class'] = 'cell';
646 +
647 + if (empty($readonly)) {
648 + include acym_getPartial('fields', 'input');
649 + } else {
650 + ob_start();
651 + include acym_getPartial('fields', 'input');
652 + $input = ob_get_clean();
653 +
654 + acym_tooltip(
586 655 [
587 - 'hoveredText' => $inputTmp,
656 + 'hoveredText' => $input,
588 657 'textShownInTooltip' => acym_translation('ACYM_CF_EDITION_BLOCKED'),
589 658 ]
590 659 );
591 660 }
592 - $return .= $inputTmp;
593 661 } elseif ($field->id == 2) {
594 - $nameAttribute = ' name="user[email]"';
595 - $uniqueId = 'email_field_'.rand(100, 900);
596 - $inputTmp = '<input
597 - autocomplete="email"
598 - id="'.$uniqueId.'"
599 - '.$nameAttribute.$placeholder.$value.$authorizedContent.$style.$maxCharacters.$readonly.'
600 - required
601 - type="email"
602 - class="cell acym__user__edit__email"
603 - '.($cmsUser ? 'readonly="readonly"' : '').'>';
604 - if (!empty($readonly)) {
605 - $inputTmp = acym_tooltip(
662 + $uniqueId = 'email_field_'.acym_rand(100, 900);
663 +
664 + unset($data['data-required']);
665 + $data['type'] = 'email';
666 + $data['name'] = 'user[email]';
667 + $data['autocomplete'] = 'email';
668 + $data['id'] = $uniqueId;
669 + $data['class'] = 'cell acym__user__edit__email';
670 + $data['required'] = true;
671 + $data['readonly'] = $cmsUser;
672 +
673 + if (empty($readonly)) {
674 + include acym_getPartial('fields', 'input');
675 + } else {
676 + ob_start();
677 + include acym_getPartial('fields', 'input');
678 + $input = ob_get_clean();
679 +
680 + acym_tooltip(
606 681 [
607 - 'hoveredText' => $inputTmp,
682 + 'hoveredText' => $input,
608 683 'textShownInTooltip' => acym_translation('ACYM_CF_EDITION_BLOCKED'),
609 684 ]
610 685 );
611 686 }
612 - $return .= $inputTmp;
687 +
613 688 if ($displayFront && !$cmsUser && !empty($this->config->get('email_spellcheck'))) {
614 - $return .= '<ul acym-data-field="'.$uniqueId.'" class="acym_email_suggestions" style="display: none;"></ul>';
689 + echo '<ul acym-data-field="'.esc_attr($uniqueId).'" class="acym_email_suggestions" style="display: none;"></ul>';
615 690 }
616 691 } elseif ($field->type === 'language') {
617 692 $attributesSelectField['class'] = 'acym__select';
618 - $selectTmp = acym_select(
619 - $this->getLanguagesForDropdown(),
620 - 'user[language]',
621 - $defaultValue,
622 - $attributesSelectField
623 - );
624 -
625 - if (!empty($readonly)) {
626 - $selectTmp = acym_tooltip(
693 + if (empty($readonly)) {
694 + acym_select(
695 + $this->getLanguagesForDropdown(),
696 + 'user[language]',
697 + $defaultValue,
698 + $attributesSelectField,
699 + 'value',
700 + 'text',
701 + null,
702 + false,
703 + true
704 + );
705 + } else {
706 + acym_tooltip(
627 707 [
628 - 'hoveredText' => $selectTmp,
708 + 'hoveredText' => acym_select(
709 + $this->getLanguagesForDropdown(),
710 + 'user[language]',
711 + $defaultValue,
712 + $attributesSelectField
713 + ),
629 714 'textShownInTooltip' => acym_translation('ACYM_CF_EDITION_BLOCKED'),
630 715 ]
631 716 );
632 717 }
633 - $return .= $selectTmp;
634 718 } elseif ($field->type === 'text') {
635 - $return .= '<input '.$nameAttribute.$placeholder.$required.$value.$authorizedContent.$style.$maxCharacters.' type="text">';
719 + $data['type'] = 'text';
720 + $data['name'] = $name;
721 + include acym_getPartial('fields', 'input');
636 722 } elseif ($field->type === 'textarea') {
637 - $return .= '<textarea '.$nameAttribute.$required.$maxCharacters.' rows="'.intval($field->option->rows).'" cols="'.intval(
638 - $field->option->columns
639 - ).'">'.$defaultValue.'</textarea>';
723 + echo '<textarea name="'.esc_attr($name).'"';
724 + if (!empty($data['data-required'])) {
725 + echo ' data-required="'.esc_attr($data['data-required']).'"';
726 + }
727 + if (!empty($data['aria-required'])) {
728 + echo ' aria-required="true"';
729 + }
730 + if (isset($data['maxCharacters'])) {
731 + echo ' maxlength="'.esc_attr($data['maxCharacters']).'"';
732 + }
733 + echo ' rows="'.intval($field->option->rows).'" cols="'.intval($field->option->columns).'">';
734 + echo esc_html(is_scalar($defaultValue) ? $defaultValue : '');
735 + echo '</textarea>';
640 736 } elseif ($field->type === 'radio') {
641 737 if ($displayFront) {
642 - foreach ($valuesArray as $key => $oneValue) {
643 - $isCkecked = $defaultValue == $key ? 'checked' : '';
644 - $return .= '<label><input '.$nameAttribute.$required.' type="radio" value="'.acym_escape(
645 - $key
646 - ).'" '.$isCkecked.'> '.$oneValue.'</label>';
647 - }
738 + $data['name'] = $name;
739 + $data['values'] = $valuesArray;
740 + $data['value'] = $defaultValue;
741 + include acym_getPartial('fields', 'radio');
648 742 } else {
649 - $return .= acym_radio(
743 + acym_radio(
650 744 $valuesArray,
651 745 $name.'[]',
652 746 $defaultValue,
653 747 $field->required ? ['data-required' => $requiredJson] : []
@@ -653,38 +747,25 @@
653 747 $field->required ? ['data-required' => $requiredJson] : []
654 748 );
655 749 }
656 750 } elseif ($field->type === 'checkbox') {
657 - $return .= '<input type="hidden" name="'.acym_escape($name).'" value="">';
658 - if ($displayFront) {
659 - foreach ($valuesArray as $key => $oneValue) {
660 - $checked = in_array($key, $defaultValue) ? 'checked' : '';
661 - $return .= '<label><input '.$required.' type="checkbox" name="'.$name.'['.acym_escape($key).']" value="'.acym_escape(
662 - $key
663 - ).'" '.$checked.'> '.$oneValue.'</label>';
664 - }
665 - } else {
666 - foreach ($valuesArray as $key => $oneValue) {
667 - if (in_array($key, $defaultValue)) {
668 - $labelClass = '';
669 - $attributes = 'checked '.$required;
670 - } else {
671 - $labelClass = 'class="cell margin-top-1"';
672 - $attributes = '';
673 - }
674 - $return .= '<label '.$labelClass.'>';
675 - $return .= '<input '.$attributes.' type="checkbox" name="'.$name.'['.acym_escape(
676 - $key
677 - ).']" class="acym__users__creation__fields__checkbox">'.$oneValue.'</label>';
678 - }
679 - }
751 + $data['name'] = $name;
752 + $data['values'] = $valuesArray;
753 + $data['value'] = $defaultValue;
754 + $data['displayFront'] = $displayFront;
755 + include acym_getPartial('fields', 'checkbox');
680 756 } elseif ($field->type === 'single_dropdown') {
681 757 $attributesSelectField['class'] = 'acym__custom__fields__select__form acym__select';
682 - $return .= acym_select(
758 + acym_select(
683 759 $valuesArray,
684 760 $name,
685 761 $defaultValue,
686 - $attributesSelectField
762 + $attributesSelectField,
763 + 'value',
764 + 'text',
765 + null,
766 + false,
767 + true
687 768 );
688 769 } elseif ($field->type === 'multiple_dropdown') {
689 770 $attributes = [
690 771 'class' => 'acym__custom__fields__select__multiple__form acym__select',
@@ -689,11 +770,22 @@
689 770 $attributes = [
690 771 'class' => 'acym__custom__fields__select__multiple__form acym__select',
691 772 'style' => empty($field->option->size) ? '' : 'width:'.$field->option->size.'px',
692 773 ];
693 - if ($field->required) $attributes['data-required'] = $displayFront ? acym_escape($requiredJson) : $requiredJson;
774 + if ($field->required) {
775 + $attributes['data-required'] = $displayFront ? esc_attr($requiredJson) : $requiredJson;
776 + $attributes['aria-required'] = 'true';
777 + }
694 778
695 - $return .= acym_selectMultiple($valuesArray, $name, $defaultValue, $attributes);
779 + acym_selectMultiple(
780 + $valuesArray,
781 + $name,
782 + $defaultValue,
783 + $attributes,
784 + 'value',
785 + 'text',
786 + true
787 + );
696 788 } elseif ($field->type === 'date') {
697 789 $attributes = [
698 790 'class' => 'acym__custom__fields__select__form acym__select',
699 791 'acym-field-type' => 'date',
@@ -700,22 +792,42 @@
700 792 ];
701 793 if (!empty($required)) {
702 794 $attributes['data-required'] = $requiredJson;
703 795 }
704 - $return .= acym_displayDateFormat(
705 - $field->option->format,
706 - $name.'[]',
707 - $defaultValue,
708 - $attributes
796 +
797 + echo wp_kses(
798 + acym_displayDateFormat(
799 + $field->option->format,
800 + $name.'[]',
801 + $defaultValue,
802 + $attributes
803 + ),
804 + array_merge_recursive(
805 + SecurityHelper::ALLOWED_HTML_SELECT,
806 + [
807 + 'select' => [
808 + 'data-required' => true,
809 + 'acym-field-type' => true,
810 + 'aria-label' => true,
811 + ],
812 + 'div' => [
813 + 'class' => true,
814 + ],
815 + ]
816 + )
709 817 );
710 818 } elseif ($field->type === 'file') {
711 819 if ($displayFront) {
712 - $return .= '<input '.$nameAttribute.$required.' type="file">';
820 + unset($data['placeholder'], $data['value'], $data['authorizedContent'], $data['style'], $data['maxCharacters']);
821 + $data['type'] = 'file';
822 + $data['name'] = $name;
823 + include acym_getPartial('fields', 'input');
713 824 } else {
714 825 $downloadUrl = '';
715 826 if (!empty($defaultValue) && !empty($user->id)) {
716 827 $fileName = is_array($defaultValue) ? $defaultValue[0] : $defaultValue;
717 - if (!empty($fileName)) {
828 + $fileName = basename($fileName);
829 + if (!empty($fileName) && acym_fileNameValid($fileName)) {
718 830 $uploadFolder = trim(acym_cleanPath(html_entity_decode(acym_getFilesFolder(true))), DS.' ').DS;
719 831 $userFilePath = $uploadFolder.'userfiles'.DS.$user->id.DS.$fileName;
720 832 $legacyFilePath = $uploadFolder.'userfiles'.DS.$fileName;
721 833
@@ -725,43 +837,68 @@
725 837 $downloadUrl = ACYM_LIVE.str_replace(DS, '/', $legacyFilePath);
726 838 }
727 839 }
728 840 }
729 - $return .= acym_inputFile($name.'[]', $defaultValue, '', $required, $downloadUrl);
841 + $attributes = [];
842 + if (!empty($required)) {
843 + $attributes['data-required'] = $requiredJson;
844 + }
845 + acym_inputFile($name.'[]', $defaultValue, '', $attributes, $downloadUrl);
730 846 }
731 847 } elseif ($field->type === 'phone') {
732 848 $indicator = !empty($defaultValue[0]) ? $defaultValue[0] : '';
733 849 $number = !empty($defaultValue[1]) ? $defaultValue[1] : '';
734 850
735 - if ($displayOutside) $return .= '<div class="cell margin-top-1 grid-x"><div class="acym__users__creation__fields__title cell">'.$field->name.'</div>';
736 - $return .= '<div class="cell large-5 medium-4 padding-next-1">';
737 - $return .= acym_generateCountryNumber($name.'[code]', $indicator);
738 - $return .= '</div>';
739 - $return .= '<input autocomplete="tel-national" '.$placeholder.$required.$style.$maxCharacters.' class="cell large-7 medium-8" type="tel" name="'.$name.'[phone]" value="'.acym_escape(
740 - $number
741 - ).'">';
742 - if ($displayOutside) $return .= '</div>';
851 + if ($displayOutside) {
852 + $phoneLabelId = 'acym__field__label__'.intval($field->id).'_'.acym_rand(1000, 9999);
853 + echo '<div class="cell margin-top-1 grid-x" role="group" aria-labelledby="'.esc_attr($phoneLabelId).'"'.($field->required ? ' aria-required="true"' : '').'>';
854 + echo '<div id="'.esc_attr($phoneLabelId).'" class="acym__users__creation__fields__title cell">'.wp_kses(
855 + $field->name,
856 + SecurityHelper::ALLOWED_HTML_FIELD_NAME
857 + ).'</div>';
858 + }
859 + echo '<div class="cell large-5 medium-4 padding-next-1">';
860 + acym_generateCountryNumber($name.'[code]', $indicator);
861 + echo '</div>';
862 +
863 + unset($data['authorizedContent']);
864 + $data['type'] = 'tel';
865 + $data['name'] = $name.'[phone]';
866 + $data['autocomplete'] = 'tel-national';
867 + $data['class'] = 'cell large-7 medium-8';
868 + $data['value'] = $number;
869 + if ($displayOutside) {
870 + $data['aria-label'] = acym_translation('ACYM_PHONE_NUMBER');
871 + }
872 + include acym_getPartial('fields', 'input');
873 +
874 + if ($displayOutside) {
875 + echo '</div>';
876 + }
743 877 } elseif ($field->type === 'custom_text') {
744 - $return .= $field->option->custom_text;
878 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Admin-defined custom HTML content, intentionally rendered as-is.
879 + echo $field->option->custom_text;
745 880 }
746 881
747 - $labelTypes = ['text', 'textarea', 'single_dropdown', 'multiple_dropdown', 'custom_text', 'language'];
748 - if ($displayOutside && (in_array($field->id, [1, 2]) || in_array($field->type, $labelTypes))) {
749 - $return .= '</label>';
882 + if ($useLabel) {
883 + echo '</label>';
750 884 }
751 - if ($displayOutside && in_array($field->type, ['date', 'radio', 'checkbox'])) {
752 - $return .= '</div>';
885 + if ($useTitle) {
886 + echo '</div>';
753 887 }
754 888
755 - $return .= '<div class="acym__field__error__block" data-acym-field-id="'.intval($field->id).'"></div>';
889 + echo '<div class="acym__field__error__block" data-acym-field-id="'.intval($field->id).'" aria-live="assertive"></div>';
756 890 if ($displayFront && $extraErrors && !acym_isAdmin()) {
757 - $return .= '<div class="acym__message__invalid__field acym__color__error" style="display: none;">';
758 - $return .= '<i class="acymicon-times-circle acym__cross__invalid acym__color__error"></i>'.acym_translation('ACYM_THANKS_TO_FILL_IN_THIS_FIELD').'</div>';
891 + echo '<div class="acym__message__invalid__field acym__color__error" style="display: none;">';
892 + echo '<i class="acymicon-times-circle acym__cross__invalid acym__color__error"></i>'.esc_html(acym_translation('ACYM_THANKS_TO_FILL_IN_THIS_FIELD')).'</div>';
759 893 }
760 -
761 - return $return;
762 894 }
763 895
896 + /**
897 + * @param mixed $defaultValue
898 + *
899 + * @return mixed
900 + */
764 901 private function prepareDefaultValue($defaultValue, string $fieldType, ?object $user)
765 902 {
766 903 if ($fieldType === 'language') {
767 904 if (empty($defaultValue) && !empty($user->language)) {
@@ -825,8 +962,9 @@
825 962 }
826 963
827 964 acym_arrayToInteger($elements);
828 965
966 + // Core fields can't be disabled
829 967 $excludedFields = acym_loadResultArray('SELECT `id` FROM #__acym_field WHERE `core` = 1');
830 968 acym_arrayToInteger($excludedFields);
831 969
832 970 $elements = array_diff($elements, $excludedFields);
@@ -890,43 +1028,46 @@
890 1028
891 1029 return $dataLanguages;
892 1030 }
893 1031
894 - public function setEmailConfirmationField(bool $displayOutside, object $field, string $container = 'div', bool $displayInline = false): string
1032 + public function setEmailConfirmationField(bool $displayOutside, object $field, string $container = 'div', bool $displayInline = false): void
895 1033 {
896 - $uniqueId = 'email_confirmation_field_'.rand(100, 900);
1034 + $uniqueId = 'email_confirmation_field_'.acym_rand(100, 900);
897 1035
898 - $return = '<'.$container.' class="onefield acym_email_confirmation_field acyfield_text">';
1036 + echo '<'.esc_attr($container).' class="onefield acym_email_confirmation_field acyfield_text">';
899 1037 if ($displayOutside) {
900 - $return .= '<label class="cell margin-top-1">';
901 - $return .= '<span class="acym__users__creation__fields__title">'.acym_translation('ACYM_EMAIL_CONFIRMATION').'</span>';
1038 + echo '<label class="cell margin-top-1">';
1039 + echo '<span class="acym__users__creation__fields__title">'.esc_html(acym_translation('ACYM_EMAIL_CONFIRMATION')).'</span>';
902 1040 }
903 1041
904 - $style = empty($field->option->size) ? '' : ' style="width:'.intval($field->option->size).'px"';
905 1042 $placeholder = !$displayOutside ? acym_translation('ACYM_EMAIL_CONFIRMATION') : '';
906 - $return .= '<input id="'.acym_escape($uniqueId).'"
907 - '.$style.'
908 - required
909 - type="email"
910 - class="cell acym__user__edit__email"
911 - name="user[email_confirmation]"
912 - placeholder="'.acym_escape($placeholder).'" />';
913 - $return .= '<span class="acym__field__error__block"></span>';
1043 + echo '<input id="'.esc_attr($uniqueId).'"
1044 + '.(empty($field->option->size) ? '' : ' style="width:'.intval($field->option->size).'px"').'
1045 + required
1046 + type="email"
1047 + class="cell acym__user__edit__email"
1048 + name="user[email_confirmation]"';
1049 + if (!$displayOutside) {
1050 + echo ' aria-label="'.esc_attr(acym_translation('ACYM_EMAIL_CONFIRMATION')).'"';
1051 + }
1052 + echo ' placeholder="'.esc_attr($placeholder).'" />';
1053 + echo '<span class="acym__field__error__block" aria-live="assertive"></span>';
914 1054
915 1055 $extraErrors = $this->config->get('extra_errors', '0');
916 1056 if ($extraErrors && !acym_isAdmin()) {
917 - $return .= '<span class="acym__message__invalid__field acym__color__error" style="display: none;">';
918 - $return .= '<i class="acymicon-times-circle acym__cross__invalid acym__color__error"></i>'.acym_translation('ACYM_THANKS_TO_FILL_IN_THIS_FIELD').'</span>';
1057 + echo '<span class="acym__message__invalid__field acym__color__error" style="display: none;">';
1058 + echo '<i class="acymicon-times-circle acym__cross__invalid acym__color__error"></i>';
1059 + echo esc_html(acym_translation('ACYM_THANKS_TO_FILL_IN_THIS_FIELD'));
1060 + echo '</span>';
919 1061 }
920 1062
921 1063 if ($displayOutside) {
922 - $return .= '</label>';
1064 + echo '</label>';
923 1065 }
924 - $return .= '</'.$container.'>';
1066 + echo '</'.esc_attr($container).'>';
925 1067
926 1068 if ($container === 'td' && !$displayInline) {
927 - $return .= '</tr>';
1069 + //TODO either open tr here, or close it where setEmailConfirmationField is called
1070 + echo '</tr>';
928 1071 }
929 -
930 - return $return;
931 1072 }
932 1073 }