From 3aada9949d51024a92a8b5c6cb70d12f9c3cac16 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 21 Dec 2025 19:59:48 +0000
Subject: [PATCH] =auth refactored via rest, referral system set up for Jane, some javascript consolidation

---
 inc/utility/Validator.php |  241 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 241 insertions(+), 0 deletions(-)

diff --git a/inc/utility/Validator.php b/inc/utility/Validator.php
index 6cd291f..d6505b2 100644
--- a/inc/utility/Validator.php
+++ b/inc/utility/Validator.php
@@ -12,6 +12,24 @@
 {
 	private array $errors = [];
 	private array $warnings = [];
+	protected array $validSchemaTypes = [
+		'content' => [
+			'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
 	{
@@ -20,6 +38,8 @@
 		$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;
 	}
 	/**
@@ -499,4 +519,225 @@
 			}
 		}
 	}
+
+	/**
+	 * 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;
+	}
 }

--
Gitblit v1.10.0