PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 2.21.13
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v2.21.13
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 2.10.2 All 137 releases
bit-form / includes / Core / Util / FileHandler.php

FileHandler.php in Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder 2.21.13, at includes/Core/Util/FileHandler.php

902 lines 29.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace BitCode\BitForm\Core\Util;
4
5 use BitCode\BitForm\Admin\Form\Helpers;
6 use BitCode\BitForm\Core\Form\FormManager;
7 use BitCode\BitForm\enshrined\svgSanitize\Sanitizer;
8
9 if (!defined('ABSPATH')) {
10 exit;
11 }
12 final class FileHandler
13 {
14 public function rmrf($dir)
15 {
16 $fs = self::initWpFilesystem();
17 if ($fs && $fs->exists($dir)) {
18 $fs->delete($dir, true);
19 }
20 }
21
22 public function cpyr($source, $destination)
23 {
24 if (is_dir($source)) {
25 wp_mkdir_p($destination);
26 // chmod($destination, 0744);
27 $objects = scandir($source);
28 foreach ($objects as $object) {
29 if ('.' !== $object && '..' !== $object) {
30 if (is_dir($source . DIRECTORY_SEPARATOR . $object) && !is_link($source . DIRECTORY_SEPARATOR . $object)) {
31 cpyr($source . DIRECTORY_SEPARATOR . $object, $destination . DIRECTORY_SEPARATOR . $object);
32 } elseif (is_file($source . DIRECTORY_SEPARATOR . $object)) {
33 copy($source . DIRECTORY_SEPARATOR . $object, $destination . DIRECTORY_SEPARATOR . $object);
34 // chmod($destination. DIRECTORY_SEPARATOR .$object, 0644);
35 } else {
36 symlink($source . DIRECTORY_SEPARATOR . $object, $destination . DIRECTORY_SEPARATOR . $object);
37 }
38 }
39 }
40 } else {
41 copy($source, $destination);
42 }
43 }
44
45 public function moveUploadedFiles($file_details, $form_id, $entry_id)
46 {
47 require_once ABSPATH . 'wp-admin/includes/file.php';
48
49 $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
63 if (is_array($file_details['name'])) {
64 foreach ($file_details['name'] as $key => $value) {
65 if (!empty($value)) {
66 $fileNameCount = 1;
67 $file_upoalded[$key] = sanitize_file_name($value);
68 while (file_exists($_upload_dir . DIRECTORY_SEPARATOR . $file_upoalded[$key])) {
69 $fileNameWithSeparator = BITFORMS_BF_SEPARATOR . $fileNameCount;
70 $file_upoalded[$key] = sanitize_file_name(preg_replace('/(.[a-z A-Z 0-9]+)$/', "{$fileNameWithSeparator}$1", $value));
71 $fileNameCount = $fileNameCount + 1;
72 if (11 === $fileNameCount) {
73 break;
74 }
75 }
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'])) {
87 unset($file_upoalded[$key]);
88 } else {
89 $file_upoalded[$key] = basename($upload_result['file']);
90 }
91 }
92 }
93 } else {
94 if (!empty($file_details['name'])) {
95 $fileNameCount = 1;
96 $file_upoalded[0] = sanitize_file_name($file_details['name']);
97 while (file_exists($_upload_dir . DIRECTORY_SEPARATOR . $file_upoalded[0])) {
98 $fileNameWithSeparator = BITFORMS_BF_SEPARATOR . $fileNameCount;
99 $file_upoalded[0] = sanitize_file_name(preg_replace('/(.[a-z A-Z 0-9]+)$/', "{$fileNameWithSeparator}$1", $file_details['name']));
100 $fileNameCount = $fileNameCount + 1;
101 if (11 === $fileNameCount) {
102 break;
103 }
104 }
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'])) {
116 unset($file_upoalded[0]);
117 } else {
118 $file_upoalded[0] = basename($upload_result['file']);
119 }
120 }
121 }
122 return $file_upoalded;
123 }
124
125 public function deleteFiles($form_id, $entry_id, $files)
126 {
127 $_upload_dir = self::getEntriesFileUploadDir($form_id, $entry_id);
128 foreach ($files as $name) {
129 wp_delete_file($_upload_dir . DIRECTORY_SEPARATOR . $name);
130 }
131 }
132
133 public static function getFileUploadError($code)
134 {
135 $errors = [
136 0 => __('Unknown upload error', 'bit-form'),
137 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini.', 'bit-form'),
138 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.', 'bit-form'),
139 3 => __('The uploaded file was only partially uploaded.', 'bit-form'),
140 4 => __('No file was uploaded.', 'bit-form'),
141 6 => __('Missing a temporary folder.', 'bit-form'),
142 7 => __('Failed to write file to disk.', 'bit-form'),
143 8 => __('A PHP extension stopped the file upload.', 'bit-form'),
144 ];
145 return $errors[$code];
146 }
147
148 public static function fileCopy($tmpdir, $destinationDir, $file)
149 {
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;
159 }
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 }
184
185 public static function tempDirToUploadDir($submitted_data, $fields, $formId, $entryID)
186 {
187 $upload_dir = wp_upload_dir();
188 $tempDir = $upload_dir['basedir'] . '/bitforms/temp';
189 $destinationDir = self::getEntriesFileUploadDir($formId, $entryID) . DIRECTORY_SEPARATOR;
190 self::createIndexFile($destinationDir);
191
192 foreach ($submitted_data as $key => $data) {
193 if (isset($fields[$key]) && 'advanced-file-up' === $fields[$key]['type']) {
194 $files = $data;
195 $fldData = $submitted_data[$key];
196 $files = explode(',', $fldData);
197 if (is_array($files) && count($files) > 0) {
198 foreach ($files as $file) {
199 self::fileCopy($tempDir, $destinationDir, trim($file));
200 }
201 } else {
202 self::fileCopy($tempDir, $destinationDir, trim($files));
203 }
204 if (!empty($files)) {
205 $submitted_data[$key] = $files;
206 }
207 }
208 }
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 }
219
220 return $submitted_data;
221 }
222
223 private function getByteSizeByUnit($sizeString)
224 {
225 // split 2MB into 2 and MB
226 $size = preg_replace('/[^0-9\.]/', '', $sizeString);
227 $unit = preg_replace('/[^a-zA-Z]/', '', $sizeString);
228 $unit = strtolower($unit);
229 if ('kb' === $unit) {
230 return $size * 1024;
231 } elseif ('mb' === $unit) {
232 return $size * 1024 * 1024;
233 } elseif ('gb' === $unit) {
234 return $size * 1024 * 1024 * 1024;
235 } else {
236 return $size;
237 }
238 }
239
240 public function validation($field_key, $file_details, $form_id)
241 {
242 if (!function_exists('wp_check_filetype_and_ext')) {
243 require_once ABSPATH . 'wp-admin/includes/file.php';
244 }
245
246 $formManager = FormManager::getInstance($form_id);
247 $form_contents = $formManager->getFormContent();
248 $field_content_details = $form_contents->fields;
249 $fieldDetail = $field_content_details->{$field_key};
250 $fieldType = $fieldDetail->typ;
251 $maxSizeDetails = [];
252 $allowFileTypes = [];
253 $maxSize = null;
254 if ('file-up' === $fieldType) {
255 $allowFileTypes = !empty($fieldDetail->config->allowedFileType) ? $fieldDetail->config->allowedFileType : [];
256 if (!empty($fieldDetail->config->allowMaxSize)) {
257 if (!empty($fieldDetail->config->maxSize)) {
258 $maxSizeDetails['maxSize'] = $fieldDetail->config->maxSize . $fieldDetail->config->sizeUnit;
259 }
260 if (!empty($fieldDetail->config->isItTotalMax)) {
261 $maxSizeDetails['maxTotalFileSize'] = $fieldDetail->config->maxSize . $fieldDetail->config->sizeUnit;
262 }
263 }
264 if (!empty($allowFileTypes)) {
265 $allowFileTypes = explode(',', $allowFileTypes);
266 }
267 } elseif ('advanced-file-up' === $fieldType) {
268 $allowFileTypes = !empty($fieldDetail->config->allowFileTypeValidation) ? $fieldDetail->config->acceptedFileTypes : [];
269 if (!empty($fieldDetail->config->allowFileSizeValidation)) {
270 if (!empty($fieldDetail->config->maxFileSize)) {
271 $maxSizeDetails['maxSize'] = $fieldDetail->config->maxFileSize;
272 }
273 if (!empty($fieldDetail->config->maxTotalFileSize)) {
274 $maxSizeDetails['maxTotalFileSize'] = $fieldDetail->config->maxTotalFileSize;
275 }
276 }
277 }
278 if (!empty($maxSizeDetails['maxSize'])) {
279 $maxSize = $this->getByteSizeByUnit($maxSizeDetails['maxSize']);
280 }
281 $maxTotalFileSize = null;
282 if (!empty($maxSizeDetails['maxTotalFileSize'])) {
283 $maxTotalFileSize = $this->getByteSizeByUnit($maxSizeDetails['maxTotalFileSize']);
284 }
285
286 if ($formManager->isRepeatedField($field_key)) {
287 foreach ($file_details['name'] as $rowIndex => $file) {
288 if (!empty($file)) {
289 $fileDetails = [
290 'name' => $file,
291 'type' => $file_details['type'][$rowIndex],
292 'tmp_name' => $file_details['tmp_name'][$rowIndex],
293 'error' => $file_details['error'][$rowIndex],
294 'size' => $file_details['size'][$rowIndex],
295 ];
296 $validateState = $this->validateFileInfo($fieldType, $fileDetails, $allowFileTypes, $maxSize, $maxTotalFileSize);
297 if (!empty($validateState) && !empty($validateState['message'])) {
298 return $validateState;
299 }
300 }
301 }
302 } else {
303 return $this->validateFileInfo($fieldType, $file_details, $allowFileTypes, $maxSize, $maxTotalFileSize);
304 }
305 return [];
306 }
307
308 private function validateFileInfo($fieldType, $file_details, $allowFileTypes, $maxSize, $maxTotalFileSize)
309 {
310 $errorMessage = [
311 'message' => '',
312 'error_type'=> '',
313 ];
314 if (is_array($file_details['name'])) {
315 $totalSize = 0;
316 foreach ($file_details['name'] as $key => $file) {
317 if (!empty($file)) {
318 $fileInfo = [
319 'name' => $file,
320 'type' => $file_details['type'][$key],
321 'tmp_name' => $file_details['tmp_name'][$key],
322 'error' => $file_details['error'][$key],
323 'size' => $file_details['size'][$key],
324 ];
325 $totalSize += $fileInfo['size'];
326 $validateState = $this->validateSingleFile($fieldType, $fileInfo, $allowFileTypes, $maxSize);
327 if (!empty($validateState)) {
328 return $validateState;
329 }
330 }
331 }
332 if (isset($maxTotalFileSize) && !is_null($maxTotalFileSize) && $totalSize > $maxTotalFileSize) {
333 $errorMessage['message'] = __('Total File size is too large', 'bit-form');
334 $errorMessage['error_type'] = 'file_size_error';
335 return $errorMessage;
336 }
337 } else {
338 $validateState = $this->validateSingleFile($fieldType, $file_details, $allowFileTypes, $maxSize);
339 if (!empty($validateState)) {
340 return $validateState;
341 }
342 }
343
344 return $errorMessage;
345 }
346
347 private function validateSingleFile($fieldType, &$file, $allowTypes, $maxSize = null)
348 {
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 !== $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', 'text/x-php', 'application/x-msdownload', 'application/x-msdos-program',
400 'application/x-sh', 'application/x-csh', 'text/x-shellscript', 'application/java-archive'
401 ]);
402 $denyExt = apply_filters('bitform_filter_upload_denied_extensions', ['php', 'phtml', 'phar', 'htaccess', 'html', 'js', 'exe', 'sh', 'bat', 'cmd']);
403 if (in_array($detectedMime, $disallowedMimes, true) || in_array($fileExtension, $denyExt, true)) {
404 return ['message' => __('This file type is not allowed', 'bit-form'), 'error_type' => 'file_type_error'];
405 }
406
407 // 4) Normalize allowlist: support both extensions (.jpg or jpg) and MIME types
408 // --- ALLOWLIST NORMALIZATION (extensions + MIME) ---
409 $normalizedAllow = array_values(array_unique(array_map(static function ($t) {
410 return strtolower(trim((string)$t));
411 }, (array)$allowTypes)));
412
413 $allowExts = [];
414 $allowMimes = [];
415 foreach ($normalizedAllow as $t) {
416 if ('' === $t) {
417 continue;
418 }
419 if (false !== strpos($t, '/')) {
420 // looks like a MIME
421 $allowMimes[] = $t;
422 } else {
423 // extension: may include leading dot; normalize without dot
424 $allowExts[] = ltrim($t, '.');
425 }
426 }
427
428 $allowExts = array_values(array_unique($allowExts));
429 $allowMimes = array_values(array_unique($allowMimes));
430
431 $hasAllowlist = (!empty($allowExts) || !empty($allowMimes));
432 $extMatch = in_array($fileExtension, $allowExts, true);
433 $mimeMatch = in_array($detectedMime, $allowMimes, true);
434
435 // 5) Require BOTH a legit WP mapping AND a match to the allowlist
436 $wpOk = ('' !== $wpExt && '' !== $wpType);
437
438 if ($hasAllowlist) {
439 if (!($extMatch || $mimeMatch)) {
440 return ['message' => __('File type is not allowed', 'bit-form'), 'error_type' => 'file_type_error'];
441 }
442 } else {
443 if (!$wpOk) {
444 return ['message' => __('File type is not allowed', 'bit-form'), 'error_type' => 'file_type_error'];
445 }
446 }
447
448 // 5) Special SVG handling (by MIME, not just extension)
449 if ('svg' === $fileExtension || 'image/svg+xml' === $detectedMime || 'image/svg+xml' === $wpType) {
450 if ('image/svg+xml' !== $detectedMime) {
451 return ['message' => __('Invalid SVG', 'bit-form'), 'error_type' => 'file_type_error'];
452 }
453
454 $dirty = file_get_contents($file['tmp_name']);
455 $svg_sanitizer = new Sanitizer();
456 $clean = $svg_sanitizer->sanitize($dirty);
457 if (false === $clean) {
458 return ['message' => __('SVG file is not valid', 'bit-form'), 'error_type' => 'file_type_error'];
459 }
460 file_put_contents($file['tmp_name'], $clean, LOCK_EX);
461 // Re-check size post-sanitize
462 if (!empty($maxSize) && filesize($file['tmp_name']) > $maxSize) {
463 return ['message' => __('File size is too large after sanitation', 'bit-form'), 'error_type' => 'file_size_error'];
464 }
465 }
466
467 if ('pdf' === $fileExtension || 'application/pdf' === $detectedMime || 'application/pdf' === $wpType) {
468 $blockedList = [
469 '/\/JS\b/',
470 '/\/JavaScript\b/',
471 '/eval\(/i',
472 '/app\.alert/i',
473 '/console\.log\b/i',
474 '/document\.write\b/i',
475
476 '/\/GoToR\b/i',
477 '/\/Launch\b/i',
478
479 '/\/EmbeddedFile\b/i',
480 '/\/EmbeddedFiles\b/i',
481 '/\/Filespec\b/i',
482 '/\/FileAttachment\b/i',
483
484 '/\/SubmitForm\b/i',
485 '/\/ResetForm\b/i',
486 '/\/ImportData\b/i',
487
488 '/\/RichMedia\b/i',
489 ];
490
491 $pdfContent = file_get_contents($file['tmp_name']);
492 foreach ($blockedList as $blockedKeyWrd) {
493 if (preg_match($blockedKeyWrd, $pdfContent)) {
494 return ['message' => __('Invalid file', 'bit-form'), 'error_type' => 'file_type_error'];
495 }
496 }
497 }
498
499 // --- Allow developer to make a final decision override (optional) ---
500 $final = apply_filters('bit_form_upload_allow_file', true, [ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
501 'file_name' => $fileName,
502 'extension' => $fileExtension,
503 'detected_mime' => $detectedMime,
504 'wp_ext' => $wpExt,
505 'wp_type' => $wpType,
506 'allow_exts' => $allowExts,
507 'allow_mimes' => $allowMimes,
508 'has_allowlist' => $hasAllowlist,
509 ]);
510 if (true !== $final) {
511 // If a dev returns a WP_Error, you could extract message/type; here we just block.
512 return ['message' => __('File not allowed by policy', 'bit-form'), 'error_type' => 'file_policy_block'];
513 }
514 return null; // success
515 }
516
517 public static function deleteIsFileExists($path)
518 {
519 if (file_exists($path)) {
520 wp_delete_file($path);
521 }
522 }
523
524 public static function getEntriesFileUploadDir($form_id, $entry_id)
525 {
526 $uploadDir = rtrim(BITFORMS_UPLOAD_DIR, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $form_id . DIRECTORY_SEPARATOR;
527 $encrypted_directoryId = Helpers::getEncryptedEntryId($entry_id);
528 $encryptedDirectory = $uploadDir . $encrypted_directoryId;
529 if (is_dir($encryptedDirectory)) {
530 return $encryptedDirectory;
531 }
532 $oldEntriesFileUploadDir = self::getOldEntriesFileUploadDir($uploadDir, $entry_id);
533 return $oldEntriesFileUploadDir;
534
535 }
536
537 public static function getOldEntriesFileUploadDir($uploadDir, $entry_id)
538 {
539 $authSaltEncryptedEntryId = Helpers::getAuthSaltEncryptToken($entry_id);
540 $authSaltEncryptedDirectory = $uploadDir . $authSaltEncryptedEntryId;
541 if (!empty($authSaltEncryptedEntryId) && is_dir($authSaltEncryptedDirectory)) {
542 return $authSaltEncryptedDirectory;
543 }
544 $previousEntryDirectory = $uploadDir . $entry_id;
545 return $previousEntryDirectory;
546 }
547
548 private static function replaceDocumentRoot($path)
549 {
550 $relativePath = str_replace(ABSPATH, '', $path); // Remove absolute server path
551 return site_url($relativePath); // Prepend with domain
552 }
553
554 public static function getEntriesFileUploadURL($form_id, $entry_id)
555 {
556 $documentRoot = self::getEntriesFileUploadDir($form_id, $entry_id);
557 $url = self::replaceDocumentRoot($documentRoot);
558 return $url;
559 }
560
561 public static function createIndexFile($directory)
562 {
563 if (wp_mkdir_p($directory)) {
564 $indexFilePath = rtrim($directory, '/') . '/index.php';
565 if (!file_exists($indexFilePath)) {
566 try {
567 if (false === self::writeFile($indexFilePath, "<?php\n// No direct access allowed.")) {
568 throw new \Exception("Failed to create index.php in $directory");
569 }
570 } catch (\Exception $e) {
571 Log::debug_log('File creation Failed:' . $e->getMessage()); // Log the error for debugging
572 }
573 }
574 }
575 return false;
576 }
577
578 /**
579 * Initialise and return the WP_Filesystem abstraction layer.
580 *
581 * @return WP_Filesystem_Base|false
582 */
583 private static function initWpFilesystem()
584 {
585 global $wp_filesystem;
586 if (empty($wp_filesystem)) {
587 require_once ABSPATH . 'wp-admin/includes/file.php';
588 WP_Filesystem();
589 }
590 return $wp_filesystem;
591 }
592
593 /**
594 * Write (overwrite) content to a file using WP_Filesystem.
595 *
596 * @param string $filePath Absolute path to the file.
597 * @param string $content Content to write.
598 * @return bool True on success, false on failure.
599 */
600 public static function writeFile($filePath, $content)
601 {
602 $fs = self::initWpFilesystem();
603 if ($fs) {
604 return $fs->put_contents($filePath, $content, FS_CHMOD_FILE);
605 }
606 // Fallback: file_put_contents is acceptable when WP_Filesystem is unavailable.
607 return false !== file_put_contents($filePath, $content); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
608 }
609
610 /**
611 * Append content to a file using WP_Filesystem.
612 * WP_Filesystem has no native append; we read + concatenate + write.
613 *
614 * @param string $filePath Absolute path to the file.
615 * @param string $content Content to append.
616 * @return bool True on success, false on failure.
617 */
618 public static function appendFile($filePath, $content)
619 {
620 $fs = self::initWpFilesystem();
621 if ($fs) {
622 $existing = $fs->exists($filePath) ? (string) $fs->get_contents($filePath) : '';
623 return $fs->put_contents($filePath, $existing . $content, FS_CHMOD_FILE);
624 }
625 // Fallback: file_put_contents is acceptable when WP_Filesystem is unavailable.
626 return false !== file_put_contents($filePath, $content, FILE_APPEND | LOCK_EX); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
627 }
628
629 /**
630 * Read and return the full content of a file using WP_Filesystem.
631 *
632 * @param string $filePath Absolute path to the file.
633 * @return string File contents, or empty string if unreadable.
634 */
635 public static function readFile($filePath)
636 {
637 $fs = self::initWpFilesystem();
638 if ($fs && $fs->exists($filePath)) {
639 $content = $fs->get_contents($filePath);
640 return false !== $content ? $content : '';
641 }
642 // Fallback.
643 if (file_exists($filePath)) {
644 $content = file_get_contents($filePath); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
645 return false !== $content ? $content : '';
646 }
647 return '';
648 }
649
650 public static function processRepeaterAttachment($repeaterKey, $fileKey, $fieldValue, $basePath, &$attachments)
651 {
652 if (!isset($fieldValue[$repeaterKey]) || !is_array($fieldValue[$repeaterKey])) {
653 return;
654 }
655
656 foreach ($fieldValue[$repeaterKey] as $repeaterRow) {
657 if (!isset($repeaterRow[$fileKey]) || empty($repeaterRow[$fileKey])) {
658 continue;
659 }
660
661 $fileValue = $repeaterRow[$fileKey];
662 self::addAttachmentFiles($fileValue, $basePath, $attachments);
663 }
664 }
665
666 public static function processRegularAttachment($fileKey, $fieldValue, $basePath, &$attachments)
667 {
668 if (!isset($fieldValue[$fileKey]) || empty($fieldValue[$fileKey])) {
669 return;
670 }
671
672 $fileValue = $fieldValue[$fileKey];
673 self::addAttachmentFiles($fileValue, $basePath, $attachments);
674 }
675
676 private static function addAttachmentFiles($fileValue, $basePath, &$attachments)
677 {
678 $baseResolved = realpath($basePath);
679 if (false === $baseResolved) {
680 Log::debug_log([
681 'message' => 'FileHandler::addAttachmentFiles base path invalid',
682 'basePath' => $basePath,
683 ]);
684 return;
685 }
686
687 $addFile = static function ($candidate) use ($baseResolved, &$attachments) {
688 $safeFile = is_string($candidate) ? trim($candidate) : '';
689 if ('' === $safeFile) {
690 return;
691 }
692 $path = $baseResolved . DIRECTORY_SEPARATOR . $safeFile;
693 $resolved = realpath($path);
694 if (false === $resolved || 0 !== strpos($resolved, $baseResolved . DIRECTORY_SEPARATOR)) {
695 Log::debug_log([
696 'message' => 'FileHandler::addAttachmentFiles blocked path traversal',
697 'file' => $candidate,
698 'resolved' => $resolved,
699 'baseResolved' => $baseResolved,
700 ]);
701 return;
702 }
703 if (is_readable($resolved)) {
704 $attachments[] = $resolved;
705 }
706 };
707
708 if (is_array($fileValue)) {
709 foreach ($fileValue as $singleFile) {
710 $addFile($singleFile);
711 }
712 return;
713 }
714
715 $addFile($fileValue);
716 }
717
718 /**
719 * Return file type by checking with mime type
720 * @param string $mime
721 * @return string
722 */
723 public static function getFileTypeByMime(string $mime)
724 {
725 $mime = strtolower($mime);
726
727 if (preg_match('/^image\//', $mime)) {
728 return 'image';
729 }
730
731 $compressed = [
732 'application/zip',
733 'application/x-rar-compressed',
734 'application/x-7z-compressed',
735 'application/gzip',
736 'application/x-tar',
737 'application/x-gtar',
738 'application/x-bzip2',
739 'application/x-archive',
740 'application/vnd.debian.binary-package',
741 ];
742 if (in_array($mime, $compressed, true)) {
743 return 'compressed';
744 }
745
746 $presentation = [
747 'application/vnd.ms-powerpoint',
748 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
749 'application/vnd.oasis.opendocument.presentation',
750 'application/vnd.apple.keynote',
751 ];
752 if (in_array($mime, $presentation, true)) {
753 return 'presentation';
754 }
755
756 $document = [
757 'application/pdf',
758 'application/msword',
759 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
760 'application/rtf',
761 'text/plain',
762 'application/vnd.oasis.opendocument.text',
763 'application/x-tex',
764 'text/rtf',
765 ];
766 if (in_array($mime, $document, true)) {
767 return 'document';
768 }
769
770 $data = [
771 'text/csv',
772 'application/xml',
773 'text/xml',
774 'application/sql',
775 'application/vnd.ms-excel',
776 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
777 'application/x-sqlite3',
778 'application/octet-stream', // generic binary (could be db files)
779 ];
780 if (in_array($mime, $data, true)) {
781 return 'data';
782 }
783
784 if (preg_match('/^audio\//', $mime)) {
785 return 'audio';
786 }
787
788 if (preg_match('/^video\//', $mime)) {
789 return 'video';
790 }
791
792 return 'other';
793 }
794
795 /**
796 * Return file type by checking with extension
797 * @param string $extension
798 * @return string
799 */
800 public static function getFileTypeByExtension($extension)
801 {
802 switch (strtolower($extension)) {
803 case 'xbm':
804 case 'tif':
805 case 'pjp':
806 case 'pjpeg':
807 case 'svgz':
808 case 'jpg':
809 case 'jpeg':
810 case 'ico':
811 case 'tiff':
812 case 'gif':
813 case 'svg':
814 case 'bmp':
815 case 'png':
816 case 'jfif':
817 case 'webp':
818 return 'image';
819
820 case '7z':
821 case 'arj':
822 case 'deb':
823 case 'pkg':
824 case 'rar':
825 case 'rpm':
826 case 'gz':
827 case 'z':
828 case 'zip':
829 return 'compressed';
830
831 case 'key':
832 case 'odp':
833 case 'pps':
834 case 'ppt':
835 case 'pptx':
836 return 'presentation';
837
838 case '_rf_':
839 case 'doc':
840 case 'docx':
841 case 'odt':
842 case 'pdf':
843 case 'rtf':
844 case 'tex':
845 case 'txt':
846 case 'wks':
847 case 'wps':
848 case 'wpd':
849 return 'document';
850
851 case 'csv':
852 case 'dat':
853 case 'db':
854 case 'dbf':
855 case 'log':
856 case 'mdb':
857 case 'sav':
858 case 'sql':
859 case 'tar':
860 case 'sqlite':
861 case 'xml':
862 return 'data';
863
864 case 'opus':
865 case 'flac':
866 case 'webm':
867 case 'weba':
868 case 'wav':
869 case 'ogg':
870 case 'm4a':
871 case 'mp3':
872 case 'oga':
873 case 'mid':
874 case 'amr':
875 case 'aiff':
876 case 'wma':
877 case 'au':
878 case 'acc':
879 case 'wpl':
880 return 'audio';
881
882 case 'ogm':
883 case 'wmv':
884 case 'mpg':
885 case 'ogv':
886 case 'mov':
887 case 'asx':
888 case 'mpeg':
889 case 'mp4':
890 case 'm4v':
891 case 'avi':
892 case '3gp':
893 case 'flv':
894 case 'mkv':
895 case 'swf':
896 return 'video';
897 default:
898 return 'other';
899 }
900 }
901 }
902