| @@ -142,10 +142,11 @@ | ||
| 142 | 142 | } |
| 143 | 143 | |
| 144 | 144 | public function store(int $userID, array $fields, bool $ajax = false): void |
| 145 | 145 | { |
| 146 | + $uploadedFileFields = []; | |
| 146 | 147 | $customField = acym_getVar('array', 'customField', [], 'FILES'); |
| 147 | - if (!empty($customField)) { | |
| 148 | + if (!empty($customField['tmp_name'])) { | |
| 148 | 149 | $uploadFolder = trim(acym_cleanPath(html_entity_decode(acym_getFilesFolder(true))), DS.' ').DS; |
| 149 | 150 | $uploadPath = acym_cleanPath(ACYM_ROOT.$uploadFolder.'userfiles'.DS.$userID.DS); |
| 150 | 151 | if (!file_exists($uploadPath)) { |
| 151 | 152 | acym_createDir($uploadPath); |
| @@ -160,10 +161,11 @@ | ||
| 160 | 161 | $fileName = $customField['name'][$key]; |
| 161 | 162 | while (is_array($fileName) && isset($fileName[0])) { |
| 162 | 163 | $fileName = $fileName[0]; |
| 163 | 164 | } |
| 165 | + $fileName = basename($fileName); | |
| 164 | 166 | |
| 165 | - if (!preg_match('#\.('.implode('|', $allowedExtensions).')$#Ui', $fileName)) { | |
| 167 | + if (!acym_fileNameValid($fileName) || !preg_match('#\.('.implode('|', array_map('preg_quote', $allowedExtensions)).')$#i', $fileName)) { | |
| 166 | 168 | $ext = substr($fileName, strrpos($fileName, '.') + 1); |
| 167 | 169 | if ($ajax) { |
| 168 | 170 | $this->errors[] = acym_translationSprintf( |
| 169 | 171 | 'ACYM_ACCEPTED_TYPE', |
| @@ -184,9 +186,9 @@ | ||
| 184 | 186 | |
| 185 | 187 | continue; |
| 186 | 188 | } |
| 187 | 189 | |
| 188 | - if (!acym_uploadFile($value, $uploadPath.$fileName)) { | |
| 190 | + if (!acym_isPathWithinDirectory($uploadPath, $uploadPath.$fileName) || !acym_uploadFile($value, $uploadPath.$fileName)) { | |
| 189 | 191 | if ($ajax) { |
| 190 | 192 | $this->errors[] = acym_translation('ACYM_ERROR_SAVING'); |
| 191 | 193 | } else { |
| 192 | 194 | acym_enqueueMessage(acym_translation('ACYM_ERROR_SAVING'), 'error'); |
| @@ -193,9 +195,10 @@ | ||
| 193 | 195 | } |
| 194 | 196 | |
| 195 | 197 | continue; |
| 196 | 198 | } |
| 197 | - $fields[$key] = $customField['name'][$key]; | |
| 199 | + $fields[$key] = $fileName; | |
| 200 | + $uploadedFileFields[$key] = true; | |
| 198 | 201 | } |
| 199 | 202 | } |
| 200 | 203 | |
| 201 | 204 | if (empty($fields)) return; |
| @@ -248,8 +251,13 @@ | ||
| 248 | 251 | if (in_array($field->type, ['text', 'textarea']) && !empty($fieldOptions->max_characters)) { |
| 249 | 252 | $value = substr($value, 0, $fieldOptions->max_characters); |
| 250 | 253 | } |
| 251 | 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 | + | |
| 252 | 260 | // If deleting a file field, also delete the physical file |
| 253 | 261 | if ($field->type === 'file' && strlen($value) === 0) { |
| 254 | 262 | $oldValue = acym_loadResult( |
| 255 | 263 | 'SELECT `value` FROM #__acym_user_has_field WHERE `user_id` = '.intval($userID).' AND `field_id` = '.intval($id) |
| @@ -258,15 +266,21 @@ | ||
| 258 | 266 | $fileName = @json_decode($oldValue, true); |
| 259 | 267 | if (is_array($fileName)) $fileName = $fileName[0]; |
| 260 | 268 | if (empty($fileName)) $fileName = $oldValue; |
| 261 | 269 | |
| 262 | - $uploadFolder = trim(acym_cleanPath(html_entity_decode(acym_getFilesFolder(true))), DS.' ').DS; | |
| 263 | - // Try new path (with user folder) first, then legacy path (without user folder) | |
| 264 | - $filePath = acym_cleanPath(ACYM_ROOT.$uploadFolder.'userfiles'.DS.$userID.DS.$fileName); | |
| 265 | - if (!file_exists($filePath)) { | |
| 266 | - $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 | + } | |
| 267 | 282 | } |
| 268 | - acym_deleteFile($filePath); | |
| 269 | 283 | } |
| 270 | 284 | } |
| 271 | 285 | |
| 272 | 286 | // The user removed a value, don't add an empty line and remove any previous value from the bd |
| @@ -561,19 +575,45 @@ | ||
| 561 | 575 | : json_encode($defaultValue) |
| 562 | 576 | ); |
| 563 | 577 | } |
| 564 | 578 | |
| 565 | - if ( | |
| 566 | - $displayOutside | |
| 567 | - && ( | |
| 568 | - in_array($field->id, [1, 2]) | |
| 569 | - || in_array($field->type, ['text', 'textarea', 'single_dropdown', 'multiple_dropdown', 'custom_text', 'file', 'language']) | |
| 570 | - ) | |
| 571 | - ) { | |
| 572 | - echo '<label class="cell margin-top-1"><span class="acym__users__creation__fields__title">'.esc_html($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>'; | |
| 573 | 601 | } |
| 574 | - if ($displayOutside && in_array($field->type, ['date', 'radio', 'checkbox'])) { | |
| 575 | - echo '<div class="cell margin-top-1"><div class="acym__users__creation__fields__title">'.esc_html($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 | + } | |
| 576 | 616 | } |
| 577 | 617 | |
| 578 | 618 | $messageRequired = empty($field->option->error_message) ? '' : acym_translation($field->option->error_message); |
| 579 | 619 | $requiredJson = json_encode(['type' => $field->type, 'message' => $messageRequired]); |
| @@ -579,10 +619,12 @@ | ||
| 579 | 619 | $requiredJson = json_encode(['type' => $field->type, 'message' => $messageRequired]); |
| 580 | 620 | $required = ''; |
| 581 | 621 | if ($field->required) { |
| 582 | 622 | $data['data-required'] = $requiredJson; |
| 623 | + $data['aria-required'] = 'true'; | |
| 583 | 624 | $required = ' data-required="'.esc_attr($requiredJson).'"'; |
| 584 | 625 | $attributesSelectField['data-required'] = $requiredJson; |
| 626 | + $attributesSelectField['aria-required'] = 'true'; | |
| 585 | 627 | } |
| 586 | 628 | |
| 587 | 629 | $readonly = ''; |
| 588 | 630 | if (!$showField && acym_isAdmin() && acym_level(ACYM_ENTERPRISE) && $isCoreField) { |
| @@ -681,8 +723,11 @@ | ||
| 681 | 723 | echo '<textarea name="'.esc_attr($name).'"'; |
| 682 | 724 | if (!empty($data['data-required'])) { |
| 683 | 725 | echo ' data-required="'.esc_attr($data['data-required']).'"'; |
| 684 | 726 | } |
| 727 | + if (!empty($data['aria-required'])) { | |
| 728 | + echo ' aria-required="true"'; | |
| 729 | + } | |
| 685 | 730 | if (isset($data['maxCharacters'])) { |
| 686 | 731 | echo ' maxlength="'.esc_attr($data['maxCharacters']).'"'; |
| 687 | 732 | } |
| 688 | 733 | echo ' rows="'.intval($field->option->rows).'" cols="'.intval($field->option->columns).'">'; |
| @@ -727,8 +772,9 @@ | ||
| 727 | 772 | 'style' => empty($field->option->size) ? '' : 'width:'.$field->option->size.'px', |
| 728 | 773 | ]; |
| 729 | 774 | if ($field->required) { |
| 730 | 775 | $attributes['data-required'] = $displayFront ? esc_attr($requiredJson) : $requiredJson; |
| 776 | + $attributes['aria-required'] = 'true'; | |
| 731 | 777 | } |
| 732 | 778 | |
| 733 | 779 | acym_selectMultiple( |
| 734 | 780 | $valuesArray, |
| @@ -760,8 +806,9 @@ | ||
| 760 | 806 | [ |
| 761 | 807 | 'select' => [ |
| 762 | 808 | 'data-required' => true, |
| 763 | 809 | 'acym-field-type' => true, |
| 810 | + 'aria-label' => true, | |
| 764 | 811 | ], |
| 765 | 812 | 'div' => [ |
| 766 | 813 | 'class' => true, |
| 767 | 814 | ], |
| @@ -777,9 +824,10 @@ | ||
| 777 | 824 | } else { |
| 778 | 825 | $downloadUrl = ''; |
| 779 | 826 | if (!empty($defaultValue) && !empty($user->id)) { |
| 780 | 827 | $fileName = is_array($defaultValue) ? $defaultValue[0] : $defaultValue; |
| 781 | - if (!empty($fileName)) { | |
| 828 | + $fileName = basename($fileName); | |
| 829 | + if (!empty($fileName) && acym_fileNameValid($fileName)) { | |
| 782 | 830 | $uploadFolder = trim(acym_cleanPath(html_entity_decode(acym_getFilesFolder(true))), DS.' ').DS; |
| 783 | 831 | $userFilePath = $uploadFolder.'userfiles'.DS.$user->id.DS.$fileName; |
| 784 | 832 | $legacyFilePath = $uploadFolder.'userfiles'.DS.$fileName; |
| 785 | 833 | |
| @@ -800,9 +848,14 @@ | ||
| 800 | 848 | $indicator = !empty($defaultValue[0]) ? $defaultValue[0] : ''; |
| 801 | 849 | $number = !empty($defaultValue[1]) ? $defaultValue[1] : ''; |
| 802 | 850 | |
| 803 | 851 | if ($displayOutside) { |
| 804 | - echo '<div class="cell margin-top-1 grid-x"><div class="acym__users__creation__fields__title cell">'.esc_html($field->name).'</div>'; | |
| 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>'; | |
| 805 | 858 | } |
| 806 | 859 | echo '<div class="cell large-5 medium-4 padding-next-1">'; |
| 807 | 860 | acym_generateCountryNumber($name.'[code]', $indicator); |
| 808 | 861 | echo '</div>'; |
| @@ -812,8 +865,11 @@ | ||
| 812 | 865 | $data['name'] = $name.'[phone]'; |
| 813 | 866 | $data['autocomplete'] = 'tel-national'; |
| 814 | 867 | $data['class'] = 'cell large-7 medium-8'; |
| 815 | 868 | $data['value'] = $number; |
| 869 | + if ($displayOutside) { | |
| 870 | + $data['aria-label'] = acym_translation('ACYM_PHONE_NUMBER'); | |
| 871 | + } | |
| 816 | 872 | include acym_getPartial('fields', 'input'); |
| 817 | 873 | |
| 818 | 874 | if ($displayOutside) { |
| 819 | 875 | echo '</div>'; |
| @@ -822,17 +878,16 @@ | ||
| 822 | 878 | // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Admin-defined custom HTML content, intentionally rendered as-is. |
| 823 | 879 | echo $field->option->custom_text; |
| 824 | 880 | } |
| 825 | 881 | |
| 826 | - $labelTypes = ['text', 'textarea', 'single_dropdown', 'multiple_dropdown', 'custom_text', 'language']; | |
| 827 | - if ($displayOutside && (in_array($field->id, [1, 2]) || in_array($field->type, $labelTypes))) { | |
| 882 | + if ($useLabel) { | |
| 828 | 883 | echo '</label>'; |
| 829 | 884 | } |
| 830 | - if ($displayOutside && in_array($field->type, ['date', 'radio', 'checkbox'])) { | |
| 885 | + if ($useTitle) { | |
| 831 | 886 | echo '</div>'; |
| 832 | 887 | } |
| 833 | 888 | |
| 834 | - echo '<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>'; | |
| 835 | 890 | if ($displayFront && $extraErrors && !acym_isAdmin()) { |
| 836 | 891 | echo '<div class="acym__message__invalid__field acym__color__error" style="display: none;">'; |
| 837 | 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>'; |
| 838 | 893 | } |
| @@ -984,16 +1039,19 @@ | ||
| 984 | 1039 | echo '<span class="acym__users__creation__fields__title">'.esc_html(acym_translation('ACYM_EMAIL_CONFIRMATION')).'</span>'; |
| 985 | 1040 | } |
| 986 | 1041 | |
| 987 | 1042 | $placeholder = !$displayOutside ? acym_translation('ACYM_EMAIL_CONFIRMATION') : ''; |
| 988 | - echo '<input id="'.esc_attr($uniqueId).'" | |
| 989 | - '.(empty($field->option->size) ? '' : ' style="width:'.intval($field->option->size).'px"').' | |
| 990 | - required | |
| 991 | - type="email" | |
| 992 | - class="cell acym__user__edit__email" | |
| 993 | - name="user[email_confirmation]" | |
| 994 | - placeholder="'.esc_attr($placeholder).'" />'; | |
| 995 | - echo '<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>'; | |
| 996 | 1054 | |
| 997 | 1055 | $extraErrors = $this->config->get('extra_errors', '0'); |
| 998 | 1056 | if ($extraErrors && !acym_isAdmin()) { |
| 999 | 1057 | echo '<span class="acym__message__invalid__field acym__color__error" style="display: none;">'; |