From 9116bb1af36ef46d875adb0e00fb220b47befb74 Mon Sep 17 00:00:00 2001 From: arif-un Date: Mon, 24 Nov 2025 16:30:08 +0600 Subject: [PATCH 1/5] feat: added file upload rules --- src/Rules/FileRule.php | 51 ++++++++++++++++++ src/Rules/ImageDimensionsRule.php | 86 +++++++++++++++++++++++++++++ src/Rules/ImageRule.php | 76 ++++++++++++++++++++++++++ src/Rules/MaxFileRule.php | 69 ++++++++++++++++++++++++ src/Rules/MimesRule.php | 90 +++++++++++++++++++++++++++++++ 5 files changed, 372 insertions(+) create mode 100644 src/Rules/FileRule.php create mode 100644 src/Rules/ImageDimensionsRule.php create mode 100644 src/Rules/ImageRule.php create mode 100644 src/Rules/MaxFileRule.php create mode 100644 src/Rules/MimesRule.php diff --git a/src/Rules/FileRule.php b/src/Rules/FileRule.php new file mode 100644 index 0000000..975dc45 --- /dev/null +++ b/src/Rules/FileRule.php @@ -0,0 +1,51 @@ +isEmpty($value)) { + return false; + } + + if (is_numeric($value)) { + $attachmentId = (int) $value; + $filePath = get_attached_file($attachmentId); + return ! empty($filePath) && file_exists($filePath); + } + + if (is_array($value)) { + $requiredKeys = ['name', 'type', 'tmp_name', 'error', 'size']; + foreach ($requiredKeys as $key) { + if (! isset($value[$key])) { + return false; + } + } + + if ($value['error'] !== UPLOAD_ERR_OK) { + return false; + } + + if (empty($value['tmp_name']) || ! is_uploaded_file($value['tmp_name'])) { + return false; + } + + return true; + } + + return false; + } + + public function message() + { + return $this->message; + } +} diff --git a/src/Rules/ImageDimensionsRule.php b/src/Rules/ImageDimensionsRule.php new file mode 100644 index 0000000..215a86e --- /dev/null +++ b/src/Rules/ImageDimensionsRule.php @@ -0,0 +1,86 @@ +checkRequiredParameter($this->requireParameters); + + if ($this->isEmpty($value)) { + return false; + } + + $maxWidth = (int) $this->getParameter('width'); + $maxHeight = (int) $this->getParameter('height'); + + $dimensions = $this->getImageDimensions($value); + if (! $dimensions) { + return false; + } + + $width = $dimensions['width']; + $height = $dimensions['height']; + + return $width <= $maxWidth && $height <= $maxHeight; + } + + private function getImageDimensions($value) + { + $filePath = null; + + if (is_numeric($value)) { + $attachmentId = (int) $value; + $filePath = get_attached_file($attachmentId); + if (empty($filePath) || ! file_exists($filePath)) { + return null; + } + + $metadata = wp_get_attachment_metadata($attachmentId); + if ($metadata && isset($metadata['width']) && isset($metadata['height'])) { + return [ + 'width' => (int) $metadata['width'], + 'height' => (int) $metadata['height'], + ]; + } + } + + if (is_array($value) && isset($value['tmp_name'])) { + if (empty($value['tmp_name']) || ! is_uploaded_file($value['tmp_name'])) { + return null; + } + $filePath = $value['tmp_name']; + } + + if ($filePath && file_exists($filePath)) { + $imageSize = @getimagesize($filePath); + if ($imageSize && isset($imageSize[0]) && isset($imageSize[1])) { + return [ + 'width' => (int) $imageSize[0], + 'height' => (int) $imageSize[1], + ]; + } + } + + return null; + } + + public function getParamKeys() + { + return $this->requireParameters; + } + + public function message() + { + return $this->message; + } +} diff --git a/src/Rules/ImageRule.php b/src/Rules/ImageRule.php new file mode 100644 index 0000000..8e27ad8 --- /dev/null +++ b/src/Rules/ImageRule.php @@ -0,0 +1,76 @@ +isEmpty($value)) { + return false; + } + + $fileInfo = $this->getFileInfo($value); + if (!$fileInfo) { + return false; + } + + $fileType = $fileInfo['type']; + $fileName = $fileInfo['name']; + + $allowedImageTypes = wp_get_image_mime_types(); + + if (in_array($fileType, $allowedImageTypes, true)) { + return true; + } + + $wpFileType = wp_check_filetype($fileName); + if ($wpFileType && isset($wpFileType['type'])) { + if (in_array($wpFileType['type'], $allowedImageTypes, true)) { + return true; + } + } + + return false; + } + + private function getFileInfo($value) + { + // Handle attachment ID + if (is_numeric($value)) { + $attachmentId = (int) $value; + $filePath = get_attached_file($attachmentId); + if (empty($filePath) || !file_exists($filePath)) { + return null; + } + + $wpFileType = wp_check_filetype(basename($filePath)); + return [ + 'name' => basename($filePath), + 'type' => $wpFileType['type'] ?? '', + ]; + } + + // Handle $_FILES format + if (is_array($value) && isset($value['name']) && isset($value['type'])) { + return [ + 'name' => $value['name'], + 'type' => $value['type'], + ]; + } + + return null; + } + + public function message() + { + return $this->message; + } +} + diff --git a/src/Rules/MaxFileRule.php b/src/Rules/MaxFileRule.php new file mode 100644 index 0000000..e6c18d5 --- /dev/null +++ b/src/Rules/MaxFileRule.php @@ -0,0 +1,69 @@ +checkRequiredParameter($this->requireParameters); + + if ($this->isEmpty($value)) { + return false; + } + + $maxSizeKB = (int) $this->getParameter('max'); + $maxSizeBytes = $maxSizeKB * 1024; // Convert KB to bytes + + $fileSize = $this->getFileSize($value); + if ($fileSize === false) { + return false; + } + + return $fileSize <= $maxSizeBytes; + } + + private function getFileSize($value) + { + if (is_numeric($value)) { + $attachmentId = (int) $value; + $filePath = get_attached_file($attachmentId); + if (empty($filePath) || !file_exists($filePath)) { + return false; + } + + $metadata = wp_get_attachment_metadata($attachmentId); + if ($metadata && isset($metadata['file'])) { + $fileSize = filesize($filePath); + return $fileSize !== false ? $fileSize : false; + } + + return filesize($filePath); + } + + if (is_array($value) && isset($value['size'])) { + return (int) $value['size']; + } + + return false; + } + + public function getParamKeys() + { + return $this->requireParameters; + } + + public function message() + { + return $this->message; + } +} + diff --git a/src/Rules/MimesRule.php b/src/Rules/MimesRule.php new file mode 100644 index 0000000..965f382 --- /dev/null +++ b/src/Rules/MimesRule.php @@ -0,0 +1,90 @@ +checkRequiredParameter($this->requireParameters); + + if ($this->isEmpty($value)) { + return false; + } + + $allowedMimes = $this->getParameter('mimes'); + if (empty($allowedMimes)) { + return false; + } + + $allowedTypes = array_map('trim', explode(',', $allowedMimes)); + + $fileInfo = $this->getFileInfo($value); + if (! $fileInfo) { + return false; + } + + $fileType = $fileInfo['type']; + $fileName = $fileInfo['name']; + + if (in_array($fileType, $allowedTypes, true)) { + return true; + } + + $wpFileType = wp_check_filetype($fileName); + if ($wpFileType && isset($wpFileType['ext'])) { + $extension = strtolower($wpFileType['ext']); + if (in_array($extension, $allowedTypes, true)) { + return true; + } + } + + return false; + } + + private function getFileInfo($value) + { + // Handle attachment ID + if (is_numeric($value)) { + $attachmentId = (int) $value; + $filePath = get_attached_file($attachmentId); + if (empty($filePath) || ! file_exists($filePath)) { + return null; + } + + $wpFileType = wp_check_filetype(basename($filePath)); + return [ + 'name' => basename($filePath), + 'type' => $wpFileType['type'] ?? '', + ]; + } + + // Handle $_FILES format + if (is_array($value) && isset($value['name']) && isset($value['type'])) { + return [ + 'name' => $value['name'], + 'type' => $value['type'], + ]; + } + + return null; + } + + public function getParamKeys() + { + return $this->requireParameters; + } + + public function message() + { + return $this->message; + } +} From f5730b518dd28f27ca633f26d34a65ec32e823c0 Mon Sep 17 00:00:00 2001 From: arif-un Date: Mon, 24 Nov 2025 18:26:39 +0600 Subject: [PATCH 2/5] chore: updated readme fiel --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 33e1612..2d62954 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,21 @@ Checks if the given value is a string. Checks if the string value consists of all uppercase letters. 25. **`url`**
Checks if the value is a valid URL. +26. **`file`**
+Checks if the field under validation is a valid file upload. Supports standard PHP `$_FILES` arrays and WordPress attachment IDs.
+e.g. `['avatar' => ['required', 'file']]` +27. **`mimes:type1,type2,...`**
+Checks if the file under validation has one of the allowed MIME types or extensions. Accepts comma-separated values (e.g., `jpg`, `png`, `pdf`).
+e.g. `['document' => ['required', 'file', 'mimes:pdf,doc,docx']]` +28. **`max_file:size`**
+Checks if the file size (in kilobytes) is less than or equal to the specified maximum size.
+e.g. `['avatar' => ['required', 'file', 'max_file:2048']]` (2MB limit) +29. **`image`**
+Checks if the file under validation is an image using WordPress allowed image MIME types.
+e.g. `['photo' => ['required', 'file', 'image']]` +30. **`image_dimensions:width,height`**
+Checks if the image dimensions (in pixels) are less than or equal to the specified maximum width and height.
+e.g. `['avatar' => ['required', 'file', 'image', 'image_dimensions:1920,1080']]` Missing any validation rule that you need? Refer to the [Custom Validation Rule](#custom-validation-rule) section to know how you can create and use custom validation rules in your project alongside the library. From 43f65948f7473eebac78242ce9938349d931c97b Mon Sep 17 00:00:00 2001 From: csemazharul Date: Wed, 22 Apr 2026 17:44:34 +0600 Subject: [PATCH 3/5] feat: add file upload validation rules with WordPress integration - Add FileRule, ImageRule, MaxFileRule, MimesRule with $_FILES array and attachment ID support - Add EncodingRule (mb_detect_encoding), ExtensionsRule (wp_check_filetype), MimetypesRule (content-based via wp_check_filetype_and_ext) - Fix ImageRule: replace non-existent wp_get_image_mime_types() with wp_get_ext_types() + wp_get_mime_types(); use wp_check_filetype_and_ext for content-based verification instead of trusting user-controlled $_FILES['type'] - Fix MaxFileRule: use cached metadata['filesize'] instead of redundant filesize() calls - Extend BetweenRule and SizeRule with file size (KB) awareness; guard WP functions with function_exists() for non-WP environments - Fix Rule::setParameterValues to support multi-value single-key params (e.g. mimes:jpg,png,pdf) through the Validator pipeline - Extract getAttachmentSizeBytes() to Helpers trait, eliminating duplication across MaxFileRule, BetweenRule, SizeRule Co-Authored-By: Claude Sonnet 4.6 --- src/Helpers.php | 10 ++++++ src/Rule.php | 5 ++- src/Rules/BetweenRule.php | 21 +++++++++--- src/Rules/EncodingRule.php | 61 ++++++++++++++++++++++++++++++++++ src/Rules/ExtensionsRule.php | 61 ++++++++++++++++++++++++++++++++++ src/Rules/FileRule.php | 8 ++--- src/Rules/ImageRule.php | 51 ++++++++++++++--------------- src/Rules/MaxFileRule.php | 8 +---- src/Rules/MimesRule.php | 12 ++----- src/Rules/MimetypesRule.php | 63 ++++++++++++++++++++++++++++++++++++ src/Rules/SizeRule.php | 20 ++++++++++-- 11 files changed, 265 insertions(+), 55 deletions(-) create mode 100644 src/Rules/EncodingRule.php create mode 100644 src/Rules/ExtensionsRule.php create mode 100644 src/Rules/MimetypesRule.php diff --git a/src/Helpers.php b/src/Helpers.php index 1859266..b624e70 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -25,6 +25,16 @@ protected function getValueLength($value) } + protected function getAttachmentSizeBytes(int $attachmentId, string $filePath) + { + $metadata = wp_get_attachment_metadata($attachmentId); + if ($metadata && isset($metadata['filesize'])) { + return (int) $metadata['filesize']; + } + + return filesize($filePath); + } + public function setNestedElement(&$data, $keys, $value) { $current = &$data; diff --git a/src/Rule.php b/src/Rule.php index 44c0ec8..1b2d89e 100644 --- a/src/Rule.php +++ b/src/Rule.php @@ -46,7 +46,10 @@ public function getParamKeys() public function setParameterValues($paramKeys, $paramValues): void { - if (count($paramKeys) === count($paramValues)) { + if (count($paramKeys) === 1 && count($paramValues) > 1) { + // Single-key rules that accept comma-separated values (e.g. mimes:jpg,png,pdf) + $this->params = [$paramKeys[0] => implode(',', $paramValues)]; + } elseif (count($paramKeys) === count($paramValues)) { $this->params = array_combine($paramKeys, $paramValues); } } diff --git a/src/Rules/BetweenRule.php b/src/Rules/BetweenRule.php index 67bb79e..348a2bd 100644 --- a/src/Rules/BetweenRule.php +++ b/src/Rules/BetweenRule.php @@ -17,17 +17,28 @@ public function validate($value) $this->checkRequiredParameter($this->requireParameters); $min = (int) $this->getParameter('min'); - $max = (int) $this->getParameter('max'); - $length = $this->getValueLength($value); + if (is_array($value) && isset($value['size'])) { + $sizeKB = (int) $value['size'] / 1024; + return $sizeKB >= $min && $sizeKB <= $max; + } - if ($length) { - return $length >= $min && $length <= $max; + if (is_numeric($value) && function_exists('get_attached_file')) { + $attachmentId = (int) $value; + $filePath = get_attached_file($attachmentId); + if (! empty($filePath) && file_exists($filePath)) { + $sizeKB = $this->getAttachmentSizeBytes($attachmentId, $filePath) / 1024; + return $sizeKB >= $min && $sizeKB <= $max; + } } - return false; + $length = $this->getValueLength($value); + if ($length === false) { + return false; + } + return $length >= $min && $length <= $max; } public function getParamKeys() diff --git a/src/Rules/EncodingRule.php b/src/Rules/EncodingRule.php new file mode 100644 index 0000000..4be7f80 --- /dev/null +++ b/src/Rules/EncodingRule.php @@ -0,0 +1,61 @@ +checkRequiredParameter($this->requireParameters); + + if ($this->isEmpty($value)) { + return false; + } + + $allowedEncodings = array_map('trim', explode(',', $this->getParameter('encoding'))); + + $filePath = $this->getFilePath($value); + if (! $filePath) { + return false; + } + + $contents = file_get_contents($filePath); + if ($contents === false) { + return false; + } + + return mb_detect_encoding($contents, $allowedEncodings, true) !== false; + } + + private function getFilePath($value) + { + if (is_numeric($value)) { + $filePath = get_attached_file((int) $value); + return (! empty($filePath) && file_exists($filePath)) ? $filePath : null; + } + + if (is_array($value) && isset($value['tmp_name']) && is_uploaded_file($value['tmp_name'])) { + return $value['tmp_name']; + } + + return null; + } + + public function getParamKeys() + { + return $this->requireParameters; + } + + public function message() + { + return $this->message; + } +} diff --git a/src/Rules/ExtensionsRule.php b/src/Rules/ExtensionsRule.php new file mode 100644 index 0000000..fffb713 --- /dev/null +++ b/src/Rules/ExtensionsRule.php @@ -0,0 +1,61 @@ +checkRequiredParameter($this->requireParameters); + + if ($this->isEmpty($value)) { + return false; + } + + $allowedExtensions = array_map(function ($e) { + return strtolower(trim($e)); + }, explode(',', $this->getParameter('extensions'))); + + $fileName = $this->getFileName($value); + if (! $fileName) { + return false; + } + + $wpFileType = wp_check_filetype($fileName); + $extension = strtolower($wpFileType['ext'] ?? ''); + + return ! empty($extension) && in_array($extension, $allowedExtensions, true); + } + + private function getFileName($value) + { + if (is_numeric($value)) { + $filePath = get_attached_file((int) $value); + return (! empty($filePath) && file_exists($filePath)) ? basename($filePath) : null; + } + + if (is_array($value) && isset($value['name'])) { + return $value['name']; + } + + return null; + } + + public function getParamKeys() + { + return $this->requireParameters; + } + + public function message() + { + return $this->message; + } +} diff --git a/src/Rules/FileRule.php b/src/Rules/FileRule.php index 975dc45..bb48653 100644 --- a/src/Rules/FileRule.php +++ b/src/Rules/FileRule.php @@ -23,11 +23,9 @@ public function validate($value) } if (is_array($value)) { - $requiredKeys = ['name', 'type', 'tmp_name', 'error', 'size']; - foreach ($requiredKeys as $key) { - if (! isset($value[$key])) { - return false; - } + $requiredKeys = array_flip(['name', 'type', 'tmp_name', 'error', 'size']); + if (array_diff_key($requiredKeys, $value)) { + return false; } if ($value['error'] !== UPLOAD_ERR_OK) { diff --git a/src/Rules/ImageRule.php b/src/Rules/ImageRule.php index 8e27ad8..8f97e39 100644 --- a/src/Rules/ImageRule.php +++ b/src/Rules/ImageRule.php @@ -17,57 +17,56 @@ public function validate($value) } $fileInfo = $this->getFileInfo($value); - if (!$fileInfo) { + if (! $fileInfo || empty($fileInfo['type'])) { return false; } - $fileType = $fileInfo['type']; - $fileName = $fileInfo['name']; - - $allowedImageTypes = wp_get_image_mime_types(); - - if (in_array($fileType, $allowedImageTypes, true)) { - return true; - } - - $wpFileType = wp_check_filetype($fileName); - if ($wpFileType && isset($wpFileType['type'])) { - if (in_array($wpFileType['type'], $allowedImageTypes, true)) { - return true; - } - } - - return false; + return in_array($fileInfo['type'], $this->getImageMimeTypes(), true); } private function getFileInfo($value) { - // Handle attachment ID if (is_numeric($value)) { $attachmentId = (int) $value; - $filePath = get_attached_file($attachmentId); - if (empty($filePath) || !file_exists($filePath)) { + $filePath = get_attached_file($attachmentId); + if (empty($filePath) || ! file_exists($filePath)) { return null; } - $wpFileType = wp_check_filetype(basename($filePath)); + $checked = wp_check_filetype_and_ext($filePath, basename($filePath)); return [ 'name' => basename($filePath), - 'type' => $wpFileType['type'] ?? '', + 'type' => $checked['type'] ?? '', ]; } - // Handle $_FILES format - if (is_array($value) && isset($value['name']) && isset($value['type'])) { + if (is_array($value) && isset($value['name'], $value['tmp_name'])) { + $checked = wp_check_filetype_and_ext($value['tmp_name'], $value['name']); return [ 'name' => $value['name'], - 'type' => $value['type'], + 'type' => $checked['type'] ?? '', ]; } return null; } + private function getImageMimeTypes(): array + { + $imageExtensions = wp_get_ext_types()['image'] ?? []; + $imageMimes = []; + foreach (wp_get_mime_types() as $exts => $mime) { + foreach (explode('|', $exts) as $ext) { + if (in_array($ext, $imageExtensions, true)) { + $imageMimes[] = $mime; + break; + } + } + } + + return array_values(array_unique($imageMimes)); + } + public function message() { return $this->message; diff --git a/src/Rules/MaxFileRule.php b/src/Rules/MaxFileRule.php index e6c18d5..fe19a68 100644 --- a/src/Rules/MaxFileRule.php +++ b/src/Rules/MaxFileRule.php @@ -40,13 +40,7 @@ private function getFileSize($value) return false; } - $metadata = wp_get_attachment_metadata($attachmentId); - if ($metadata && isset($metadata['file'])) { - $fileSize = filesize($filePath); - return $fileSize !== false ? $fileSize : false; - } - - return filesize($filePath); + return $this->getAttachmentSizeBytes($attachmentId, $filePath); } if (is_array($value) && isset($value['size'])) { diff --git a/src/Rules/MimesRule.php b/src/Rules/MimesRule.php index 965f382..a2eadbc 100644 --- a/src/Rules/MimesRule.php +++ b/src/Rules/MimesRule.php @@ -32,19 +32,13 @@ public function validate($value) return false; } - $fileType = $fileInfo['type']; - $fileName = $fileInfo['name']; - - if (in_array($fileType, $allowedTypes, true)) { + if (in_array($fileInfo['type'], $allowedTypes, true)) { return true; } - $wpFileType = wp_check_filetype($fileName); + $wpFileType = wp_check_filetype($fileInfo['name']); if ($wpFileType && isset($wpFileType['ext'])) { - $extension = strtolower($wpFileType['ext']); - if (in_array($extension, $allowedTypes, true)) { - return true; - } + return in_array(strtolower($wpFileType['ext']), $allowedTypes, true); } return false; diff --git a/src/Rules/MimetypesRule.php b/src/Rules/MimetypesRule.php new file mode 100644 index 0000000..a1a77e9 --- /dev/null +++ b/src/Rules/MimetypesRule.php @@ -0,0 +1,63 @@ +checkRequiredParameter($this->requireParameters); + + if ($this->isEmpty($value)) { + return false; + } + + $allowedMimes = array_map('trim', explode(',', $this->getParameter('mimetypes'))); + + $fileInfo = $this->getFileInfo($value); + if (! $fileInfo || empty($fileInfo['type'])) { + return false; + } + + return in_array($fileInfo['type'], $allowedMimes, true); + } + + private function getFileInfo($value) + { + if (is_numeric($value)) { + $attachmentId = (int) $value; + $filePath = get_attached_file($attachmentId); + if (empty($filePath) || ! file_exists($filePath)) { + return null; + } + + $checked = wp_check_filetype_and_ext($filePath, basename($filePath)); + return ['type' => $checked['type'] ?? '']; + } + + if (is_array($value) && isset($value['name'], $value['tmp_name'])) { + $checked = wp_check_filetype_and_ext($value['tmp_name'], $value['name']); + return ['type' => $checked['type'] ?? '']; + } + + return null; + } + + public function getParamKeys() + { + return $this->requireParameters; + } + + public function message() + { + return $this->message; + } +} diff --git a/src/Rules/SizeRule.php b/src/Rules/SizeRule.php index 4f78694..5ca3ea0 100644 --- a/src/Rules/SizeRule.php +++ b/src/Rules/SizeRule.php @@ -1,10 +1,12 @@ checkRequiredParameter($this->requireParameters); - $size = $this->getParameter('size'); + $size = (int) $this->getParameter('size'); + + if (is_array($value) && isset($value['size'])) { + return (int) round($value['size'] / 1024) === $size; + } + + if (is_numeric($value) && function_exists('get_attached_file')) { + $attachmentId = (int) $value; + $filePath = get_attached_file($attachmentId); + if (! empty($filePath) && file_exists($filePath)) { + return (int) round($this->getAttachmentSizeBytes($attachmentId, $filePath) / 1024) === $size; + } + } if (is_string($value)) { - return strlen($value) == $size; + return strlen($value) === $size; } + if (is_int($value)) { return $value === $size; } @@ -25,6 +40,7 @@ public function validate($value): bool if (is_array($value)) { return count($value) === $size; } + return false; } From c21175036f8dfe78e2e52e465672efc861bff30c Mon Sep 17 00:00:00 2001 From: csemazharul Date: Wed, 22 Apr 2026 18:10:52 +0600 Subject: [PATCH 4/5] fix: address PR review findings from gemini-code-assist - EncodingRule: read only first 32KB via WP_Filesystem (falls back to file_get_contents) to prevent OOM on large files; rename helper to getFileContentForEncodingDetection for clarity - MimesRule: replace trust of user-controlled $_FILES['type'] with content-based wp_check_filetype_and_ext(), consistent with ImageRule - BetweenRule: align KB conversion to use (int) round() matching SizeRule, eliminating inconsistency flagged in review - README: document new file rules (extensions, mimetypes, encoding) and add file size notes to between/size rules Co-Authored-By: Claude Sonnet 4.6 --- README.md | 13 +++++++++++++ src/Rules/BetweenRule.php | 4 ++-- src/Rules/EncodingRule.php | 14 +++++++++++++- src/Rules/MimesRule.php | 6 +++--- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2d62954..1a73990 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,8 @@ Checks if the field under validation falls within the range of `:min` and `:max` - For string data, the value corresponds to the number of characters. - For numeric data, the value corresponds to a given integer value. - For an array, the value corresponds to the count of the array. + - For file uploads (`$_FILES` array or WordPress attachment ID), the value corresponds to the file size in kilobytes.
+ e.g. `['avatar' => ['required', 'file', 'between:100,2048']]` (between 100KB and 2MB) 4. **`date`**
Checks if the field under validation is a valid date according to the `strtotime` PHP function. 5. **`digit_between:min,max`**
@@ -185,6 +187,8 @@ Checks if the field under validation has exactly the same size as `:size`. - For string data, the value corresponds to the number of characters. - For numeric data, the value corresponds to a given integer value. - For an array, the value corresponds to the count of the array. + - For file uploads (`$_FILES` array or WordPress attachment ID), the value corresponds to the file size in kilobytes.
+ e.g. `['document' => ['required', 'file', 'size:512']]` (exactly 512KB) 23. **`string`**
Checks if the given value is a string. 24. **`uppercase`**
@@ -206,6 +210,15 @@ e.g. `['photo' => ['required', 'file', 'image']]` 30. **`image_dimensions:width,height`**
Checks if the image dimensions (in pixels) are less than or equal to the specified maximum width and height.
e.g. `['avatar' => ['required', 'file', 'image', 'image_dimensions:1920,1080']]` +31. **`extensions:ext1,ext2,...`**
+Checks if the file has one of the specified extensions (based on filename only, no content inspection). Accepts comma-separated extensions without dots.
+e.g. `['document' => ['required', 'file', 'extensions:pdf,doc,docx']]` +32. **`mimetypes:type1,type2,...`**
+Checks if the file's actual MIME type (verified from file content via `wp_check_filetype_and_ext`) matches one of the specified types. More secure than `mimes` because it inspects file content, not just the filename or browser-supplied type.
+e.g. `['upload' => ['required', 'file', 'mimetypes:image/jpeg,image/png,application/pdf']]` +33. **`encoding:enc1,enc2,...`**
+Checks if the file's character encoding matches one of the specified encodings. Useful for validating text files (CSV, JSON, XML, etc.). Accepts comma-separated encoding names supported by PHP's `mb_detect_encoding`.
+e.g. `['csv' => ['required', 'file', 'encoding:UTF-8,ASCII']]` Missing any validation rule that you need? Refer to the [Custom Validation Rule](#custom-validation-rule) section to know how you can create and use custom validation rules in your project alongside the library. diff --git a/src/Rules/BetweenRule.php b/src/Rules/BetweenRule.php index 348a2bd..108da93 100644 --- a/src/Rules/BetweenRule.php +++ b/src/Rules/BetweenRule.php @@ -20,7 +20,7 @@ public function validate($value) $max = (int) $this->getParameter('max'); if (is_array($value) && isset($value['size'])) { - $sizeKB = (int) $value['size'] / 1024; + $sizeKB = (int) round($value['size'] / 1024); return $sizeKB >= $min && $sizeKB <= $max; } @@ -28,7 +28,7 @@ public function validate($value) $attachmentId = (int) $value; $filePath = get_attached_file($attachmentId); if (! empty($filePath) && file_exists($filePath)) { - $sizeKB = $this->getAttachmentSizeBytes($attachmentId, $filePath) / 1024; + $sizeKB = (int) round($this->getAttachmentSizeBytes($attachmentId, $filePath) / 1024); return $sizeKB >= $min && $sizeKB <= $max; } } diff --git a/src/Rules/EncodingRule.php b/src/Rules/EncodingRule.php index 4be7f80..c39c017 100644 --- a/src/Rules/EncodingRule.php +++ b/src/Rules/EncodingRule.php @@ -27,7 +27,7 @@ public function validate($value) return false; } - $contents = file_get_contents($filePath); + $contents = $this->getFileContentForEncodingDetection($filePath); if ($contents === false) { return false; } @@ -35,6 +35,18 @@ public function validate($value) return mb_detect_encoding($contents, $allowedEncodings, true) !== false; } + private function getFileContentForEncodingDetection($filePath) + { + if (function_exists('WP_Filesystem')) { + global $wp_filesystem; + WP_Filesystem(); + $contents = $wp_filesystem->get_contents($filePath); + return $contents !== false ? substr($contents, 0, 32768) : false; + } + + return file_get_contents($filePath, false, null, 0, 32768); + } + private function getFilePath($value) { if (is_numeric($value)) { diff --git a/src/Rules/MimesRule.php b/src/Rules/MimesRule.php index a2eadbc..3f8d0d8 100644 --- a/src/Rules/MimesRule.php +++ b/src/Rules/MimesRule.php @@ -61,11 +61,11 @@ private function getFileInfo($value) ]; } - // Handle $_FILES format - if (is_array($value) && isset($value['name']) && isset($value['type'])) { + if (is_array($value) && isset($value['name'], $value['tmp_name'])) { + $checked = wp_check_filetype_and_ext($value['tmp_name'], $value['name']); return [ 'name' => $value['name'], - 'type' => $value['type'], + 'type' => $checked['type'] ?? '', ]; } From 07f421c71964083e87d84163e3ce0312239a5ff4 Mon Sep 17 00:00:00 2001 From: csemazharul Date: Thu, 23 Apr 2026 12:56:08 +0600 Subject: [PATCH 5/5] refactor: rename ImageDimensionsRule to DimensionRule with Laravel-style constraints - Rename ImageDimensionsRule -> DimensionRule (file and class) - Rename getImageDimensions() -> getDimensions() - Replace positional width,height params with key=value constraint syntax: min_width, max_width, min_height, max_height, width, height, ratio - ratio supports fraction (3/2) and float (1.5) with 0.01 tolerance - Fix EncodingRule: check WP_Filesystem() return before using $wp_filesystem to prevent fatal error when filesystem init fails - Update README: document new dimension rule syntax with examples Co-Authored-By: Claude Sonnet 4.6 --- README.md | 7 +- src/Rules/DimensionRule.php | 146 ++++++++++++++++++++++++++++++ src/Rules/EncodingRule.php | 3 +- src/Rules/ImageDimensionsRule.php | 86 ------------------ 4 files changed, 151 insertions(+), 91 deletions(-) create mode 100644 src/Rules/DimensionRule.php delete mode 100644 src/Rules/ImageDimensionsRule.php diff --git a/README.md b/README.md index 1a73990..8d97dbd 100644 --- a/README.md +++ b/README.md @@ -207,9 +207,10 @@ e.g. `['avatar' => ['required', 'file', 'max_file:2048']]` (2MB limit) 29. **`image`**
Checks if the file under validation is an image using WordPress allowed image MIME types.
e.g. `['photo' => ['required', 'file', 'image']]` -30. **`image_dimensions:width,height`**
-Checks if the image dimensions (in pixels) are less than or equal to the specified maximum width and height.
-e.g. `['avatar' => ['required', 'file', 'image', 'image_dimensions:1920,1080']]` +30. **`dimension:constraint=value,...`**
+Checks image dimensions against one or more constraints. Available constraints: `min_width`, `max_width`, `min_height`, `max_height`, `width` (exact), `height` (exact), `ratio` (as fraction `3/2` or float `1.5`).
+e.g. `['avatar' => ['required', 'image', 'dimension:max_width=1920,max_height=1080']]`
+e.g. `['avatar' => ['required', 'image', 'dimension:min_width=100,min_height=100,ratio=16/9']]` 31. **`extensions:ext1,ext2,...`**
Checks if the file has one of the specified extensions (based on filename only, no content inspection). Accepts comma-separated extensions without dots.
e.g. `['document' => ['required', 'file', 'extensions:pdf,doc,docx']]` diff --git a/src/Rules/DimensionRule.php b/src/Rules/DimensionRule.php new file mode 100644 index 0000000..d315e03 --- /dev/null +++ b/src/Rules/DimensionRule.php @@ -0,0 +1,146 @@ +checkRequiredParameter($this->requireParameters); + + if ($this->isEmpty($value)) { + return false; + } + + $constraints = $this->parseConstraints(); + if (empty($constraints)) { + return false; + } + + $dimensions = $this->getDimensions($value); + if (! $dimensions) { + return false; + } + + return $this->checkConstraints($dimensions['width'], $dimensions['height'], $constraints); + } + + private function parseConstraints(): array + { + $raw = $this->getParameter('dimensions'); + if (empty($raw)) { + return []; + } + + $constraints = []; + foreach (explode(',', $raw) as $item) { + $parts = explode('=', $item, 2); + if (count($parts) === 2) { + $constraints[trim($parts[0])] = trim($parts[1]); + } + } + + return $constraints; + } + + private function checkConstraints(int $width, int $height, array $constraints): bool + { + $checks = [ + 'width' => static function ($w, $h, $v) { return $w === (int) $v; }, + 'height' => static function ($w, $h, $v) { return $h === (int) $v; }, + 'min_width' => static function ($w, $h, $v) { return $w >= (int) $v; }, + 'max_width' => static function ($w, $h, $v) { return $w <= (int) $v; }, + 'min_height' => static function ($w, $h, $v) { return $h >= (int) $v; }, + 'max_height' => static function ($w, $h, $v) { return $h <= (int) $v; }, + ]; + + foreach ($constraints as $key => $val) { + if ($key === 'ratio') { + if (! $this->checkRatio($width, $height, $val)) { + return false; + } + continue; + } + + if (isset($checks[$key]) && ! $checks[$key]($width, $height, $val)) { + return false; + } + } + + return true; + } + + private function checkRatio(int $width, int $height, string $ratio): bool + { + if ($height === 0) { + return false; + } + + if (strpos($ratio, '/') !== false) { + list($ratioW, $ratioH) = explode('/', $ratio, 2); + $expected = (float) $ratioW / (float) $ratioH; + } else { + $expected = (float) $ratio; + } + + return abs(($width / $height) - $expected) < 0.01; + } + + private function getDimensions($value) + { + $filePath = null; + + if (is_numeric($value)) { + $attachmentId = (int) $value; + $filePath = get_attached_file($attachmentId); + if (empty($filePath) || ! file_exists($filePath)) { + return null; + } + + $metadata = wp_get_attachment_metadata($attachmentId); + if ($metadata && isset($metadata['width'], $metadata['height'])) { + return [ + 'width' => (int) $metadata['width'], + 'height' => (int) $metadata['height'], + ]; + } + } + + if (is_array($value) && isset($value['tmp_name'])) { + if (empty($value['tmp_name']) || ! is_uploaded_file($value['tmp_name'])) { + return null; + } + $filePath = $value['tmp_name']; + } + + if ($filePath && file_exists($filePath)) { + $imageSize = @getimagesize($filePath); + if ($imageSize && isset($imageSize[0], $imageSize[1])) { + return [ + 'width' => (int) $imageSize[0], + 'height' => (int) $imageSize[1], + ]; + } + } + + return null; + } + + public function getParamKeys() + { + return $this->requireParameters; + } + + public function message() + { + return $this->message; + } +} diff --git a/src/Rules/EncodingRule.php b/src/Rules/EncodingRule.php index c39c017..a441b84 100644 --- a/src/Rules/EncodingRule.php +++ b/src/Rules/EncodingRule.php @@ -37,9 +37,8 @@ public function validate($value) private function getFileContentForEncodingDetection($filePath) { - if (function_exists('WP_Filesystem')) { + if (function_exists('WP_Filesystem') && WP_Filesystem()) { global $wp_filesystem; - WP_Filesystem(); $contents = $wp_filesystem->get_contents($filePath); return $contents !== false ? substr($contents, 0, 32768) : false; } diff --git a/src/Rules/ImageDimensionsRule.php b/src/Rules/ImageDimensionsRule.php deleted file mode 100644 index 215a86e..0000000 --- a/src/Rules/ImageDimensionsRule.php +++ /dev/null @@ -1,86 +0,0 @@ -checkRequiredParameter($this->requireParameters); - - if ($this->isEmpty($value)) { - return false; - } - - $maxWidth = (int) $this->getParameter('width'); - $maxHeight = (int) $this->getParameter('height'); - - $dimensions = $this->getImageDimensions($value); - if (! $dimensions) { - return false; - } - - $width = $dimensions['width']; - $height = $dimensions['height']; - - return $width <= $maxWidth && $height <= $maxHeight; - } - - private function getImageDimensions($value) - { - $filePath = null; - - if (is_numeric($value)) { - $attachmentId = (int) $value; - $filePath = get_attached_file($attachmentId); - if (empty($filePath) || ! file_exists($filePath)) { - return null; - } - - $metadata = wp_get_attachment_metadata($attachmentId); - if ($metadata && isset($metadata['width']) && isset($metadata['height'])) { - return [ - 'width' => (int) $metadata['width'], - 'height' => (int) $metadata['height'], - ]; - } - } - - if (is_array($value) && isset($value['tmp_name'])) { - if (empty($value['tmp_name']) || ! is_uploaded_file($value['tmp_name'])) { - return null; - } - $filePath = $value['tmp_name']; - } - - if ($filePath && file_exists($filePath)) { - $imageSize = @getimagesize($filePath); - if ($imageSize && isset($imageSize[0]) && isset($imageSize[1])) { - return [ - 'width' => (int) $imageSize[0], - 'height' => (int) $imageSize[1], - ]; - } - } - - return null; - } - - public function getParamKeys() - { - return $this->requireParameters; - } - - public function message() - { - return $this->message; - } -}