<?php
|
namespace JVBase\meta;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
|
/**
|
* Handles meta value sanitization
|
*/
|
class Sanitizer
|
{
|
|
public static function sanitize(mixed $value, array $field_config): mixed
|
{
|
$callback = static::getCallback($field_config);
|
|
if (is_array($callback)) {
|
return call_user_func([static::class, $callback[1]], $value, $field_config);
|
}
|
if (method_exists(static::class, $callback)) {
|
return static::$callback($value, $field_config);
|
}
|
|
return call_user_func($callback, $value);
|
}
|
|
public static function getCallback(array $field_config):mixed
|
{
|
return $field_config['sanitize'] ??
|
MetaTypeManager::getSanitizeCallback($field_config['type']);
|
}
|
|
protected static function sanitizeTaxonomy(string $values, array $field_config):string
|
{
|
$values = array_map('absint', explode(',', $values));
|
|
// Ensure taxonomy starts with BASE
|
$taxonomy = (str_starts_with($field_config['taxonomy'], BASE))
|
? $field_config['taxonomy']
|
: BASE . $field_config['taxonomy'];
|
|
$values = array_filter($values, fn($value) => term_exists((int)$value, $taxonomy));
|
|
return implode(',', $values);
|
}
|
|
protected static function sanitizeUser(string $values, array $field_config):string
|
{
|
$values = array_map('absint', explode(',', $values));
|
|
$values = array_filter($values, fn($value) => (bool)get_userdata((int)$value));
|
|
return implode(',', $values);
|
}
|
|
protected static function sanitizePost(string $values, array $config):string
|
{
|
$values = array_map('absint', explode(',', $values));
|
return implode(',', array_filter($values, fn($value) => (bool)get_post((int)$value)));
|
}
|
|
protected static function sanitizeTagList(array $values, array $field_config): array
|
{
|
if (empty(array_filter($values, fn($value) => !empty($value)))) {
|
return [];
|
}
|
|
if (!isset($field_config['fields']) || !is_array($field_config['fields'])) {
|
return [];
|
}
|
|
$sanitized = [];
|
|
foreach ($values as $row) {
|
if (!is_array($row)) {
|
continue;
|
}
|
|
// Clean up field names (remove prefixes like "fieldname:0:email")
|
$temp = [];
|
foreach ($row as $key => $value) {
|
$key_parts = explode(':', $key);
|
$clean_key = $key_parts[array_key_last($key_parts)];
|
$temp[$clean_key] = $value;
|
}
|
$row = $temp;
|
|
// Sanitize each field
|
$clean_row = [];
|
foreach ($field_config['fields'] as $key => $subfield_config) {
|
if (!array_key_exists($key, $row)) {
|
continue;
|
}
|
|
$subfield_config['name'] = $key; // For backwards compatibility
|
$clean_row[$key] = static::sanitize($row[$key], $subfield_config);
|
}
|
|
// Only add row if it has at least one non-empty value
|
if (!empty(array_filter($clean_row))) {
|
$sanitized[] = $clean_row;
|
}
|
}
|
|
return $sanitized;
|
}
|
|
protected static function sanitizeRepeater(array $values, array $field_config):array
|
{
|
if (empty(array_filter($values, fn($value) => !empty($value)))) {
|
return [];
|
}
|
|
|
$sanitized = [];
|
foreach ($values as $row) {
|
if (!is_array($row)) {
|
continue;
|
}
|
$temp = [];
|
foreach ($row as $key => $value) {
|
$key = explode(':', $key);
|
$temp[$key[array_key_last($key)]] = $value;
|
}
|
$row = $temp;
|
|
$clean_row = [];
|
foreach ($field_config['fields'] as $key => $subfield_config) {
|
if (!array_key_exists($key, $row)) {
|
continue;
|
}
|
$subfield_config['name'] = $key;//For backwards compatability
|
$clean_row[$key] = static::sanitize($row[$key], $subfield_config);
|
}
|
$sanitized[] = $clean_row;
|
}
|
|
return $sanitized;
|
}
|
|
protected static function sanitizeGroup(array|string $values, array $field_config):array
|
{
|
if (!is_array($values)) {
|
return [];
|
}
|
|
if (!isset($field_config['fields']) || !is_array($field_config['fields'])) {
|
return [];
|
}
|
|
// Remove group: prefix from keys if present
|
$clean_values = [];
|
foreach ($values as $key => $value) {
|
$key_parts = explode(':', $key);
|
$clean_key = $key_parts[array_key_last($key_parts)];
|
$clean_values[$clean_key] = $value;
|
}
|
|
$sanitized = [];
|
foreach ($field_config['fields'] as $key => $subfield_config) {
|
if (!array_key_exists($key, $clean_values)) {
|
// Use default value if not provided
|
$default = MetaTypeManager::getType($subfield_config['type'])['default'] ?? '';
|
$sanitized[$key] = $default;
|
continue;
|
}
|
|
$subfield_config['name'] = $key; // For backwards compatibility
|
$sanitized[$key] = static::sanitize($clean_values[$key], $subfield_config);
|
}
|
|
return $sanitized;
|
}
|
|
protected static function sanitizeSelector(string $value, array $config):string
|
{
|
if (array_key_exists('type', $config)) {
|
return match ($config['type']) {
|
'user' => self::sanitizeUser($value, $config),
|
'taxonomy'=> self::sanitizeTaxonomy($value, $config),
|
'post' => self::sanitizePost($value, $config),
|
};
|
}
|
return implode(',',array_map('absint', explode(',',$value)));
|
}
|
|
protected static function sanitizeUpload(array|string $value):string
|
{
|
if (empty($value)) {
|
return '';
|
}
|
|
// Split value into array if it's a string
|
$ids = is_array($value) ? $value : explode(',', $value);
|
|
// Filter and validate each ID
|
$valid_ids = array_filter($ids, function ($id) {
|
$id = absint($id);
|
// Verify this is a valid attachment
|
return $id > 0 && wp_attachment_is_image($id);
|
});
|
return implode(',', $valid_ids);
|
}
|
|
protected static function sanitizeLocation(array $value, array $field_config):array
|
{
|
error_log('Location field to sanitize: '.print_r($value, true));
|
return [
|
'address' => sanitize_text_field($value['address'] ?? ''),
|
'lat' => (float)($value['lat'] ?? 0),
|
'lng' => (float)($value['lng'] ?? 0),
|
// Address components
|
'street' => sanitize_text_field($value['street'] ?? ''),
|
'city' => sanitize_text_field($value['city'] ?? ''),
|
'province' => sanitize_text_field($value['province'] ?? ''),
|
'postal_code' => sanitize_text_field($value['postal_code'] ?? ''),
|
'country' => sanitize_text_field($value['country'] ?? '')
|
];
|
}
|
|
protected static function sanitizeOptions(array|string $value, array $field_config):string
|
{
|
error_log('Sanitizing options: '.print_r($value, true));
|
if (!isset($field_config['options'])) {
|
return '';
|
}
|
if (!is_array($value)) {
|
$value = array_map('trim', explode(',', $value));
|
}
|
return implode(',', array_intersect($value, array_keys($field_config['options'])));
|
}
|
|
protected static function sanitizeDate(string $value, array $field_config):string
|
{
|
$timestamp = strtotime($value);
|
return $timestamp ? date('Y-m-d', $timestamp) : '';
|
}
|
|
protected static function sanitizeDateTime(string $value, array $field_config): string
|
{
|
if (empty($value)) {
|
return '';
|
}
|
|
$timestamp = strtotime($value);
|
if (!$timestamp) {
|
return '';
|
}
|
|
// Return in MySQL datetime format
|
return date('Y-m-d H:i:s', $timestamp);
|
}
|
|
protected static function sanitizeTime(string $value, array $field_config):string
|
{
|
// Remove any whitespace
|
$value = trim($value);
|
|
// Convert to lowercase for consistency
|
$value = strtolower($value);
|
|
// Pattern to match various time formats
|
$patterns = [
|
// 10am, 2pm, etc.
|
'/^(\d{1,2})(am|pm)$/' => function ($matches) {
|
$hour = (int)$matches[1];
|
if ($matches[2] === 'pm' && $hour < 12) {
|
$hour += 12;
|
} elseif ($matches[2] === 'am' && $hour === 12) {
|
$hour = 0;
|
}
|
return sprintf('%02d:00', $hour);
|
},
|
|
// 10:30am, 2:15pm, etc.
|
'/^(\d{1,2}):(\d{2})(am|pm)$/' => function ($matches) {
|
$hour = (int)$matches[1];
|
$minute = (int)$matches[2];
|
if ($matches[3] === 'pm' && $hour < 12) {
|
$hour += 12;
|
} elseif ($matches[3] === 'am' && $hour === 12) {
|
$hour = 0;
|
}
|
return sprintf('%02d:%02d', $hour, $minute);
|
},
|
|
// 14:30, 09:45, etc. (24-hour format)
|
'/^(\d{1,2}):(\d{2})$/' => function ($matches) {
|
$hour = (int)$matches[1];
|
$minute = (int)$matches[2];
|
return sprintf('%02d:%02d', $hour, $minute);
|
},
|
|
// Just hours like "14", "9" (assuming 24-hour and whole hours)
|
'/^(\d{1,2})$/' => function ($matches) {
|
$hour = (int)$matches[1];
|
return sprintf('%02d:00', $hour);
|
}
|
];
|
|
// Try each pattern
|
foreach ($patterns as $pattern => $callback) {
|
if (preg_match($pattern, $value, $matches)) {
|
$time = $callback($matches);
|
|
// Validate the resulting time
|
$hour = (int)substr($time, 0, 2);
|
$minute = (int)substr($time, 3, 2);
|
|
if ($hour >= 0 && $hour <= 23 && $minute >= 0 && $minute <= 59) {
|
return $time; // Valid time in HH:MM format
|
}
|
}
|
}
|
|
// If no pattern matched or time was invalid
|
return '';
|
}
|
|
public static function sanitizeFloat(string $value, array $config):float
|
{
|
if (is_numeric($value)) {
|
return (float) $value;
|
}
|
return 0.0;
|
}
|
|
public static function sanitizePhone(string|int $value, array $config = []):string
|
{
|
$digits = preg_replace('/\D/', '', (string) $value);
|
|
$length = strlen($digits);
|
|
if ($length < 10 || $length > 13) { // 13 = 3-digit country code + 10
|
return '';
|
}
|
|
$countryCode = $length > 10 ? substr($digits, 0, $length - 10) : null;
|
$number = substr($digits, -10);
|
|
$formatted = preg_replace('/(\d{3})(\d{3})(\d{4})/', '$1-$2-$3', $number);
|
|
return $countryCode ? '+' . $countryCode . '-' . $formatted : $formatted;
|
}
|
}
|