PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 2.15.3
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v2.15.3
3.3.1 V-3.3.0 3.2.2 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 V3.0.3 V3.0.2 -3.0.1 V_3.0.0 1.1.1 1.1.8 1.2 1.3 1.4 1.4.18 1.5.2 1.9 2.0 2.10.0 2.10.1 All 138 releases
← All changes | includes/Core/Util/FileHandler.php +74 -689 3.1.22.15.3 View file →
@@ -1,22 +1,29 @@
1 1 <?php
2 2
3 3 namespace BitCode\BitForm\Core\Util;
4 4
5 -use BitCode\BitForm\Admin\Form\Helpers;
6 5 use BitCode\BitForm\Core\Form\FormManager;
7 6 use BitCode\BitForm\enshrined\svgSanitize\Sanitizer;
8 7
9 -if (!defined('ABSPATH')) {
10 - exit;
11 -}
12 8 final class FileHandler
13 9 {
14 10 public function rmrf($dir)
15 11 {
16 - $fs = self::initWpFilesystem();
17 - if ($fs && $fs->exists($dir)) {
18 - $fs->delete($dir, true);
12 + if (is_dir($dir)) {
13 + $objects = scandir($dir);
14 + foreach ($objects as $object) {
15 + if ('.' !== $object && '..' !== $object) {
16 + if (is_dir($dir . DIRECTORY_SEPARATOR . $object) && !is_link($dir . DIRECTORY_SEPARATOR . $object)) {
17 + $this->rmrf($dir . DIRECTORY_SEPARATOR . $object);
18 + } else {
19 + wp_delete_file($dir . DIRECTORY_SEPARATOR . $object);
20 + }
21 + }
22 + }
23 + rmdir($dir);
24 + } else {
25 + wp_delete_file($dir);
19 26 }
20 27 }
21 28
22 29 public function cpyr($source, $destination)
@@ -21,9 +28,9 @@
21 28
22 29 public function cpyr($source, $destination)
23 30 {
24 31 if (is_dir($source)) {
25 - wp_mkdir_p($destination);
32 + mkdir($destination);
26 33 // chmod($destination, 0744);
27 34 $objects = scandir($source);
28 35 foreach ($objects as $object) {
29 36 if ('.' !== $object && '..' !== $object) {
@@ -43,28 +50,17 @@
43 50 }
44 51
45 52 public function moveUploadedFiles($file_details, $form_id, $entry_id)
46 53 {
47 - require_once ABSPATH . 'wp-admin/includes/file.php';
48 -
49 54 $file_upoalded = [];
50 - $_upload_dir = self::getEntriesFileUploadDir($form_id, $entry_id);
51 - $_upload_url = self::getEntriesFileUploadURL($form_id, $entry_id);
52 - $this::createIndexFile($_upload_dir);
53 -
54 - $upload_dir_filter = function ($uploads) use ($_upload_dir, $_upload_url) {
55 - $uploads['path'] = $_upload_dir;
56 - $uploads['url'] = $_upload_url;
57 - $uploads['subdir'] = '';
58 - $uploads['basedir'] = dirname($_upload_dir);
59 - $uploads['baseurl'] = dirname($_upload_url);
60 - return $uploads;
61 - };
62 -
55 + $_upload_dir = BITFORMS_UPLOAD_DIR . DIRECTORY_SEPARATOR . $form_id . DIRECTORY_SEPARATOR . $entry_id;
56 + wp_mkdir_p($_upload_dir);
63 57 if (is_array($file_details['name'])) {
64 58 foreach ($file_details['name'] as $key => $value) {
59 + //check accepted filetype in_array($file_details['name'][$key], $supported_files) else \
65 60 if (!empty($value)) {
66 61 $fileNameCount = 1;
62 + // $file_upoalded[$key] = time()."_$value";
67 63 $file_upoalded[$key] = sanitize_file_name($value);
68 64 while (file_exists($_upload_dir . DIRECTORY_SEPARATOR . $file_upoalded[$key])) {
69 65 $fileNameWithSeparator = BITFORMS_BF_SEPARATOR . $fileNameCount;
70 66 $file_upoalded[$key] = sanitize_file_name(preg_replace('/(.[a-z A-Z 0-9]+)$/', "{$fileNameWithSeparator}$1", $value));
@@ -72,22 +68,11 @@
72 68 if (11 === $fileNameCount) {
73 69 break;
74 70 }
75 71 }
76 - $file = [
77 - 'name' => $file_upoalded[$key],
78 - 'type' => $file_details['type'][$key] ?? '',
79 - 'tmp_name' => $file_details['tmp_name'][$key],
80 - 'error' => $file_details['error'][$key] ?? 0,
81 - 'size' => $file_details['size'][$key] ?? 0,
82 - ];
83 - add_filter('upload_dir', $upload_dir_filter);
84 - $upload_result = wp_handle_upload($file, ['test_form' => false]);
85 - remove_filter('upload_dir', $upload_dir_filter);
86 - if (isset($upload_result['error'])) {
72 + $move_status = \move_uploaded_file($file_details['tmp_name'][$key], $_upload_dir . DIRECTORY_SEPARATOR . $file_upoalded[$key]);
73 + if (!$move_status) {
87 74 unset($file_upoalded[$key]);
88 - } else {
89 - $file_upoalded[$key] = basename($upload_result['file']);
90 75 }
91 76 }
92 77 }
93 78 } else {
@@ -101,22 +86,11 @@
101 86 if (11 === $fileNameCount) {
102 87 break;
103 88 }
104 89 }
105 - $file = [
106 - 'name' => $file_upoalded[0],
107 - 'type' => $file_details['type'] ?? '',
108 - 'tmp_name' => $file_details['tmp_name'],
109 - 'error' => $file_details['error'] ?? 0,
110 - 'size' => $file_details['size'] ?? 0,
111 - ];
112 - add_filter('upload_dir', $upload_dir_filter);
113 - $upload_result = wp_handle_upload($file, ['test_form' => false]);
114 - remove_filter('upload_dir', $upload_dir_filter);
115 - if (isset($upload_result['error'])) {
90 + $move_status = \move_uploaded_file($file_details['tmp_name'], $_upload_dir . DIRECTORY_SEPARATOR . $file_upoalded[0]);
91 + if (!$move_status) {
116 92 unset($file_upoalded[0]);
117 - } else {
118 - $file_upoalded[0] = basename($upload_result['file']);
119 93 }
120 94 }
121 95 }
122 96 return $file_upoalded;
@@ -121,60 +95,14 @@
121 95 }
122 96 return $file_upoalded;
123 97 }
124 98
125 - public static function isSafeFileName($name)
126 - {
127 - if (!is_string($name)) {
128 - return false;
129 - }
130 -
131 - $trimmed = trim($name);
132 - if ('' === $trimmed) {
133 - return false;
134 - }
135 -
136 - $baseName = basename($trimmed);
137 - if ('' === $baseName || $baseName !== $trimmed || 'index.php' === $baseName) {
138 - return false;
139 - }
140 -
141 - return sanitize_file_name($trimmed) === $trimmed;
142 - }
143 -
144 99 public function deleteFiles($form_id, $entry_id, $files)
145 100 {
146 - $_upload_dir = self::getEntriesFileUploadDir($form_id, $entry_id);
147 - $resolvedBitformsUploadDir = realpath(BITFORMS_UPLOAD_DIR);
148 - $resolvedUploadDir = realpath($_upload_dir);
149 - if (false === $resolvedBitformsUploadDir || false === $resolvedUploadDir) {
150 - return;
101 + $_upload_dir = BITFORMS_UPLOAD_DIR . DIRECTORY_SEPARATOR . $form_id . DIRECTORY_SEPARATOR . $entry_id;
102 + foreach ($files as $name) {
103 + wp_delete_file($_upload_dir . DIRECTORY_SEPARATOR . $name);
151 104 }
152 -
153 - $bitformsUploadDirPrefix = trailingslashit(wp_normalize_path($resolvedBitformsUploadDir));
154 - $uploadDirPrefix = trailingslashit(wp_normalize_path($resolvedUploadDir));
155 - if (0 !== strpos($uploadDirPrefix, $bitformsUploadDirPrefix)) {
156 - return;
157 - }
158 -
159 - foreach ((array) $files as $name) {
160 - if (!self::isSafeFileName($name)) {
161 - continue;
162 - }
163 -
164 - $candidatePath = $resolvedUploadDir . DIRECTORY_SEPARATOR . $name;
165 - $resolvedPath = realpath($candidatePath);
166 - if (false === $resolvedPath || !is_file($resolvedPath)) {
167 - continue;
168 - }
169 -
170 - $normalizedPath = wp_normalize_path($resolvedPath);
171 - if (0 !== strpos($normalizedPath, $uploadDirPrefix)) {
172 - continue;
173 - }
174 -
175 - wp_delete_file($resolvedPath);
176 - }
177 105 }
178 106
179 107 public static function getFileUploadError($code)
180 108 {
@@ -192,41 +120,13 @@
192 120 }
193 121
194 122 public static function fileCopy($tmpdir, $destinationDir, $file)
195 123 {
196 - $tmpBase = realpath($tmpdir);
197 - $destBase = realpath($destinationDir);
198 - if (false === $tmpBase || false === $destBase) {
199 - Log::debug_log([
200 - 'message' => 'FileHandler::fileCopy base path invalid',
201 - 'tmpdir' => $tmpdir,
202 - 'destinationDir' => $destinationDir,
203 - ]);
204 - return;
124 + $tmpFile = $tmpdir . DIRECTORY_SEPARATOR . $file;
125 + $newFile = $destinationDir . DIRECTORY_SEPARATOR . $file;
126 + if (file_exists($tmpFile)) {
127 + copy($tmpFile, $newFile);
205 128 }
206 -
207 - $safeFile = is_string($file) ? trim($file) : '';
208 - if ('' === $safeFile) {
209 - return;
210 - }
211 -
212 - $candidate = $tmpBase . DIRECTORY_SEPARATOR . $safeFile;
213 - $resolved = realpath($candidate);
214 - if (false === $resolved || 0 !== strpos($resolved, $tmpBase . DIRECTORY_SEPARATOR)) {
215 - Log::debug_log([
216 - 'message' => 'FileHandler::fileCopy blocked path traversal',
217 - 'file' => $file,
218 - 'resolved' => $resolved,
219 - 'tmpBase' => $tmpBase,
220 - ]);
221 - return;
222 - }
223 - if (!is_readable($resolved)) {
224 - return;
225 - }
226 -
227 - $newFile = $destBase . DIRECTORY_SEPARATOR . basename($resolved);
228 - copy($resolved, $newFile);
229 129 }
230 130
231 131 public static function tempDirToUploadDir($submitted_data, $fields, $formId, $entryID)
232 132 {
@@ -231,10 +131,12 @@
231 131 public static function tempDirToUploadDir($submitted_data, $fields, $formId, $entryID)
232 132 {
233 133 $upload_dir = wp_upload_dir();
234 134 $tempDir = $upload_dir['basedir'] . '/bitforms/temp';
235 - $destinationDir = self::getEntriesFileUploadDir($formId, $entryID) . DIRECTORY_SEPARATOR;
236 - self::createIndexFile($destinationDir);
135 + $destinationDir = BITFORMS_UPLOAD_DIR . DIRECTORY_SEPARATOR . $formId . DIRECTORY_SEPARATOR . $entryID . DIRECTORY_SEPARATOR;
136 + if (!is_dir($destinationDir)) {
137 + mkdir($destinationDir);
138 + }
237 139
238 140 foreach ($submitted_data as $key => $data) {
239 141 if (isset($fields[$key]) && 'advanced-file-up' === $fields[$key]['type']) {
240 142 $files = $data;
@@ -251,18 +153,11 @@
251 153 $submitted_data[$key] = $files;
252 154 }
253 155 }
254 156 }
255 - $tempBase = realpath($tempDir);
256 - if (false !== $tempBase) {
257 - $tmpFiles = glob($tempBase . DIRECTORY_SEPARATOR . '*');
258 - foreach ((array) $tmpFiles as $tmpFile) {
259 - $resolved = realpath($tmpFile);
260 - if (false !== $resolved && 0 === strpos($resolved, $tempBase . DIRECTORY_SEPARATOR)) {
261 - wp_delete_file($resolved);
262 - }
263 - }
264 - }
157 + array_map('unlink', array_filter(
158 + (array) array_merge(glob("$tempDir/*"))
159 + ));
265 160
266 161 return $submitted_data;
267 162 }
268 163
@@ -288,9 +183,9 @@
288 183 if (!function_exists('wp_check_filetype_and_ext')) {
289 184 require_once ABSPATH . 'wp-admin/includes/file.php';
290 185 }
291 186
292 - $formManager = FormManager::getInstance($form_id);
187 + $formManager = new FormManager($form_id);
293 188 $form_contents = $formManager->getFormContent();
294 189 $field_content_details = $form_contents->fields;
295 190 $fieldDetail = $field_content_details->{$field_key};
296 191 $fieldType = $fieldDetail->typ;
@@ -353,14 +248,14 @@
353 248
354 249 private function validateFileInfo($fieldType, $file_details, $allowFileTypes, $maxSize, $maxTotalFileSize)
355 250 {
356 251 $errorMessage = [
357 - 'message' => '',
358 - 'error_type' => '',
252 + 'message' => '',
253 + 'error_type'=> '',
359 254 ];
360 255 if (is_array($file_details['name'])) {
361 256 $totalSize = 0;
362 - foreach ($file_details['name'] as $key => $file) {
257 + foreach ($file_details['name'] as $key => $file) {
363 258 if (!empty($file)) {
364 259 $fileInfo = [
365 260 'name' => $file,
366 261 'type' => $file_details['type'][$key],
@@ -391,180 +286,48 @@
391 286 }
392 287
393 288 private function validateSingleFile($fieldType, &$file, $allowTypes, $maxSize = null)
394 289 {
395 - // 0) Basic sanity & transport integrity
396 - if (!is_array($file) || empty($file['tmp_name'])) {
397 - // return ['message' => __('No file uploaded.', 'bit-form'), 'error_type' => 'file_missing'];
398 - return null;
399 - }
400 - if (!isset($file['error']) || UPLOAD_ERR_OK !== (int)$file['error']) {
401 - return ['message' => __('Upload failed', 'bit-form'), 'error_type' => 'file_upload_error'];
402 - }
403 - if (!is_uploaded_file($file['tmp_name'])) {
404 - return ['message' => __('Untrusted upload source', 'bit-form'), 'error_type' => 'file_upload_error'];
405 - }
406 - if (!is_file($file['tmp_name']) || !is_readable($file['tmp_name'])) {
407 - return ['message' => __('Temporary file not accessible', 'bit-form'), 'error_type' => 'file_upload_error'];
408 - }
409 -
410 - $fileName = sanitize_file_name((string)($file['name'] ?? ''));
411 - if ('' === $fileName) {
412 - return ['message' => __('Empty filename', 'bit-form'), 'error_type' => 'file_type_error'];
413 - }
414 -
415 - // 1) Enforce max size (header + actual)
416 - $onDiskSize = @filesize($file['tmp_name']);
417 - if (false === $onDiskSize) {
418 - return ['message' => __('Cannot read file size', 'bit-form'), 'error_type' => 'file_upload_error'];
419 - }
420 - if (!empty($maxSize) && $onDiskSize > $maxSize) {
421 - return ['message' => __('File size is too large', 'bit-form'), 'error_type' => 'file_size_error'];
422 - }
423 -
424 - // 2) Determine ext + MIME using WP + finfo
425 - $wpCheck = wp_check_filetype_and_ext($file['tmp_name'], $fileName); // ['ext'=>'jpg','type'=>'image/jpeg']
426 - $fileExtension = strtolower((string)(empty($wpCheck['ext']) ? pathinfo($fileName, PATHINFO_EXTENSION) : $wpCheck['ext']));
427 - $wpExt = strtolower((string)(empty($wpCheck['ext']) ? $fileExtension : $wpCheck['ext']));
428 - $wpType = strtolower((string)($wpCheck['type'] ?? ''));
429 - $fi = function_exists('finfo_open') ? @finfo_open(FILEINFO_MIME_TYPE) : false;
430 - $detectedMime = $fi ? @finfo_file($fi, $file['tmp_name']) : false;
431 - if ($fi) {
432 - @finfo_close($fi);
433 - }
434 - if (!$detectedMime && function_exists('mime_content_type')) {
435 - $detectedMime = @mime_content_type($file['tmp_name']);
436 - }
437 - if (!$detectedMime) {
438 - $detectedMime = '' !== $wpType ? $wpType : 'application/octet-stream';
439 - }
440 - $detectedMime = strtolower(trim($detectedMime));
441 -
442 - // Hard-block risky types regardless
443 - // 3) Block obvious executable types regardless of allow list
444 - $disallowedMimes = apply_filters('bitform_filter_upload_disallowed_mimes', [
445 - 'application/x-php',
446 - 'text/x-php',
447 - 'application/x-msdownload',
448 - 'application/x-msdos-program',
449 - 'application/x-sh',
450 - 'application/x-csh',
451 - 'text/x-shellscript',
452 - 'application/java-archive'
453 - ]);
454 - $denyExt = apply_filters('bitform_filter_upload_denied_extensions', ['php', 'phtml', 'phar', 'htaccess', 'html', 'js', 'exe', 'sh', 'bat', 'cmd']);
455 - if (in_array($detectedMime, $disallowedMimes, true) || in_array($fileExtension, $denyExt, true)) {
456 - return ['message' => __('This file type is not allowed', 'bit-form'), 'error_type' => 'file_type_error'];
457 - }
458 -
459 - // 4) Normalize allowlist: support both extensions (.jpg or jpg) and MIME types
460 - // --- ALLOWLIST NORMALIZATION (extensions + MIME) ---
461 - $normalizedAllow = array_values(array_unique(array_map(static function ($t) {
462 - return strtolower(trim((string)$t));
463 - }, (array)$allowTypes)));
464 -
465 - $allowExts = [];
466 - $allowMimes = [];
467 - foreach ($normalizedAllow as $t) {
468 - if ('' === $t) {
469 - continue;
290 + $fileName = sanitize_file_name($file['name']);
291 + if (!empty($fileName)) {
292 + $fileSize = $file['size'];
293 + if (!empty($maxSize) && $fileSize > $maxSize) {
294 + return [
295 + 'message' => __('File size is too large', 'bit-form'),
296 + 'error_type'=> 'file_size_error',
297 + ];
470 298 }
471 - if (false !== strpos($t, '/')) {
472 - // looks like a MIME
473 - $allowMimes[] = $t;
474 - } else {
475 - // extension: may include leading dot; normalize without dot
476 - $allowExts[] = ltrim($t, '.');
477 - }
478 - }
479 299
480 - $allowExts = array_values(array_unique($allowExts));
481 - $allowMimes = array_values(array_unique($allowMimes));
482 -
483 - $hasAllowlist = (!empty($allowExts) || !empty($allowMimes));
484 - $extMatch = in_array($fileExtension, $allowExts, true);
485 - $mimeMatch = in_array($detectedMime, $allowMimes, true);
486 -
487 - // 5) Require BOTH a legit WP mapping AND a match to the allowlist
488 - $wpOk = ('' !== $wpExt && '' !== $wpType);
489 -
490 - if ($hasAllowlist) {
491 - if (!($extMatch || $mimeMatch)) {
492 - return ['message' => __('File type is not allowed', 'bit-form'), 'error_type' => 'file_type_error'];
300 + $fileExtension = pathinfo($fileName, PATHINFO_EXTENSION);
301 + $fileExtAllowedByWp = wp_check_filetype_and_ext($file['tmp_name'], $fileName);
302 + $isAllowedFileType = in_array('.' . $fileExtension, $allowTypes);
303 + if ('advanced-file-up' === $fieldType && !empty($allowTypes)) {
304 + if (function_exists('mime_content_type')) {
305 + $fileMimeType = mime_content_type($file['tmp_name']);
306 + } else {
307 + $fileMimeType = $fileExtAllowedByWp['type'];
308 + }
309 + $isAllowedFileType = in_array($fileMimeType, $allowTypes);
493 310 }
494 - } else {
495 - if (!$wpOk) {
496 - return ['message' => __('File type is not allowed', 'bit-form'), 'error_type' => 'file_type_error'];
311 + if ((!empty($allowTypes) && !$isAllowedFileType) || (empty($allowTypes) && empty($fileExtAllowedByWp['ext']))) {
312 + return [
313 + 'message' => __(($fileExtension ? ".{$fileExtension}" : 'empty') . ' file extension is not allowed', 'bit-form'),
314 + 'error_type'=> 'file_type_error',
315 + ];
497 316 }
498 - }
499 -
500 - // 5) Special SVG handling (by MIME, not just extension)
501 - if ('svg' === $fileExtension || 'image/svg+xml' === $detectedMime || 'image/svg+xml' === $wpType) {
502 - if ('image/svg+xml' !== $detectedMime) {
503 - return ['message' => __('Invalid SVG', 'bit-form'), 'error_type' => 'file_type_error'];
504 - }
505 -
506 - $dirty = file_get_contents($file['tmp_name']);
507 - $svg_sanitizer = new Sanitizer();
508 - $clean = $svg_sanitizer->sanitize($dirty);
509 - if (false === $clean) {
510 - return ['message' => __('SVG file is not valid', 'bit-form'), 'error_type' => 'file_type_error'];
511 - }
512 - file_put_contents($file['tmp_name'], $clean, LOCK_EX);
513 - // Re-check size post-sanitize
514 - if (!empty($maxSize) && filesize($file['tmp_name']) > $maxSize) {
515 - return ['message' => __('File size is too large after sanitation', 'bit-form'), 'error_type' => 'file_size_error'];
516 - }
517 - }
518 -
519 - if ('pdf' === $fileExtension || 'application/pdf' === $detectedMime || 'application/pdf' === $wpType) {
520 - $blockedList = [
521 - '/\/JS\b/',
522 - '/\/JavaScript\b/',
523 - '/eval\(/i',
524 - '/app\.alert/i',
525 - '/console\.log\b/i',
526 - '/document\.write\b/i',
527 -
528 - '/\/GoToR\b/i',
529 - '/\/Launch\b/i',
530 -
531 - '/\/EmbeddedFile\b/i',
532 - '/\/EmbeddedFiles\b/i',
533 - '/\/Filespec\b/i',
534 - '/\/FileAttachment\b/i',
535 -
536 - '/\/SubmitForm\b/i',
537 - '/\/ResetForm\b/i',
538 - '/\/ImportData\b/i',
539 -
540 - '/\/RichMedia\b/i',
541 - ];
542 -
543 - $pdfContent = file_get_contents($file['tmp_name']);
544 - foreach ($blockedList as $blockedKeyWrd) {
545 - if (preg_match($blockedKeyWrd, $pdfContent)) {
546 - return ['message' => __('Invalid file', 'bit-form'), 'error_type' => 'file_type_error'];
317 + if ('svg' === $fileExtension) {
318 + $svg_sanitizer = new Sanitizer();
319 + $dirty_svg = file_get_contents($file['tmp_name']);
320 + $clean_svg = $svg_sanitizer->sanitize($dirty_svg);
321 + if (false === $clean_svg) {
322 + return [
323 + 'message' => __('SVG file is not valid', 'bit-form'),
324 + 'error_type'=> 'file_type_error',
325 + ];
547 326 }
327 + file_put_contents($file['tmp_name'], $clean_svg);
548 328 }
549 329 }
550 -
551 - // --- Allow developer to make a final decision override (optional) ---
552 - $final = apply_filters('bitform_filter_upload_allow_file', true, [
553 - 'file_name' => $fileName,
554 - 'extension' => $fileExtension,
555 - 'detected_mime' => $detectedMime,
556 - 'wp_ext' => $wpExt,
557 - 'wp_type' => $wpType,
558 - 'allow_exts' => $allowExts,
559 - 'allow_mimes' => $allowMimes,
560 - 'has_allowlist' => $hasAllowlist,
561 - ]);
562 - if (true !== $final) {
563 - // If a dev returns a WP_Error, you could extract message/type; here we just block.
564 - return ['message' => __('File not allowed by policy', 'bit-form'), 'error_type' => 'file_policy_block'];
565 - }
566 - return null; // success
567 330 }
568 331
569 332 public static function deleteIsFileExists($path)
570 333 {
@@ -569,385 +332,7 @@
569 332 public static function deleteIsFileExists($path)
570 333 {
571 334 if (file_exists($path)) {
572 335 wp_delete_file($path);
573 - }
574 - }
575 -
576 - public static function getEntriesFileUploadDir($form_id, $entry_id)
577 - {
578 - $uploadDir = rtrim(BITFORMS_UPLOAD_DIR, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $form_id . DIRECTORY_SEPARATOR;
579 - $encrypted_directoryId = Helpers::getEncryptedEntryId($entry_id);
580 - $encryptedDirectory = $uploadDir . $encrypted_directoryId;
581 - if (is_dir($encryptedDirectory)) {
582 - return $encryptedDirectory;
583 - }
584 - $oldEntriesFileUploadDir = Helpers::getOldEntriesFileUploadDir($uploadDir, $entry_id);
585 - if (!empty($oldEntriesFileUploadDir) && is_dir($oldEntriesFileUploadDir)) {
586 - return $oldEntriesFileUploadDir;
587 - }
588 - return $encryptedDirectory;
589 - }
590 -
591 - private static function replaceDocumentRoot($path)
592 - {
593 - $uploadDir = wp_upload_dir();
594 - $basedir = wp_normalize_path(rtrim($uploadDir['basedir'], '/\\'));
595 - $baseurl = rtrim($uploadDir['baseurl'], '/');
596 - $normalPath = wp_normalize_path($path);
597 -
598 - if (0 === strpos($normalPath, $basedir . '/') || $normalPath === $basedir) {
599 - return $baseurl . substr($normalPath, strlen($basedir));
600 - }
601 -
602 - // Fallback: path outside uploads dir — strip ABSPATH and prepend home URL.
603 - return home_url(ltrim(str_replace(wp_normalize_path(ABSPATH), '', $normalPath), '/'));
604 - }
605 -
606 - public static function getEntriesFileUploadURL($form_id, $entry_id)
607 - {
608 - $documentRoot = self::getEntriesFileUploadDir($form_id, $entry_id);
609 - $url = self::replaceDocumentRoot($documentRoot);
610 - return $url;
611 - }
612 -
613 - public static function createIndexFile($directory)
614 - {
615 - if (wp_mkdir_p($directory)) {
616 - $indexFilePath = rtrim($directory, '/') . '/index.php';
617 - if (!file_exists($indexFilePath)) {
618 - try {
619 - if (false === self::writeFile($indexFilePath, "<?php\n// No direct access allowed.")) {
620 - throw new \Exception("Failed to create index.php in $directory");
621 - }
622 - } catch (\Exception $e) {
623 - Log::debug_log('File creation Failed:' . $e->getMessage()); // Log the error for debugging
624 - }
625 - }
626 - }
627 - return false;
628 - }
629 -
630 - /**
631 - * Initialise and return the WP_Filesystem abstraction layer.
632 - *
633 - * @return WP_Filesystem_Base|false
634 - */
635 - private static function initWpFilesystem()
636 - {
637 - global $wp_filesystem;
638 - if (empty($wp_filesystem)) {
639 - require_once ABSPATH . 'wp-admin/includes/file.php';
640 - WP_Filesystem();
641 - }
642 - return $wp_filesystem;
643 - }
644 -
645 - /**
646 - * Write (overwrite) content to a file using WP_Filesystem.
647 - *
648 - * @param string $filePath Absolute path to the file.
649 - * @param string $content Content to write.
650 - * @return bool True on success, false on failure.
651 - */
652 - public static function writeFile($filePath, $content)
653 - {
654 - $fs = self::initWpFilesystem();
655 - if ($fs) {
656 - return $fs->put_contents($filePath, $content, FS_CHMOD_FILE);
657 - }
658 - // Fallback: file_put_contents is acceptable when WP_Filesystem is unavailable.
659 - return false !== file_put_contents($filePath, $content);
660 - }
661 -
662 - /**
663 - * Append content to a file using WP_Filesystem.
664 - * WP_Filesystem has no native append; we read + concatenate + write.
665 - *
666 - * @param string $filePath Absolute path to the file.
667 - * @param string $content Content to append.
668 - * @return bool True on success, false on failure.
669 - */
670 - public static function appendFile($filePath, $content)
671 - {
672 - $fs = self::initWpFilesystem();
673 - if ($fs) {
674 - $existing = $fs->exists($filePath) ? (string) $fs->get_contents($filePath) : '';
675 - return $fs->put_contents($filePath, $existing . $content, FS_CHMOD_FILE);
676 - }
677 - // Fallback: file_put_contents is acceptable when WP_Filesystem is unavailable.
678 - return false !== file_put_contents($filePath, $content, FILE_APPEND | LOCK_EX);
679 - }
680 -
681 - /**
682 - * Read and return the full content of a file using WP_Filesystem.
683 - *
684 - * @param string $filePath Absolute path to the file.
685 - * @return string File contents, or empty string if unreadable.
686 - */
687 - public static function readFile($filePath)
688 - {
689 - $fs = self::initWpFilesystem();
690 - if ($fs && $fs->exists($filePath)) {
691 - $content = $fs->get_contents($filePath);
692 - return false !== $content ? $content : '';
693 - }
694 - // Fallback.
695 - if (file_exists($filePath)) {
696 - $content = file_get_contents($filePath);
697 - return false !== $content ? $content : '';
698 - }
699 - return '';
700 - }
701 -
702 - public static function processRepeaterAttachment($repeaterKey, $fileKey, $fieldValue, $basePath, &$attachments)
703 - {
704 - if (!isset($fieldValue[$repeaterKey]) || !is_array($fieldValue[$repeaterKey])) {
705 - return;
706 - }
707 -
708 - foreach ($fieldValue[$repeaterKey] as $repeaterRow) {
709 - if (!isset($repeaterRow[$fileKey]) || empty($repeaterRow[$fileKey])) {
710 - continue;
711 - }
712 -
713 - $fileValue = $repeaterRow[$fileKey];
714 - self::addAttachmentFiles($fileValue, $basePath, $attachments);
715 - }
716 - }
717 -
718 - public static function processRegularAttachment($fileKey, $fieldValue, $basePath, &$attachments)
719 - {
720 - if (!isset($fieldValue[$fileKey]) || empty($fieldValue[$fileKey])) {
721 - return;
722 - }
723 -
724 - $fileValue = $fieldValue[$fileKey];
725 - self::addAttachmentFiles($fileValue, $basePath, $attachments);
726 - }
727 -
728 - private static function addAttachmentFiles($fileValue, $basePath, &$attachments)
729 - {
730 - $baseResolved = realpath($basePath);
731 - if (false === $baseResolved) {
732 - Log::debug_log([
733 - 'message' => 'FileHandler::addAttachmentFiles base path invalid',
734 - 'basePath' => $basePath,
735 - ]);
736 - return;
737 - }
738 -
739 - $addFile = static function ($candidate) use ($baseResolved, &$attachments) {
740 - $safeFile = is_string($candidate) ? trim($candidate) : '';
741 - if ('' === $safeFile) {
742 - return;
743 - }
744 - $path = $baseResolved . DIRECTORY_SEPARATOR . $safeFile;
745 - $resolved = realpath($path);
746 - if (false === $resolved || 0 !== strpos($resolved, $baseResolved . DIRECTORY_SEPARATOR)) {
747 - Log::debug_log([
748 - 'message' => 'FileHandler::addAttachmentFiles blocked path traversal',
749 - 'file' => $candidate,
750 - 'resolved' => $resolved,
751 - 'baseResolved' => $baseResolved,
752 - ]);
753 - return;
754 - }
755 - if (is_readable($resolved)) {
756 - $attachments[] = $resolved;
757 - }
758 - };
759 -
760 - if (is_array($fileValue)) {
761 - foreach ($fileValue as $singleFile) {
762 - $addFile($singleFile);
763 - }
764 - return;
765 - }
766 -
767 - $addFile($fileValue);
768 - }
769 -
770 - /**
771 - * Return file type by checking with mime type
772 - * @param string $mime
773 - * @return string
774 - */
775 - public static function getFileTypeByMime(string $mime)
776 - {
777 - $mime = strtolower($mime);
778 -
779 - if (preg_match('/^image\//', $mime)) {
780 - return 'image';
781 - }
782 -
783 - $compressed = [
784 - 'application/zip',
785 - 'application/x-rar-compressed',
786 - 'application/x-7z-compressed',
787 - 'application/gzip',
788 - 'application/x-tar',
789 - 'application/x-gtar',
790 - 'application/x-bzip2',
791 - 'application/x-archive',
792 - 'application/vnd.debian.binary-package',
793 - ];
794 - if (in_array($mime, $compressed, true)) {
795 - return 'compressed';
796 - }
797 -
798 - $presentation = [
799 - 'application/vnd.ms-powerpoint',
800 - 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
801 - 'application/vnd.oasis.opendocument.presentation',
802 - 'application/vnd.apple.keynote',
803 - ];
804 - if (in_array($mime, $presentation, true)) {
805 - return 'presentation';
806 - }
807 -
808 - $document = [
809 - 'application/pdf',
810 - 'application/msword',
811 - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
812 - 'application/rtf',
813 - 'text/plain',
814 - 'application/vnd.oasis.opendocument.text',
815 - 'application/x-tex',
816 - 'text/rtf',
817 - ];
818 - if (in_array($mime, $document, true)) {
819 - return 'document';
820 - }
821 -
822 - $data = [
823 - 'text/csv',
824 - 'application/xml',
825 - 'text/xml',
826 - 'application/sql',
827 - 'application/vnd.ms-excel',
828 - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
829 - 'application/x-sqlite3',
830 - 'application/octet-stream', // generic binary (could be db files)
831 - ];
832 - if (in_array($mime, $data, true)) {
833 - return 'data';
834 - }
835 -
836 - if (preg_match('/^audio\//', $mime)) {
837 - return 'audio';
838 - }
839 -
840 - if (preg_match('/^video\//', $mime)) {
841 - return 'video';
842 - }
843 -
844 - return 'other';
845 - }
846 -
847 - /**
848 - * Return file type by checking with extension
849 - * @param string $extension
850 - * @return string
851 - */
852 - public static function getFileTypeByExtension($extension)
853 - {
854 - switch (strtolower($extension)) {
855 - case 'xbm':
856 - case 'tif':
857 - case 'pjp':
858 - case 'pjpeg':
859 - case 'svgz':
860 - case 'jpg':
861 - case 'jpeg':
862 - case 'ico':
863 - case 'tiff':
864 - case 'gif':
865 - case 'svg':
866 - case 'bmp':
867 - case 'png':
868 - case 'jfif':
869 - case 'webp':
870 - return 'image';
871 -
872 - case '7z':
873 - case 'arj':
874 - case 'deb':
875 - case 'pkg':
876 - case 'rar':
877 - case 'rpm':
878 - case 'gz':
879 - case 'z':
880 - case 'zip':
881 - return 'compressed';
882 -
883 - case 'key':
884 - case 'odp':
885 - case 'pps':
886 - case 'ppt':
887 - case 'pptx':
888 - return 'presentation';
889 -
890 - case '_rf_':
891 - case 'doc':
892 - case 'docx':
893 - case 'odt':
894 - case 'pdf':
895 - case 'rtf':
896 - case 'tex':
897 - case 'txt':
898 - case 'wks':
899 - case 'wps':
900 - case 'wpd':
901 - return 'document';
902 -
903 - case 'csv':
904 - case 'dat':
905 - case 'db':
906 - case 'dbf':
907 - case 'log':
908 - case 'mdb':
909 - case 'sav':
910 - case 'sql':
911 - case 'tar':
912 - case 'sqlite':
913 - case 'xml':
914 - return 'data';
915 -
916 - case 'opus':
917 - case 'flac':
918 - case 'webm':
919 - case 'weba':
920 - case 'wav':
921 - case 'ogg':
922 - case 'm4a':
923 - case 'mp3':
924 - case 'oga':
925 - case 'mid':
926 - case 'amr':
927 - case 'aiff':
928 - case 'wma':
929 - case 'au':
930 - case 'acc':
931 - case 'wpl':
932 - return 'audio';
933 -
934 - case 'ogm':
935 - case 'wmv':
936 - case 'mpg':
937 - case 'ogv':
938 - case 'mov':
939 - case 'asx':
940 - case 'mpeg':
941 - case 'mp4':
942 - case 'm4v':
943 - case 'avi':
944 - case '3gp':
945 - case 'flv':
946 - case 'mkv':
947 - case 'swf':
948 - return 'video';
949 - default:
950 - return 'other';
951 336 }
952 337 }
953 338 }