alreadySubmittedText = __( "File already submitted.", 'contact-forms' ); // translators: %size% and %extensions% are custom placeholders replaced via str_replace(), not sprintf(). $this->limitsText = __( 'Maximum filesize: %size%. Allowed extensions: %extensions%.', 'contact-forms' ); // phpcs:ignore WordPress.WP.I18n.UnorderedPlaceholdersText -- Custom placeholders used with str_replace(), not sprintf() $this->errorsText = array( 'upload' => __( 'Upload failed, please retry. If the problem persists please contact us', 'contact-forms' ), 'size' => __( 'Uploaded file is too big', 'contact-forms' ), 'empty' => __( 'Uploaded file is empty', 'contact-forms' ), 'name' => __( 'File name is not valid. Please rename it avoiding unusual characters', 'contact-forms' ), // translators: %extensions% is a custom placeholder replaced via str_replace(). 'ext' => __( 'File extension not allowed. Allowed extensions are %extensions%', 'contact-forms' ), ); // Use WordPress upload directory function instead of hardcoded path $wp_upload_dir = wp_upload_dir(); $accua_upload_dir = $wp_upload_dir['basedir'] . '/accua-forms'; // Create directory if it doesn't exist using WordPress function if (!file_exists($accua_upload_dir)) { wp_mkdir_p($accua_upload_dir); } // Use trailingslashit to ensure path ends with a slash $this->destPath = trailingslashit($accua_upload_dir); $this->maxSize = self::file_upload_max_size(); parent::__construct($label, $name, $properties); } public function __sleep() { return array("attributes", "label", "validation", 'alreadySubmittedText', 'validExtensions', 'maxSize', 'limitsText', 'errorsText', 'destPath'); } public function render() { if(empty($this->attributes["value"])){ // Build help text for limits $helpText = ''; if ($this->limitsText !== ''){ if ($this->validExtensions) { $validExtensions = implode(', ', $this->validExtensions); } else { $validExtensions = '*'; } $search = array('%size%', '%extensions%'); $replace = array(self::format_size($this->maxSize), $validExtensions); $helpText = str_replace($search, $replace, $this->limitsText); } // Build accept attribute for file extensions $acceptAttr = ''; if ($this->validExtensions && is_array($this->validExtensions)) { $acceptAttr = '.' . implode(',.', $this->validExtensions); } // Generate unique IDs for ARIA associations $inputId = esc_attr($this->getID() ?: 'accua-file-' . $this->getName()); $helpId = $inputId . '-help'; $announceId = $inputId . '-announce'; // Open wrapper with data attributes for JS echo '
'; // Dropzone container echo '
'; // Visual content for dropzone echo ''; echo ' '; echo esc_html__('Drag & drop file here or', 'contact-forms') . ' '; echo '' . esc_html__('browse', 'contact-forms') . ''; echo ''; // Set accept attribute on native file input for browser file picker filtering if ($acceptAttr !== '') { $this->attributes['accept'] = $acceptAttr; } // Hidden native file input (for accessibility and form submission) parent::render(); echo '
'; // .accua-file-dropzone // Help text if ($helpText !== '') { echo '

' . esc_html($helpText) . '

'; } // File list container (populated by JS) echo ''; // Screen reader announcement region echo '
'; echo '
'; // .accua-file-upload-wrapper } else { echo esc_html($this->alreadySubmittedText); } } public function getAlreadySubmittedText() { return $this->alreadySubmittedText; } public function hasHelpText() { return !empty($this->limitsText); } public function appendToPostHTML($html) { $this->postHTML = ($this->postHTML ?? '') . $html; } public static function format_size($size) { if ($size >= 1073741824) { return round($size/1073741824, 2).' GB'; } else if ($size >= 1048576) { return round($size/1048576, 2).' MB'; } else if ($size >= 1024) { return round($size/1024, 2).' KB'; } else if ($size > 0) { return round($size).' B'; } else { return '-'; } } public static function parse_size($value) { $value = strtolower( trim( $value ) ); $bytes = (float) $value; if ( false !== strpos( $value, 'g' ) ) { $bytes *= 1073741824; // 1024 * 1024 * 1024 } elseif ( false !== strpos( $value, 'm' ) ) { $bytes *= 1048576; // 1024 * 1024 } elseif ( false !== strpos( $value, 'k' ) ) { $bytes *= 1024; } return min( $bytes, PHP_INT_MAX ); } public static function file_upload_max_size() { static $max_size = -1; if ($max_size < 0) { $max_size = wp_max_upload_size(); } return $max_size; } public function setMaxSize($size) { $phpMaxSize = self::file_upload_max_size(); $size = self::parse_size($size); return $this->maxSize = ($phpMaxSize < $size) ? $phpMaxSize : $size; } public function setErrorsText($errors){ if(is_array($errors)) { $this->errorsText = $errors + $this->errorsText; } } public function setDestPath($path) { // Check if path is absolute BEFORE normalizing (path_is_absolute handles both Unix and Windows) $is_absolute = path_is_absolute($path); // Normalize path separators for consistent handling $path = wp_normalize_path($path); // Handle relative paths by prepending ABSPATH if (!$is_absolute) { $path = path_join(ABSPATH, $path); } // Create directory with proper WordPress function and secure permissions if (!file_exists($path)) { wp_mkdir_p($path); } // Verify the directory exists and is writable if (!is_dir($path) || !wp_is_writable($path)) { // Fall back to WordPress uploads directory if target path is not writable $upload_dir = wp_upload_dir(); $path = $upload_dir['basedir'] . '/accua-forms'; wp_mkdir_p($path); } return $this->destPath = trailingslashit($path); } public function handle_upload($filedata){ $valid = true; $file = array( 'dest_path' => $this->destPath, ); if (!empty($filedata['error'])){ $valid = false; switch($filedata['error']){ case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: $file['errors'][] = $this->errorsText['size']; break; default: $file['errors'][] = $this->errorsText['upload']; } } else if ($filedata['size'] <= 0) { $valid = false; $file['errors'][] = $this->errorsText['empty']; } else if ($this->maxSize > 0 && $filedata['size'] > $this->maxSize) { $valid = false; $file['errors'][] = $this->errorsText['size']; } else { $file['size'] = $filedata['size']; } if (isset($filedata['name']) && $filedata['name'] !== ''){ $file['name'] = $filedata['name']; if ((strpos($file['name'], "\0") !== false) || (strpbrk($file['name'], "\1\2\3\4\5\6\7\10\11\12\13\14\15\16\17\20\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37\177\\/:*?\"<>|") !== false) ) { $valid = false; $file['errors'][] = $this->errorsText['name']; } else if ($this->validExtensions) { $ext = strrchr($file['name'], '.'); $ext = ($ext === false) ? '' : strtolower(ltrim($ext, '.')); $valid_ext_lower = array_map('strtolower', $this->validExtensions); if (!in_array($ext, $valid_ext_lower, true)) { $valid = false; $file['errors'][] = str_replace('%extensions%', implode(', ',$this->validExtensions), $this->errorsText['ext']); } } } else { $file['name'] = ''; if ($valid) { $valid = false; $file['errors'][] = $this->errorsText['name']; } } if ($valid) { // Additional security check - validate MIME type $file_info = wp_check_filetype_and_ext($filedata['tmp_name'], $file['name']); if (empty($file_info['type'])) { $valid = false; $file['errors'][] = __('Invalid file type detected', 'contact-forms'); } else { // Make sure destination directory exists if (!is_dir($this->destPath)) { wp_mkdir_p($this->destPath); } // Generate a unique filename with sanitization do { $tmpname = 'tmp' . wp_generate_password(8, false) . '_' . sanitize_file_name($file['name']); } while (is_file($this->destPath.$tmpname)); // Move uploaded file // phpcs:ignore Generic.PHP.ForbiddenFunctions.Found -- move_uploaded_file is required for file uploads, no WP alternative $valid = move_uploaded_file($filedata['tmp_name'], $this->destPath.$tmpname); if ($valid) { $file['tmp_name'] = $tmpname; // Set proper file permissions // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- Direct chmod needed for upload permissions chmod($this->destPath.$tmpname, 0644); } else { $file['errors'][] = $this->errorsText['upload']; } } } return $file; } }