PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / V3.3.2
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder vV3.3.2
V3.3.2 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 All 139 releases
← All changes | includes/Core/Util/FileHandler.php +169 -13 3.2.2 → V3.3.2 View file →
@@ -317,25 +317,145 @@
317 317 }
318 318 }
319 319 }
320 320
321 - private function getByteSizeByUnit($sizeString)
321 + private function getByteSizeByUnit($sizeString, $base = 1024)
322 322 {
323 323 // split 2MB into 2 and MB
324 - $size = preg_replace('/[^0-9\.]/', '', $sizeString);
325 - $unit = preg_replace('/[^a-zA-Z]/', '', $sizeString);
324 + $size = (float) preg_replace('/[^0-9\.]/', '', (string) $sizeString);
325 + $unit = preg_replace('/[^a-zA-Z]/', '', (string) $sizeString);
326 326 $unit = strtolower($unit);
327 327 if ('kb' === $unit) {
328 - return $size * 1024;
328 + return $size * $base;
329 329 } elseif ('mb' === $unit) {
330 - return $size * 1024 * 1024;
330 + return $size * $base * $base;
331 331 } elseif ('gb' === $unit) {
332 - return $size * 1024 * 1024 * 1024;
332 + return $size * $base * $base * $base;
333 333 } else {
334 334 return $size;
335 335 }
336 336 }
337 337
338 + /**
339 + * How many files an Advanced File Upload field takes per submission, mirroring FilePond: a
340 + * single-file field holds one, and maxFiles only applies once multiple files are allowed.
341 + *
342 + * @param object|null $config field config
343 + *
344 + * @return int|null null when there is no limit
345 + */
346 + public static function getMaxFilesLimit($config)
347 + {
348 + if (!is_object($config)) {
349 + return null;
350 + }
351 + if (empty($config->allowMultiple)) {
352 + return 1;
353 + }
354 + $max = isset($config->maxFiles) && is_numeric($config->maxFiles) ? (int) $config->maxFiles : 0;
355 +
356 + return $max > 0 ? $max : null;
357 + }
358 +
359 + /**
360 + * Files submitted for one field: names already uploaded on select (comma list or array) plus
361 + * the files sent with the form.
362 + *
363 + * @param mixed $uploadedNames posted value of the field
364 + * @param mixed $postedFileNames $_FILES[field]['name']
365 + *
366 + * @return int
367 + */
368 + public static function countSubmittedFiles($uploadedNames, $postedFileNames)
369 + {
370 + $count = 0;
371 + foreach ([$uploadedNames, $postedFileNames] as $source) {
372 + $names = is_array($source) ? $source : (is_scalar($source) ? explode(',', (string) $source) : []);
373 + foreach ($names as $name) {
374 + if (is_scalar($name) && '' !== trim((string) $name)) {
375 + $count++;
376 + }
377 + }
378 + }
379 +
380 + return $count;
381 + }
382 +
383 + /**
384 + * The browser drops files past the limit, so more can only arrive from a crafted request.
385 + *
386 + * @param object|null $config field config
387 + * @param mixed $uploadedNames posted value of the field
388 + * @param mixed $postedFileNames $_FILES[field]['name']
389 + *
390 + * @return string error message, '' when within the limit
391 + */
392 + public static function getMaxFilesError($config, $uploadedNames, $postedFileNames)
393 + {
394 + $limit = self::getMaxFilesLimit($config);
395 + if (null === $limit || self::countSubmittedFiles($uploadedNames, $postedFileNames) <= $limit) {
396 + return '';
397 + }
398 +
399 + return sprintf(
400 + /* translators: %d: maximum number of files */
401 + _n('You can upload only %d file.', 'You can upload up to %d files.', $limit, 'bit-form'),
402 + $limit
403 + );
404 + }
405 +
406 + /**
407 + * getMaxFilesError() for a field of a stored form.
408 + *
409 + * @param int|string $formId
410 + * @param string $fieldKey
411 + * @param mixed $uploadedNames posted value of the field
412 + * @param mixed $postedFileNames $_FILES[field]['name']
413 + *
414 + * @return string
415 + */
416 + public static function getMaxFilesErrorForField($formId, $fieldKey, $uploadedNames, $postedFileNames)
417 + {
418 + $formContent = FormManager::getInstance($formId)->getFormContent();
419 + $field = is_object($formContent) && isset($formContent->fields->{$fieldKey}) ? $formContent->fields->{$fieldKey} : null;
420 + if (!is_object($field) || !isset($field->typ) || 'advanced-file-up' !== $field->typ) {
421 + return '';
422 + }
423 +
424 + return self::getMaxFilesError(isset($field->config) ? $field->config : null, $uploadedNames, $postedFileNames);
425 + }
426 +
427 + /**
428 + * Whether a MIME matches a `type/*` entry of the allow list, as FilePond accepts it. Markup and
429 + * script types never match a wildcard: they must be listed explicitly.
430 + *
431 + * @param mixed $mime detected MIME, lowercase
432 + * @param array $allowMimes allow-listed MIME entries, lowercase
433 + *
434 + * @return bool
435 + */
436 + public static function matchesWildcardMime($mime, $allowMimes)
437 + {
438 + $neverByWildcard = [
439 + 'text/html',
440 + 'application/xhtml+xml',
441 + 'text/javascript',
442 + 'application/javascript',
443 + 'application/x-javascript',
444 + 'application/ecmascript',
445 + 'text/ecmascript',
446 + ];
447 + if (!is_string($mime) || in_array($mime, $neverByWildcard, true)) {
448 + return false;
449 + }
450 + $slash = strpos($mime, '/');
451 + if (false === $slash || 0 === $slash) {
452 + return false;
453 + }
454 +
455 + return in_array(substr($mime, 0, $slash) . '/*', (array) $allowMimes, true);
456 + }
457 +
338 458 public function validation($field_key, $file_details, $form_id)
339 459 {
340 460 if (!function_exists('wp_check_filetype_and_ext')) {
341 461 require_once ABSPATH . 'wp-admin/includes/file.php';
@@ -348,8 +468,10 @@
348 468 $fieldType = $fieldDetail->typ;
349 469 $maxSizeDetails = [];
350 470 $allowFileTypes = [];
351 471 $maxSize = null;
472 + $minSize = null;
473 + $fieldConfig = isset($fieldDetail->config) && is_object($fieldDetail->config) ? $fieldDetail->config : null;
352 474 if ('file-up' === $fieldType) {
353 475 $allowFileTypes = !empty($fieldDetail->config->allowedFileType) ? $fieldDetail->config->allowedFileType : [];
354 476 if (!empty($fieldDetail->config->allowMaxSize)) {
355 477 if (!empty($fieldDetail->config->maxSize)) {
@@ -370,8 +492,14 @@
370 492 }
371 493 if (!empty($fieldDetail->config->maxTotalFileSize)) {
372 494 $maxSizeDetails['maxTotalFileSize'] = $fieldDetail->config->maxTotalFileSize;
373 495 }
496 + // A client transform re-encodes images, so the uploaded copy can legitimately come out
497 + // smaller than the original the browser checked.
498 + if (!empty($fieldDetail->config->minFileSize) && empty($fieldDetail->config->allowImageTransform)) {
499 + // FilePond counts 1KB as 1000 bytes; a 1024 base would reject files the browser accepted.
500 + $minSize = $this->getByteSizeByUnit($fieldDetail->config->minFileSize, 1000);
501 + }
374 502 }
375 503 }
376 504 if (!empty($maxSizeDetails['maxSize'])) {
377 505 $maxSize = $this->getByteSizeByUnit($maxSizeDetails['maxSize']);
@@ -390,9 +518,13 @@
390 518 'tmp_name' => $file_details['tmp_name'][$rowIndex],
391 519 'error' => $file_details['error'][$rowIndex],
392 520 'size' => $file_details['size'][$rowIndex],
393 521 ];
394 - $validateState = $this->validateFileInfo($fieldType, $fileDetails, $allowFileTypes, $maxSize, $maxTotalFileSize);
522 + $countState = $this->maxFilesState($fieldType, $fieldConfig, $file);
523 + if (!empty($countState)) {
524 + return $countState;
525 + }
526 + $validateState = $this->validateFileInfo($fieldType, $fileDetails, $allowFileTypes, $maxSize, $maxTotalFileSize, $minSize);
395 527 if (!empty($validateState) && !empty($validateState['message'])) {
396 528 return $validateState;
397 529 }
398 530 }
@@ -397,15 +529,36 @@
397 529 }
398 530 }
399 531 }
400 532 } else {
401 - return $this->validateFileInfo($fieldType, $file_details, $allowFileTypes, $maxSize, $maxTotalFileSize);
533 + $countState = $this->maxFilesState($fieldType, $fieldConfig, isset($file_details['name']) ? $file_details['name'] : null);
534 + if (!empty($countState)) {
535 + return $countState;
536 + }
537 + return $this->validateFileInfo($fieldType, $file_details, $allowFileTypes, $maxSize, $maxTotalFileSize, $minSize);
402 538 }
403 539 return [];
404 540 }
405 541
406 - private function validateFileInfo($fieldType, $file_details, $allowFileTypes, $maxSize, $maxTotalFileSize)
542 + /**
543 + * @param string $fieldType
544 + * @param object|null $fieldConfig
545 + * @param mixed $postedFileNames $_FILES[field]['name'] (or one repeater row of it)
546 + *
547 + * @return array validation state, empty when within the limit
548 + */
549 + private function maxFilesState($fieldType, $fieldConfig, $postedFileNames)
407 550 {
551 + if ('advanced-file-up' !== $fieldType) {
552 + return [];
553 + }
554 + $message = self::getMaxFilesError($fieldConfig, null, $postedFileNames);
555 +
556 + return '' === $message ? [] : ['message' => $message, 'error_type' => 'file_count_error'];
557 + }
558 +
559 + private function validateFileInfo($fieldType, $file_details, $allowFileTypes, $maxSize, $maxTotalFileSize, $minSize = null)
560 + {
408 561 $errorMessage = [
409 562 'message' => '',
410 563 'error_type' => '',
411 564 ];
@@ -420,9 +573,9 @@
420 573 'error' => $file_details['error'][$key],
421 574 'size' => $file_details['size'][$key],
422 575 ];
423 576 $totalSize += $fileInfo['size'];
424 - $validateState = $this->validateSingleFile($fieldType, $fileInfo, $allowFileTypes, $maxSize);
577 + $validateState = $this->validateSingleFile($fieldType, $fileInfo, $allowFileTypes, $maxSize, $minSize);
425 578 if (!empty($validateState)) {
426 579 return $validateState;
427 580 }
428 581 }
@@ -432,9 +585,9 @@
432 585 $errorMessage['error_type'] = 'file_size_error';
433 586 return $errorMessage;
434 587 }
435 588 } else {
436 - $validateState = $this->validateSingleFile($fieldType, $file_details, $allowFileTypes, $maxSize);
589 + $validateState = $this->validateSingleFile($fieldType, $file_details, $allowFileTypes, $maxSize, $minSize);
437 590 if (!empty($validateState)) {
438 591 return $validateState;
439 592 }
440 593 }
@@ -441,9 +594,9 @@
441 594
442 595 return $errorMessage;
443 596 }
444 597
445 - private function validateSingleFile($fieldType, &$file, $allowTypes, $maxSize = null)
598 + private function validateSingleFile($fieldType, &$file, $allowTypes, $maxSize = null, $minSize = null)
446 599 {
447 600 // 0) Basic sanity & transport integrity
448 601 if (!is_array($file) || empty($file['tmp_name'])) {
449 602 // return ['message' => __('No file uploaded.', 'bit-form'), 'error_type' => 'file_missing'];
@@ -471,8 +624,11 @@
471 624 }
472 625 if (!empty($maxSize) && $onDiskSize > $maxSize) {
473 626 return ['message' => __('File size is too large', 'bit-form'), 'error_type' => 'file_size_error'];
474 627 }
628 + if (!empty($minSize) && $onDiskSize < $minSize) {
629 + return ['message' => __('File size is too small', 'bit-form'), 'error_type' => 'file_size_error'];
630 + }
475 631
476 632 // 2) Determine ext + MIME using WP + finfo
477 633 $wpCheck = wp_check_filetype_and_ext($file['tmp_name'], $fileName); // ['ext'=>'jpg','type'=>'image/jpeg']
478 634 $fileExtension = strtolower((string)(empty($wpCheck['ext']) ? pathinfo($fileName, PATHINFO_EXTENSION) : $wpCheck['ext']));
@@ -533,9 +689,9 @@
533 689 $allowMimes = array_values(array_unique($allowMimes));
534 690
535 691 $hasAllowlist = (!empty($allowExts) || !empty($allowMimes));
536 692 $extMatch = in_array($fileExtension, $allowExts, true);
537 - $mimeMatch = in_array($detectedMime, $allowMimes, true);
693 + $mimeMatch = in_array($detectedMime, $allowMimes, true) || self::matchesWildcardMime($detectedMime, $allowMimes);
538 694
539 695 // 5) Require BOTH a legit WP mapping AND a match to the allowlist
540 696 $wpOk = ('' !== $wpExt && '' !== $wpType);
541 697