[ 'Article', 'NewsArticle', 'BlogPosting', 'VisualArtwork', 'Product', 'Service', 'Event', 'Person', 'CreativeWork', 'MedicalProcedure', 'HowTo', 'Recipe', 'Review', ], 'taxonomy' => [ 'CollectionPage', 'DefinedTerm', 'ItemList', ], 'user' => [ 'Person', ], ]; protected array $validModifiers = [ 'first', 'last', 'join', 'truncate', 'strip', 'lower', 'upper', 'title', 'count', 'get', 'default', 'date', 'image_url', 'excerpt', 'plural' ]; public function validateAll():array { $success = []; $success['content'] = $this->validateContentConfig(JVB_CONTENT); $success['terms'] = $this->validateTaxonomyConfig(JVB_TAXONOMY); $success['user'] = $this->validateUserConfig(JVB_USER); $success['crossReference'] = $this->validateCrossReferences(JVB_CONTENT, JVB_TAXONOMY, JVB_USER); $success['seo'] = $this->validateSEOConfig(); $success['schema'] = $this->validateSchemaConfig(JVB_SCHEMA ?? []); return $success; } /** * Validate JVB_CONTENT configuration */ public function validateContentConfig(array $config): bool { $this->errors = []; $this->warnings = []; foreach ($config as $slug => $settings) { $this->validateSlug($slug, 'content'); $this->validateContentSettings($slug, $settings); } if (!empty($this->errors)) { error_log('Validation result: '.print_r($this->errors, true)); } if (!empty($this->warnings)) { error_log('Warnings: '.print_r($this->warnings, true)); } return empty($this->errors); } /** * Validate JVB_TAXONOMY configuration */ public function validateTaxonomyConfig(array $config): bool { $this->errors = []; $this->warnings = []; foreach ($config as $slug => $settings) { $this->validateSlug($slug, 'taxonomy'); $this->validateTaxonomySettings($slug, $settings); } if (!empty($this->errors)) { error_log('Validation result: '.print_r($this->errors, true)); } if (!empty($this->warnings)) { error_log('Warnings: '.print_r($this->warnings, true)); } return empty($this->errors); } /** * Validate JVB_USER configuration */ public function validateUserConfig(array $config): bool { $this->errors = []; $this->warnings = []; foreach ($config as $slug => $settings) { $this->validateSlug($slug, 'user_role'); $this->validateUserSettings($slug, $settings); } if (!empty($this->errors)) { error_log('Validation result: '.print_r($this->errors, true)); } if (!empty($this->warnings)) { error_log('Warnings: '.print_r($this->warnings, true)); } return empty($this->errors); } /** * Validate cross-references between configs */ public function validateCrossReferences(array $content, array $taxonomy, array $user): bool { $this->errors = []; $this->warnings = []; // Check taxonomy -> content references foreach ($taxonomy as $taxSlug => $taxConfig) { foreach ($taxConfig['for_content'] ?? [] as $contentType) { if (!isset($content[$contentType])) { $this->addError( "taxonomy.{$taxSlug}.for_content", "References non-existent content type '{$contentType}'" ); } } // Check is_owned_by references foreach ($taxConfig['is_owned_by'] ?? [] as $role) { if (!isset($user[$role])) { $this->addError( "taxonomy.{$taxSlug}.is_owned_by", "References non-existent user role '{$role}'" ); } } } // Check user -> content references foreach ($user as $userSlug => $userConfig) { // Check profile reference if (isset($userConfig['profile']) && !isset($content[$userConfig['profile']])) { $this->addError( "user.{$userSlug}.profile", "References non-existent content type '{$userConfig['profile']}'" ); } // Check can_create references $this->validateCreatableContent($userSlug, $userConfig['can_create'] ?? [], $content, $taxonomy); } // Check field section references foreach ($content as $contentSlug => $contentConfig) { $this->validateFieldSections($contentSlug, $contentConfig, 'content'); } if (!empty($this->errors)) { error_log('Validation result: '.print_r($this->errors, true)); } if (!empty($this->warnings)) { error_log('Warnings: '.print_r($this->warnings, true)); } return empty($this->errors); } /** * Validate slug format */ private function validateSlug(string $slug, string $type): void { // Check for valid characters if (!preg_match('/^[a-z0-9_-]+$/', $slug)) { $this->addError( "{$type}.{$slug}", "Slug must contain only lowercase letters, numbers, hyphens, and underscores" ); } // Check length if (strlen($slug) > 20) { $this->addWarning( "{$type}.{$slug}", "Slug is longer than 20 characters, which may cause issues" ); } // Check for reserved WordPress terms $reserved = ['post', 'page', 'attachment', 'revision', 'nav_menu_item', 'author', 'category', 'tag']; if (in_array($slug, $reserved)) { $this->addError( "{$type}.{$slug}", "Slug '{$slug}' is a reserved WordPress term" ); } } /** * Validate content type settings */ private function validateContentSettings(string $slug, array $settings): void { // Required fields if (empty($settings['singular'])) { $this->addError("content.{$slug}", "Missing required 'singular' label"); } if (empty($settings['plural'])) { $this->addError("content.{$slug}", "Missing required 'plural' label"); } // Validate boolean flags $booleanFields = [ 'hide_single', 'show_feed', 'show_directory', 'karma', 'favouritable', 'responses', 'is_calendar', 'single_image' ]; foreach ($booleanFields as $field) { if (isset($settings[$field]) && !is_bool($settings[$field])) { $this->addError( "content.{$slug}.{$field}", "Field '{$field}' must be a boolean value" ); } } // Validate fields configuration if (isset($settings['fields'])) { $this->validateFieldsConfig($slug, $settings['fields'], 'content'); } // Validate sections if fields exist if (!empty($settings['fields']) && !empty($settings['sections'])) { $this->validateSectionsConfig($slug, $settings['sections']); } // Check for conflicting settings if (($settings['hide_single'] ?? false) && ($settings['show_directory'] ?? false)) { $this->addWarning( "content.{$slug}", "Content type has both 'hide_single' and 'show_directory' enabled" ); } } /** * Validate taxonomy settings */ private function validateTaxonomySettings(string $slug, array $settings): void { // Required fields if (empty($settings['singular'])) { $this->addError("taxonomy.{$slug}", "Missing required 'singular' label"); } if (empty($settings['plural'])) { $this->addError("taxonomy.{$slug}", "Missing required 'plural' label"); } // Validate for_content if (empty($settings['for_content']) || !is_array($settings['for_content'])) { $this->addError( "taxonomy.{$slug}", "Missing or invalid 'for_content' array" ); } // Validate content taxonomy specific settings if ($settings['is_content'] ?? false) { if (empty($settings['content_table'])) { $this->addWarning( "taxonomy.{$slug}", "Content taxonomy missing 'content_table' configuration" ); } } // Validate ownership settings if ($settings['is_ownable'] ?? false) { if (empty($settings['is_owned_by'])) { $this->addError( "taxonomy.{$slug}", "Ownable taxonomy missing 'is_owned_by' configuration" ); } } } /** * Validate user role settings */ private function validateUserSettings(string $slug, array $settings): void { // Validate dashboard access if ($settings['has_dashboard'] ?? false) { if (empty($settings['can_create']) && empty($settings['profile'])) { $this->addWarning( "user.{$slug}", "User has dashboard access but no content creation or profile" ); } } // Validate registration fields if ($settings['can_register'] ?? false) { if (empty($settings['register_fields'])) { $this->addWarning( "user.{$slug}", "User can register but has no registration fields defined" ); } } // Validate profile consistency if (!empty($settings['profile']) && empty($settings['has_dashboard'])) { $this->addWarning( "user.{$slug}", "User has profile type but no dashboard access" ); } } /** * Validate fields configuration */ private function validateFieldsConfig(string $slug, array $fields, string $type): void { foreach ($fields as $fieldName => $fieldConfig) { // Check for required field properties if (empty($fieldConfig['type'])) { $this->addError( "{$type}.{$slug}.fields.{$fieldName}", "Field missing required 'type' property" ); } // Validate field type $validTypes = [ 'text', 'textarea', 'number', 'email', 'url', 'select', 'radio', 'checkbox', 'true_false', 'date', 'time', 'datetime', 'color', 'upload', 'image', 'file', 'gallery', 'repeater', 'location', 'user', 'taxonomy', 'set' ]; if (isset($fieldConfig['type']) && !in_array($fieldConfig['type'], $validTypes)) { $this->addError( "{$type}.{$slug}.fields.{$fieldName}", "Invalid field type '{$fieldConfig['type']}'" ); } // Validate field-specific configurations $this->validateFieldTypeConfig($fieldName, $fieldConfig, "{$type}.{$slug}"); } } /** * Validate field type specific configuration */ private function validateFieldTypeConfig(string $fieldName, array $config, string $path): void { switch ($config['type'] ?? '') { case 'select': case 'radio': case 'checkbox': if (empty($config['options'])) { $this->addError( "{$path}.fields.{$fieldName}", "Field type '{$config['type']}' requires 'options' array" ); } break; case 'taxonomy': if (empty($config['taxonomy'])) { $this->addError( "{$path}.fields.{$fieldName}", "Taxonomy field requires 'taxonomy' property" ); } break; case 'repeater': if (empty($config['fields'])) { $this->addError( "{$path}.fields.{$fieldName}", "Repeater field requires 'fields' definition array" ); } break; case 'number': if (isset($config['min']) && isset($config['max']) && $config['min'] > $config['max']) { $this->addError( "{$path}.fields.{$fieldName}", "Number field 'min' value cannot be greater than 'max'" ); } break; } } /** * Validate sections configuration */ private function validateSectionsConfig(string $slug, array $sections): void { foreach ($sections as $sectionSlug => $sectionConfig) { if (empty($sectionConfig['label'])) { $this->addError( "content.{$slug}.sections.{$sectionSlug}", "Section missing required 'label'" ); } } } /** * Validate field sections match defined sections */ private function validateFieldSections(string $slug, array $config, string $type): void { if (empty($config['fields']) || empty($config['sections'])) { return; } $definedSections = array_keys($config['sections']); foreach ($config['fields'] as $fieldName => $fieldConfig) { if (isset($fieldConfig['section']) && !in_array($fieldConfig['section'], $definedSections)) { $this->addError( "{$type}.{$slug}.fields.{$fieldName}", "Field references non-existent section '{$fieldConfig['section']}'" ); } } } /** * Validate creatable content references */ private function validateCreatableContent(string $userSlug, array $canCreate, array $content, array $taxonomy): void { foreach ($canCreate as $item) { if (is_array($item)) { foreach ($item as $subType => $types) { foreach ($types as $type) { if (!isset($content[$type]) && !isset($taxonomy[$type])) { $this->addError( "user.{$userSlug}.can_create", "References non-existent type '{$type}'" ); } } } } else { if (!isset($content[$item]) && !isset($taxonomy[$item])) { $this->addError( "user.{$userSlug}.can_create", "References non-existent type '{$item}'" ); } } } } /** * Add error message */ private function addError(string $path, string $message): void { $this->errors[] = "[{$path}] {$message}"; } /** * Add warning message */ private function addWarning(string $path, string $message): void { $this->warnings[] = "[{$path}] {$message}"; } /** * Get validation errors */ public function getErrors(): array { return $this->errors; } /** * Get validation warnings */ public function getWarnings(): array { return $this->warnings; } /** * Output validation results to error log */ public function logResults(): void { if (!empty($this->errors)) { error_log('[ConfigValidator] Validation Errors:'); foreach ($this->errors as $error) { error_log(" - {$error}"); } } if (!empty($this->warnings) && WP_DEBUG) { error_log('[ConfigValidator] Validation Warnings:'); foreach ($this->warnings as $warning) { error_log(" - {$warning}"); } } } /** * Validate SEO configurations across all types */ public function validateSEOConfig(): bool { $this->errors = []; $this->warnings = []; foreach (JVB_CONTENT ?? [] as $slug => $config) { if (isset($config['seo'])) { $this->validateTypeSEOConfig($slug, $config['seo'], 'content', $config); } } foreach (JVB_TAXONOMY ?? [] as $slug => $config) { if (isset($config['seo'])) { $this->validateTypeSEOConfig($slug, $config['seo'], 'taxonomy', $config); } } foreach (JVB_USER ?? [] as $slug => $config) { if (isset($config['seo'])) { $this->validateTypeSEOConfig($slug, $config['seo'], 'user', $config); } } $this->logResults(); return empty($this->errors); } /** * Validate SEO config for a specific type */ private function validateTypeSEOConfig(string $slug, array $seo, string $objectType, array $fullConfig): void { $path = "{$objectType}.{$slug}.seo"; $availableFields = $this->getAvailableSEOFields($slug, $objectType, $fullConfig); if (isset($seo['schema_type'])) { $validTypes = $this->validSchemaTypes[$objectType] ?? $this->validSchemaTypes['content']; if (!in_array($seo['schema_type'], $validTypes)) { $this->addWarning("{$path}.schema_type", "'{$seo['schema_type']}' may not be valid. Common types: " . implode(', ', array_slice($validTypes, 0, 5))); } } if (isset($seo['field_map'])) { foreach ($seo['field_map'] as $prop => $source) { $this->validateFieldSource($source, $availableFields, "{$path}.field_map.{$prop}"); } } if (isset($seo['meta']['title'])) { $this->validatePatternString($seo['meta']['title'], $availableFields, "{$path}.meta.title"); } if (isset($seo['meta']['description'])) { $this->validatePatternString($seo['meta']['description'], $availableFields, "{$path}.meta.description"); } } /** * Validate a field source reference */ private function validateFieldSource(string $source, array $availableFields, string $path): void { if (empty($source)) { return; } if (str_contains($source, '{{')) { $this->validatePatternString($source, $availableFields, $path); return; } $field = explode('|', $source)[0]; $field = explode('.', $field)[0]; if (!in_array($field, $availableFields) && !in_array($field, ['site', 'author', 'meta', 'terms'])) { $this->addWarning($path, "Field '{$field}' may not exist"); } } /** * Validate pattern string syntax */ private function validatePatternString(string $pattern, array $availableFields, string $path): void { preg_match_all('/\{\{([^}]+)\}\}/', $pattern, $matches); foreach ($matches[1] as $token) { $token = trim($token); if (empty($token)) { $this->addError($path, "Empty placeholder {{}} found"); continue; } $parts = explode('|', $token); $field = trim(explode('.', $parts[0])[0]); if (!in_array($field, $availableFields) && !in_array($field, ['site', 'author', 'meta', 'terms'])) { $this->addWarning($path, "Field '{$field}' in pattern may not exist"); } if (isset($parts[1])) { $modifier = trim(explode(':', $parts[1])[0]); if (!in_array($modifier, $this->validModifiers)) { $this->addWarning($path, "Unknown modifier '|{$modifier}'"); } } } } /** * Validate JVB_SCHEMA configuration */ public function validateSchemaConfig(array $schema): bool { $this->errors = []; $this->warnings = []; if (isset($schema['business'])) { $this->validateBusinessSchema($schema['business']); } if (isset($schema['faqs']['items'])) { foreach ($schema['faqs']['items'] as $i => $faq) { if (empty($faq['question'])) { $this->addError("schema.faqs.items[{$i}].question", "FAQ question required"); } if (empty($faq['answer'])) { $this->addError("schema.faqs.items[{$i}].answer", "FAQ answer required"); } } } $this->logResults(); return empty($this->errors); } /** * Validate business schema */ private function validateBusinessSchema(array $config): void { $path = 'schema.business'; if (empty($config['name'])) { $this->addError("{$path}.name", "Business name required"); } if (isset($config['url']) && !filter_var($config['url'], FILTER_VALIDATE_URL)) { $this->addError("{$path}.url", "Invalid URL"); } if (isset($config['email']) && !filter_var($config['email'], FILTER_VALIDATE_EMAIL)) { $this->addError("{$path}.email", "Invalid email"); } if (isset($config['geo'])) { $lat = $config['geo']['lat'] ?? null; $lng = $config['geo']['lng'] ?? null; if ($lat !== null && (!is_numeric($lat) || $lat < -90 || $lat > 90)) { $this->addError("{$path}.geo.lat", "Latitude must be -90 to 90"); } if ($lng !== null && (!is_numeric($lng) || $lng < -180 || $lng > 180)) { $this->addError("{$path}.geo.lng", "Longitude must be -180 to 180"); } } if (isset($config['opening_hours'])) { $days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; foreach ($config['opening_hours'] as $day => $data) { if (!in_array(strtolower($day), $days)) { $this->addWarning("{$path}.opening_hours.{$day}", "Invalid day"); } if (is_array($data) && empty($data['closed'])) { if (isset($data['open']) && !preg_match('/^\d{2}:\d{2}$/', $data['open'])) { $this->addWarning("{$path}.opening_hours.{$day}.open", "Use HH:MM format"); } } } } if (isset($config['aggregate_rating'])) { $value = $config['aggregate_rating']['value'] ?? null; if ($value !== null && (!is_numeric($value) || $value < 0 || $value > 5)) { $this->addError("{$path}.aggregate_rating.value", "Rating must be 0-5"); } } if (isset($config['same_as'])) { foreach ($config['same_as'] as $i => $link) { $url = is_array($link) ? ($link['url'] ?? '') : $link; if (!empty($url) && !filter_var($url, FILTER_VALIDATE_URL)) { $this->addError("{$path}.same_as[{$i}]", "Invalid URL: {$url}"); } } } } /** * Get available fields for SEO validation */ private function getAvailableSEOFields(string $slug, string $objectType, array $config): array { $fields = match($objectType) { 'content' => ['post_title', 'post_excerpt', 'post_content', 'post_date', 'post_modified', 'post_thumbnail', 'permalink'], 'taxonomy' => ['term_name', 'term_description', 'term_slug', 'permalink', 'count'], 'user' => ['display_name', 'first_name', 'last_name', 'user_email', 'description', 'permalink'], default => [] }; if (!empty($config['fields'])) { $fields = array_merge($fields, array_keys($config['fields'])); } return $fields; } }