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 +72 -641 -3.0.12.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;
@@ -123,9 +97,9 @@
123 97 }
124 98
125 99 public function deleteFiles($form_id, $entry_id, $files)
126 100 {
127 - $_upload_dir = self::getEntriesFileUploadDir($form_id, $entry_id);
101 + $_upload_dir = BITFORMS_UPLOAD_DIR . DIRECTORY_SEPARATOR . $form_id . DIRECTORY_SEPARATOR . $entry_id;
128 102 foreach ($files as $name) {
129 103 wp_delete_file($_upload_dir . DIRECTORY_SEPARATOR . $name);
130 104 }
131 105 }
@@ -146,41 +120,13 @@
146 120 }
147 121
148 122 public static function fileCopy($tmpdir, $destinationDir, $file)
149 123 {
150 - $tmpBase = realpath($tmpdir);
151 - $destBase = realpath($destinationDir);
152 - if (false === $tmpBase || false === $destBase) {
153 - Log::debug_log([
154 - 'message' => 'FileHandler::fileCopy base path invalid',
155 - 'tmpdir' => $tmpdir,
156 - 'destinationDir' => $destinationDir,
157 - ]);
158 - return;
124 + $tmpFile = $tmpdir . DIRECTORY_SEPARATOR . $file;
125 + $newFile = $destinationDir . DIRECTORY_SEPARATOR . $file;
126 + if (file_exists($tmpFile)) {
127 + copy($tmpFile, $newFile);
159 128 }
160 -
161 - $safeFile = is_string($file) ? trim($file) : '';
162 - if ('' === $safeFile) {
163 - return;
164 - }
165 -
166 - $candidate = $tmpBase . DIRECTORY_SEPARATOR . $safeFile;
167 - $resolved = realpath($candidate);
168 - if (false === $resolved || 0 !== strpos($resolved, $tmpBase . DIRECTORY_SEPARATOR)) {
169 - Log::debug_log([
170 - 'message' => 'FileHandler::fileCopy blocked path traversal',
171 - 'file' => $file,
172 - 'resolved' => $resolved,
173 - 'tmpBase' => $tmpBase,
174 - ]);
175 - return;
176 - }
177 - if (!is_readable($resolved)) {
178 - return;
179 - }
180 -
181 - $newFile = $destBase . DIRECTORY_SEPARATOR . basename($resolved);
182 - copy($resolved, $newFile);
183 129 }
184 130
185 131 public static function tempDirToUploadDir($submitted_data, $fields, $formId, $entryID)
186 132 {
@@ -185,10 +131,12 @@
185 131 public static function tempDirToUploadDir($submitted_data, $fields, $formId, $entryID)
186 132 {
187 133 $upload_dir = wp_upload_dir();
188 134 $tempDir = $upload_dir['basedir'] . '/bitforms/temp';
189 - $destinationDir = self::getEntriesFileUploadDir($formId, $entryID) . DIRECTORY_SEPARATOR;
190 - 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 + }
191 139
192 140 foreach ($submitted_data as $key => $data) {
193 141 if (isset($fields[$key]) && 'advanced-file-up' === $fields[$key]['type']) {
194 142 $files = $data;
@@ -205,18 +153,11 @@
205 153 $submitted_data[$key] = $files;
206 154 }
207 155 }
208 156 }
209 - $tempBase = realpath($tempDir);
210 - if (false !== $tempBase) {
211 - $tmpFiles = glob($tempBase . DIRECTORY_SEPARATOR . '*');
212 - foreach ((array) $tmpFiles as $tmpFile) {
213 - $resolved = realpath($tmpFile);
214 - if (false !== $resolved && 0 === strpos($resolved, $tempBase . DIRECTORY_SEPARATOR)) {
215 - wp_delete_file($resolved);
216 - }
217 - }
218 - }
157 + array_map('unlink', array_filter(
158 + (array) array_merge(glob("$tempDir/*"))
159 + ));
219 160
220 161 return $submitted_data;
221 162 }
222 163
@@ -242,9 +183,9 @@
242 183 if (!function_exists('wp_check_filetype_and_ext')) {
243 184 require_once ABSPATH . 'wp-admin/includes/file.php';
244 185 }
245 186
246 - $formManager = FormManager::getInstance($form_id);
187 + $formManager = new FormManager($form_id);
247 188 $form_contents = $formManager->getFormContent();
248 189 $field_content_details = $form_contents->fields;
249 190 $fieldDetail = $field_content_details->{$field_key};
250 191 $fieldType = $fieldDetail->typ;
@@ -307,14 +248,14 @@
307 248
308 249 private function validateFileInfo($fieldType, $file_details, $allowFileTypes, $maxSize, $maxTotalFileSize)
309 250 {
310 251 $errorMessage = [
311 - 'message' => '',
312 - 'error_type' => '',
252 + 'message' => '',
253 + 'error_type'=> '',
313 254 ];
314 255 if (is_array($file_details['name'])) {
315 256 $totalSize = 0;
316 - foreach ($file_details['name'] as $key => $file) {
257 + foreach ($file_details['name'] as $key => $file) {
317 258 if (!empty($file)) {
318 259 $fileInfo = [
319 260 'name' => $file,
320 261 'type' => $file_details['type'][$key],
@@ -345,180 +286,48 @@
345 286 }
346 287
347 288 private function validateSingleFile($fieldType, &$file, $allowTypes, $maxSize = null)
348 289 {
349 - // 0) Basic sanity & transport integrity
350 - if (!is_array($file) || empty($file['tmp_name'])) {
351 - // return ['message' => __('No file uploaded.', 'bit-form'), 'error_type' => 'file_missing'];
352 - return null;
353 - }
354 - if (!isset($file['error']) || UPLOAD_ERR_OK !== (int)$file['error']) {
355 - return ['message' => __('Upload failed', 'bit-form'), 'error_type' => 'file_upload_error'];
356 - }
357 - if (!is_uploaded_file($file['tmp_name'])) {
358 - return ['message' => __('Untrusted upload source', 'bit-form'), 'error_type' => 'file_upload_error'];
359 - }
360 - if (!is_file($file['tmp_name']) || !is_readable($file['tmp_name'])) {
361 - return ['message' => __('Temporary file not accessible', 'bit-form'), 'error_type' => 'file_upload_error'];
362 - }
363 -
364 - $fileName = sanitize_file_name((string)($file['name'] ?? ''));
365 - if ('' === $fileName) {
366 - return ['message' => __('Empty filename', 'bit-form'), 'error_type' => 'file_type_error'];
367 - }
368 -
369 - // 1) Enforce max size (header + actual)
370 - $onDiskSize = @filesize($file['tmp_name']);
371 - if (false === $onDiskSize) {
372 - return ['message' => __('Cannot read file size', 'bit-form'), 'error_type' => 'file_upload_error'];
373 - }
374 - if (!empty($maxSize) && $onDiskSize > $maxSize) {
375 - return ['message' => __('File size is too large', 'bit-form'), 'error_type' => 'file_size_error'];
376 - }
377 -
378 - // 2) Determine ext + MIME using WP + finfo
379 - $wpCheck = wp_check_filetype_and_ext($file['tmp_name'], $fileName); // ['ext'=>'jpg','type'=>'image/jpeg']
380 - $fileExtension = strtolower((string)(empty($wpCheck['ext']) ? pathinfo($fileName, PATHINFO_EXTENSION) : $wpCheck['ext']));
381 - $wpExt = strtolower((string)(empty($wpCheck['ext']) ? $fileExtension : $wpCheck['ext']));
382 - $wpType = strtolower((string)($wpCheck['type'] ?? ''));
383 - $fi = function_exists('finfo_open') ? @finfo_open(FILEINFO_MIME_TYPE) : false;
384 - $detectedMime = $fi ? @finfo_file($fi, $file['tmp_name']) : false;
385 - if ($fi) {
386 - @finfo_close($fi);
387 - }
388 - if (!$detectedMime && function_exists('mime_content_type')) {
389 - $detectedMime = @mime_content_type($file['tmp_name']);
390 - }
391 - if (!$detectedMime) {
392 - $detectedMime = '' !== $wpType ? $wpType : 'application/octet-stream';
393 - }
394 - $detectedMime = strtolower(trim($detectedMime));
395 -
396 - // Hard-block risky types regardless
397 - // 3) Block obvious executable types regardless of allow list
398 - $disallowedMimes = apply_filters('bitform_filter_upload_disallowed_mimes', [
399 - 'application/x-php',
400 - 'text/x-php',
401 - 'application/x-msdownload',
402 - 'application/x-msdos-program',
403 - 'application/x-sh',
404 - 'application/x-csh',
405 - 'text/x-shellscript',
406 - 'application/java-archive'
407 - ]);
408 - $denyExt = apply_filters('bitform_filter_upload_denied_extensions', ['php', 'phtml', 'phar', 'htaccess', 'html', 'js', 'exe', 'sh', 'bat', 'cmd']);
409 - if (in_array($detectedMime, $disallowedMimes, true) || in_array($fileExtension, $denyExt, true)) {
410 - return ['message' => __('This file type is not allowed', 'bit-form'), 'error_type' => 'file_type_error'];
411 - }
412 -
413 - // 4) Normalize allowlist: support both extensions (.jpg or jpg) and MIME types
414 - // --- ALLOWLIST NORMALIZATION (extensions + MIME) ---
415 - $normalizedAllow = array_values(array_unique(array_map(static function ($t) {
416 - return strtolower(trim((string)$t));
417 - }, (array)$allowTypes)));
418 -
419 - $allowExts = [];
420 - $allowMimes = [];
421 - foreach ($normalizedAllow as $t) {
422 - if ('' === $t) {
423 - 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 + ];
424 298 }
425 - if (false !== strpos($t, '/')) {
426 - // looks like a MIME
427 - $allowMimes[] = $t;
428 - } else {
429 - // extension: may include leading dot; normalize without dot
430 - $allowExts[] = ltrim($t, '.');
431 - }
432 - }
433 299
434 - $allowExts = array_values(array_unique($allowExts));
435 - $allowMimes = array_values(array_unique($allowMimes));
436 -
437 - $hasAllowlist = (!empty($allowExts) || !empty($allowMimes));
438 - $extMatch = in_array($fileExtension, $allowExts, true);
439 - $mimeMatch = in_array($detectedMime, $allowMimes, true);
440 -
441 - // 5) Require BOTH a legit WP mapping AND a match to the allowlist
442 - $wpOk = ('' !== $wpExt && '' !== $wpType);
443 -
444 - if ($hasAllowlist) {
445 - if (!($extMatch || $mimeMatch)) {
446 - 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);
447 310 }
448 - } else {
449 - if (!$wpOk) {
450 - 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 + ];
451 316 }
452 - }
453 -
454 - // 5) Special SVG handling (by MIME, not just extension)
455 - if ('svg' === $fileExtension || 'image/svg+xml' === $detectedMime || 'image/svg+xml' === $wpType) {
456 - if ('image/svg+xml' !== $detectedMime) {
457 - return ['message' => __('Invalid SVG', 'bit-form'), 'error_type' => 'file_type_error'];
458 - }
459 -
460 - $dirty = file_get_contents($file['tmp_name']);
461 - $svg_sanitizer = new Sanitizer();
462 - $clean = $svg_sanitizer->sanitize($dirty);
463 - if (false === $clean) {
464 - return ['message' => __('SVG file is not valid', 'bit-form'), 'error_type' => 'file_type_error'];
465 - }
466 - file_put_contents($file['tmp_name'], $clean, LOCK_EX);
467 - // Re-check size post-sanitize
468 - if (!empty($maxSize) && filesize($file['tmp_name']) > $maxSize) {
469 - return ['message' => __('File size is too large after sanitation', 'bit-form'), 'error_type' => 'file_size_error'];
470 - }
471 - }
472 -
473 - if ('pdf' === $fileExtension || 'application/pdf' === $detectedMime || 'application/pdf' === $wpType) {
474 - $blockedList = [
475 - '/\/JS\b/',
476 - '/\/JavaScript\b/',
477 - '/eval\(/i',
478 - '/app\.alert/i',
479 - '/console\.log\b/i',
480 - '/document\.write\b/i',
481 -
482 - '/\/GoToR\b/i',
483 - '/\/Launch\b/i',
484 -
485 - '/\/EmbeddedFile\b/i',
486 - '/\/EmbeddedFiles\b/i',
487 - '/\/Filespec\b/i',
488 - '/\/FileAttachment\b/i',
489 -
490 - '/\/SubmitForm\b/i',
491 - '/\/ResetForm\b/i',
492 - '/\/ImportData\b/i',
493 -
494 - '/\/RichMedia\b/i',
495 - ];
496 -
497 - $pdfContent = file_get_contents($file['tmp_name']);
498 - foreach ($blockedList as $blockedKeyWrd) {
499 - if (preg_match($blockedKeyWrd, $pdfContent)) {
500 - 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 + ];
501 326 }
327 + file_put_contents($file['tmp_name'], $clean_svg);
502 328 }
503 329 }
504 -
505 - // --- Allow developer to make a final decision override (optional) ---
506 - $final = apply_filters('bitform_filter_upload_allow_file', true, [
507 - 'file_name' => $fileName,
508 - 'extension' => $fileExtension,
509 - 'detected_mime' => $detectedMime,
510 - 'wp_ext' => $wpExt,
511 - 'wp_type' => $wpType,
512 - 'allow_exts' => $allowExts,
513 - 'allow_mimes' => $allowMimes,
514 - 'has_allowlist' => $hasAllowlist,
515 - ]);
516 - if (true !== $final) {
517 - // If a dev returns a WP_Error, you could extract message/type; here we just block.
518 - return ['message' => __('File not allowed by policy', 'bit-form'), 'error_type' => 'file_policy_block'];
519 - }
520 - return null; // success
521 330 }
522 331
523 332 public static function deleteIsFileExists($path)
524 333 {
@@ -523,385 +332,7 @@
523 332 public static function deleteIsFileExists($path)
524 333 {
525 334 if (file_exists($path)) {
526 335 wp_delete_file($path);
527 - }
528 - }
529 -
530 - public static function getEntriesFileUploadDir($form_id, $entry_id)
531 - {
532 - $uploadDir = rtrim(BITFORMS_UPLOAD_DIR, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $form_id . DIRECTORY_SEPARATOR;
533 - $encrypted_directoryId = Helpers::getEncryptedEntryId($entry_id);
534 - $encryptedDirectory = $uploadDir . $encrypted_directoryId;
535 - if (is_dir($encryptedDirectory)) {
536 - return $encryptedDirectory;
537 - }
538 - $oldEntriesFileUploadDir = Helpers::getOldEntriesFileUploadDir($uploadDir, $entry_id);
539 - if (!empty($oldEntriesFileUploadDir) && is_dir($oldEntriesFileUploadDir)) {
540 - return $oldEntriesFileUploadDir;
541 - }
542 - return $encryptedDirectory;
543 - }
544 -
545 - private static function replaceDocumentRoot($path)
546 - {
547 - $uploadDir = wp_upload_dir();
548 - $basedir = wp_normalize_path(rtrim($uploadDir['basedir'], '/\\'));
549 - $baseurl = rtrim($uploadDir['baseurl'], '/');
550 - $normalPath = wp_normalize_path($path);
551 -
552 - if (0 === strpos($normalPath, $basedir . '/') || $normalPath === $basedir) {
553 - return $baseurl . substr($normalPath, strlen($basedir));
554 - }
555 -
556 - // Fallback: path outside uploads dir — strip ABSPATH and prepend home URL.
557 - return home_url(ltrim(str_replace(wp_normalize_path(ABSPATH), '', $normalPath), '/'));
558 - }
559 -
560 - public static function getEntriesFileUploadURL($form_id, $entry_id)
561 - {
562 - $documentRoot = self::getEntriesFileUploadDir($form_id, $entry_id);
563 - $url = self::replaceDocumentRoot($documentRoot);
564 - return $url;
565 - }
566 -
567 - public static function createIndexFile($directory)
568 - {
569 - if (wp_mkdir_p($directory)) {
570 - $indexFilePath = rtrim($directory, '/') . '/index.php';
571 - if (!file_exists($indexFilePath)) {
572 - try {
573 - if (false === self::writeFile($indexFilePath, "<?php\n// No direct access allowed.")) {
574 - throw new \Exception("Failed to create index.php in $directory");
575 - }
576 - } catch (\Exception $e) {
577 - Log::debug_log('File creation Failed:' . $e->getMessage()); // Log the error for debugging
578 - }
579 - }
580 - }
581 - return false;
582 - }
583 -
584 - /**
585 - * Initialise and return the WP_Filesystem abstraction layer.
586 - *
587 - * @return WP_Filesystem_Base|false
588 - */
589 - private static function initWpFilesystem()
590 - {
591 - global $wp_filesystem;
592 - if (empty($wp_filesystem)) {
593 - require_once ABSPATH . 'wp-admin/includes/file.php';
594 - WP_Filesystem();
595 - }
596 - return $wp_filesystem;
597 - }
598 -
599 - /**
600 - * Write (overwrite) content to a file using WP_Filesystem.
601 - *
602 - * @param string $filePath Absolute path to the file.
603 - * @param string $content Content to write.
604 - * @return bool True on success, false on failure.
605 - */
606 - public static function writeFile($filePath, $content)
607 - {
608 - $fs = self::initWpFilesystem();
609 - if ($fs) {
610 - return $fs->put_contents($filePath, $content, FS_CHMOD_FILE);
611 - }
612 - // Fallback: file_put_contents is acceptable when WP_Filesystem is unavailable.
613 - return false !== file_put_contents($filePath, $content);
614 - }
615 -
616 - /**
617 - * Append content to a file using WP_Filesystem.
618 - * WP_Filesystem has no native append; we read + concatenate + write.
619 - *
620 - * @param string $filePath Absolute path to the file.
621 - * @param string $content Content to append.
622 - * @return bool True on success, false on failure.
623 - */
624 - public static function appendFile($filePath, $content)
625 - {
626 - $fs = self::initWpFilesystem();
627 - if ($fs) {
628 - $existing = $fs->exists($filePath) ? (string) $fs->get_contents($filePath) : '';
629 - return $fs->put_contents($filePath, $existing . $content, FS_CHMOD_FILE);
630 - }
631 - // Fallback: file_put_contents is acceptable when WP_Filesystem is unavailable.
632 - return false !== file_put_contents($filePath, $content, FILE_APPEND | LOCK_EX);
633 - }
634 -
635 - /**
636 - * Read and return the full content of a file using WP_Filesystem.
637 - *
638 - * @param string $filePath Absolute path to the file.
639 - * @return string File contents, or empty string if unreadable.
640 - */
641 - public static function readFile($filePath)
642 - {
643 - $fs = self::initWpFilesystem();
644 - if ($fs && $fs->exists($filePath)) {
645 - $content = $fs->get_contents($filePath);
646 - return false !== $content ? $content : '';
647 - }
648 - // Fallback.
649 - if (file_exists($filePath)) {
650 - $content = file_get_contents($filePath);
651 - return false !== $content ? $content : '';
652 - }
653 - return '';
654 - }
655 -
656 - public static function processRepeaterAttachment($repeaterKey, $fileKey, $fieldValue, $basePath, &$attachments)
657 - {
658 - if (!isset($fieldValue[$repeaterKey]) || !is_array($fieldValue[$repeaterKey])) {
659 - return;
660 - }
661 -
662 - foreach ($fieldValue[$repeaterKey] as $repeaterRow) {
663 - if (!isset($repeaterRow[$fileKey]) || empty($repeaterRow[$fileKey])) {
664 - continue;
665 - }
666 -
667 - $fileValue = $repeaterRow[$fileKey];
668 - self::addAttachmentFiles($fileValue, $basePath, $attachments);
669 - }
670 - }
671 -
672 - public static function processRegularAttachment($fileKey, $fieldValue, $basePath, &$attachments)
673 - {
674 - if (!isset($fieldValue[$fileKey]) || empty($fieldValue[$fileKey])) {
675 - return;
676 - }
677 -
678 - $fileValue = $fieldValue[$fileKey];
679 - self::addAttachmentFiles($fileValue, $basePath, $attachments);
680 - }
681 -
682 - private static function addAttachmentFiles($fileValue, $basePath, &$attachments)
683 - {
684 - $baseResolved = realpath($basePath);
685 - if (false === $baseResolved) {
686 - Log::debug_log([
687 - 'message' => 'FileHandler::addAttachmentFiles base path invalid',
688 - 'basePath' => $basePath,
689 - ]);
690 - return;
691 - }
692 -
693 - $addFile = static function ($candidate) use ($baseResolved, &$attachments) {
694 - $safeFile = is_string($candidate) ? trim($candidate) : '';
695 - if ('' === $safeFile) {
696 - return;
697 - }
698 - $path = $baseResolved . DIRECTORY_SEPARATOR . $safeFile;
699 - $resolved = realpath($path);
700 - if (false === $resolved || 0 !== strpos($resolved, $baseResolved . DIRECTORY_SEPARATOR)) {
701 - Log::debug_log([
702 - 'message' => 'FileHandler::addAttachmentFiles blocked path traversal',
703 - 'file' => $candidate,
704 - 'resolved' => $resolved,
705 - 'baseResolved' => $baseResolved,
706 - ]);
707 - return;
708 - }
709 - if (is_readable($resolved)) {
710 - $attachments[] = $resolved;
711 - }
712 - };
713 -
714 - if (is_array($fileValue)) {
715 - foreach ($fileValue as $singleFile) {
716 - $addFile($singleFile);
717 - }
718 - return;
719 - }
720 -
721 - $addFile($fileValue);
722 - }
723 -
724 - /**
725 - * Return file type by checking with mime type
726 - * @param string $mime
727 - * @return string
728 - */
729 - public static function getFileTypeByMime(string $mime)
730 - {
731 - $mime = strtolower($mime);
732 -
733 - if (preg_match('/^image\//', $mime)) {
734 - return 'image';
735 - }
736 -
737 - $compressed = [
738 - 'application/zip',
739 - 'application/x-rar-compressed',
740 - 'application/x-7z-compressed',
741 - 'application/gzip',
742 - 'application/x-tar',
743 - 'application/x-gtar',
744 - 'application/x-bzip2',
745 - 'application/x-archive',
746 - 'application/vnd.debian.binary-package',
747 - ];
748 - if (in_array($mime, $compressed, true)) {
749 - return 'compressed';
750 - }
751 -
752 - $presentation = [
753 - 'application/vnd.ms-powerpoint',
754 - 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
755 - 'application/vnd.oasis.opendocument.presentation',
756 - 'application/vnd.apple.keynote',
757 - ];
758 - if (in_array($mime, $presentation, true)) {
759 - return 'presentation';
760 - }
761 -
762 - $document = [
763 - 'application/pdf',
764 - 'application/msword',
765 - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
766 - 'application/rtf',
767 - 'text/plain',
768 - 'application/vnd.oasis.opendocument.text',
769 - 'application/x-tex',
770 - 'text/rtf',
771 - ];
772 - if (in_array($mime, $document, true)) {
773 - return 'document';
774 - }
775 -
776 - $data = [
777 - 'text/csv',
778 - 'application/xml',
779 - 'text/xml',
780 - 'application/sql',
781 - 'application/vnd.ms-excel',
782 - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
783 - 'application/x-sqlite3',
784 - 'application/octet-stream', // generic binary (could be db files)
785 - ];
786 - if (in_array($mime, $data, true)) {
787 - return 'data';
788 - }
789 -
790 - if (preg_match('/^audio\//', $mime)) {
791 - return 'audio';
792 - }
793 -
794 - if (preg_match('/^video\//', $mime)) {
795 - return 'video';
796 - }
797 -
798 - return 'other';
799 - }
800 -
801 - /**
802 - * Return file type by checking with extension
803 - * @param string $extension
804 - * @return string
805 - */
806 - public static function getFileTypeByExtension($extension)
807 - {
808 - switch (strtolower($extension)) {
809 - case 'xbm':
810 - case 'tif':
811 - case 'pjp':
812 - case 'pjpeg':
813 - case 'svgz':
814 - case 'jpg':
815 - case 'jpeg':
816 - case 'ico':
817 - case 'tiff':
818 - case 'gif':
819 - case 'svg':
820 - case 'bmp':
821 - case 'png':
822 - case 'jfif':
823 - case 'webp':
824 - return 'image';
825 -
826 - case '7z':
827 - case 'arj':
828 - case 'deb':
829 - case 'pkg':
830 - case 'rar':
831 - case 'rpm':
832 - case 'gz':
833 - case 'z':
834 - case 'zip':
835 - return 'compressed';
836 -
837 - case 'key':
838 - case 'odp':
839 - case 'pps':
840 - case 'ppt':
841 - case 'pptx':
842 - return 'presentation';
843 -
844 - case '_rf_':
845 - case 'doc':
846 - case 'docx':
847 - case 'odt':
848 - case 'pdf':
849 - case 'rtf':
850 - case 'tex':
851 - case 'txt':
852 - case 'wks':
853 - case 'wps':
854 - case 'wpd':
855 - return 'document';
856 -
857 - case 'csv':
858 - case 'dat':
859 - case 'db':
860 - case 'dbf':
861 - case 'log':
862 - case 'mdb':
863 - case 'sav':
864 - case 'sql':
865 - case 'tar':
866 - case 'sqlite':
867 - case 'xml':
868 - return 'data';
869 -
870 - case 'opus':
871 - case 'flac':
872 - case 'webm':
873 - case 'weba':
874 - case 'wav':
875 - case 'ogg':
876 - case 'm4a':
877 - case 'mp3':
878 - case 'oga':
879 - case 'mid':
880 - case 'amr':
881 - case 'aiff':
882 - case 'wma':
883 - case 'au':
884 - case 'acc':
885 - case 'wpl':
886 - return 'audio';
887 -
888 - case 'ogm':
889 - case 'wmv':
890 - case 'mpg':
891 - case 'ogv':
892 - case 'mov':
893 - case 'asx':
894 - case 'mpeg':
895 - case 'mp4':
896 - case 'm4v':
897 - case 'avi':
898 - case '3gp':
899 - case 'flv':
900 - case 'mkv':
901 - case 'swf':
902 - return 'video';
903 - default:
904 - return 'other';
905 336 }
906 337 }
907 338 }