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 +81 -748 3.2.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,26 +131,24 @@
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 - $consumedFiles = [];
239 -
240 140 foreach ($submitted_data as $key => $data) {
241 141 if (isset($fields[$key]) && 'advanced-file-up' === $fields[$key]['type']) {
142 + $files = $data;
242 143 $fldData = $submitted_data[$key];
243 - // A repeater row (or a pre-split value) hands this field over as an array; explode()
244 - // on an array is a TypeError on PHP 8, which would fatal mid-submission.
245 - $files = \is_array($fldData) ? $fldData : explode(',', \is_scalar($fldData) ? (string) $fldData : '');
246 - foreach ($files as $file) {
247 - $safeFile = \is_scalar($file) ? trim((string) $file) : '';
248 - if ('' === $safeFile) {
249 - continue;
144 + $files = explode(',', $fldData);
145 + if (is_array($files) && count($files) > 0) {
146 + foreach ($files as $file) {
147 + self::fileCopy($tempDir, $destinationDir, trim($file));
250 148 }
251 - self::fileCopy($tempDir, $destinationDir, $safeFile);
252 - $consumedFiles[] = $safeFile;
149 + } else {
150 + self::fileCopy($tempDir, $destinationDir, trim($files));
253 151 }
254 152 if (!empty($files)) {
255 153 $submitted_data[$key] = $files;
256 154 }
@@ -255,70 +153,15 @@
255 153 $submitted_data[$key] = $files;
256 154 }
257 155 }
258 156 }
157 + array_map('unlink', array_filter(
158 + (array) array_merge(glob("$tempDir/*"))
159 + ));
259 160
260 - self::cleanupTempUploads($tempDir, $consumedFiles);
261 -
262 161 return $submitted_data;
263 162 }
264 163
265 - /**
266 - * Clear the shared temp upload staging area after a submission has taken what it needs.
267 - *
268 - * @param string $tempDir shared staging directory
269 - * @param array $consumedFiles file names copied into the entry directory by this submission
270 - *
271 - * @return void
272 - */
273 - private static function cleanupTempUploads($tempDir, array $consumedFiles)
274 - {
275 - $tempBase = realpath($tempDir);
276 - if (false === $tempBase) {
277 - return;
278 - }
279 - $boundary = $tempBase . DIRECTORY_SEPARATOR;
280 -
281 - // Never remove the directory-hardening files the uploads dir relies on.
282 - $protected = ['index.php', 'index.html', '.htaccess'];
283 -
284 - foreach ($consumedFiles as $file) {
285 - $resolved = realpath($boundary . $file);
286 - if (false === $resolved || 0 !== strpos($resolved, $boundary) || !is_file($resolved)) {
287 - continue;
288 - }
289 - if (\in_array(basename($resolved), $protected, true)) {
290 - continue;
291 - }
292 - wp_delete_file($resolved);
293 - }
294 -
295 - // Sweep abandoned uploads. Anything still here after the retention window belongs to a form
296 - // that was never submitted. Filterable so a site with very long multi-step forms can extend
297 - // it; 0 or less disables the sweep entirely.
298 - $retention = apply_filters('bitform_temp_upload_retention', DAY_IN_SECONDS);
299 - $retention = is_numeric($retention) ? (int) $retention : DAY_IN_SECONDS;
300 - if ($retention <= 0) {
301 - return;
302 - }
303 -
304 - $cutoff = time() - $retention;
305 - $tmpFiles = glob($boundary . '*');
306 - foreach ((array) $tmpFiles as $tmpFile) {
307 - $resolved = realpath($tmpFile);
308 - if (false === $resolved || 0 !== strpos($resolved, $boundary) || !is_file($resolved)) {
309 - continue;
310 - }
311 - if (\in_array(basename($resolved), $protected, true)) {
312 - continue;
313 - }
314 - $modified = @filemtime($resolved);
315 - if (false !== $modified && $modified < $cutoff) {
316 - wp_delete_file($resolved);
317 - }
318 - }
319 - }
320 -
321 164 private function getByteSizeByUnit($sizeString)
322 165 {
323 166 // split 2MB into 2 and MB
324 167 $size = preg_replace('/[^0-9\.]/', '', $sizeString);
@@ -340,9 +183,9 @@
340 183 if (!function_exists('wp_check_filetype_and_ext')) {
341 184 require_once ABSPATH . 'wp-admin/includes/file.php';
342 185 }
343 186
344 - $formManager = FormManager::getInstance($form_id);
187 + $formManager = new FormManager($form_id);
345 188 $form_contents = $formManager->getFormContent();
346 189 $field_content_details = $form_contents->fields;
347 190 $fieldDetail = $field_content_details->{$field_key};
348 191 $fieldType = $fieldDetail->typ;
@@ -405,14 +248,14 @@
405 248
406 249 private function validateFileInfo($fieldType, $file_details, $allowFileTypes, $maxSize, $maxTotalFileSize)
407 250 {
408 251 $errorMessage = [
409 - 'message' => '',
410 - 'error_type' => '',
252 + 'message' => '',
253 + 'error_type'=> '',
411 254 ];
412 255 if (is_array($file_details['name'])) {
413 256 $totalSize = 0;
414 - foreach ($file_details['name'] as $key => $file) {
257 + foreach ($file_details['name'] as $key => $file) {
415 258 if (!empty($file)) {
416 259 $fileInfo = [
417 260 'name' => $file,
418 261 'type' => $file_details['type'][$key],
@@ -443,180 +286,48 @@
443 286 }
444 287
445 288 private function validateSingleFile($fieldType, &$file, $allowTypes, $maxSize = null)
446 289 {
447 - // 0) Basic sanity & transport integrity
448 - if (!is_array($file) || empty($file['tmp_name'])) {
449 - // return ['message' => __('No file uploaded.', 'bit-form'), 'error_type' => 'file_missing'];
450 - return null;
451 - }
452 - if (!isset($file['error']) || UPLOAD_ERR_OK !== (int)$file['error']) {
453 - return ['message' => __('Upload failed', 'bit-form'), 'error_type' => 'file_upload_error'];
454 - }
455 - if (!is_uploaded_file($file['tmp_name'])) {
456 - return ['message' => __('Untrusted upload source', 'bit-form'), 'error_type' => 'file_upload_error'];
457 - }
458 - if (!is_file($file['tmp_name']) || !is_readable($file['tmp_name'])) {
459 - return ['message' => __('Temporary file not accessible', 'bit-form'), 'error_type' => 'file_upload_error'];
460 - }
461 -
462 - $fileName = sanitize_file_name((string)($file['name'] ?? ''));
463 - if ('' === $fileName) {
464 - return ['message' => __('Empty filename', 'bit-form'), 'error_type' => 'file_type_error'];
465 - }
466 -
467 - // 1) Enforce max size (header + actual)
468 - $onDiskSize = @filesize($file['tmp_name']);
469 - if (false === $onDiskSize) {
470 - return ['message' => __('Cannot read file size', 'bit-form'), 'error_type' => 'file_upload_error'];
471 - }
472 - if (!empty($maxSize) && $onDiskSize > $maxSize) {
473 - return ['message' => __('File size is too large', 'bit-form'), 'error_type' => 'file_size_error'];
474 - }
475 -
476 - // 2) Determine ext + MIME using WP + finfo
477 - $wpCheck = wp_check_filetype_and_ext($file['tmp_name'], $fileName); // ['ext'=>'jpg','type'=>'image/jpeg']
478 - $fileExtension = strtolower((string)(empty($wpCheck['ext']) ? pathinfo($fileName, PATHINFO_EXTENSION) : $wpCheck['ext']));
479 - $wpExt = strtolower((string)(empty($wpCheck['ext']) ? $fileExtension : $wpCheck['ext']));
480 - $wpType = strtolower((string)($wpCheck['type'] ?? ''));
481 - $fi = function_exists('finfo_open') ? @finfo_open(FILEINFO_MIME_TYPE) : false;
482 - $detectedMime = $fi ? @finfo_file($fi, $file['tmp_name']) : false;
483 - if ($fi) {
484 - @finfo_close($fi);
485 - }
486 - if (!$detectedMime && function_exists('mime_content_type')) {
487 - $detectedMime = @mime_content_type($file['tmp_name']);
488 - }
489 - if (!$detectedMime) {
490 - $detectedMime = '' !== $wpType ? $wpType : 'application/octet-stream';
491 - }
492 - $detectedMime = strtolower(trim($detectedMime));
493 -
494 - // Hard-block risky types regardless
495 - // 3) Block obvious executable types regardless of allow list
496 - $disallowedMimes = apply_filters('bitform_filter_upload_disallowed_mimes', [
497 - 'application/x-php',
498 - 'text/x-php',
499 - 'application/x-msdownload',
500 - 'application/x-msdos-program',
501 - 'application/x-sh',
502 - 'application/x-csh',
503 - 'text/x-shellscript',
504 - 'application/java-archive'
505 - ]);
506 - $denyExt = apply_filters('bitform_filter_upload_denied_extensions', ['php', 'phtml', 'phar', 'htaccess', 'html', 'js', 'exe', 'sh', 'bat', 'cmd']);
507 - if (in_array($detectedMime, $disallowedMimes, true) || in_array($fileExtension, $denyExt, true)) {
508 - return ['message' => __('This file type is not allowed', 'bit-form'), 'error_type' => 'file_type_error'];
509 - }
510 -
511 - // 4) Normalize allowlist: support both extensions (.jpg or jpg) and MIME types
512 - // --- ALLOWLIST NORMALIZATION (extensions + MIME) ---
513 - $normalizedAllow = array_values(array_unique(array_map(static function ($t) {
514 - return strtolower(trim((string)$t));
515 - }, (array)$allowTypes)));
516 -
517 - $allowExts = [];
518 - $allowMimes = [];
519 - foreach ($normalizedAllow as $t) {
520 - if ('' === $t) {
521 - 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 + ];
522 298 }
523 - if (false !== strpos($t, '/')) {
524 - // looks like a MIME
525 - $allowMimes[] = $t;
526 - } else {
527 - // extension: may include leading dot; normalize without dot
528 - $allowExts[] = ltrim($t, '.');
529 - }
530 - }
531 299
532 - $allowExts = array_values(array_unique($allowExts));
533 - $allowMimes = array_values(array_unique($allowMimes));
534 -
535 - $hasAllowlist = (!empty($allowExts) || !empty($allowMimes));
536 - $extMatch = in_array($fileExtension, $allowExts, true);
537 - $mimeMatch = in_array($detectedMime, $allowMimes, true);
538 -
539 - // 5) Require BOTH a legit WP mapping AND a match to the allowlist
540 - $wpOk = ('' !== $wpExt && '' !== $wpType);
541 -
542 - if ($hasAllowlist) {
543 - if (!($extMatch || $mimeMatch)) {
544 - 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);
545 310 }
546 - } else {
547 - if (!$wpOk) {
548 - 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 + ];
549 316 }
550 - }
551 -
552 - // 5) Special SVG handling (by MIME, not just extension)
553 - if ('svg' === $fileExtension || 'image/svg+xml' === $detectedMime || 'image/svg+xml' === $wpType) {
554 - if ('image/svg+xml' !== $detectedMime) {
555 - return ['message' => __('Invalid SVG', 'bit-form'), 'error_type' => 'file_type_error'];
556 - }
557 -
558 - $dirty = file_get_contents($file['tmp_name']);
559 - $svg_sanitizer = new Sanitizer();
560 - $clean = $svg_sanitizer->sanitize($dirty);
561 - if (false === $clean) {
562 - return ['message' => __('SVG file is not valid', 'bit-form'), 'error_type' => 'file_type_error'];
563 - }
564 - file_put_contents($file['tmp_name'], $clean, LOCK_EX);
565 - // Re-check size post-sanitize
566 - if (!empty($maxSize) && filesize($file['tmp_name']) > $maxSize) {
567 - return ['message' => __('File size is too large after sanitation', 'bit-form'), 'error_type' => 'file_size_error'];
568 - }
569 - }
570 -
571 - if ('pdf' === $fileExtension || 'application/pdf' === $detectedMime || 'application/pdf' === $wpType) {
572 - $blockedList = [
573 - '/\/JS\b/',
574 - '/\/JavaScript\b/',
575 - '/eval\(/i',
576 - '/app\.alert/i',
577 - '/console\.log\b/i',
578 - '/document\.write\b/i',
579 -
580 - '/\/GoToR\b/i',
581 - '/\/Launch\b/i',
582 -
583 - '/\/EmbeddedFile\b/i',
584 - '/\/EmbeddedFiles\b/i',
585 - '/\/Filespec\b/i',
586 - '/\/FileAttachment\b/i',
587 -
588 - '/\/SubmitForm\b/i',
589 - '/\/ResetForm\b/i',
590 - '/\/ImportData\b/i',
591 -
592 - '/\/RichMedia\b/i',
593 - ];
594 -
595 - $pdfContent = file_get_contents($file['tmp_name']);
596 - foreach ($blockedList as $blockedKeyWrd) {
597 - if (preg_match($blockedKeyWrd, $pdfContent)) {
598 - 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 + ];
599 326 }
327 + file_put_contents($file['tmp_name'], $clean_svg);
600 328 }
601 329 }
602 -
603 - // --- Allow developer to make a final decision override (optional) ---
604 - $final = apply_filters('bitform_filter_upload_allow_file', true, [
605 - 'file_name' => $fileName,
606 - 'extension' => $fileExtension,
607 - 'detected_mime' => $detectedMime,
608 - 'wp_ext' => $wpExt,
609 - 'wp_type' => $wpType,
610 - 'allow_exts' => $allowExts,
611 - 'allow_mimes' => $allowMimes,
612 - 'has_allowlist' => $hasAllowlist,
613 - ]);
614 - if (true !== $final) {
615 - // If a dev returns a WP_Error, you could extract message/type; here we just block.
616 - return ['message' => __('File not allowed by policy', 'bit-form'), 'error_type' => 'file_policy_block'];
617 - }
618 - return null; // success
619 330 }
620 331
621 332 public static function deleteIsFileExists($path)
622 333 {
@@ -621,385 +332,7 @@
621 332 public static function deleteIsFileExists($path)
622 333 {
623 334 if (file_exists($path)) {
624 335 wp_delete_file($path);
625 - }
626 - }
627 -
628 - public static function getEntriesFileUploadDir($form_id, $entry_id)
629 - {
630 - $uploadDir = rtrim(BITFORMS_UPLOAD_DIR, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $form_id . DIRECTORY_SEPARATOR;
631 - $encrypted_directoryId = Helpers::getEncryptedEntryId($entry_id);
632 - $encryptedDirectory = $uploadDir . $encrypted_directoryId;
633 - if (is_dir($encryptedDirectory)) {
634 - return $encryptedDirectory;
635 - }
636 - $oldEntriesFileUploadDir = Helpers::getOldEntriesFileUploadDir($uploadDir, $entry_id);
637 - if (!empty($oldEntriesFileUploadDir) && is_dir($oldEntriesFileUploadDir)) {
638 - return $oldEntriesFileUploadDir;
639 - }
640 - return $encryptedDirectory;
641 - }
642 -
643 - private static function replaceDocumentRoot($path)
644 - {
645 - $uploadDir = wp_upload_dir();
646 - $basedir = wp_normalize_path(rtrim($uploadDir['basedir'], '/\\'));
647 - $baseurl = rtrim($uploadDir['baseurl'], '/');
648 - $normalPath = wp_normalize_path($path);
649 -
650 - if (0 === strpos($normalPath, $basedir . '/') || $normalPath === $basedir) {
651 - return $baseurl . substr($normalPath, strlen($basedir));
652 - }
653 -
654 - // Fallback: path outside uploads dir — strip ABSPATH and prepend home URL.
655 - return home_url(ltrim(str_replace(wp_normalize_path(ABSPATH), '', $normalPath), '/'));
656 - }
657 -
658 - public static function getEntriesFileUploadURL($form_id, $entry_id)
659 - {
660 - $documentRoot = self::getEntriesFileUploadDir($form_id, $entry_id);
661 - $url = self::replaceDocumentRoot($documentRoot);
662 - return $url;
663 - }
664 -
665 - public static function createIndexFile($directory)
666 - {
667 - if (wp_mkdir_p($directory)) {
668 - $indexFilePath = rtrim($directory, '/') . '/index.php';
669 - if (!file_exists($indexFilePath)) {
670 - try {
671 - if (false === self::writeFile($indexFilePath, "<?php\n// No direct access allowed.")) {
672 - throw new \Exception("Failed to create index.php in $directory");
673 - }
674 - } catch (\Exception $e) {
675 - Log::debug_log('File creation Failed:' . $e->getMessage()); // Log the error for debugging
676 - }
677 - }
678 - }
679 - return false;
680 - }
681 -
682 - /**
683 - * Initialise and return the WP_Filesystem abstraction layer.
684 - *
685 - * @return WP_Filesystem_Base|false
686 - */
687 - private static function initWpFilesystem()
688 - {
689 - global $wp_filesystem;
690 - if (empty($wp_filesystem)) {
691 - require_once ABSPATH . 'wp-admin/includes/file.php';
692 - WP_Filesystem();
693 - }
694 - return $wp_filesystem;
695 - }
696 -
697 - /**
698 - * Write (overwrite) content to a file using WP_Filesystem.
699 - *
700 - * @param string $filePath Absolute path to the file.
701 - * @param string $content Content to write.
702 - * @return bool True on success, false on failure.
703 - */
704 - public static function writeFile($filePath, $content)
705 - {
706 - $fs = self::initWpFilesystem();
707 - if ($fs) {
708 - return $fs->put_contents($filePath, $content, FS_CHMOD_FILE);
709 - }
710 - // Fallback: file_put_contents is acceptable when WP_Filesystem is unavailable.
711 - return false !== file_put_contents($filePath, $content);
712 - }
713 -
714 - /**
715 - * Append content to a file using WP_Filesystem.
716 - * WP_Filesystem has no native append; we read + concatenate + write.
717 - *
718 - * @param string $filePath Absolute path to the file.
719 - * @param string $content Content to append.
720 - * @return bool True on success, false on failure.
721 - */
722 - public static function appendFile($filePath, $content)
723 - {
724 - $fs = self::initWpFilesystem();
725 - if ($fs) {
726 - $existing = $fs->exists($filePath) ? (string) $fs->get_contents($filePath) : '';
727 - return $fs->put_contents($filePath, $existing . $content, FS_CHMOD_FILE);
728 - }
729 - // Fallback: file_put_contents is acceptable when WP_Filesystem is unavailable.
730 - return false !== file_put_contents($filePath, $content, FILE_APPEND | LOCK_EX);
731 - }
732 -
733 - /**
734 - * Read and return the full content of a file using WP_Filesystem.
735 - *
736 - * @param string $filePath Absolute path to the file.
737 - * @return string File contents, or empty string if unreadable.
738 - */
739 - public static function readFile($filePath)
740 - {
741 - $fs = self::initWpFilesystem();
742 - if ($fs && $fs->exists($filePath)) {
743 - $content = $fs->get_contents($filePath);
744 - return false !== $content ? $content : '';
745 - }
746 - // Fallback.
747 - if (file_exists($filePath)) {
748 - $content = file_get_contents($filePath);
749 - return false !== $content ? $content : '';
750 - }
751 - return '';
752 - }
753 -
754 - public static function processRepeaterAttachment($repeaterKey, $fileKey, $fieldValue, $basePath, &$attachments)
755 - {
756 - if (!isset($fieldValue[$repeaterKey]) || !is_array($fieldValue[$repeaterKey])) {
757 - return;
758 - }
759 -
760 - foreach ($fieldValue[$repeaterKey] as $repeaterRow) {
761 - if (!isset($repeaterRow[$fileKey]) || empty($repeaterRow[$fileKey])) {
762 - continue;
763 - }
764 -
765 - $fileValue = $repeaterRow[$fileKey];
766 - self::addAttachmentFiles($fileValue, $basePath, $attachments);
767 - }
768 - }
769 -
770 - public static function processRegularAttachment($fileKey, $fieldValue, $basePath, &$attachments)
771 - {
772 - if (!isset($fieldValue[$fileKey]) || empty($fieldValue[$fileKey])) {
773 - return;
774 - }
775 -
776 - $fileValue = $fieldValue[$fileKey];
777 - self::addAttachmentFiles($fileValue, $basePath, $attachments);
778 - }
779 -
780 - private static function addAttachmentFiles($fileValue, $basePath, &$attachments)
781 - {
782 - $baseResolved = realpath($basePath);
783 - if (false === $baseResolved) {
784 - Log::debug_log([
785 - 'message' => 'FileHandler::addAttachmentFiles base path invalid',
786 - 'basePath' => $basePath,
787 - ]);
788 - return;
789 - }
790 -
791 - $addFile = static function ($candidate) use ($baseResolved, &$attachments) {
792 - $safeFile = is_string($candidate) ? trim($candidate) : '';
793 - if ('' === $safeFile) {
794 - return;
795 - }
796 - $path = $baseResolved . DIRECTORY_SEPARATOR . $safeFile;
797 - $resolved = realpath($path);
798 - if (false === $resolved || 0 !== strpos($resolved, $baseResolved . DIRECTORY_SEPARATOR)) {
799 - Log::debug_log([
800 - 'message' => 'FileHandler::addAttachmentFiles blocked path traversal',
801 - 'file' => $candidate,
802 - 'resolved' => $resolved,
803 - 'baseResolved' => $baseResolved,
804 - ]);
805 - return;
806 - }
807 - if (is_readable($resolved)) {
808 - $attachments[] = $resolved;
809 - }
810 - };
811 -
812 - if (is_array($fileValue)) {
813 - foreach ($fileValue as $singleFile) {
814 - $addFile($singleFile);
815 - }
816 - return;
817 - }
818 -
819 - $addFile($fileValue);
820 - }
821 -
822 - /**
823 - * Return file type by checking with mime type
824 - * @param string $mime
825 - * @return string
826 - */
827 - public static function getFileTypeByMime(string $mime)
828 - {
829 - $mime = strtolower($mime);
830 -
831 - if (preg_match('/^image\//', $mime)) {
832 - return 'image';
833 - }
834 -
835 - $compressed = [
836 - 'application/zip',
837 - 'application/x-rar-compressed',
838 - 'application/x-7z-compressed',
839 - 'application/gzip',
840 - 'application/x-tar',
841 - 'application/x-gtar',
842 - 'application/x-bzip2',
843 - 'application/x-archive',
844 - 'application/vnd.debian.binary-package',
845 - ];
846 - if (in_array($mime, $compressed, true)) {
847 - return 'compressed';
848 - }
849 -
850 - $presentation = [
851 - 'application/vnd.ms-powerpoint',
852 - 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
853 - 'application/vnd.oasis.opendocument.presentation',
854 - 'application/vnd.apple.keynote',
855 - ];
856 - if (in_array($mime, $presentation, true)) {
857 - return 'presentation';
858 - }
859 -
860 - $document = [
861 - 'application/pdf',
862 - 'application/msword',
863 - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
864 - 'application/rtf',
865 - 'text/plain',
866 - 'application/vnd.oasis.opendocument.text',
867 - 'application/x-tex',
868 - 'text/rtf',
869 - ];
870 - if (in_array($mime, $document, true)) {
871 - return 'document';
872 - }
873 -
874 - $data = [
875 - 'text/csv',
876 - 'application/xml',
877 - 'text/xml',
878 - 'application/sql',
879 - 'application/vnd.ms-excel',
880 - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
881 - 'application/x-sqlite3',
882 - 'application/octet-stream', // generic binary (could be db files)
883 - ];
884 - if (in_array($mime, $data, true)) {
885 - return 'data';
886 - }
887 -
888 - if (preg_match('/^audio\//', $mime)) {
889 - return 'audio';
890 - }
891 -
892 - if (preg_match('/^video\//', $mime)) {
893 - return 'video';
894 - }
895 -
896 - return 'other';
897 - }
898 -
899 - /**
900 - * Return file type by checking with extension
901 - * @param string $extension
902 - * @return string
903 - */
904 - public static function getFileTypeByExtension($extension)
905 - {
906 - switch (strtolower($extension)) {
907 - case 'xbm':
908 - case 'tif':
909 - case 'pjp':
910 - case 'pjpeg':
911 - case 'svgz':
912 - case 'jpg':
913 - case 'jpeg':
914 - case 'ico':
915 - case 'tiff':
916 - case 'gif':
917 - case 'svg':
918 - case 'bmp':
919 - case 'png':
920 - case 'jfif':
921 - case 'webp':
922 - return 'image';
923 -
924 - case '7z':
925 - case 'arj':
926 - case 'deb':
927 - case 'pkg':
928 - case 'rar':
929 - case 'rpm':
930 - case 'gz':
931 - case 'z':
932 - case 'zip':
933 - return 'compressed';
934 -
935 - case 'key':
936 - case 'odp':
937 - case 'pps':
938 - case 'ppt':
939 - case 'pptx':
940 - return 'presentation';
941 -
942 - case '_rf_':
943 - case 'doc':
944 - case 'docx':
945 - case 'odt':
946 - case 'pdf':
947 - case 'rtf':
948 - case 'tex':
949 - case 'txt':
950 - case 'wks':
951 - case 'wps':
952 - case 'wpd':
953 - return 'document';
954 -
955 - case 'csv':
956 - case 'dat':
957 - case 'db':
958 - case 'dbf':
959 - case 'log':
960 - case 'mdb':
961 - case 'sav':
962 - case 'sql':
963 - case 'tar':
964 - case 'sqlite':
965 - case 'xml':
966 - return 'data';
967 -
968 - case 'opus':
969 - case 'flac':
970 - case 'webm':
971 - case 'weba':
972 - case 'wav':
973 - case 'ogg':
974 - case 'm4a':
975 - case 'mp3':
976 - case 'oga':
977 - case 'mid':
978 - case 'amr':
979 - case 'aiff':
980 - case 'wma':
981 - case 'au':
982 - case 'acc':
983 - case 'wpl':
984 - return 'audio';
985 -
986 - case 'ogm':
987 - case 'wmv':
988 - case 'mpg':
989 - case 'ogv':
990 - case 'mov':
991 - case 'asx':
992 - case 'mpeg':
993 - case 'mp4':
994 - case 'm4v':
995 - case 'avi':
996 - case '3gp':
997 - case 'flv':
998 - case 'mkv':
999 - case 'swf':
1000 - return 'video';
1001 - default:
1002 - return 'other';
1003 336 }
1004 337 }
1005 338 }