PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 3.3.1
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v3.3.1
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 +967 -174 1.93.3.1 View file →
@@ -1,212 +1,1005 @@
1 1 <?php
2 2
3 3 namespace BitCode\BitForm\Core\Util;
4 4
5 +use BitCode\BitForm\Admin\Form\Helpers;
5 6 use BitCode\BitForm\Core\Form\FormManager;
6 -use WP_Error;
7 +use BitCode\BitForm\enshrined\svgSanitize\Sanitizer;
7 8
9 +if (!defined('ABSPATH')) {
10 + exit;
11 +}
8 12 final class FileHandler
9 13 {
10 - public function rmrf($dir)
11 - {
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 - unlink($dir. DIRECTORY_SEPARATOR .$object);
20 - }
21 - }
22 - }
23 - rmdir($dir);
24 - } else {
25 - unlink($dir);
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 + }
26 38 }
39 + }
40 + } else {
41 + copy($source, $destination);
27 42 }
43 + }
28 44
29 - public function cpyr($source, $destination)
30 - {
31 - if (is_dir($source)) {
32 - mkdir($destination);
33 - // chmod($destination, 0744);
34 - $objects = scandir($source);
35 - foreach ($objects as $object) {
36 - if ($object != "." && $object != "..") {
37 - if (is_dir($source. DIRECTORY_SEPARATOR .$object) && !is_link($source.DIRECTORY_SEPARATOR.$object)) {
38 - cpyr($source. DIRECTORY_SEPARATOR .$object, $destination. DIRECTORY_SEPARATOR .$object);
39 - } elseif (is_file($source. DIRECTORY_SEPARATOR .$object)) {
40 - copy($source. DIRECTORY_SEPARATOR .$object, $destination. DIRECTORY_SEPARATOR .$object);
41 - // chmod($destination. DIRECTORY_SEPARATOR .$object, 0644);
42 - } else {
43 - symlink($source. DIRECTORY_SEPARATOR .$object, $destination. DIRECTORY_SEPARATOR .$object);
44 - }
45 - }
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;
46 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]);
47 117 } else {
48 - copy($source, $destination);
118 + $file_upoalded[0] = basename($upload_result['file']);
49 119 }
120 + }
50 121 }
122 + return $file_upoalded;
123 + }
51 124
52 - private function getByteSizeByUnit($sizeString)
53 - {
54 - // split 2MB into 2 and MB
55 - $size = preg_replace('/[^0-9\.]/', '', $sizeString);
56 - $unit = preg_replace('/[^a-zA-Z]/', '', $sizeString);
57 - $unit = strtolower($unit);
58 - if ('kb' === $unit) {
59 - return $size * 1024;
60 - } elseif ('mb' === $unit) {
61 - return $size * 1024 * 1024;
62 - } elseif ('gb' === $unit) {
63 - return $size * 1024 * 1024 * 1024;
64 - } else {
65 - return $size;
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 + public function deleteFiles($form_id, $entry_id, $files)
145 + {
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;
151 + }
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 + }
178 +
179 + public static function getFileUploadError($code)
180 + {
181 + $errors = [
182 + 0 => __('Unknown upload error', 'bit-form'),
183 + 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini.', 'bit-form'),
184 + 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.', 'bit-form'),
185 + 3 => __('The uploaded file was only partially uploaded.', 'bit-form'),
186 + 4 => __('No file was uploaded.', 'bit-form'),
187 + 6 => __('Missing a temporary folder.', 'bit-form'),
188 + 7 => __('Failed to write file to disk.', 'bit-form'),
189 + 8 => __('A PHP extension stopped the file upload.', 'bit-form'),
190 + ];
191 + return $errors[$code];
192 + }
193 +
194 + public static function fileCopy($tmpdir, $destinationDir, $file)
195 + {
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;
205 + }
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 + }
230 +
231 + public static function tempDirToUploadDir($submitted_data, $fields, $formId, $entryID)
232 + {
233 + $upload_dir = wp_upload_dir();
234 + $tempDir = $upload_dir['basedir'] . '/bitforms/temp';
235 + $destinationDir = self::getEntriesFileUploadDir($formId, $entryID) . DIRECTORY_SEPARATOR;
236 + self::createIndexFile($destinationDir);
237 +
238 + $consumedFiles = [];
239 +
240 + foreach ($submitted_data as $key => $data) {
241 + if (isset($fields[$key]) && 'advanced-file-up' === $fields[$key]['type']) {
242 + $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;
250 + }
251 + self::fileCopy($tempDir, $destinationDir, $safeFile);
252 + $consumedFiles[] = $safeFile;
66 253 }
254 + if (!empty($files)) {
255 + $submitted_data[$key] = $files;
256 + }
257 + }
67 258 }
68 259
69 - public function validation($field_key, $file_details, $form_id)
70 - {
71 - if (!function_exists('wp_check_filetype_and_ext')) {
72 - require_once ABSPATH . 'wp-admin/includes/file.php';
260 + self::cleanupTempUploads($tempDir, $consumedFiles);
261 +
262 + return $submitted_data;
263 + }
264 +
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 + private function getByteSizeByUnit($sizeString)
322 + {
323 + // split 2MB into 2 and MB
324 + $size = preg_replace('/[^0-9\.]/', '', $sizeString);
325 + $unit = preg_replace('/[^a-zA-Z]/', '', $sizeString);
326 + $unit = strtolower($unit);
327 + if ('kb' === $unit) {
328 + return $size * 1024;
329 + } elseif ('mb' === $unit) {
330 + return $size * 1024 * 1024;
331 + } elseif ('gb' === $unit) {
332 + return $size * 1024 * 1024 * 1024;
333 + } else {
334 + return $size;
335 + }
336 + }
337 +
338 + public function validation($field_key, $file_details, $form_id)
339 + {
340 + if (!function_exists('wp_check_filetype_and_ext')) {
341 + require_once ABSPATH . 'wp-admin/includes/file.php';
342 + }
343 +
344 + $formManager = FormManager::getInstance($form_id);
345 + $form_contents = $formManager->getFormContent();
346 + $field_content_details = $form_contents->fields;
347 + $fieldDetail = $field_content_details->{$field_key};
348 + $fieldType = $fieldDetail->typ;
349 + $maxSizeDetails = [];
350 + $allowFileTypes = [];
351 + $maxSize = null;
352 + if ('file-up' === $fieldType) {
353 + $allowFileTypes = !empty($fieldDetail->config->allowedFileType) ? $fieldDetail->config->allowedFileType : [];
354 + if (!empty($fieldDetail->config->allowMaxSize)) {
355 + if (!empty($fieldDetail->config->maxSize)) {
356 + $maxSizeDetails['maxSize'] = $fieldDetail->config->maxSize . $fieldDetail->config->sizeUnit;
73 357 }
358 + if (!empty($fieldDetail->config->isItTotalMax)) {
359 + $maxSizeDetails['maxTotalFileSize'] = $fieldDetail->config->maxSize . $fieldDetail->config->sizeUnit;
360 + }
361 + }
362 + if (!empty($allowFileTypes)) {
363 + $allowFileTypes = explode(',', $allowFileTypes);
364 + }
365 + } elseif ('advanced-file-up' === $fieldType) {
366 + $allowFileTypes = !empty($fieldDetail->config->allowFileTypeValidation) ? $fieldDetail->config->acceptedFileTypes : [];
367 + if (!empty($fieldDetail->config->allowFileSizeValidation)) {
368 + if (!empty($fieldDetail->config->maxFileSize)) {
369 + $maxSizeDetails['maxSize'] = $fieldDetail->config->maxFileSize;
370 + }
371 + if (!empty($fieldDetail->config->maxTotalFileSize)) {
372 + $maxSizeDetails['maxTotalFileSize'] = $fieldDetail->config->maxTotalFileSize;
373 + }
374 + }
375 + }
376 + if (!empty($maxSizeDetails['maxSize'])) {
377 + $maxSize = $this->getByteSizeByUnit($maxSizeDetails['maxSize']);
378 + }
379 + $maxTotalFileSize = null;
380 + if (!empty($maxSizeDetails['maxTotalFileSize'])) {
381 + $maxTotalFileSize = $this->getByteSizeByUnit($maxSizeDetails['maxTotalFileSize']);
382 + }
74 383
75 - $formManager = new FormManager($form_id);
76 - $form_contents = $formManager->getFormContent();
77 - $field_content_details = $form_contents->fields;
78 - $fieldDetail = $field_content_details->{$field_key};
79 - $fieldType = $fieldDetail->typ;
80 - $maxSizeDetails = [];
81 - $allowFileTypes = [];
82 - if ('file-up' === $fieldType) {
83 - $allowFileTypes = !empty($fieldDetail->exts) ? $fieldDetail->exts : '';
84 - if (!empty($fieldDetail->mxUp)) {
85 - if (!empty($fieldDetail->mxUp)) {
86 - $maxSizeDetails['maxSize'] = $fieldDetail->mxUp . $fieldDetail->unit;
87 - }
88 - }
89 - if (!empty($allowFileTypes)) {
90 - $allowFileTypes = explode(',', $allowFileTypes);
91 - }
384 + if ($formManager->isRepeatedField($field_key)) {
385 + foreach ($file_details['name'] as $rowIndex => $file) {
386 + if (!empty($file)) {
387 + $fileDetails = [
388 + 'name' => $file,
389 + 'type' => $file_details['type'][$rowIndex],
390 + 'tmp_name' => $file_details['tmp_name'][$rowIndex],
391 + 'error' => $file_details['error'][$rowIndex],
392 + 'size' => $file_details['size'][$rowIndex],
393 + ];
394 + $validateState = $this->validateFileInfo($fieldType, $fileDetails, $allowFileTypes, $maxSize, $maxTotalFileSize);
395 + if (!empty($validateState) && !empty($validateState['message'])) {
396 + return $validateState;
397 + }
92 398 }
93 - if (!empty($maxSizeDetails['maxSize'])) {
94 - $maxSize = $this->getByteSizeByUnit($maxSizeDetails['maxSize']);
399 + }
400 + } else {
401 + return $this->validateFileInfo($fieldType, $file_details, $allowFileTypes, $maxSize, $maxTotalFileSize);
402 + }
403 + return [];
404 + }
405 +
406 + private function validateFileInfo($fieldType, $file_details, $allowFileTypes, $maxSize, $maxTotalFileSize)
407 + {
408 + $errorMessage = [
409 + 'message' => '',
410 + 'error_type' => '',
411 + ];
412 + if (is_array($file_details['name'])) {
413 + $totalSize = 0;
414 + foreach ($file_details['name'] as $key => $file) {
415 + if (!empty($file)) {
416 + $fileInfo = [
417 + 'name' => $file,
418 + 'type' => $file_details['type'][$key],
419 + 'tmp_name' => $file_details['tmp_name'][$key],
420 + 'error' => $file_details['error'][$key],
421 + 'size' => $file_details['size'][$key],
422 + ];
423 + $totalSize += $fileInfo['size'];
424 + $validateState = $this->validateSingleFile($fieldType, $fileInfo, $allowFileTypes, $maxSize);
425 + if (!empty($validateState)) {
426 + return $validateState;
427 + }
95 428 }
429 + }
430 + if (isset($maxTotalFileSize) && !is_null($maxTotalFileSize) && $totalSize > $maxTotalFileSize) {
431 + $errorMessage['message'] = __('Total File size is too large', 'bit-form');
432 + $errorMessage['error_type'] = 'file_size_error';
433 + return $errorMessage;
434 + }
435 + } else {
436 + $validateState = $this->validateSingleFile($fieldType, $file_details, $allowFileTypes, $maxSize);
437 + if (!empty($validateState)) {
438 + return $validateState;
439 + }
440 + }
96 441
97 - $errorMessage = [
98 - 'message' => '',
99 - 'error_type'=> '',
100 - ];
442 + return $errorMessage;
443 + }
101 444
102 - if (is_array($file_details['name'])) {
103 - $totalSize = 0;
104 - foreach ($file_details['name'] as $key => $file) {
105 - if (!empty($file)) {
106 - $fileInfo = [
107 - 'name' => $file,
108 - 'type' => $file_details['type'][$key],
109 - 'tmp_name' => $file_details['tmp_name'][$key],
110 - 'error' => $file_details['error'][$key],
111 - 'size' => $file_details['size'][$key],
112 - ];
113 - $totalSize += $fileInfo['size'];
114 - $validateState = $this->validateSingleFile($fieldType, $fileInfo, $allowFileTypes, $maxSize);
115 - if (!empty($validateState)) {
116 - return $validateState;
117 - }
118 - }
119 - }
120 - if (!is_null($maxTotalFileSize) && $totalSize > $maxTotalFileSize) {
121 - $errorMessage['message'] = __('Total File size is too large', 'bit-form');
122 - $errorMessage['error_type'] = 'file_size_error';
123 - return $errorMessage;
124 - }
125 - } else {
126 - $validateState = $this->validateSingleFile($fieldType, $file_details, $allowFileTypes, $maxSize);
127 - if (!empty($validateState)) {
128 - return $validateState;
129 - }
445 + private function validateSingleFile($fieldType, &$file, $allowTypes, $maxSize = null)
446 + {
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;
522 + }
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 +
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'];
545 + }
546 + } else {
547 + if (!$wpOk) {
548 + return ['message' => __('File type is not allowed', 'bit-form'), 'error_type' => 'file_type_error'];
549 + }
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'];
130 599 }
600 + }
601 + }
131 602
132 - return $errorMessage;
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'];
133 617 }
618 + return null; // success
619 + }
134 620
135 - private function validateSingleFile($fieldType, $file, $allowTypes, $maxSize)
136 - {
137 - $fileName = sanitize_file_name($file['name']);
138 - if (!empty($fileName)) {
139 - $fileSize = $file['size'];
140 - if (!empty($maxSize) && $fileSize > $maxSize) {
141 - return [
142 - 'message' => __('File size is too large', 'bit-form'),
143 - 'error_type'=> 'file_size_error',
144 - ];
145 - }
621 + public static function deleteIsFileExists($path)
622 + {
623 + if (file_exists($path)) {
624 + wp_delete_file($path);
625 + }
626 + }
146 627
147 - $fileExtension = pathinfo($fileName, PATHINFO_EXTENSION);
148 - $fileExtAllowedByWp = wp_check_filetype_and_ext($file['tmp_name'], $fileName);
149 - $isAllowedFileType = in_array('.' . $fileExtension, $allowTypes);
150 - if ((!empty($allowTypes) && !$isAllowedFileType) || (empty($allowTypes) && empty($fileExtAllowedByWp['ext']))) {
151 - return [
152 - 'message' => __(($fileExtension ? ".{$fileExtension}" : 'empty') . ' file extension is not allowed', 'bit-form'),
153 - 'error_type'=> 'file_type_error',
154 - ];
155 - }
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
156 676 }
677 + }
157 678 }
679 + return false;
680 + }
158 681
159 - public function moveUploadedFiles($file_details, $form_id, $entry_id)
160 - {
161 - $file_upoalded = array();
162 - $_upload_dir = BITFORMS_UPLOAD_DIR.DIRECTORY_SEPARATOR.$form_id.DIRECTORY_SEPARATOR.$entry_id;
163 - wp_mkdir_p($_upload_dir);
164 - if (is_array($file_details['name'])) {
165 - foreach ($file_details['name'] as $key => $value) {
166 - //check accepted filetype in_array($file_details['name'][$key], $supported_files) else \
167 - if (!empty($value)) {
168 - $fileNamePattern = '/\_/';
169 - $file_upoalded[$key] = preg_replace($fileNamePattern, '', $value);
170 - $extension = pathinfo($file_upoalded[$key], PATHINFO_EXTENSION);
171 - if (in_array($extension, ['php', 'html', 'xhtml', 'js'])) {
172 - $extension = 'txt';
173 - }
174 - $uniqueName = wp_generate_uuid4();
175 - $UploadedFieldName = $uniqueName . "." . $extension;
176 - $file_upoalded[$key] = $UploadedFieldName.'_'. $file_upoalded[$key];
177 - $move_status = \move_uploaded_file($file_details['tmp_name'][$key], $_upload_dir.DIRECTORY_SEPARATOR.$UploadedFieldName);
178 - if (!$move_status) {
179 - unset($file_upoalded[$key]);
180 - }
181 - }
182 - }
183 - } else {
184 - if (!empty($file_details['name'])) {
185 - $fileNamePattern = '/\_/';
186 - $fileName = sanitize_file_name($file_details['name']);
187 - $file_upoalded[0] = preg_replace($fileNamePattern, '', $fileName);
188 - $extension = pathinfo($file_upoalded[0], PATHINFO_EXTENSION);
189 - if (in_array($extension, ['php', 'html', 'xhtml', 'js'])) {
190 - $extension = 'txt';
191 - }
192 - $uniqueName= wp_generate_uuid4();
193 - $UploadedFieldName = $uniqueName . "." . $extension;
194 - $move_status = \move_uploaded_file($file_details['tmp_name'], $_upload_dir.DIRECTORY_SEPARATOR.$UploadedFieldName);
195 - $file_upoalded[0] = $UploadedFieldName.'_'.$file_upoalded[0];
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 + }
196 696
197 - if (!$move_status) {
198 - unset($file_upoalded[0]);
199 - }
200 - }
201 - }
202 - return $file_upoalded;
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);
203 709 }
710 + // Fallback: file_put_contents is acceptable when WP_Filesystem is unavailable.
711 + return false !== file_put_contents($filePath, $content);
712 + }
204 713
205 - public function deleteFiles($form_id, $entry_id, $files)
206 - {
207 - $_upload_dir = BITFORMS_UPLOAD_DIR.DIRECTORY_SEPARATOR.$form_id.DIRECTORY_SEPARATOR.$entry_id;
208 - foreach ($files as $name) {
209 - unlink($_upload_dir.DIRECTORY_SEPARATOR.$name);
210 - }
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);
211 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 + }
1004 + }
212 1005 }