From 46d681c6b825d21b3f698d793c4e630c687d90ad Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 21 May 2026 21:41:53 +0000
Subject: [PATCH] =Major CustomBlocks.php overhaul, expanding block support and customization from the editor. theme.json should now be updated on new themes to set brand colours, etc. Also note: major change to .col vs .row alignment: simplifying it to .top .bottom vs the confusion of the differences for .col/.row .start and .a-start

---
 inc/integrations/Integrations.php |  268 ++++++++++++++++++++++++++++-------------------------
 1 files changed, 142 insertions(+), 126 deletions(-)

diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index e3ef859..5a14350 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -2,10 +2,12 @@
 namespace JVBase\integrations;
 
 use Exception;
-use JVBase\managers\CacheManager;
-use JVBase\managers\UploadManager;
-use JVBase\meta\MetaManager;
+use JVBase\managers\Cache;
+use JVBase\meta\Form;
+use JVBase\meta\Meta;
 use JVBase\managers\ErrorHandler;
+use JVBase\registrar\helpers\AddIntegrationFields;
+use JVBase\registrar\Registrar;
 use WP_Error;
 use WP_Post;
 use WP_REST_Request;
@@ -62,7 +64,7 @@
 	 */
 	protected array $credentials = [];      // Service credentials (API keys, tokens, etc.)
 	protected ?int $userID = null;          // User context for user-specific integrations
-
+	private bool $token_refresh_attempted = false;  // Circuit breaker for token refresh
 
 	protected array $fields = []; 		// The fields to generate that become credentials
 	protected array $advanced = []; 	// The fields that are optional settings
@@ -91,12 +93,13 @@
 		'test_connection' => 'Test Connection',
 	];
 
+	protected array $allowedContent = [];
 
 	/**
 	 * Caching Configuration
 	 */
 	protected ?string $cacheName = null;
-	protected CacheManager $cache;
+	protected Cache $cache;
 	protected array $cacheStrategy = [
 		'aggressive' => 3600,  // 1 hour for stable data (e.g., profile info)
 		'moderate' => 300,     // 5 minutes for semi-dynamic data (e.g., posts)
@@ -131,8 +134,8 @@
 		'update' => false,     // Can update already-shared posts
 		'delete' => false,     // Can remove posts from the service
 	];
-	protected array $syncPostTypes = []; // Post types that can be synced (e.g., ['artwork', 'tattoo']): usually built by querying the global JVB_CONTENT if the integration name exists as a key in [ 'integrations' => []]
-	protected array $syncTaxonomies = []; // Post types that can be synced (e.g., ['artwork', 'tattoo']): usually built by querying the global JVB_CONTENT if the integration name exists as a key in [ 'integrations' => []]
+	protected array $syncPostTypes = []; // Post types that can be synced (e.g., ['artwork', 'tattoo']): usually built by Registrar.php if the integration name exists as a key in [ 'integrations' => []]
+	protected array $syncTaxonomies = []; // Post types that can be synced (e.g., ['artwork', 'tattoo']): usually built by Registrar.php if the integration name exists as a key in [ 'integrations' => []]
 	protected array $contentTypes = [];   // Integration's available content types. Set by child classes' getContentTypes
 	protected bool $has_content = false; // Whether integration has content that can sync
 	/**
@@ -167,7 +170,7 @@
 	{
 		$this->cacheName = $this->cacheName ?: $this->service_name;
 		$this->userID = $userID;
-		$this->cache = CacheManager::for('integrations_' . $this->cacheName, $this->ttl);
+		$this->cache = Cache::for('integrations_' . $this->cacheName, $this->ttl);
 
 		// Load error stats from cache
 		$this->loadErrorStats();
@@ -258,10 +261,6 @@
 		if (!empty($this->credentials['expires_at'])) {
 			$expires_at = intval($this->credentials['expires_at']);
 			if ($expires_at <= time()) {
-				// Token expired, try to refresh
-				if (!empty($this->credentials['refresh_token'])) {
-					return $this->refreshOAuthToken();
-				}
 				return false;
 			}
 		}
@@ -299,12 +298,10 @@
 		$postTypes = get_option($key, false);
 		if (!$postTypes) {
 			$postTypes = [];
-
-			// Get from JVB_CONTENT
-
-			foreach (JVB_CONTENT as $type => $config) {
-				if (array_key_exists('integrations', $config) && array_key_exists($this->service_name, $config['integrations'])) {
-					$postTypes[] = $type;
+			foreach (Registrar::getRegistered('post') as $registrar) {
+				$registrar = Registrar::getInstance($registrar);
+				if ($registrar->hasIntegration($this->service_name)) {
+					$postTypes[] = $registrar->getSlug();
 				}
 			}
 
@@ -322,11 +319,10 @@
 		if (!$taxonomies) {
 			// Combine both content and taxonomy filtering
 			$taxonomies = [];
-			// Get from JVB_TAXONOMY (content taxonomies)
-			foreach (JVB_TAXONOMY as $type => $config) {
-				if (jvbCheck('is_content', $config) &&
-					isset($config['integrations'][$this->service_name])) {
-					$taxonomies[] = $type;
+			foreach (Registrar::getFeatured('is_content', 'term') as $type) {
+				$registrar = Registrar::getInstance($type);
+				if ($registrar->hasIntegration($this->service_name)) {
+					$taxonomies[] = $registrar->getSlug();
 				}
 			}
 
@@ -434,7 +430,6 @@
 		} else {
 			$result =  $this->$method();
 		}
-		error_log('Action result: '.print_r($result, true));
 		if (is_wp_error($result)) {
 			return [
 				'success' => false,
@@ -655,9 +650,7 @@
 		}
 
 		try {
-			error_log('Credentials to save: '.print_r($this->credentials, true));
 			if ($this->isOAuthService && !$this->hasOAuthCredentials()){
-				error_log('Just saving credentials, we don\'t have OAuth setup yet...');
 				//If this is an OAuth service, we might only be saving the app credentials first
 				$result = true;
 			} else {
@@ -676,7 +669,7 @@
 
 	protected function clearCache():array
 	{
-		$success = $this->cache->clear();
+		$success = $this->cache->flush();
 		return [
 			'success'	=> $success,
 		];
@@ -765,6 +758,9 @@
 		array  $options = []
 	): array|WP_Error
 	{
+		if (!$this->is_healthy) {
+			return new WP_Error('unhealthy', 'Connection marked unhealthy. Skipping fetch');
+		}
 		$this->ensureInitialized();
 		if (!$this->isSetUp()){
 			$this->logError('Connection not setup for '.$this->service_name, [
@@ -778,24 +774,21 @@
 			return new WP_Error('rate_limit', 'Rate limit exceeded. Please try again later.');
 		}
 
-		// Debug: Check if credentials are loaded
-		error_log('['.$this->service_name.'] Make Request - Credentials loaded: ' . (!empty($this->credentials) ? 'Yes' : 'No'));
-		error_log('With Credentials: '.print_r($this->credentials, true));
 
 		$attempt = 0;
 		$lastError = null;
 
 		while ($attempt < $this->maxRetries) {
 			try {
-				$this->logDebug('[Integrations] Making request to '.$this->service_name);
+//				$this->logDebug('[Integrations] Making request to '.$this->service_name);
 				$result = $this->executeRequest($method, $endpoint, $data, $baseKey, $options);
 				if (!$result) {
 					return new WP_Error('Request Error');
 				}
 				$this->recordRequest($method, $endpoint);
-				$this->logDebug('[Integrations]', [
-					'response' => $result
-				]);
+//				$this->logDebug('[Integrations]', [
+//					'response' => $result
+//				]);
 				// Reset error stats on success
 				$this->resetErrorStats();
 				return $result;
@@ -844,7 +837,7 @@
 			return null;
 		}
 
-		$this->logDebug("$method request to: $url: ".print_r($args, true));
+//		$this->logDebug("$method request to: $url: ".print_r($args, true));
 
 		// Make the request
 		$response = match($method) {
@@ -875,9 +868,9 @@
 			if ($retry_count === 0) {
 				$retry_count++;
 
-				$this->logDebug('Got 401, attempting token refresh...');
+//				$this->logDebug('Got 401, attempting token refresh...');
 				if ($this->refreshOAuthToken()) {
-					$this->logDebug('Token refreshed successfully, retrying request...');
+//					$this->logDebug('Token refreshed successfully, retrying request...');
 
 					// Rebuild request args with new token
 					$args = $this->buildRequestArgs($method, $data, $options);
@@ -924,13 +917,17 @@
 		bool   $force = false
 	): ?array
 	{
-		$cacheKey = $this->buildCacheKey('GET', $endpoint, $params);
-		$ttl = $this->cacheStrategy[$cacheStrategy] ?? $this->ttl;
+		$cacheKey = $this->buildCacheKey('GET', $endpoint, $params, $baseKey);
+
+		$ttl = is_int($cacheStrategy)
+			? max(0, $cacheStrategy)
+			: ($this->cacheStrategy[$cacheStrategy] ?? $this->ttl);
 
 		if (!$force && $ttl > 0) {
 			$cached = $this->cache->get($cacheKey);
 			if ($cached !== false) {
-				$this->logDebug("Cache hit for: $cacheKey");
+
+//				$this->logDebug("Cache hit for: $cacheKey");
 				return $cached;
 			}
 		}
@@ -944,7 +941,6 @@
 
 		return $result;
 	}
-
 	/**
 	 * Check if response contains an error
 	 * Override in child classes for service-specific error detection
@@ -1217,14 +1213,10 @@
 
 	public function handleAjaxResponse()
 	{
-		error_log('Ajax Response: '.print_r($_GET, true));
 
 		$code = $_GET['code'];
 		$state =  $_GET['state'];
 
-		error_log('OAuth Callback - Code: ' . $code);
-		error_log('OAuth Callback - State: ' . $state);
-
 
 		$state_parts = explode('|', $state);
 		$state_key = $state_parts[0] ?? '';
@@ -1232,16 +1224,13 @@
 		$user_id = ($user_id === 0) ? null : $user_id;
 		$return_url = isset($state_parts[2]) ? base64_decode($state_parts[2]) : admin_url('admin.php?page=jvb-integrations');
 
-		error_log('Service: '.print_r($this->service_name, true));
 		$state_data = get_transient('oauth_state_' . $state_key);
-		error_log('State Data: '.print_r($state_data, true));
 		if (!$state_data || $state_data['service'] !== $this->service_name) {
 			wp_die('Invalid state parameter', 'OAuth Error');
 		}
 
 		// Delete the transient to prevent reuse
 		delete_transient('oauth_state_' . $state_key);
-		error_log('Return URL: '.print_r($return_url, true));
 		// Handle error from OAuth provider
 		if (array_key_exists('error', $_GET)) {
 			$error_description = $_GET['error_description'] ?? 'Authorization denied';
@@ -1393,17 +1382,29 @@
 			if ($this->isOAuthService && $this->hasOAuthCredentials()) {
 				// Check if token is expired first
 				if (!$this->isOAuthValid()) {
-					$this->logDebug('OAuth token expired, attempting refresh');
-					if (!$this->refreshOAuthToken()) {
-						$this->logError('Failed to refresh expired OAuth token');
+					// Only attempt refresh once per request
+					if (!$this->token_refresh_attempted) {
+						$this->token_refresh_attempted = true;
+//						$this->logDebug('OAuth token expired, attempting refresh');
+
+						if (!$this->refreshOAuthToken()) {
+							$this->logError('Failed to refresh expired OAuth token - stopping execution');
+							// Token refresh failed - DO NOT continue making API requests
+							return;
+						}
+					} else {
+						// Already attempted refresh in this request
+//						$this->logDebug('Token refresh already attempted, skipping');
+						return;
 					}
 				}
 				// Check if we should proactively refresh (before expiry)
-				elseif ($this->shouldRefreshToken()) {
-					$this->logDebug('OAuth token should be refreshed proactively');
+				elseif ($this->shouldRefreshToken() && !$this->token_refresh_attempted) {
+					$this->token_refresh_attempted = true;
+//					$this->logDebug('OAuth token should be refreshed proactively');
 					if (!$this->refreshOAuthToken()) {
 						$this->logError('Failed to proactively refresh OAuth token');
-						// Not critical - token is still valid
+						// Not critical - token is still valid, so continue
 					}
 				}
 			}
@@ -1427,6 +1428,7 @@
 		// Switch context
 		$this->userID = $user_id;
 		$this->credentials = [];
+		$this->resetTokenRefreshFlag();  // ADD THIS LINE
 
 		$this->ensureInitialized();
 	}
@@ -1624,12 +1626,7 @@
 			$params = $this->addOAuthParams($params);
 		}
 
-		$auth_url = $this->oauth['authorize'] . '?' . http_build_query($params);
-
-		// Debug log for troubleshooting
-		error_log("Generated OAuth URL for {$this->service_name}: " . $auth_url);
-
-		return $auth_url;
+		return $this->oauth['authorize'] . '?' . http_build_query($params);
 	}
 
 	/**
@@ -1921,7 +1918,6 @@
 			return false;
 		}
 
-		// Build refresh request data
 		$request_data = [
 			'client_id' => $this->credentials['client_id'],
 			'client_secret' => $this->credentials['client_secret'],
@@ -1929,12 +1925,24 @@
 			'grant_type' => 'refresh_token'
 		];
 
-		// Use centralized OAuth request method
 		$response = $this->makeOAuthRequest('POST', $this->oauth['token'], $request_data);
 
 		if (is_wp_error($response)) {
-			$this->logError('Failed to refresh Square token', [
-				'error' => $response->get_error_message()
+			$error_message = $response->get_error_message();
+
+			if (str_contains($error_message, 'invalid_grant')) {
+				$this->logError('OAuth refresh token is invalid - user must re-authorize', [
+					'error' => $error_message
+				], 'critical');
+
+				// Mark unhealthy immediately
+				$this->error_stats['consecutive_errors'] = $this->error_threshold;
+				$this->is_healthy = false;
+				$this->saveErrorStats();
+			}
+
+			$this->logError('Failed to refresh OAuth token for '.$this->service_name, [
+				'error' => $error_message
 			]);
 			return false;
 		}
@@ -1943,7 +1951,7 @@
 			$this->credentials['access_token'] = $response['access_token'];
 			$this->credentials['expires_at'] = time() + ($response['expires_in'] ?? 2592000); // 30 days
 
-			// Note: Square returns the SAME refresh token
+			// Note: Some services return the SAME refresh token
 			if (isset($response['refresh_token'])) {
 				$this->credentials['refresh_token'] = $response['refresh_token'];
 			}
@@ -2073,20 +2081,11 @@
 	 */
 	protected function mapFieldsToService(int $postID, array $mapping): array
 	{
-		$meta_manager = new MetaManager($postID, 'post');
-		$post = get_post($postID);
+		$meta_manager = Meta::forPost($postID);
 		$service_data = [];
 
 		foreach ($mapping as $wp_field => $service_field) {
-			$value = null;
-
-			// Check if it's a post field
-			if (property_exists($post, $wp_field)) {
-				$value = $post->$wp_field;
-			} else {
-				// It's a meta field
-				$value = $meta_manager->getValue($wp_field);
-			}
+			$value = $meta_manager->get($wp_field);
 
 			if ($value !== null && $value !== '') {
 				$this->setNestedValue($service_data, $service_field, $value);
@@ -2125,52 +2124,54 @@
 	 */
 	public function handleSavePost(int $postID, WP_Post $post, bool $update): void
 	{
-		error_log('Testing For Save Post');
-		if (jvbNoSaveIt($postID, $post)) {
-			error_log('Excluded by jvbNoSaveIt');
+		if (!$postID || $postID === 0) {
 			return;
 		}
+		$postType = jvbNoBase($post->post_type);
+
+		if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
+		if (wp_is_post_revision($postID)) return;
+
+		$registrar = Registrar::getInstance($postType);
+		if (!$registrar){
+			return;
+		}
+
 		if (empty($this->syncPostTypes)) {
-			error_log('No Syncable post types');
 			return;
 		}
 
-		$config = JVB_CONTENT[jvbNoBase($post->post_type)]??null;
-		if (!$config) {
-			error_log('No Config set');
-			return;
-		}
-
-		$settings = $config['integrations'][$this->service_name]??null;
+		$settings = $registrar->hasIntegration($this->service_name)??null;
 		if (!$settings) {
-			error_log('No settings');
 			return;
 		}
 
+		$settings = $registrar->getIntegrationConfig($this->service_name);
+		if (!$settings){
+			return;
+		}
+
+
 		$fields = $this->getSyncFields($postID, 'post', ['schedule_'.$this->service_name]);
-		error_log('Fields to check: '.print_r($fields, true));
 		if (!$fields['share_to_'.$this->service_name]) {
 			return;
 		}
 
 		$isShared = isset($fields["_{$this->service_name}_item_id"]);
 		if ($update && $isShared && !$fields['_keep_synced_'.$this->service_name]) {
-			error_log('Do not keep synced, not syncing with '.$this->service_name);
 			return;
 		}
 
 		if ($post->post_status !== 'publish' && !$isShared) {
-			error_log('Not published and not already shared');
 			return;
 		}
-		error_log('Sending to integration for processing...');
 		$this->handleTheSavePost($postID, $post, $update, $settings);
 	}
 
 
 	protected function getSyncFields(int $postID, string $type, array $additional = []):array
 	{
-		$meta = new MetaManager($postID, $type);
+		$meta = new Meta($postID, $type);
 		$fieldsToCheck = [
 			'share_to_' . $this->service_name,
 			'_keep_synced_' . $this->service_name,
@@ -2253,13 +2254,13 @@
 		if (!in_array($noBase, $this->syncTaxonomies)) {
 			return;
 		}
-		// Check if taxonomy is content-type
-		$config = JVB_TAXONOMY[$noBase] ?? null;
-		if (!$config || !($config['is_content'] ?? false)) {
+		$registrar = Registrar::getInstance($noBase);
+		if (!$registrar->hasFeature('is_content')) {
 			return;
 		}
 
-		$settings = $config['integrations'][$this->service_name] ?? null;
+
+		$settings = $registrar->getIntegrationConfig($this->service_name);
 		if (!$settings) {
 			return;
 		}
@@ -2636,11 +2637,6 @@
 		];
 		$this->is_healthy = true;
 		$this->saveErrorStats();
-
-		$this->logDebug('Integration health manually reset', [
-			'reset_by' => get_current_user_id(),
-			'reset_time' => time()
-		]);
 	}
 
 	/**
@@ -2775,9 +2771,6 @@
 
 		// Check for duplicate processing (idempotency)
 		if ($this->isWebhookProcessed($payload)) {
-			$this->logDebug('Webhook already processed', [
-				'webhook_id' => $this->extractWebhookId($payload)
-			]);
 			return true; // Return true to prevent retries
 		}
 
@@ -2939,7 +2932,7 @@
 			return '';
 		}
 
-		$meta = new MetaManager($this->userID, 'integrations');
+		$meta = Meta::forOptions($this->userID.'_integrations');
 		$is_connected = $this->isSetUp();
 		$credentials = $this->getCredentials();
 
@@ -2952,7 +2945,7 @@
 		?>
 		<form id="<?=$this->service_name?>" class="integration <?php echo $is_connected ? 'connected' : 'disconnected'; ?>"
 			 data-service="<?php echo esc_attr($this->service_name); ?>">
-			<div class="header row btw">
+			<div class="header row x-btw">
 				<h3><?php echo esc_html($this->title); ?></h3>
 				<div class="setup">
 					<?php if ($is_connected): ?>
@@ -3016,7 +3009,7 @@
 						$config['value'] = $credentials[$name]??'';
 						$config['autocomplete'] = 'off';
 						$config['base'] = $this->service_name.'_';
-						$meta->render('form', $name, $config);
+						echo Form::render($name, '', $config);
 					}
 				}
 				if ($this->handleWebhooks) {
@@ -3047,7 +3040,7 @@
 							$config['value'] = $credentials[$name]??'';
 							$config['base'] = $this->service_name.'_';
 							$config['autocomplete'] = 'off';
-							$meta->render('form', $name, $config);
+							Form::render($name,null, $config);
 						}
 						?>
 					</details>
@@ -3063,7 +3056,7 @@
 				}
 				?>
 			</div>
-			<div class="actions row btw wrap">
+			<div class="actions row x-btw wrap">
 				<?php
 				foreach ($this->buttons as $action => $label) {
 					if (!$is_connected && $action !== 'save_credentials') {
@@ -3073,7 +3066,7 @@
 					switch ($action) {
 						case 'save_credentials':
 							$title = $label;
-							$label = jvbIcon('save');
+							$label = jvbIcon('floppy-disk');
 							break;
 						case 'clear_credentials':
 							$title = $label;
@@ -3190,7 +3183,6 @@
 		// Verify nonce
 		$nonce_field = 'jvb_integration_nonce_' . $service;
 		$nonce_action = 'jvb_integration_save_' . $service;
-		error_log('handleAdminPost: '.print_r($_POST, true));
 		if (!isset($_POST[$nonce_field]) || !wp_verify_nonce($_POST[$nonce_field], $nonce_action)) {
 			wp_die('Security check failed');
 		}
@@ -3287,7 +3279,7 @@
 		if (empty($types)) {
 			return;
 		}
-		$meta = new MetaManager($this->userID, 'integrations');
+		$meta = Meta::forOptions($this->userID.'_integrations');
 		?>
 		<form>
 			<h1><?= $this->title?> Defaults:</h1>
@@ -3299,16 +3291,19 @@
 
 				$config['base'] = $this->service_name.'_';
 				$config['autocomplete'] = 'off';
-				$meta->render('form', $name, $config);
+				echo Form::render($name, null, $config);
 			}
 			foreach ($this->syncPostTypes as $type) {
-				$config = JVB_CONTENT[$type];
+				$registrar = Registrar::getInstance($type);
+
+				$icon = $registrar->getIcon();
+				$icon = $icon === '' ? jvbDefaultIcon() : $icon;
 				?>
 				<details>
-					<summary><?= jvbIcon($config['icon']) ?><?= $config['singular']?> Defaults</summary>
+					<summary><?= jvbIcon($icon) ?><?= $registrar->getSingular()?> Defaults</summary>
 					<?php
-					$fields = new \JVBase\registry\providers\IntegrationFieldProvider();
-					$fields = $fields->getIntegrationFields($this->service_name, $config);
+					$fields = new AddIntegrationFields($this->service_name);
+					$fields = $fields->getIntegrationFields();
 					foreach($fields as $name=>$c) {
 						$c['required'] = false;
 						if ($c['type'] === 'number') {
@@ -3319,7 +3314,7 @@
 							$c['hint'] = $c['description'];
 							unset($c['description']);
 						}
-						$meta->render('form', $name, $c);
+						echo Form::render($name, null, $c);
 					}
 					?>
 				</details>
@@ -3349,17 +3344,16 @@
 		$enabled = get_option($key);
 		if (!$enabled) {
 			$enabled = [];
-			foreach (array_merge(JVB_CONTENT, JVB_TAXONOMY) as $content => $config) {
-				if (!array_key_exists('integrations', $config)) {
+			foreach (Registrar::getRegistered() as $registrar) {
+				$registrar = Registrar::getInstance($registrar);
+				if (!$registrar->hasIntegration($this->service_name)) {
 					continue;
 				}
-				if (!array_key_exists($this->service_name, $config['integrations'])) {
+				$type = $registrar->getIntegration($this->service_name)->getContent_type();
+				if (!$type) {
 					continue;
 				}
-				if (!array_key_exists('content_type', $config['integrations'][$this->service_name])) {
-					continue;
-				}
-				$type = $config['integrations'][$this->service_name]['content_type'];
+
 				if (!in_array($type, $enabled)) {
 					$enabled[] = $type;
 				}
@@ -3527,4 +3521,26 @@
 			throw new Exception('Failed to save JPEG image');
 		}
 	}
+	/**
+	 * Reset token refresh attempt flag
+	 * Called automatically when switching users
+	 */
+	protected function resetTokenRefreshFlag(): void
+	{
+		$this->token_refresh_attempted = false;
+	}
+
+	public function getAllowedContent():array
+	{
+		return $this->allowedContent;
+	}
+
+	/**
+	 * Used by JVBase\registrar\helpers\AddIntegrationFields.php
+	 * @return array
+	 */
+	public function getAdditionalFields(?string $content_type = null):array
+	{
+		return [];
+	}
 }

--
Gitblit v1.10.0