diff --git a/README.md b/README.md index 33e1612..8d97dbd 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,12 +187,39 @@ 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`**
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. **`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']]` +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/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..108da93 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) round($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 = (int) round($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/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 new file mode 100644 index 0000000..a441b84 --- /dev/null +++ b/src/Rules/EncodingRule.php @@ -0,0 +1,72 @@ +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 = $this->getFileContentForEncodingDetection($filePath); + if ($contents === false) { + return false; + } + + return mb_detect_encoding($contents, $allowedEncodings, true) !== false; + } + + private function getFileContentForEncodingDetection($filePath) + { + if (function_exists('WP_Filesystem') && WP_Filesystem()) { + global $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)) { + $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 new file mode 100644 index 0000000..bb48653 --- /dev/null +++ b/src/Rules/FileRule.php @@ -0,0 +1,49 @@ +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 = array_flip(['name', 'type', 'tmp_name', 'error', 'size']); + if (array_diff_key($requiredKeys, $value)) { + 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/ImageRule.php b/src/Rules/ImageRule.php new file mode 100644 index 0000000..8f97e39 --- /dev/null +++ b/src/Rules/ImageRule.php @@ -0,0 +1,75 @@ +isEmpty($value)) { + return false; + } + + $fileInfo = $this->getFileInfo($value); + if (! $fileInfo || empty($fileInfo['type'])) { + return false; + } + + return in_array($fileInfo['type'], $this->getImageMimeTypes(), 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 [ + 'name' => basename($filePath), + '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 [ + 'name' => $value['name'], + '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 new file mode 100644 index 0000000..fe19a68 --- /dev/null +++ b/src/Rules/MaxFileRule.php @@ -0,0 +1,63 @@ +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; + } + + return $this->getAttachmentSizeBytes($attachmentId, $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..3f8d0d8 --- /dev/null +++ b/src/Rules/MimesRule.php @@ -0,0 +1,84 @@ +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; + } + + if (in_array($fileInfo['type'], $allowedTypes, true)) { + return true; + } + + $wpFileType = wp_check_filetype($fileInfo['name']); + if ($wpFileType && isset($wpFileType['ext'])) { + return in_array(strtolower($wpFileType['ext']), $allowedTypes, 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'] ?? '', + ]; + } + + 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' => $checked['type'] ?? '', + ]; + } + + return null; + } + + public function getParamKeys() + { + return $this->requireParameters; + } + + public function message() + { + return $this->message; + } +} 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; }