<?php
|
namespace JVBase\meta;
|
|
use DateTime;
|
use JVBase\meta\MetaTypeManager;
|
use WP_Error;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
/**
|
* Handles meta value validation
|
*/
|
class MetaValidator
|
{
|
protected MetaTypeManager $type_manager;
|
protected array $errors = [];
|
|
public function __construct()
|
{
|
$this->type_manager = new MetaTypeManager();
|
}
|
|
public function validate(mixed $value, array $field_config):bool
|
{
|
$this->errors = []; // Reset errors
|
$type = $field_config['type'];
|
|
// Required field check
|
if (!empty($field_config['required']) && empty($value)) {
|
$this->addError($field_config['name'], __('This field is required', 'jvb'));
|
return false;
|
}
|
|
// Type-specific validation
|
$method = "validate_{$type}";
|
if (method_exists($this, $method)) {
|
return $this->$method($value, $field_config);
|
}
|
|
return true;
|
}
|
|
|
protected function validateNumber(float $value, array $field_config):bool|WP_Error
|
{
|
if (empty($value)) {
|
if (!empty($field_config['required'])) { // ✅ Correct variable
|
return new \WP_Error('required_field', 'This field is required');
|
}
|
return true;
|
}
|
|
if (!is_numeric($value)) {
|
$this->addError($field_config['name'], __('Must be a number', 'jvb'));
|
return false;
|
}
|
|
if (array_key_exists('min', $field_config) && $value < $field_config['min']) {
|
$this->addError(
|
$field_config['name'],
|
sprintf(__('Must be at least %s', 'jvb'), $field_config['min'])
|
);
|
return false;
|
}
|
|
if (array_key_exists('max', $field_config) && $value > $field_config['max']) {
|
$this->addError(
|
$field_config['name'],
|
sprintf(__('Must not exceed %s', 'jvb'), $field_config['max'])
|
);
|
return false;
|
}
|
|
return true;
|
}
|
|
protected function validateEmail(string $value, array $config):bool|WP_Error
|
{
|
if (empty($value)) {
|
if (!empty($config['required'])) { // ✅ Correct variable
|
return new \WP_Error('required_field', 'This field is required');
|
}
|
return true;
|
}
|
|
// Validate email format
|
if (!is_email($value)) {
|
$this->addError($config['name'], __('Invalid email address', 'jvb'));
|
return false;
|
}
|
|
return true;
|
}
|
|
|
protected function validateGroup(array $value, array $config):bool
|
{
|
if (empty($value) || !is_array($value)) {
|
if (!empty($config['required'])) {
|
$this->addError($config['name'], __('This field is required', 'jvb'));
|
return false;
|
}
|
return true;
|
}
|
|
// Validate each sub-field
|
if (!empty($config['fields']) && is_array($config['fields'])) {
|
foreach ($config['fields'] as $subFieldName => $subFieldConfig) {
|
if (isset($value[$subFieldName])) {
|
$subFieldConfig['name'] = $subFieldName;
|
$isValid = $this->validate($value[$subFieldName], $subFieldConfig);
|
if (!$isValid) {
|
return false;
|
}
|
}
|
}
|
}
|
|
return true;
|
}
|
|
protected function validateGallery(array|string $value, array $field):bool|WP_Error
|
{
|
if (empty($value)) {
|
if (!empty($field['required'])) {
|
return new WP_Error('required_field', 'This field is required');
|
}
|
return true;
|
}
|
|
$ids = is_array($value) ? $value : explode(',', $value);
|
|
// Check maximum images if specified
|
if (!empty($field['max_images']) && count($ids) > $field['max_images']) {
|
return new WP_Error(
|
'max_images_exceeded',
|
sprintf('Maximum of %d images allowed', $field['max_images'])
|
);
|
}
|
|
// Validate each ID is an actual image attachment
|
foreach ($ids as $id) {
|
$id = absint($id);
|
if ($id <= 0 || !wp_attachment_is_image($id)) {
|
return new WP_Error('invalid_image', 'One or more invalid images');
|
}
|
}
|
|
return true;
|
}
|
|
protected function validateUrl(string $value, array $config):bool|WP_Error
|
{
|
if (empty($value)) {
|
if (!empty($field['required'])) {
|
return new WP_Error('required_field', 'This field is required');
|
}
|
return true;
|
}
|
$check = filter_var($value, FILTER_VALIDATE_URL) !== false;
|
if (!$check) {
|
$this->addError(
|
$config['name'],
|
__('Must be a valid URL', 'jvb')
|
);
|
}
|
return $check;
|
}
|
|
protected function validateDate(string $value, array $config):bool|WP_Error
|
{
|
if (empty($value)) {
|
if (!empty($field['required'])) {
|
return new WP_Error('required_field', 'This field is required');
|
}
|
return true;
|
}
|
$timestamp = strtotime($value);
|
if ($timestamp === false) {
|
$this->addError(
|
$config['name'],
|
__('It\'s gotta be a time, bro', 'jvb')
|
);
|
return false;
|
}
|
|
if (isset($config['min_date']) && $timestamp < strtotime($config['min_date'])) {
|
return false;
|
}
|
|
if (isset($config['max_date']) && $timestamp > strtotime($config['max_date'])) {
|
return false;
|
}
|
|
return true;
|
}
|
|
protected function validateDatetime(string $value, array $config):bool
|
{
|
if (empty($value)) {
|
if (!empty($config['required'])) {
|
$this->addError($config['name'], __('This field is required', 'jvb'));
|
return false;
|
}
|
return true;
|
}
|
|
// Try to parse the datetime
|
$date = DateTime::createFromFormat('Y-m-d H:i:s', $value);
|
if (!$date) {
|
// Try alternative formats
|
$formats = ['Y-m-d\TH:i:s', 'Y-m-d\TH:i', 'Y-m-d H:i'];
|
foreach ($formats as $format) {
|
$date = DateTime::createFromFormat($format, $value);
|
if ($date) break;
|
}
|
}
|
|
if (!$date) {
|
$this->addError($config['name'], __('Invalid datetime format', 'jvb'));
|
return false;
|
}
|
|
$timestamp = $date->getTimestamp();
|
|
// Validate datetime range if specified
|
if (isset($config['min_datetime'])) {
|
$min_timestamp = strtotime($config['min_datetime']);
|
if ($timestamp < $min_timestamp) {
|
$min_date = new DateTime($config['min_datetime']);
|
$this->addError(
|
$config['name'],
|
sprintf(__('DateTime must be after %s', 'jvb'), $min_date->format('F j, Y g:i A'))
|
);
|
return false;
|
}
|
}
|
|
if (isset($config['max_datetime'])) {
|
$max_timestamp = strtotime($config['max_datetime']);
|
if ($timestamp > $max_timestamp) {
|
$max_date = new DateTime($config['max_datetime']);
|
$this->addError(
|
$config['name'],
|
sprintf(__('DateTime must be before %s', 'jvb'), $max_date->format('F j, Y g:i A'))
|
);
|
return false;
|
}
|
}
|
|
return true;
|
}
|
|
protected function validateTime(string $value, array $config):bool
|
{
|
if (empty($value)) {
|
if (!empty($config['required'])) {
|
$this->addError($config['name'], __('This field is required', 'jvb'));
|
return false;
|
}
|
return true;
|
}
|
|
// Check if time is in valid format (HH:MM)
|
if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $value)) {
|
$this->addError($config['name'], __('Time must be in HH:MM format', 'jvb'));
|
return false;
|
}
|
|
// Validate time range if specified
|
if (isset($config['min_time'])) {
|
if (strtotime($value) < strtotime($config['min_time'])) {
|
$this->addError(
|
$config['name'],
|
sprintf(__('Time must be after %s', 'jvb'), $config['min_time'])
|
);
|
return false;
|
}
|
}
|
|
if (isset($config['max_time'])) {
|
if (strtotime($value) > strtotime($config['max_time'])) {
|
$this->addError(
|
$config['name'],
|
sprintf(__('Time must be before %s', 'jvb'), $config['max_time'])
|
);
|
return false;
|
}
|
}
|
|
return true;
|
}
|
|
protected function validateTagList(array $value, array $config): bool
|
{
|
if (empty($value)) {
|
if (!empty($config['required'])) {
|
$this->addError($config['name'], __('This field is required', 'jvb'));
|
return false;
|
}
|
return true;
|
}
|
|
if (!is_array($value)) {
|
$this->addError($config['name'], __('Invalid data format', 'jvb'));
|
return false;
|
}
|
|
// Check min/max items
|
if (isset($config['min_items']) && count($value) < $config['min_items']) {
|
$this->addError(
|
$config['name'],
|
sprintf(__('Minimum of %d items required', 'jvb'), $config['min_items'])
|
);
|
return false;
|
}
|
|
if (isset($config['max_items']) && count($value) > $config['max_items']) {
|
$this->addError(
|
$config['name'],
|
sprintf(__('Maximum of %d items allowed', 'jvb'), $config['max_items'])
|
);
|
return false;
|
}
|
|
// Validate each item's fields
|
if (!isset($config['fields']) || !is_array($config['fields'])) {
|
return true;
|
}
|
|
foreach ($value as $index => $row) {
|
if (!is_array($row)) {
|
continue;
|
}
|
|
foreach ($config['fields'] as $field_name => $field_config) {
|
if (!isset($row[$field_name])) {
|
continue;
|
}
|
|
$field_config['name'] = "{$config['name']}[{$index}][{$field_name}]";
|
|
if (!$this->validate($row[$field_name], $field_config)) {
|
return false;
|
}
|
}
|
}
|
|
return true;
|
}
|
|
protected function validateRepeater(array $value, array $config):bool|WP_Error
|
{
|
if (empty($value)) {
|
if (!empty($field['required'])) {
|
return new WP_Error('required_field', 'This field is required');
|
}
|
return true;
|
}
|
if (!is_array($value)) {
|
return false;
|
}
|
|
if (isset($config['min_rows']) && count($value) < $config['min_rows']) {
|
return false;
|
}
|
|
if (isset($config['max_rows']) && count($value) > $config['max_rows']) {
|
return false;
|
}
|
|
foreach ($value as $row) {
|
foreach ($config['fields'] as $field_name => $field_config) {
|
if (!isset($row[$field_name])) {
|
continue;
|
}
|
if (!$this->validate($row[$field_name], $field_config)) {
|
return false;
|
}
|
}
|
}
|
|
return true;
|
}
|
|
public function getErrors():array
|
{
|
return $this->errors;
|
}
|
|
public function hasErrors():bool
|
{
|
return !empty($this->errors);
|
}
|
|
protected function addError(string $field, string $message):void
|
{
|
if (!isset($this->errors[$field])) {
|
$this->errors[$field] = [];
|
}
|
$this->errors[$field][] = $message;
|
}
|
|
protected function validateText(string $value, array $config): bool
|
{
|
if (empty($value)) {
|
if (!empty($config['required'])) {
|
$this->addError($config['name'], __('This field is required', 'jvb'));
|
return false;
|
}
|
return true;
|
}
|
|
// Check character limit
|
if (isset($config['limit']) && strlen($value) > $config['limit']) {
|
$this->addError(
|
$config['name'],
|
sprintf(__('Must not exceed %d characters', 'jvb'), $config['limit'])
|
);
|
return false;
|
}
|
|
// Pattern validation if specified
|
if (isset($config['pattern']) && !preg_match($config['pattern'], $value)) {
|
$this->addError($config['name'], __('Invalid format', 'jvb'));
|
return false;
|
}
|
|
return true;
|
}
|
|
protected function validateTextarea(string $value, array $config): bool
|
{
|
return $this->validateText($value, $config); // Reuse text validation
|
}
|
|
protected function validateSelect(string $value, array $config): bool
|
{
|
if (empty($value)) {
|
if (!empty($config['required'])) {
|
$this->addError($config['name'], __('This field is required', 'jvb'));
|
return false;
|
}
|
return true;
|
}
|
|
if (!isset($config['options']) || !array_key_exists($value, $config['options'])) {
|
$this->addError($config['name'], __('Invalid selection', 'jvb'));
|
return false;
|
}
|
|
return true;
|
}
|
|
protected function validateRadio(string $value, array $config): bool
|
{
|
return $this->validateSelect($value, $config); // Same logic
|
}
|
|
protected function validateSet(array|string $value, array $config): bool
|
{
|
if (empty($value)) {
|
if (!empty($config['required'])) {
|
$this->addError($config['name'], __('This field is required', 'jvb'));
|
return false;
|
}
|
return true;
|
}
|
|
if (!is_array($value)) {
|
$value = explode(',', $value);
|
}
|
|
if (!isset($config['options'])) {
|
return true;
|
}
|
|
$invalid_values = array_diff($value, array_keys($config['options']));
|
if (!empty($invalid_values)) {
|
$this->addError($config['name'], __('Invalid selections', 'jvb'));
|
return false;
|
}
|
|
return true;
|
}
|
|
protected function validateCheckbox(array|string $value, array $config): bool
|
{
|
return $this->validateSet($value, $config); // Same logic
|
}
|
|
protected function validateImage(int $value, array $config): bool
|
{
|
if (empty($value)) {
|
if (!empty($config['required'])) {
|
$this->addError($config['name'], __('This field is required', 'jvb'));
|
return false;
|
}
|
return true;
|
}
|
|
if (!wp_attachment_is_image($value)) {
|
$this->addError($config['name'], __('Invalid image', 'jvb'));
|
return false;
|
}
|
|
return true;
|
}
|
|
protected function validateTaxonomy(array|string $value, array $config): bool
|
{
|
if (empty($value)) {
|
if (!empty($config['required'])) {
|
$this->addError($config['name'], __('This field is required', 'jvb'));
|
return false;
|
}
|
return true;
|
}
|
|
if (!is_array($value)) {
|
$value = explode(',', $value);
|
}
|
|
$taxonomy = (str_starts_with($config['taxonomy'], BASE))
|
? $config['taxonomy']
|
: BASE . $config['taxonomy'];
|
|
foreach ($value as $term_id) {
|
if (!term_exists((int)$term_id, $taxonomy)) {
|
$this->addError($config['name'], __('Invalid term selected', 'jvb'));
|
return false;
|
}
|
}
|
|
return true;
|
}
|
|
protected function validateUser(array|string $value, array $config): bool
|
{
|
if (empty($value)) {
|
if (!empty($config['required'])) {
|
$this->addError($config['name'], __('This field is required', 'jvb'));
|
return false;
|
}
|
return true;
|
}
|
|
if (!is_array($value)) {
|
$value = explode(',', $value);
|
}
|
|
foreach ($value as $user_id) {
|
if (!get_userdata((int)$user_id)) {
|
$this->addError($config['name'], __('Invalid user selected', 'jvb'));
|
return false;
|
}
|
}
|
|
return true;
|
}
|
|
protected function validateLocation(array $value, array $config): bool
|
{
|
if (empty($value)) {
|
if (!empty($config['required'])) {
|
$this->addError($config['name'], __('This field is required', 'jvb'));
|
return false;
|
}
|
return true;
|
}
|
|
if (!is_array($value)) {
|
$this->addError($config['name'], __('Location must be an array', 'jvb'));
|
return false;
|
}
|
|
// Validate required location fields
|
$required_fields = ['lat', 'lng'];
|
foreach ($required_fields as $field) {
|
if (!isset($value[$field]) || !is_numeric($value[$field])) {
|
$this->addError($config['name'], __('Invalid location coordinates', 'jvb'));
|
return false;
|
}
|
}
|
|
return true;
|
}
|
|
protected function validateTrueFalse(mixed $value, array $config): bool
|
{
|
if (!empty($config['required']) && empty($value)) {
|
$this->addError($config['name'], __('This field is required', 'jvb'));
|
return false;
|
}
|
|
return true; // Boolean values are always valid after sanitization
|
}
|
}
|