*/ public static function inputAttributes(array $field, string $field_id, string $name): array { $attributes = isset($field['attributes']) && is_array($field['attributes']) ? $field['attributes'] : []; if (isset($attributes['autocomplete'])) { return $attributes; } $autocomplete = self::autocompleteValue($field, $field_id, $name); if ($autocomplete !== '') $attributes['autocomplete'] = $autocomplete; return $attributes; } /** * @param mixed[] $fields * @return mixed[] */ public static function normalize(array $fields, bool $allow_system_conditions = false): array { $normalized = self::prepare($fields); foreach ($normalized as $field_id => $field) { if (!self::hasCondition($field)) { unset($normalized[$field_id]['condition']); continue; } if (!self::conditionIsValid((string) $field_id, $field, $normalized, $allow_system_conditions)) { unset($normalized[$field_id]['condition']); } } foreach (self::cyclicFieldIds($normalized) as $field_id) { unset($normalized[$field_id]['condition']); } return $normalized; } /** * @param mixed[] $fields * @return string[] */ public static function configurationErrors(array $fields, bool $allow_system_conditions = false, string $context = ''): array { $prepared = self::prepare($fields); $errors = []; $context = trim($context); foreach ($prepared as $field_id => $field) { if (!self::hasCondition($field)) continue; if (!self::conditionIsValid((string) $field_id, $field, $prepared, $allow_system_conditions)) { $errors[] = sprintf( // translators: Placeholder values. esc_html__('%s has an invalid conditional display rule.', 'daytidy'), self::fieldLabel($field, (string) $field_id, $context) ); } } if (count(self::cyclicFieldIds($prepared))) { $errors[] = $context // translators: Placeholder values. ? sprintf(esc_html__('%s contains a circular conditional display chain.', 'daytidy'), $context) : esc_html__('A form contains a circular conditional display chain.', 'daytidy'); } return $errors; } /** * @param mixed[] $fields * @param mixed[] $values * @return mixed[] */ public static function pruneValues(array $fields, array $values): array { $fields = self::normalize($fields); $pruned = []; foreach ($values as $field_id => $value) { if (!isset($fields[$field_id])) continue; if (!self::isVisible((string) $field_id, $fields, $values)) continue; $pruned[$field_id] = $value; } return $pruned; } /** * @param mixed[] $fields * @param mixed[] $values * @return mixed[] */ public static function sanitizeValues(array $fields, array $values): array { $fields = self::normalize($fields); $values = self::pruneValues($fields, $values); $sanitized = []; foreach ($fields as $field_id => $field) { if (!array_key_exists($field_id, $values)) continue; $value = $values[$field_id]; $type = (string) ($field['type'] ?? ''); if ($type === Checkboxes::TYPE && is_array($value)) { $sanitized[$field_id] = array_map('sanitize_text_field', $value); continue; } if (is_array($value)) { $sanitized[$field_id] = array_map('sanitize_text_field', $value); continue; } if ($type === Textarea::TYPE) { $sanitized[$field_id] = sanitize_textarea_field((string) $value); } else if ($type === Url::TYPE) { $sanitized[$field_id] = sanitize_url((string) $value); } else if ($type === Email::TYPE) { $sanitized[$field_id] = sanitize_email((string) $value); } else if ($type === Agreement::TYPE) { $sanitized[$field_id] = $value ? '1' : '0'; } else { $sanitized[$field_id] = sanitize_text_field((string) $value); } } return $sanitized; } /** * @param mixed[] $fields * @param mixed[] $values * @return mixed[] */ public static function validateValues(array $fields, array $values): array { $fields = self::normalize($fields); $values = self::pruneValues($fields, $values); $errors = []; foreach ($fields as $field_id => $field) { if (!is_numeric($field_id)) continue; if (!self::isVisible((string) $field_id, $fields, $values)) continue; $type = (string) ($field['type'] ?? ''); $label = (string) ($field['label'] ?? ''); $value = $values[$field_id] ?? ''; $required = isset($field['required']) && $field['required']; if ($type === Email::TYPE && !self::isEmpty($value, $type) && !is_email((string) $value)) { $errors[$field_id] = [ 'errors' => ['invalid'], 'message' => esc_html__('Invalid Email', 'daytidy'), ]; } if ($required && self::isEmpty($value, $type)) { $errors[$field_id] = [ 'errors' => ['required'], 'message' => $type === Agreement::TYPE ? esc_html__('Please accept the terms.', 'daytidy') // translators: Placeholder values. : sprintf(esc_html__('Please fill %s field.', 'daytidy'), strtolower($label)), ]; } } return $errors; } /** * @param mixed[] $field * @param mixed[] $fields * @param mixed[] $values */ public static function isFieldVisible(string $field_id, array $field, array $fields, array $values): bool { $fields = self::normalize($fields); if (!isset($fields[$field_id])) { $fields[$field_id] = $field; } return self::isVisible((string) $field_id, $fields, $values); } /** * @param mixed $value */ public static function isEmpty($value, string $type = ''): bool { if ($type === Agreement::TYPE) return (string) $value !== '1'; if (is_array($value)) return !count($value); return trim((string) $value) === ''; } /** * @param mixed[] $fields * @return mixed[] */ private static function prepare(array $fields): array { $prepared = []; foreach ($fields as $field_id => $field) { if ($field_id === ':d:' || !is_array($field)) continue; $field['type'] = isset($field['type']) && trim((string) $field['type']) ? sanitize_key((string) $field['type']) : 'text'; if (isset($field['options']) && is_array($field['options'])) { unset($field['options'][':o:']); $field['options'] = array_map('sanitize_text_field', $field['options']); } if (isset($field['condition']) && is_array($field['condition'])) { $field['condition'] = [ 'field' => sanitize_text_field((string) ($field['condition']['field'] ?? '')), 'value' => sanitize_text_field((string) ($field['condition']['value'] ?? '')), ]; } $prepared[$field_id] = $field; } return $prepared; } /** * @param mixed[] $field */ private static function hasCondition(array $field): bool { return Arr::isset_and_array($field, 'condition') && trim((string) ($field['condition']['field'] ?? '')) !== '' && trim((string) ($field['condition']['value'] ?? '')) !== ''; } /** * @param mixed[] $field * @param mixed[] $fields */ private static function conditionIsValid(string $field_id, array $field, array $fields, bool $allow_system_conditions): bool { if (!$allow_system_conditions && isset($field['system']) && trim((string) $field['system']) !== '') return false; if (!self::hasCondition($field)) return true; $controller_id = (string) $field['condition']['field']; $expected = (string) $field['condition']['value']; if ($controller_id === $field_id || !isset($fields[$controller_id])) return false; $controller = $fields[$controller_id]; $controller_type = (string) ($controller['type'] ?? ''); if (!in_array($controller_type, self::conditionTypes(), true)) return false; if ($controller_type === Agreement::TYPE) return $expected === '1'; $options = isset($controller['options']) && is_array($controller['options']) ? $controller['options'] : []; if (!count($options)) return false; return array_key_exists($expected, $options); } /** * @param mixed[] $fields * @return string[] */ private static function cyclicFieldIds(array $fields): array { $cyclic = []; foreach ($fields as $field_id => $field) { $seen = []; $current = (string) $field_id; while (isset($fields[$current]) && self::hasCondition($fields[$current])) { if (isset($seen[$current])) { $cyclic[] = $current; break; } $seen[$current] = true; $current = (string) $fields[$current]['condition']['field']; } } return array_values(array_unique($cyclic)); } /** * @param mixed[] $fields * @param mixed[] $values * @param string[] $stack */ private static function isVisible(string $field_id, array $fields, array $values, array $stack = []): bool { if (!isset($fields[$field_id])) return false; $field = $fields[$field_id]; if (!self::hasCondition($field)) return true; if (in_array($field_id, $stack, true)) return false; $condition = $field['condition']; $controller_id = (string) $condition['field']; if (!isset($fields[$controller_id])) return false; if (!self::isVisible($controller_id, $fields, $values, array_merge($stack, [$field_id]))) return false; $controller = $fields[$controller_id]; $controller_value = $values[$controller_id] ?? null; return self::matches($controller, $controller_value, (string) $condition['value']); } /** * @param mixed[] $controller * @param mixed $value */ private static function matches(array $controller, $value, string $expected): bool { $type = (string) ($controller['type'] ?? ''); if ($type === Agreement::TYPE) return $value == '1'; if ($type === Checkboxes::TYPE && is_array($value)) return in_array($expected, $value); return (string) $value === $expected; } /** * @param mixed[] $field */ private static function fieldLabel(array $field, string $field_id, string $context): string { $label = trim((string) ($field['label'] ?? '')); // translators: Placeholder values. if ($label === '') $label = sprintf(esc_html__('Field %s', 'daytidy'), $field_id); if ($context !== '') $label = $context . ': ' . $label; return $label; } /** * @param mixed[] $field */ private static function autocompleteValue(array $field, string $field_id, string $name): string { $token = self::autocompleteToken($field, $name); if ($token === '') return ''; if ($token === 'on') return $token; $section = self::autocompleteSection($name, $field_id); return trim($section . ' ' . $token); } /** * @param mixed[] $field */ private static function autocompleteToken(array $field, string $name): string { $type = (string) ($field['type'] ?? ''); $system = (string) ($field['system'] ?? ''); $label = self::normalizedLabel((string) ($field['label'] ?? '')); if ($system === 'first_name') return 'given-name'; if ($system === 'last_name') return 'family-name'; if ($system === 'email') return 'email'; if ($label === 'first name' || $label === 'given name') return 'given-name'; if ($label === 'last name' || $label === 'family name' || $label === 'surname') return 'family-name'; if ($label === 'full name' || $label === 'name') return 'name'; if ($label === 'email' || $label === 'e mail' || $type === Email::TYPE) return 'email'; if ($label === 'phone' || $label === 'phone number' || $label === 'mobile' || $label === 'telephone' || $type === Tel::TYPE) return 'tel'; if ($label === 'company' || $label === 'organization') return 'organization'; if ($label === 'address' || $label === 'street address') return 'street-address'; if ($label === 'address line 1') return 'address-line1'; if ($label === 'address line 2') return 'address-line2'; if ($label === 'city') return 'address-level2'; if ($label === 'state' || $label === 'province' || $label === 'region') return 'address-level1'; if ($label === 'postal code' || $label === 'postcode' || $label === 'zip' || $label === 'zip code') return 'postal-code'; if ($label === 'country') return 'country-name'; if ($label === 'username') return 'username'; if ($label === 'password' || $type === Password::TYPE) return strpos($name, '[registration]') !== false ? 'new-password' : 'current-password'; if ($type === Url::TYPE) return 'url'; if (in_array($type, ['text', 'textarea', 'number'], true)) return 'on'; return ''; } private static function autocompleteSection(string $name, string $field_id): string { if (strpos($name, 'daytidy[form]') === 0) return 'section-daytidy-booking'; if (strpos($name, 'daytidy[registration]') === 0) return 'section-daytidy-registration'; if (preg_match('/^daytidy\[participants\]\[([^\]]+)\]/', $name, $matches)) { return 'section-daytidy-participant-' . sanitize_key((string) $matches[1]); } return 'section-daytidy-field-' . sanitize_key($field_id); } private static function normalizedLabel(string $label): string { $label = strtolower(remove_accents($label)); $label = (string) preg_replace('/[^a-z0-9]+/', ' ', $label); return trim($label); } }