PluginProbe
Contact Forms by Cimatti / 2.1.0
Contact Forms by Cimatti v2.1.0
2.3.6 2.3.5 2.3.0 2.2.32 2.2.4 2.2.0 2.1.2 2.1.1 trunk 1.0 1.1 1.2 1.2.1 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 All 62 releases
contact-forms / classes / Element / File.php

File.php in Contact Forms by Cimatti 2.1.0, at classes/Element/File.php

288 lines 10.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 class AccuaForm_Element_File extends Element_File {
3 protected $destPath;
4 protected $alreadySubmittedText;
5 // Modern default extensions: documents, images, archives
6 protected $validExtensions = array(
7 // Documents
8 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp', 'txt', 'rtf', 'csv',
9 // Images
10 'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'heic',
11 // Archives
12 'zip', 'rar', '7z', 'tar', 'gz'
13 );
14 protected $maxSize; protected $limitsText;
15 protected $errorsText;
16 public function __construct($label, $name, ?array $properties = null) {
17 $this->alreadySubmittedText = __( "File already submitted.", 'contact-forms' );
18 // translators: %size% and %extensions% are custom placeholders replaced via str_replace(), not sprintf().
19 $this->limitsText = __( 'Maximum filesize: %size%. Allowed extensions: %extensions%.', 'contact-forms' ); // phpcs:ignore WordPress.WP.I18n.UnorderedPlaceholdersText -- Custom placeholders used with str_replace(), not sprintf()
20 $this->errorsText = array(
21 'upload' => __( 'Upload failed, please retry. If the problem persists please contact us', 'contact-forms' ),
22 'size' => __( 'Uploaded file is too big', 'contact-forms' ),
23 'empty' => __( 'Uploaded file is empty', 'contact-forms' ),
24 'name' => __( 'File name is not valid. Please rename it avoiding unusual characters', 'contact-forms' ),
25 // translators: %extensions% is a custom placeholder replaced via str_replace().
26 'ext' => __( 'File extension not allowed. Allowed extensions are %extensions%', 'contact-forms' ),
27 );
28
29 // Use WordPress upload directory function instead of hardcoded path
30 $wp_upload_dir = wp_upload_dir();
31 $accua_upload_dir = $wp_upload_dir['basedir'] . '/accua-forms';
32
33 // Create directory if it doesn't exist using WordPress function
34 if (!file_exists($accua_upload_dir)) {
35 wp_mkdir_p($accua_upload_dir);
36 }
37
38 // Use trailingslashit to ensure path ends with a slash
39 $this->destPath = trailingslashit($accua_upload_dir);
40 $this->maxSize = self::file_upload_max_size();
41 parent::__construct($label, $name, $properties);
42 }
43
44 public function __sleep() {
45 return array("attributes", "label", "validation", 'alreadySubmittedText', 'validExtensions', 'maxSize', 'limitsText', 'errorsText', 'destPath');
46 }
47
48 public function render() {
49 if(empty($this->attributes["value"])){
50 // Build help text for limits
51 $helpText = '';
52 if ($this->limitsText !== ''){
53 if ($this->validExtensions) {
54 $validExtensions = implode(', ', $this->validExtensions);
55 } else {
56 $validExtensions = '*';
57 }
58 $search = array('%size%', '%extensions%');
59 $replace = array(self::format_size($this->maxSize), $validExtensions);
60 $helpText = str_replace($search, $replace, $this->limitsText);
61 }
62
63 // Build accept attribute for file extensions
64 $acceptAttr = '';
65 if ($this->validExtensions && is_array($this->validExtensions)) {
66 $acceptAttr = '.' . implode(',.', $this->validExtensions);
67 }
68
69 // Generate unique IDs for ARIA associations
70 $inputId = esc_attr($this->getID() ?: 'accua-file-' . $this->getName());
71 $helpId = $inputId . '-help';
72 $announceId = $inputId . '-announce';
73
74 // Open wrapper with data attributes for JS
75 echo '<div class="accua-file-upload-wrapper" data-max-size="' . esc_attr($this->maxSize) . '" data-accept="' . esc_attr($acceptAttr) . '">';
76
77 // Dropzone container
78 echo '<div class="accua-file-dropzone" role="button" tabindex="0" ';
79 echo 'aria-describedby="' . esc_attr($helpId) . '" ';
80 echo 'aria-dropeffect="none">';
81
82 // Visual content for dropzone
83 echo '<span class="accua-file-dropzone-text">';
84 echo '<span class="accua-file-dropzone-icon" aria-hidden="true">📁</span> ';
85 echo esc_html__('Drag & drop file here or', 'contact-forms') . ' ';
86 echo '<span class="accua-file-browse-btn">' . esc_html__('browse', 'contact-forms') . '</span>';
87 echo '</span>';
88
89 // Set accept attribute on native file input for browser file picker filtering
90 if ($acceptAttr !== '') {
91 $this->attributes['accept'] = $acceptAttr;
92 }
93
94 // Hidden native file input (for accessibility and form submission)
95 parent::render();
96
97 echo '</div>'; // .accua-file-dropzone
98
99 // Help text
100 if ($helpText !== '') {
101 echo '<p class="pfbc-help" id="' . esc_attr($helpId) . '">' . esc_html($helpText) . '</p>';
102 }
103
104 // File list container (populated by JS)
105 echo '<div class="accua-file-list" hidden aria-live="polite" aria-relevant="additions removals"></div>';
106
107 // Screen reader announcement region
108 echo '<div class="accua-file-sr-announcement" id="' . esc_attr($announceId) . '" aria-live="polite" aria-atomic="true" class="screen-reader-text"></div>';
109
110 echo '</div>'; // .accua-file-upload-wrapper
111
112 } else {
113 echo esc_html($this->alreadySubmittedText);
114 }
115 }
116
117 public function getAlreadySubmittedText() {
118 return $this->alreadySubmittedText;
119 }
120
121 public function hasHelpText() {
122 return !empty($this->limitsText);
123 }
124
125 public function appendToPostHTML($html) {
126 $this->postHTML = ($this->postHTML ?? '') . $html;
127 }
128
129 public static function format_size($size) {
130 if ($size >= 1073741824) {
131 return round($size/1073741824, 2).' GB';
132 } else if ($size >= 1048576) {
133 return round($size/1048576, 2).' MB';
134 } else if ($size >= 1024) {
135 return round($size/1024, 2).' KB';
136 } else if ($size > 0) {
137 return round($size).' B';
138 } else {
139 return '-';
140 }
141 }
142
143 public static function parse_size($value) {
144 $value = strtolower( trim( $value ) );
145 $bytes = (float) $value;
146
147 if ( false !== strpos( $value, 'g' ) ) {
148 $bytes *= 1073741824; // 1024 * 1024 * 1024
149 } elseif ( false !== strpos( $value, 'm' ) ) {
150 $bytes *= 1048576; // 1024 * 1024
151 } elseif ( false !== strpos( $value, 'k' ) ) {
152 $bytes *= 1024;
153 }
154
155 return min( $bytes, PHP_INT_MAX );
156 }
157
158 public static function file_upload_max_size() {
159 static $max_size = -1;
160
161 if ($max_size < 0) {
162 $max_size = wp_max_upload_size();
163 }
164 return $max_size;
165 }
166
167 public function setMaxSize($size) {
168 $phpMaxSize = self::file_upload_max_size();
169 $size = self::parse_size($size);
170 return $this->maxSize = ($phpMaxSize < $size) ? $phpMaxSize : $size;
171 }
172
173 public function setErrorsText($errors){
174 if(is_array($errors)) {
175 $this->errorsText = $errors + $this->errorsText;
176 }
177 }
178 public function setDestPath($path) {
179 // Check if path is absolute BEFORE normalizing (path_is_absolute handles both Unix and Windows)
180 $is_absolute = path_is_absolute($path);
181
182 // Normalize path separators for consistent handling
183 $path = wp_normalize_path($path);
184
185 // Handle relative paths by prepending ABSPATH
186 if (!$is_absolute) {
187 $path = path_join(ABSPATH, $path);
188 }
189
190 // Create directory with proper WordPress function and secure permissions
191 if (!file_exists($path)) {
192 wp_mkdir_p($path);
193 }
194
195 // Verify the directory exists and is writable
196 if (!is_dir($path) || !wp_is_writable($path)) {
197 // Fall back to WordPress uploads directory if target path is not writable
198 $upload_dir = wp_upload_dir();
199 $path = $upload_dir['basedir'] . '/accua-forms';
200 wp_mkdir_p($path);
201 }
202
203 return $this->destPath = trailingslashit($path);
204 }
205
206 public function handle_upload($filedata){
207 $valid = true;
208 $file = array(
209 'dest_path' => $this->destPath,
210 );
211
212 if (!empty($filedata['error'])){
213 $valid = false;
214 switch($filedata['error']){
215 case UPLOAD_ERR_INI_SIZE:
216 case UPLOAD_ERR_FORM_SIZE:
217 $file['errors'][] = $this->errorsText['size'];
218 break;
219 default:
220 $file['errors'][] = $this->errorsText['upload'];
221 }
222 } else if ($filedata['size'] <= 0) {
223 $valid = false;
224 $file['errors'][] = $this->errorsText['empty'];
225 } else if ($this->maxSize > 0 && $filedata['size'] > $this->maxSize) {
226 $valid = false;
227 $file['errors'][] = $this->errorsText['size'];
228 } else {
229 $file['size'] = $filedata['size'];
230 }
231
232 if (isset($filedata['name']) && $filedata['name'] !== ''){
233 $file['name'] = $filedata['name'];
234 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) ) {
235 $valid = false;
236 $file['errors'][] = $this->errorsText['name'];
237 } else if ($this->validExtensions) {
238 $ext = strrchr($file['name'], '.');
239 $ext = ($ext === false) ? '' : strtolower(ltrim($ext, '.'));
240 $valid_ext_lower = array_map('strtolower', $this->validExtensions);
241 if (!in_array($ext, $valid_ext_lower, true)) {
242 $valid = false;
243 $file['errors'][] = str_replace('%extensions%', implode(', ',$this->validExtensions), $this->errorsText['ext']);
244 }
245 }
246 } else {
247 $file['name'] = '';
248 if ($valid) {
249 $valid = false;
250 $file['errors'][] = $this->errorsText['name'];
251 }
252 }
253 if ($valid) {
254 // Additional security check - validate MIME type
255 $file_info = wp_check_filetype_and_ext($filedata['tmp_name'], $file['name']);
256 if (empty($file_info['type'])) {
257 $valid = false;
258 $file['errors'][] = __('Invalid file type detected', 'contact-forms');
259 } else {
260 // Make sure destination directory exists
261 if (!is_dir($this->destPath)) {
262 wp_mkdir_p($this->destPath);
263 }
264
265 // Generate a unique filename with sanitization
266 do {
267 $tmpname = 'tmp' . wp_generate_password(8, false) . '_' . sanitize_file_name($file['name']);
268 } while (is_file($this->destPath.$tmpname));
269
270 // Move uploaded file
271 // phpcs:ignore Generic.PHP.ForbiddenFunctions.Found -- move_uploaded_file is required for file uploads, no WP alternative
272 $valid = move_uploaded_file($filedata['tmp_name'], $this->destPath.$tmpname);
273 if ($valid) {
274 $file['tmp_name'] = $tmpname;
275 // Set proper file permissions
276 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- Direct chmod needed for upload permissions
277 chmod($this->destPath.$tmpname, 0644);
278 } else {
279 $file['errors'][] = $this->errorsText['upload'];
280 }
281 }
282
283 }
284
285 return $file;
286 }
287 }
288