From de317675a8069b747cb253ba3e2b5dc394ca36ef Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sat, 18 Jul 2026 21:44:51 +0000
Subject: [PATCH] =Base Integrations mainly done - doing a test with setting up the Square integration to follow
---
inc/integrations/Integrations.php | 3081 +---------------------------------------------------------
1 files changed, 92 insertions(+), 2,989 deletions(-)
diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index 08c375f..f9a34ef 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -4,12 +4,8 @@
use Exception;
use JVBase\managers\Cache;
use JVBase\managers\UploadManager;
-use JVBase\meta\MetaManager;
-use JVBase\managers\ErrorHandler;
+use JVBase\registrar\Registrar;
use WP_Error;
-use WP_Post;
-use WP_REST_Request;
-use WP_REST_Response;
if (!defined('ABSPATH')) {
exit;
@@ -26,419 +22,76 @@
*/
abstract class Integrations
{
+ use _Base;
+
/**
* API Configuration
* These properties define how the integration connects to external services
*/
- protected string $service_name; // Unique identifier for this service (e.g., 'square', 'facebook')
protected string|array $apiBase = ''; // Base URL(s) for API endpoints. Array format: ['base' => '', 'auth' => '']
protected array $apiEndpoints = []; // Valid endpoint paths for this service
- protected string $apiVersion = ''; // API version string (e.g., 'v2', '2024-01-01')
protected int $refresh_interval = 0; //seconds before expiry to refresh tokens. 0 to disable
- /**
- * OAuth Configuration
- * Set these properties if your service uses OAuth authentication
- */
- protected bool $isOAuthService = false;
- protected array $oauth = [
- 'authorize' => '', // OAuth authorization endpoint
- 'token' => '', // Token exchange endpoint
- 'revoke' => '', // Token revocation endpoint (optional)
- 'scopes' => [], // Required OAuth scopes
- 'redirect_uri' => '', // OAuth callback URL
- ];
-
- /**
- * Display Properties
- * Used for UI rendering in admin interfaces
- */
- public string $title; // Human-readable service name (e.g., 'Google My Business')
- public string $icon; // Phosphoricons icon slug
/**
* Credentials & State
*/
- 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
- protected array $defaults = []; // Default overrides
- protected string $defaultContent = 'post'; //Default integration content type, is none is set. MUST EXIST as array key in $this->>getContentTypes
- protected array $instructions = []; // Each item in array becomes an item in the list of instructions
-
- protected bool $supportsWebp = true;
- /**
- * Client Management
- * For services that benefit from persistent connections
- */
- protected ?object $client = null; // Persistent client instance
-
- protected array $actions = [
- 'save_credentials' => 'saveCredentials',
- 'clear_credentials' => 'deleteCredentials',
- 'clear_cache' => 'clearCache',
- 'test_connection' => 'testConnection',
- ];
-
- protected array $buttons = [
- 'save_credentials' => 'Save Changes',
- 'clear_credentials' => 'Clear Credentials',
- 'clear_cache'=> 'Clear Cache',
- 'test_connection' => 'Test Connection',
- ];
-
+ protected array $allowedContent = [];
/**
* Caching Configuration
*/
- protected ?string $cacheName = null;
- 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)
'minimal' => 60, // 1 minute for frequently changing data
'none' => 0 // No caching for real-time data
];
- protected int $ttl = 3600; // Default cache TTL in seconds
- /**
- * Rate Limiting Configuration
- * Override these values based on service-specific limits
- */
- protected array $rate_limits = [
- 'per_second' => 2,
- 'per_minute' => 30,
- 'per_hour' => 1000
- ];
- protected array $oauth_rate_limits = [
- 'per_second' => 1, // Max 1 OAuth request per second
- 'per_minute' => 10, // Max 10 OAuth requests per minute
- 'per_hour' => 100, // Max 100 OAuth requests per hour
- ];
- protected array $request_history = [];
- protected int $max_history_size = 100;
- /**
- * Post Syncing Capabilities
- * Define what sync operations this integration supports
- */
- protected array $canSync = [
- 'initial' => false, // Can share new posts to the service
- '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 $contentTypes = []; // Integration's available content types. Set by child classes' getContentTypes
- protected bool $has_content = false; // Whether integration has content that can sync
- /**
- * Error Handling Configuration
- */
- protected array $lastError = [];
- protected int $maxRetries = 3;
- protected array $retryDelays = [1, 2, 5]; // Exponential backoff in seconds
-
- /**
- * Validation Rules
- * Define validation rules for credentials and data
- */
- protected array $validationRules = [];
-
- /**
- * Error tracking properties
- */
- protected array $error_stats = [
- 'total_errors' => 0,
- 'consecutive_errors' => 0,
- 'last_success' => null,
- 'error_types' => []
- ];
-
- protected ?ErrorHandler $errorHandler = null;
- protected int $error_threshold = 5; // Consecutive errors before marking unhealthy
- protected bool $is_healthy = true;
- protected bool $handleWebhooks = false;
-
- public function __construct(?int $userID = null)
+ protected function __construct(?int $userID = null)
{
$this->cacheName = $this->cacheName ?: $this->service_name;
$this->userID = $userID;
- $this->cache = Cache::for('integrations_' . $this->cacheName, $this->ttl);
-
- // Load error stats from cache
- $this->loadErrorStats();
+ $this->cache = Cache::for('integrations_' . $this->cacheName);
$this->getPostTypes();
$this->getTaxonomies();
- $this->setContentTypes();
+ $this->getUsers();
+
+ if (method_exists($this, 'setContentTypes')) {
+ $this->setContentTypes();
+ }
$this->registerHooks();
- if ($this->isOAuthService) {
- $this->actions['get_oauth_url'] = 'getOAuthUrlAction';
- $this->actions['connect_oauth'] = 'handleOAuthConnect';
- $this->actions['disconnect_oauth'] = 'handleOAuthDisconnect';
- $this->actions['refresh_token'] = 'refreshOAuthToken';
- $this->buttons['refresh_token'] = 'Refresh OAuth Connection';
- $this->buttons['disconnect_oauth'] = 'Disconnect OAuth';
- }
- if ($this->handleWebhooks) {
- $this->advanced['webhook_signature_key'] = [
- 'label' => 'Webhook Signature Key',
- 'type' => 'text',
- 'subtype' => 'password',
- 'placeholder' => 'Enter webhook signature key',
- 'hint' => 'Used to verify webhook authenticity (recommended)'
- ];
+
+ if (method_exists($this, 'setQueueTypes')) {
+ $this->setQueueTypes();
}
- add_filter('jvbShouldRenderMeta', [$this, 'checkRenderField'], 10, 4);
- }
+ $this->initializeRateLimiters();
- protected function setContentTypes():void
- {
-
- }
- public function getContentTypes(string $type):array
- {
-
- return (array_key_exists($type, $this->contentTypes)) ? $this->contentTypes[$type] : $this->contentTypes;
- }
-
- public function checkRenderField($shouldRender, $name, $type, $objectType):bool
- {
- if ($type !== 'form') {
- return $shouldRender;
+ if (method_exists($this, 'initializeActions')) {
+ $this->initializeActions();
}
-
- if (!$this->isSetUp() && str_contains($name, $this->service_name)) {
- return false;
+ if (method_exists($this, 'addOAuthActions')) {
+ $this->addOAuthActions();
}
- return $shouldRender;
- }
-
- public function handleOAuthConnect(): array
- {
- if (!$this->isOAuthService) {
- return ['success' => false, 'message' => 'Not an OAuth service'];
- }
-
- // This would typically redirect to OAuth provider
- // Child classes can override for specific behavior
- $auth_url = $this->getOAuthUrl();
-
- if ($auth_url) {
- return [
- 'success' => true,
- 'redirect' => $auth_url,
- ];
- }
-
- return ['success' => false, 'message' => 'Failed to generate OAuth URL'];
- }
-
- /**
- * Check if OAuth token is valid and not expired
- * @return bool
- */
- public function isOAuthValid(): bool
- {
- if (!$this->isOAuthService) {
- return false;
- }
-
- // Check if we have tokens
- if (empty($this->credentials['access_token'])) {
- return false;
- }
-
- // Check token expiry if stored
- if (!empty($this->credentials['expires_at'])) {
- $expires_at = intval($this->credentials['expires_at']);
- if ($expires_at <= time()) {
- return false;
- }
- }
-
- // For services without expiry info, do a test API call
- return true;
-// return $this->testConnection();
- }
-
-
-
- public function getOAuthUrlAction(array $data = []): array
- {
- if (!$this->isOAuthService) {
- return ['success' => false, 'message' => 'Not an OAuth service'];
- }
-
- $return_url = $data['return_url'] ?? null;
- $auth_url = $this->getOAuthUrl($return_url);
-
- if ($auth_url) {
- return [
- 'success' => true,
- 'auth_url' => $auth_url,
- 'popup' => true
- ];
- }
-
- return ['success' => false, 'message' => 'Failed to generate OAuth URL'];
}
protected function getPostTypes(): void
{
- $key = BASE . $this->service_name . '_sync_post_types';
- $postTypes = get_option($key, false);
- if (!$postTypes) {
- $postTypes = [];
+ $this->syncPostTypes = Registrar::withIntegration($this->service_name, 'post');
+ }
- // 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;
- }
- }
-
- update_option($key, $postTypes);
- }
-
- $this->syncPostTypes = $postTypes;
+ protected function getUsers():void
+ {
+ $this->syncUsers = Registrar::withIntegration($this->service_name, 'user');
}
protected function getTaxonomies():void
{
- $key = BASE . $this->service_name . '_sync_taxonomies';
- $taxonomies = get_option($key, false);
-
- 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;
- }
- }
-
- update_option($key, $taxonomies);
- }
-
- $this->syncTaxonomies = $taxonomies;
- }
-
- protected function getRedirectUri(): string
- {
-// if (!empty($this->oauth['redirect_uri'])) {
-// return $this->oauth['redirect_uri'];
-// }
- if ($this->isOAuthService) {
- return admin_url('admin-ajax.php?action=' . BASE . $this->service_name . '_oauth_callback');
- }
- // Changed from admin-ajax.php to REST endpoint
- return rest_url('jvb/v1/oauth/callback?service=' . $this->service_name);
- }
-
- /**
- * Used by IntegrationsRoutes.php
- * @param string $code
- * @param string $state
- * @return array
- */
- public function handleOAuthCode(string $code, string $state): array
- {
- try {
- $this->loadCredentials();
- $tokens = $this->exchangeOAuthCode($code);
-
- if (!$tokens) {
- return ['success' => false, 'message' => 'Failed to exchange authorization code'];
- }
-
- $credentials = array_merge($this->credentials, $tokens);
- $credentials = $this->addCredentialData($credentials, $tokens);
- $saved = $this->saveCredentials($credentials, false);
-
- if ($saved) {
- return ['success' => true, 'message' => 'Successfully connected'];
- }
-
- return ['success' => false, 'message' => 'Failed to save credentials'];
- } catch (Exception $e) {
- $this->logError('OAuth code exchange failed', ['error' => $e->getMessage()]);
- return ['success' => false, 'message' => 'OAuth error: ' . $e->getMessage()];
- }
- }
-
- /**
- * @param string $action
- * @param mixed|null $data
- * @return array [
- * 'success' => {bool},
- * 'message' => {string} optional message,
- * 'data' => {mixed} optional data to return
- * ]
- */
- public function processAction(string $action, mixed $data = null):array
- {
- if (!$this->checkCapabilities($action, $this->userID)) {
- $this->logError('User cannot perform this action');
- return [
- 'success' => false,
- 'message' => 'Insufficient permissions'
- ];
- }
-
- if (!array_key_exists($action, $this->actions)) {
- $this->logError('Attempted action not set up', [
- 'action' => $action,
- 'stored' => $this->actions,
- 'service' => $this->service_name,
- 'method' => 'processAction'
- ]);
- return [
- 'success' => false,
- 'message' => 'Invalid action'
- ];
- }
- if (!method_exists($this, $this->actions[$action])) {
- $this->logError('Attempted method not set up', [
- 'attempted' => $this->actions['action'],
- 'service' => $this->service_name,
- 'method' => 'processAction'
- ]);
- return [
- 'success' => false,
- 'message' => 'Action not set up'
- ];
- }
-
- $method = $this->actions[$action];
- // Log the action for debugging
- $this->logDebug("Processing action", [
- 'action' => $action,
- 'method' => $method,
- 'data' => $data
- ]);
- if ($data !== null) {
- $result = $this->$method($data);
- } else {
- $result = $this->$method();
- }
- if (is_wp_error($result)) {
- return [
- 'success' => false,
- 'message' => $result->get_error_message()
- ];
- }
-
- return is_array($result) ? $result : ['success' => true, 'data' => $result];
-
+ $this->syncTaxonomies = Registrar::withIntegration($this->service_name, 'term');
}
/*********************************************************************
@@ -457,22 +110,6 @@
*/
abstract protected function initialize(): void;
- /**
- * Get request headers for API calls
- *
- * Return an associative array of headers required for API requests.
- * Common headers include Authorization, Content-Type, Accept, etc.
- *
- * Example:
- * return [
- * 'Authorization' => 'Bearer ' . $this->credentials['access_token'],
- * 'Content-Type' => 'application/json',
- * 'Accept' => 'application/json',
- * ];
- *
- * @return array Associative array of header names and values
- */
- abstract protected function getRequestHeaders(): array;
/**
* Render the connection settings form
@@ -495,46 +132,33 @@
* OPTIONAL OVERRIDE METHODS - IMPLEMENT AS NEEDED
*********************************************************************/
/**
- * Save credentials using CredentialsManager
+ * Save credentials using Auth
*/
public function saveCredentials(array $credentials, bool $test = true):array
{
try {
// Process and validate credentials
if (!$this->validateCredentials($credentials)) {
- $this->logError('Invalid credentials');
- return [
- 'success' => false,
- 'message' => 'Credentials not formatted correctly'
- ];
+ $this->logError('saveCredentials', 'Invalid credentials');
+ return $this->response(false, 'Credentials not formatted correctly');
}
+ //Merge the new credentials with the old ones (new stuff overriding any old stuff)
$old = $this->getCredentials();
$credentials = array_merge($old, $credentials);
- // Temporarily set credentials for testing
- $this->credentials = $credentials;
- $this->initialize();
-
// Test the connection before saving
if ($test) {
if (!$this->testConnection(true)) {
- $this->logError('Connection test failed');
- // Revert to old credentials
- $this->credentials = $old;
- $this->initialize();
+ $this->logError('saveCredentials', 'Connection test failed');
- return [
- 'success' => false,
- 'message' => 'Connection failed. Please check your credentials.',
- 'test_failed' => true
- ];
+ return $this->response(false, 'Connection failed. Please check your credentials');
}
}
// Connection successful, save credentials
- $stored = CredentialsManager::getInstance()->storeCredentials(
+ $stored = Auth::getInstance()->storeCredentials(
$this->service_name,
$credentials,
$this->userID
@@ -544,31 +168,18 @@
$this->updateLastTestedTime();
$this->clearCache();
- return [
- 'success' => true,
- 'message' => 'Credentials validated and saved successfully',
- 'reload' => true
- ];
+ return $this->response(true, 'Credentials validated and saved successfully');
}
- return [
- 'success' => false,
- 'message' => 'Failed to save credentials to database'
- ];
+ return $this->response(false, 'Failed to save credentials to database');
} catch (\Exception $e) {
// Revert to old credentials on any error
- $old = CredentialsManager::getInstance()->getCredentials(
+ $old = Auth::getInstance()->getCredentials(
$this->service_name,
$this->userID
);
- $this->credentials = $old;
- $this->initialize();
-
- return [
- 'success' => false,
- 'message' => 'Validation error: ' . $e->getMessage()
- ];
+ return $this->response(false, 'Validation error: '.print_r($e->getMessage(), true));
}
}
@@ -577,10 +188,8 @@
*/
public function deleteCredentials():array
{
- $success = CredentialsManager::getInstance()->deleteCredentials($this->service_name, $this->userID);
- return [
- 'success' => $success
- ];
+ $success = Auth::getInstance()->deleteCredentials($this->service_name, $this->userID);
+ return $this->response(true, 'Deleted successfully');
}
/**
* Validate credentials before storing
@@ -669,9 +278,9 @@
protected function clearCache():array
{
- $success = $this->cache->flush();
+ $this->cache->flush();
return [
- 'success' => $success,
+ 'success' => true,
];
}
@@ -707,319 +316,6 @@
/******************************************************************
POST SYNC
******************************************************************/
- /**
- * Handle post save for syncing
- *
- * Override to implement custom sync logic when posts are saved.
- * Check the $settings array for post type specific configuration.
- *
- * @param int $postID The post ID
- * @param WP_Post $post The post object
- * @param bool $update Whether this is an update
- * @param array $settings Post type integration settings
- * @return void
- */
- protected function handleTheSavePost(int $postID, WP_Post $post, bool $update, array $settings): void
- {
- // Override in child classes that support post syncing
- // Example implementation:
- /*
- $fields = $this->getSyncFields($postID, 'post', ['schedule_' . $this->service_name]);
- $options = $fields['schedule_' . $this->service_name] !== ''
- ? ['scheduled' => strtotime($fields['schedule_' . $this->service_name])]
- : [];
-
- $this->queueOperation(
- 'sync_post',
- ['post_id' => $postID, 'is_update' => $update],
- $options
- );
- */
- }
-
- /*********************************************************************
- * API REQUEST METHODS
- *********************************************************************/
-
- /**
- * Make an API request with automatic retry and error handling
- * @param string $method
- * @param string $endpoint
- * @param array $data
- * @param string|null $baseKey array key for the $this->apiEndpoints
- * @param array $options
- * @return WP_Error|array
- */
- protected function makeRequest(
- string $method,
- string $endpoint,
- array $data = [],
- ?string $baseKey = null,
- 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, [
- 'method' => 'makeRequest',
- ]);
- return new WP_Error('Error', 'Connection is not set up to make request');
- }
-
- // Rate limiting check
- if (!$this->checkRateLimit()) {
- return new WP_Error('rate_limit', 'Rate limit exceeded. Please try again later.');
- }
-
-
- $attempt = 0;
- $lastError = null;
-
- while ($attempt < $this->maxRetries) {
- try {
-// $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
-// ]);
- // Reset error stats on success
- $this->resetErrorStats();
- return $result;
-
- } catch (Exception $e) {
- $lastError = $e;
- $this->recordRequest($method, $endpoint);
- $attempt++;
-
- // Retry with backoff for server errors
- if ($attempt < $this->maxRetries && !$this->isClientError($e)) {
- $delay = pow(2, $attempt) * 1000000; // 2^attempt seconds in microseconds
- $jitter = rand(0, $delay * 0.3); // Add 0-30% jitter
- usleep($delay + $jitter);
- } else {
- break;
- }
-
- }
- }
-
- $this->logError('API request failed after retries', [
- 'method' => $method,
- 'endpoint' => $endpoint,
- 'attempts' => $attempt,
- 'error' => $lastError ? $lastError->getMessage() : 'Unknown error'
- ]);
-
- return new WP_Error('api_error', $lastError ? $lastError->getMessage() : 'API request failed');
- }
-
- /**
- * Execute the actual request
- */
- protected function executeRequest(
- string $method,
- string $endpoint,
- array $data = [],
- ?string $baseKey = null,
- array $options = []
- ): ?array
- {
- $args = $this->buildRequestArgs($method, $data, $options);
- $url = $this->getApiUrl($endpoint, $baseKey);
- if (!$url) {
- return null;
- }
-
-// $this->logDebug("$method request to: $url: ".print_r($args, true));
-
- // Make the request
- $response = match($method) {
- 'GET' => wp_remote_get($url, $args),
- 'POST' => wp_remote_post($url, $args),
- 'PUT', 'PATCH', 'DELETE' => wp_remote_request($url, array_merge($args, ['method' => $method])),
- default => null
- };
-
- if (!$response) {
- $this->logError("Unsupported HTTP method $method for $this->service_name");
- return null;
- }
-
- if (is_wp_error($response)) {
- $this->logError("Error for: $this->service_name in executeRequest", ['message' => $response->get_error_message()]);
- return null;
- }
-
- $response_code = wp_remote_retrieve_response_code($response);
- $body = wp_remote_retrieve_body($response);
-
- // Handle 401 - try to refresh token and retry once
- if ($response_code === 401 && $this->isOAuthService && !empty($this->credentials['refresh_token'])) {
- // Avoid infinite retry loop - only retry once
- static $retry_count = 0;
-
- if ($retry_count === 0) {
- $retry_count++;
-
-// $this->logDebug('Got 401, attempting token refresh...');
- if ($this->refreshOAuthToken()) {
-// $this->logDebug('Token refreshed successfully, retrying request...');
-
- // Rebuild request args with new token
- $args = $this->buildRequestArgs($method, $data, $options);
-
- // Retry the request
- $response = match($method) {
- 'GET' => wp_remote_get($url, $args),
- 'POST' => wp_remote_post($url, $args),
- 'PUT', 'PATCH', 'DELETE' => wp_remote_request($url, array_merge($args, ['method' => $method])),
- default => null
- };
-
- if ($response && !is_wp_error($response)) {
- $response_code = wp_remote_retrieve_response_code($response);
- $body = wp_remote_retrieve_body($response);
- }
- }
- $retry_count = 0; // Reset for next request
- }
- }
-
- if ($response_code >= 400) {
- $this->handleApiError($response_code, $body, $endpoint);
- }
-
- $decoded = json_decode($body, true);
- if (json_last_error() !== JSON_ERROR_NONE) {
- return ['raw_response' => $body];
- }
-
- $this->updateLastTestedTime();
-
- return $decoded;
- }
-
- /**
- * GET request with caching
- */
- protected function getRequest(
- string $endpoint,
- array $params = [],
- ?string $baseKey = null,
- string $cacheStrategy = 'moderate',
- bool $force = false
- ): ?array
- {
- $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");
- return $cached;
- }
- }
-
- $result = $this->makeRequest('GET', $endpoint, $params, $baseKey);
-
- // Only cache successful responses (not WP_Error and not error objects)
- if ($result && !is_wp_error($result) && !$this->isErrorResponse($result) && $ttl > 0) {
- $this->cache->set($cacheKey, $result, $ttl);
- }
-
- return $result;
- }
- /**
- * Check if response contains an error
- * Override in child classes for service-specific error detection
- */
- protected function isErrorResponse(array $response): bool
- {
- // Common error patterns across APIs
- return isset($response['error'])
- || isset($response['errors'])
- || isset($response['error_description']);
- }
-
- /**
- * POST request
- */
- protected function postRequest(string $endpoint, array $data = [], ?string $baseKey = null): ?array
- {
- return $this->makeRequest('POST', $endpoint, $data, $baseKey);
- }
-
- /**
- * PUT request
- */
- protected function putRequest(string $endpoint, array $data = [], ?string $baseKey = null): ?array
- {
- return $this->makeRequest('PUT', $endpoint, $data, $baseKey);
- }
-
- /**
- * DELETE request
- */
- protected function deleteRequest(string $endpoint, array $data = [], ?string $baseKey = null): ?array
- {
- return $this->makeRequest('DELETE', $endpoint, $data, $baseKey);
- }
-
- /**
- * PATCH request
- */
- protected function patchRequest(string $endpoint, array $data = [], ?string $baseKey = null): WP_Error|array
- {
- return $this->makeRequest('PATCH', $endpoint, $data, $baseKey);
- }
-
-
-
- /**
- * Build request arguments
- */
- protected function buildRequestArgs(string $method, array $data, array $options): array
- {
- $args = [
- 'method' => $method,
- 'headers' => $this->getRequestHeaders(),
- 'timeout' => $options['timeout'] ?? 30,
- 'sslverify' => $options['sslverify'] ?? true,
- 'redirection' => $options['redirection'] ?? 5,
- 'blocking' => $options['blocking'] ?? true,
- 'compress' => $options['compress'] ?? true,
- 'decompress' => $options['decompress'] ?? true,
- ];
-
- if ($method === 'GET' && !empty($data)) {
- // Add query parameters for GET requests
- $args['body'] = $data;
- } elseif (!empty($data)) {
- $content_type = $args['headers']['Content-Type'] ?? 'application/json';
-
- if (str_contains($content_type, 'application/json')) {
- $args['body'] = json_encode($data);
- } elseif (str_contains($content_type, 'application/x-www-form-urlencoded')) {
- $args['body'] = http_build_query($data);
- } else {
- $args['body'] = $data;
- }
- }
-
- return $args;
- }
/*********************************************************************
* SYNC METHODS
@@ -1040,11 +336,8 @@
):bool {
$queue = JVB()->queue();
- // Add service prefix to operation type
- $operation_type = strtolower($this->service_name) . '_' . $type;
-
$queued = $queue->queueOperation(
- $operation_type,
+ $type,
$this->userID ?? 0,
array_merge($data, ['service' => $this->service_name, 'user' => $this->userID??0]),
array_merge([
@@ -1086,117 +379,25 @@
*********************************************************************/
/**
- * Get cache duration based on endpoint
- *
- * @param string $endpoint The API endpoint
- * @return int Cache duration in seconds
- */
- protected function getCacheDuration(string $endpoint): int
- {
- // Override in child classes for endpoint-specific caching
- // Example: user profiles = aggressive, posts = moderate, analytics = minimal
- return $this->ttl;
- }
-
- /**
- * Check if we're within rate limits
- *
- * @return bool True if within limits
- */
- protected function checkRateLimit(): bool
- {
- $now = time();
-
- // Clean old history
- $this->request_history = array_filter($this->request_history, function($timestamp) use ($now) {
- return ($now - $timestamp) < 3600; // Keep last hour
- });
-
- // Trim to max size
- if (count($this->request_history) > $this->max_history_size) {
- $this->request_history = array_slice($this->request_history, -$this->max_history_size);
- }
-
- // Check limits
- $recent_counts = [
- 'second' => 0,
- 'minute' => 0,
- 'hour' => count($this->request_history)
- ];
-
- foreach ($this->request_history as $timestamp) {
- if ($now - $timestamp <= 1) $recent_counts['second']++;
- if ($now - $timestamp <= 60) $recent_counts['minute']++;
- }
-
- if ($recent_counts['second'] >= $this->rate_limits['per_second']) return false;
- if ($recent_counts['minute'] >= $this->rate_limits['per_minute']) return false;
- if ($recent_counts['hour'] >= $this->rate_limits['per_hour']) return false;
-
- return true;
- }
-
- protected function checkOAuthRateLimit(): bool
- {
- $cache_key = 'oauth_rate_limit_' . $this->service_name . '_' . $this->userID;
- $attempts = get_transient($cache_key) ?: [];
-
- $now = time();
-
- // Clean old attempts
- $attempts = array_filter($attempts, function($timestamp) use ($now) {
- return ($now - $timestamp) < 3600; // Keep last hour
- });
-
- // Check per-minute limit
- $recent = array_filter($attempts, function($timestamp) use ($now) {
- return ($now - $timestamp) < 60;
- });
-
- if (count($recent) >= 5) { // Max 5 OAuth attempts per minute
- return false;
- }
-
- // Add current attempt
- $attempts[] = $now;
-
- // Save updated attempts
- set_transient($cache_key, $attempts, 3600);
-
- return true;
- }
-
-
- /**
- * Record a request for rate limiting
- *
- * @param string $method HTTP method
- * @param string $endpoint API endpoint
- * @return void
- */
- protected function recordRequest(string $method, string $endpoint): void
- {
- $this->request_history[] = time();
- }
-
-
-
- /**
* Register WordPress hooks based on capabilities
*/
protected function registerHooks(): void
{
- add_filter(BASE . 'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
- add_action( 'wp_ajax_'.BASE.$this->service_name.'_oauth_callback', [$this, 'handleAjaxResponse'] );
- add_action( 'wp_ajax_nopriv_'.BASE.$this->service_name.'_oauth_callback', [$this, 'handleAjaxResponse'] );
- if ($this->handleWebhooks){
+ //Handled by IntegrationExecutor now
+// add_filter(BASE . 'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
+
+ if (method_exists($this, 'registerOAuthCallbacks')) {
+ $this->registerOAuthCallbacks();
+ }
+ if (method_exists($this, 'registerWebhookEndpoint')) {
$this->registerWebhookEndpoint();
}
// Let child classes register additional hooks if needed
$this->registerAdditionalHooks();
+
if (!empty($this->syncPostTypes)) {
- add_action('save_post', [$this, 'handleSavePost'], 20, 3);
+ $this->addSavePost();
add_action('transition_post_status', [$this, 'handlePostStatusTransition'], 10, 3);
if ($this->canSync['delete']) {
@@ -1209,64 +410,16 @@
add_action('pre_delete_term', [$this, 'handleDeleteTerm'], 10, 2);
}
}
+
+ if (method_exists($this, 'registerQueueTypes')) {
+ add_action('init', [$this, 'registerQueueTypes'], 10);
+ }
}
- public function handleAjaxResponse()
- {
-
- $code = $_GET['code'];
- $state = $_GET['state'];
- $state_parts = explode('|', $state);
- $state_key = $state_parts[0] ?? '';
- $user_id = intval($state_parts[1] ?? 0);
- $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');
-
- $state_data = get_transient('oauth_state_' . $state_key);
- 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);
- // Handle error from OAuth provider
- if (array_key_exists('error', $_GET)) {
- $error_description = $_GET['error_description'] ?? 'Authorization denied';
-
- wp_redirect(add_query_arg([
- 'error' => 'OAuth authorization denied: ' . $error_description
- ], $return_url));
- exit;
- }
-
- // Get integration instance
- $integration = JVB()->connect($this->service_name, $user_id);
-
- if (!$integration) {
- wp_die('Invalid service: ' . esc_html($this->service_name), 'OAuth Error');
- }
- // Exchange code for tokens
- $result = $integration->handleOAuthCode($code, $state);
-
- // Redirect back with result
- if ($result['success']) {
- wp_redirect(add_query_arg([
- 'success' => 'Successfully connected to ' . $integration->title
- ], $return_url));
- } else {
- // Handle failure
- $error_message = $result['message'] ?? 'Failed to complete OAuth authorization';
-
- wp_redirect(add_query_arg([
- 'error' => $error_message
- ], $return_url));
- }
- exit;
- }
/**
* Handle connection setup and credential storage
*
@@ -1287,11 +440,11 @@
}
// Store credentials
- $this->credentials = array_merge($this->credentials ?? [], $sanitized);
- $this->credentials['last_updated'] = time();
+ $credentials = array_merge($this->loadCredentials() ?? [], $sanitized);
+ $credentials['last_updated'] = time();
// Save to database
- $saved = $this->saveCredentials($this->credentials);
+ $saved = $this->saveCredentials($credentials);
if (!$saved) {
return [
@@ -1324,699 +477,34 @@
}
}
- /**
- * Load credentials from secure storage
- */
- protected function loadCredentials(): void
- {
- $this->credentials = CredentialsManager::getInstance()->getCredentials($this->service_name, $this->userID);
- }
-
public function getCredentials(): array
{
- $this->ensureInitialized();
- return $this->credentials;
+ return $this->loadCredentials();
}
- /**
- * Check if integration is properly configured
- */
- public function isSetUp(): bool
+
+
+ public function hasOAuthCredentials(?int $userID = null): bool
{
- if (empty($this->credentials)) {
- $this->loadCredentials();
+ if ($userID !== $this->userID) {
+ $this->switchUser($userID);
}
- return !empty($this->credentials);
- }
-
- public function hasOAuthCredentials(): bool
- {
- if (!$this->isOAuthService){
- return false;
- }
-
- $required = ['client_id', 'client_secret', 'access_token'];
-
- foreach ($required as $field) {
- if (empty($this->credentials[$field]) || !is_string($this->credentials[$field]) || $this->credentials[$field] === '') {
- return false;
- }
- }
- return true;
+ return $this->hasOAuth && $this->hasValidOAuth();
}
/**
* Ensure service is initialized
+ * TODO: I don't think this is necessary anymore.
*/
protected function ensureInitialized(): void
{
if (!$this->isSetUp()){
return;
}
- if (empty($this->credentials)) {
- $this->loadCredentials();
- }
-
- if (!empty($this->credentials)) {
- if ($this->isOAuthService && $this->hasOAuthCredentials()) {
- // Check if token is expired first
- if (!$this->isOAuthValid()) {
- // 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->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, so continue
- }
- }
- }
- $this->initialize();
- }
+ $this->initialize();
}
-
- /**
- * Switch user context
- */
- public function switchUser(int $user_id): void
- {
- if ($this->userID === $user_id) {
- return;
- }
-
- // Clean up current context
- $this->cleanup();
-
- // Switch context
- $this->userID = $user_id;
- $this->credentials = [];
- $this->resetTokenRefreshFlag(); // ADD THIS LINE
-
- $this->ensureInitialized();
- }
-
- public function getAsUser(int $user_id) {
- return new $this($user_id);
- }
-
- /**
- * Clean up resources
- */
- protected function cleanup(): void
- {
- // Clear sensitive data
- $this->credentials = [];
-
- // Clear request history
- $this->request_history = [];
- }
-
- /**
- * Destructor - ensure cleanup
- */
- public function __destruct()
- {
- $this->cleanup();
- }
-
-
- /***************************************************************
- ERROR HANDLING
- ***************************************************************/
- /**
- * Handle error responses
- */
- protected function handleApiError(int $code, string $body, string $endpoint): void
- {
- $message = "API Error ({$code}): ";
- $decoded = json_decode($body, true);
-
- // Extract error details
- $error_details = $this->extractErrorDetails($decoded, $body);
- $message .= $error_details['message'];
-
- // Determine error severity based on HTTP code
- $severity = $this->getErrorSeverity($code);
-
- // Build comprehensive error context
- $error_context = [
- 'service' => $this->service_name,
- 'endpoint' => $endpoint,
- 'http_code' => $code,
- 'error_type' => $this->categorizeApiError($code),
- 'error_details' => $error_details,
- 'user_id' => $this->userID,
- 'request_time' => time(),
- 'consecutive_errors' => $this->error_stats['consecutive_errors'],
- 'is_oauth' => $this->isOAuthService,
- 'api_version' => $this->apiVersion,
- 'integration_healthy' => $this->is_healthy
- ];
-
- // Add rate limit information if present
- if ($code === 429) {
- $error_context['rate_limit'] = $this->extractRateLimitInfo($decoded, $body);
- }
-
- // Log to ErrorHandler with proper severity
- $this->logError($message, $error_context, $severity);
-
- // Update error statistics
- $this->updateErrorStats($code, $endpoint);
-
- // Check if integration should be marked unhealthy
- $this->checkIntegrationHealth();
-
- // Store last error for debugging
- $this->lastError = [
- 'code' => $code,
- 'message' => $message,
- 'endpoint' => $endpoint,
- 'timestamp' => time(),
- 'context' => $error_context
- ];
- }
-
- /**
- * Check if error is a client error (4xx)
- */
- protected function isClientError(Exception $e): bool
- {
- $code = $e->getCode();
- return $code >= 400 && $code < 500;
- }
-
- /**
- * Get last error details
- */
- public function getLastError(): array
- {
- return $this->lastError;
- }
-
-
- /*****************************************************************
- OAUTH
- *****************************************************************/
- /**
- * Check user capabilities for specific actions
- * Can be overridden by child classes for custom logic
- */
- protected function checkCapabilities(string $action, ?int $userID = null): bool
- {
- // Default: require manage_options for admin actions
- $admin_actions = ['disconnect', 'update_credentials', 'clear_cache'];
-
- if (in_array($action, $admin_actions)) {
- return current_user_can('manage_options');
- }
-
- // User-specific actions
- return is_user_logged_in();
- }
-
- /*****************************************************************
- OAUTH
- *****************************************************************/
- protected function initializeOAuth():void
- {
- $this->oauth = array_merge([
- 'authorize' => '',
- 'token' => '',
- 'revoke' => '',
- 'scopes' => [],
- 'redirect_uri' => admin_url('admin-ajax.php?action=' . $this->service_name . '_oauth_callback')
- ], $this->oauth);
- }
-
- /**
- * Get OAuth authorization URL
- */
- public function getOAuthUrl(?string $return_url = null): string
- {
-
- if (!$this->isOAuthService) {
- return '';
- }
-
- if (empty($this->credentials)) {
- $this->ensureInitialized();
- }
-
- if (empty($this->oauth['authorize'])) {
- $this->logError('OAuth authorize URL not configured');
- return '';
- }
-
- // Build base parameters
- $params = [
- 'client_id' => $this->credentials['client_id'] ?? $this->credentials['app_id'] ?? '',
- 'redirect_uri' => $this->getRedirectUri(),
- 'response_type' => 'code',
- 'scope' => implode(' ', $this->oauth['scopes'] ?? [])
- ];
- $state_key = wp_generate_password(32, false);
- $user_id = $this->userID??0;
-
- // Store state data in transient (expires in 10 minutes)
- set_transient(
- 'oauth_state_' . $state_key,
- [
- 'service' => $this->service_name,
- 'user_id' => $user_id,
- 'created' => time()
- ],
- 600
- );
-
- $state_parts = [
- $state_key,
- $user_id
- ];
-
- // Add return URL if provided
- if ($return_url) {
- $state_parts[] = base64_encode($return_url);
- } else {
- $state_parts[] = base64_encode(admin_url('admin.php?page=jvb-integrations'));
- }
-
- $params['state'] = implode('|', $state_parts);
-
- // Allow child classes to modify params (they can override/remove as needed)
- if (method_exists($this, 'addOAuthParams')) {
- $params = $this->addOAuthParams($params);
- }
-
- return $this->oauth['authorize'] . '?' . http_build_query($params);
- }
-
- /**
- * Add service-specific OAuth parameters
- */
- protected function addOAuthParams(array $params): array
- {
- // Override in child classes to add service-specific params
- // e.g., access_type, prompt, etc.
- return $params;
- }
-
- protected function makeOAuthRequest(string $method, string $url, array $data = []): array|WP_Error
- {
- // Check rate limits before making request
- if (!$this->checkOAuthRateLimit()) {
- // For OAuth, we might want to be more lenient with rate limits
- // since these are critical authentication flows
- $this->logError('OAuth request rate limited, waiting before retry');
- sleep(2); // Brief pause before critical OAuth request
-
- // Check again after wait
- if (!$this->checkRateLimit()) {
- return new WP_Error(
- 'rate_limit',
- 'OAuth rate limit exceeded. Please try again in a few moments.'
- );
- }
- }
-
- // Record the request for rate limiting
- $this->recordRequest($method, $url);
-
- // Build OAuth-specific headers
- $headers = [
- 'Content-Type' => 'application/json',
- 'Accept' => 'application/json',
- ];
-
- // Add API version for services that require it (like Square)
- if (!empty($this->apiVersion)) {
- $headers['Square-Version'] = $this->apiVersion;
- }
-
- // Build request arguments
- $args = [
- 'method' => $method,
- 'headers' => $headers,
- 'timeout' => 30,
- 'sslverify' => true,
- 'body' => json_encode($data)
- ];
-
- // Make the request with retry logic
- $attempt = 0;
- $max_attempts = 2; // Fewer retries for OAuth to avoid code expiry
-
- while ($attempt < $max_attempts) {
- $attempt++;
-
- // Make the actual request
- $response = wp_remote_request($url, $args);
-
- // Check for WordPress errors
- if (is_wp_error($response)) {
- if ($attempt >= $max_attempts) {
- return $response;
- }
-
- // Wait before retry
- sleep($this->retryDelays[$attempt - 1] ?? 1);
- continue;
- }
-
- // Get response code and body
- $code = wp_remote_retrieve_response_code($response);
- $body = wp_remote_retrieve_body($response);
-
- // Parse JSON response
- $data = json_decode($body, true);
-
- // Check for successful response
- if ($code >= 200 && $code < 300) {
- // Reset error stats on success
- $this->resetErrorStats();
-
- // Return parsed data
- return $data ?: [];
- }
-
- // Handle specific OAuth error codes
- if ($code === 401) {
- // Invalid credentials - don't retry
- $this->handleApiError($code, $body, $url);
- return new WP_Error('invalid_credentials', 'Invalid OAuth credentials');
- }
-
- if ($code === 429) {
- // Rate limited - wait longer before retry
- if ($attempt < $max_attempts) {
- $retry_after = $this->extractRetryAfter($response);
- sleep($retry_after ?: 5);
- continue;
- }
- }
-
- // Check if we should retry
- if ($attempt >= $max_attempts || $code >= 400 && $code < 500) {
- // Client errors (4xx) typically shouldn't be retried
- $this->handleApiError($code, $body, $url);
-
- $error_message = $data['error_description'] ??
- $data['error'] ??
- "OAuth request failed with status {$code}";
-
- return new WP_Error('oauth_error', $error_message, ['code' => $code]);
- }
-
- // Server error - retry after delay
- sleep($this->retryDelays[$attempt - 1] ?? 1);
- }
-
- // Shouldn't reach here, but handle it
- return new WP_Error('oauth_error', 'OAuth request failed after retries');
- }
-
- /**
- * Extract retry-after header from response
- *
- * @param array|WP_Error $response WordPress HTTP response
- * @return int Seconds to wait before retry
- */
- protected function extractRetryAfter($response): int
- {
- if (is_wp_error($response)) {
- return 5;
- }
-
- $headers = wp_remote_retrieve_headers($response);
-
- if (isset($headers['retry-after'])) {
- // Could be seconds or HTTP date
- $retry_after = $headers['retry-after'];
-
- if (is_numeric($retry_after)) {
- return (int) $retry_after;
- }
-
- // Try to parse as date
- $timestamp = strtotime($retry_after);
- if ($timestamp !== false) {
- return max(0, $timestamp - time());
- }
- }
-
- return 5; // Default wait time
- }
-
- /**
- * Exchange OAuth code for tokens
- */
- protected function exchangeOAuthCode(string $code): ?array
- {
- $this->ensureInitialized();
-
- // Build request data
- $request_data = [
- 'client_id' => $this->credentials['client_id'] ?? '',
- 'client_secret' => $this->credentials['client_secret'] ?? '',
- 'code' => $code,
- 'grant_type' => 'authorization_code',
- 'redirect_uri' => $this->getRedirectUri()
- ];
-
- $oauth_endpoint = $this->oauth['token'];
- $response = $this->makeOAuthRequest('POST', $oauth_endpoint, $request_data);
-
- if (is_wp_error($response)) {
- $this->logError('OAuth token exchange failed', [
- 'error' => $response->get_error_message(),
- 'code' => $response->get_error_code()
- ]);
- return null;
- }
-
- // Parse response
- if (isset($response['access_token'])) {
- $expires_in = $response['expires_in'] ?? 2592000; // 30 days default
-
- return [
- 'access_token' => $response['access_token'],
- 'refresh_token' => $response['refresh_token'] ?? '',
- 'expires_in' => $expires_in,
- 'expires_at' => time() + $expires_in, // Calculate expiry timestamp
- 'token_type' => $response['token_type'] ?? 'Bearer',
- 'merchant_id' => $response['merchant_id'] ?? '',
- 'scope' => $response['scope'] ?? ''
- ];
- }
-
- $this->logError('Failed to obtain access token', ['response' => $response]);
- return null;
- }
-
- /**
- * Add service-specific credential data
- */
- protected function addCredentialData(array $credentials, array $tokens): array
- {
- // Override in child classes to add service-specific data
- return $credentials;
- }
-
- /**
- * Check if token should be proactively refreshed
- * Different from isOAuthValid() which checks if token is actually expired
- */
- protected function shouldRefreshToken(): bool
- {
- if (!$this->isOAuthService || $this->refresh_interval === 0) {
- return false;
- }
-
- // If no expiry info, we can't proactively refresh
- if (empty($this->credentials['expires_at'])) {
- return false;
- }
-
- $expires_at = intval($this->credentials['expires_at']);
- $time_until_expiry = $expires_at - time();
-
- // Refresh if we're within the refresh interval window
- return $time_until_expiry > 0 && $time_until_expiry <= $this->refresh_interval;
- }
- /**
- * Get time until token refresh is recommended
- * Useful for displaying in admin UI
- */
- public function getTimeUntilRefresh(): ?int
- {
- if ($this->refresh_interval === 0 || empty($this->credentials['expires_at'])) {
- return null;
- }
-
- $expires_at = intval($this->credentials['expires_at']);
- $refresh_at = $expires_at - $this->refresh_interval;
- $time_until_refresh = $refresh_at - time();
-
- return max(0, $time_until_refresh);
- }
-
- /**
- * Get token freshness status
- * Returns: 'fresh', 'should_refresh', 'expired', or 'no_expiry_info'
- */
- public function getTokenStatus(): string
- {
- if (!$this->isOAuthService) {
- return 'not_oauth';
- }
-
- if (empty($this->credentials['access_token'])) {
- return 'no_token';
- }
-
- if (empty($this->credentials['expires_at'])) {
- return 'no_expiry_info';
- }
-
- $expires_at = intval($this->credentials['expires_at']);
- $now = time();
-
- if ($expires_at <= $now) {
- return 'expired';
- }
-
- if ($this->shouldRefreshToken()) {
- return 'should_refresh';
- }
-
- return 'fresh';
- }
- /**
- * Refresh OAuth token
- */
- protected function refreshOAuthToken(): bool
- {
- if (!$this->isOAuthService || empty($this->credentials['refresh_token'])) {
- return false;
- }
-
- $request_data = [
- 'client_id' => $this->credentials['client_id'],
- 'client_secret' => $this->credentials['client_secret'],
- 'refresh_token' => $this->credentials['refresh_token'],
- 'grant_type' => 'refresh_token'
- ];
-
- $response = $this->makeOAuthRequest('POST', $this->oauth['token'], $request_data);
-
- if (is_wp_error($response)) {
- $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;
- }
-
- if (isset($response['access_token'])) {
- $this->credentials['access_token'] = $response['access_token'];
- $this->credentials['expires_at'] = time() + ($response['expires_in'] ?? 2592000); // 30 days
-
- // Note: Some services return the SAME refresh token
- if (isset($response['refresh_token'])) {
- $this->credentials['refresh_token'] = $response['refresh_token'];
- }
-
- $this->saveCredentials($this->credentials);
- return true;
- }
-
- return false;
- }
-
- /**
- * Revoke OAuth access
- */
- public function revokeOAuthAccess(): bool
- {
- if (!$this->isOAuthService || empty($this->oauth['revoke'])) {
- return false;
- }
-
- if (!empty($this->credentials['access_token'])) {
- wp_remote_post($this->oauth['revoke'], [
- 'body' => ['token' => $this->credentials['access_token']]
- ]);
- }
-
- return CredentialsManager::getInstance()->deleteCredentials($this->service_name, $this->userID);
-
- }
-
- public function handleOAuthDisconnect(): array
- {
- try {
- // Revoke the token with Square using centralized request
- if (!empty($this->credentials['access_token'])) {
- $revoke_data = [
- 'client_id' => $this->credentials['client_id'] ?? '',
- 'access_token' => $this->credentials['access_token']
- ];
-
- // Make revoke request (ignore response as revoke often returns empty)
- $this->makeOAuthRequest('POST', $this->oauth['revoke'], $revoke_data);
- }
-
- // Clear stored credentials (preserve app credentials)
- $this->credentials = [
- 'client_id' => $this->credentials['client_id'] ?? '',
- 'client_secret' => $this->credentials['client_secret'] ?? '',
- 'environment' => $this->credentials['environment'] ?? 'sandbox'
- ];
-
- $this->saveCredentials($this->credentials);
- $this->clearCache();
-
- return [
- 'success' => true,
- 'message' => 'Successfully disconnected from Square'
- ];
- } catch (Exception $e) {
- return [
- 'success' => false,
- 'message' => 'Failed to disconnect: ' . $e->getMessage()
- ];
- }
- }
/**
* Generate webhook signature key for services that require it
* @return string
@@ -2025,672 +513,13 @@
{
return wp_generate_password(32, false);
}
- /**
- * Generate OAuth state parameter
- */
- protected function generateOAuthState(?int $user_id, ?string $return_url = null): string
- {
- $user_id = $user_id ?? 0;
- $state = wp_create_nonce($this->service_name . '_oauth_' . $user_id) . '|' . $user_id;
- if ($return_url) {
- $state .= '|' . base64_encode($return_url);
- }
-
- return $state;
- }
-
- protected function getNonce(): string
- {
- return wp_create_nonce($this->service_name . '_oauth_' . $this->userID);
- }
-
- /**
- * Determine return URL after OAuth
- */
- protected function determineReturnUrl(?int $user_id): string
- {
- if ($user_id > 0) {
- return home_url('/dash/integrations/#' . $this->service_name);
- }
-
- return admin_url('admin.php?page=jvb-integrations');
- }
-
-
- /****************************************************************
- POST SYNC
- ****************************************************************/
- /**
- * Get field mapping for a post type
- */
- protected function getFieldMapping(string $post_type): array
- {
- // Apply filter for custom mapping
- return apply_filters(
- "jvb_{$this->service_name}_field_mapping",
- [],
- $post_type,
- $this
- );
- }
-
-
- /**
- * Map WordPress fields to service fields
- */
- protected function mapFieldsToService(int $postID, array $mapping): array
- {
- $meta_manager = new MetaManager($postID, 'post');
- $post = get_post($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);
- }
-
- if ($value !== null && $value !== '') {
- $this->setNestedValue($service_data, $service_field, $value);
- }
- }
-
- return apply_filters(
- "jvb_{$this->service_name}_mapped_data",
- $service_data,
- $postID,
- $mapping
- );
- }
-
- /**
- * Set nested array value using dot notation
- */
- protected function setNestedValue(array &$array, string $path, $value): void
- {
- $keys = explode('.', $path);
- $current = &$array;
-
- foreach ($keys as $i => $key) {
- if ($i === count($keys) - 1) {
- $current[$key] = $value;
- } else {
- if (!isset($current[$key])) {
- $current[$key] = [];
- }
- $current = &$current[$key];
- }
- }
- }
- /**
- * Handle post save
- */
- public function handleSavePost(int $postID, WP_Post $post, bool $update): void
- {
- if (jvbNoSaveIt($postID, $post)) {
- return;
- }
- if (empty($this->syncPostTypes)) {
- return;
- }
-
- $config = JVB_CONTENT[jvbNoBase($post->post_type)]??null;
- if (!$config) {
- return;
- }
-
- $settings = $config['integrations'][$this->service_name]??null;
- if (!$settings) {
- return;
- }
-
- $fields = $this->getSyncFields($postID, 'post', ['schedule_'.$this->service_name]);
- 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]) {
- return;
- }
-
- if ($post->post_status !== 'publish' && !$isShared) {
- return;
- }
- $this->handleTheSavePost($postID, $post, $update, $settings);
- }
-
-
- protected function getSyncFields(int $postID, string $type, array $additional = []):array
- {
- $meta = new MetaManager($postID, $type);
- $fieldsToCheck = [
- 'share_to_' . $this->service_name,
- '_keep_synced_' . $this->service_name,
- "_{$this->service_name}_item_id",
- "_{$this->service_name}_last_sync",
- "_{$this->service_name}_shared_at",
- "_{$this->service_name}_sync_status",
- ... $additional
- ];
- return $meta->getAll($fieldsToCheck);
- }
-
- /**
- * Handle post status transitions
- */
- public function handlePostStatusTransition(string $new_status, string $old_status, WP_Post $post): void
- {
- if (empty($this->syncPostTypes)) {
- return;
- }
-
- if (!in_array(jvbNoBase($post->post_type), $this->syncPostTypes)) {
- return;
- }
-
- //Map fields from our custom post types to the fields expected by the integration
- $mappedFields = $this->getFieldMapping($post->post_type);
-
- $fields = $this->getSyncFields($post->ID, 'post', $mappedFields);
-
- // Handle unpublish action
- if ($old_status === 'publish' && $new_status !== 'publish') {
-
- if ($fields["_{$this->service_name}_item_id"] && $this->canSync['delete']) {
- try {
- $this->queueOperation(
- 'delete_post',
- [
- $post,
- $fields["_{$this->service_name}_item_id"]
- ]
- );
- } catch (Exception $e) {
- $this->logError("Failed to handle unpublish for post {$post->ID}: " . $e->getMessage());
- }
- }
- }
- }
-
- /**
- * Handle post deletion
- */
- public function handleDeletePost(int $postID): void
- {
- if (!$this->canSync['delete']) {
- return;
- }
-
- $post = get_post($postID);
- if (!$post || !in_array($post->post_type, $this->syncPostTypes)) {
- return;
- }
-
- $fields = $this->getSyncFields($postID, 'post');
-
- if ($fields["_{$this->service_name}_item_id"] !== '') {
-
- $this->queueOperation(
- 'delete_post',
- [
- 'post_id' => $post->ID,
- ]
- );
- }
- }
-
- protected function handleSaveTerm($term_id, $tt_id, $taxonomy, $update, $args): void
- {
- $noBase = jvbNoBase($taxonomy);
- if (!in_array($noBase, $this->syncTaxonomies)) {
- return;
- }
- // Check if taxonomy is content-type
- $config = JVB_TAXONOMY[$noBase] ?? null;
- if (!$config || !($config['is_content'] ?? false)) {
- return;
- }
-
- $settings = $config['integrations'][$this->service_name] ?? null;
- if (!$settings) {
- return;
- }
-
- // Similar sync logic as handleSavePost but for terms
- $this->handleTheTermSave($term_id, $taxonomy, $update, $settings);
- }
-
- protected function handleTheTermSave($term_id, $taxonomy, $update, $settings) {
-
- }
-
- protected function getTermFieldMapping(string $taxonomy): array
- {
- return apply_filters(
- "jvb_{$this->service_name}_term_field_mapping",
- [],
- $taxonomy,
- $this
- );
- }
-
- /**
- * Share post to external service (override in child classes)
- */
- protected function sharePost(WP_Post $post): ?string
- {
- throw new Exception('sharePost must be implemented by child class');
- }
-
- /**
- * Update shared post on external service (override in child classes)
- */
- protected function updateSharedPost(WP_Post $post, string $external_id): void
- {
- throw new Exception('updateSharedPost must be implemented by child class');
- }
-
- /**
- * Remove post from external service (override in child classes)
- */
- protected function unsharePost(WP_Post $post, string $external_id): void
- {
- throw new Exception('unsharePost must be implemented by child class');
- }
-
- /**
- * Get sync capabilities
- */
- public function getSyncCapabilities(): array
- {
- return $this->canSync;
- }
-
- /**
- * Check if a specific sync capability is supported
- */
- public function supportsSyncCapability(string $capability): bool
- {
- return $this->canSync[$capability] ?? false;
- }
-
- /**
- * Get posts shared to this service
- */
- public function getSharedPosts(array $args = []): array
- {
- $defaults = [
- 'post_type' => $this->syncPostTypes,
- 'meta_query' => [
- [
- 'key' => BASE."_{$this->service_name}_item_id",
- 'compare' => 'EXISTS'
- ]
- ],
- 'posts_per_page' => -1
- ];
-
- $args = wp_parse_args($args, $defaults);
- return get_posts($args);
- }
-
- /**
- * Check if post is shared to this service
- */
- public function isPostShared(int $postID): bool
- {
- $external_id = get_post_meta($postID, BASE."_{$this->service_name}_item_id", true);
- return $external_id !== '';
- }
-
- /**
- * Get external ID for a post
- */
- public function getPostExternalId(int $postID): ?string
- {
- $external_id = get_post_meta($postID, BASE."_{$this->service_name}_item_id", true);
- return $external_id ?: null;
- }
/*******************************************************************
* UTILITIES
*******************************************************************/
/**
- * Get API URL for endpoint
- */
- protected function getApiUrl(string $endpoint, ?string $baseKey = null): string|false
- {
- if ($this->isOAuthService && in_array($endpoint, $this->oauth)) {
- return $endpoint;
- }
- if (is_array($this->apiBase)) {
- if ($baseKey && array_key_exists($baseKey, $this->apiBase)) {
- $base = $this->apiBase[$baseKey];
- } else {
- $base = ($this->apiBase['base'] ?? reset($this->apiBase));
- }
- } else {
- $base = $this->apiBase;
- }
-
- if (!$base || $base === '') {
- $this->logError('API base URL not configured for {$this->>service_name}');
- return false;
- }
-
- // Handle named endpoints
- if (!$this->isValidEndpoint($endpoint)) {
- $this->logError("{$endpoint} is not a valid endpoint for {$this->service_name}");
- return false;
- }
-
- // Build full URL
- $base = rtrim($base, '/');
- $endpoint = ltrim($endpoint, '/');
-
- return "{$base}/{$endpoint}";
- }
-
- /**
- * Check if an endpoint is valid, supporting both exact matches and patterns
- *
- * @param string $endpoint
- * @return bool
- */
- protected function isValidEndpoint(string $endpoint): bool
- {
- // Remove query parameters for validation
- $endpointPath = parse_url($endpoint, PHP_URL_PATH);
- if ($endpointPath === false) {
- $endpointPath = $endpoint;
- }
-
- foreach ($this->apiEndpoints as $pattern) {
- // Check for exact match first
- if ($endpointPath === $pattern) {
- return true;
- }
-
- if (str_starts_with($endpointPath, $pattern)) {
- return true;
- }
-
- // Check if pattern contains wildcards (indicated by square brackets)
- if (strpos($pattern, '[') !== false) {
- // Convert the pattern to a regex
- $regexPattern = '#^' . str_replace(['[^/]+'], ['[^/]+'], $pattern) . '(?:\?.*)?$#';
- if (preg_match($regexPattern, $endpoint)) {
- return true;
- }
- }
- }
-
- return false;
- }
-
- /**
- * Build cache key
- */
- protected function buildCacheKey(string $method, string $endpoint, array $params = []): string
- {
- $key_parts = [
- $this->service_name,
- $method,
- $endpoint
- ];
-
- if ($this->userID) {
- $key_parts[] = "user_{$this->userID}";
- }
-
- if (!empty($params)) {
- $key_parts[] = md5(serialize($params));
- }
-
- return implode('_', $key_parts);
- }
-
- /**
- * Log error message
- */
- protected function logError(string $message, array $context = [], ?string $severity = null): void
- {
- if (!$this->errorHandler) {
- $this->errorHandler = JVB()->error();
- }
- // Determine severity if not provided
- if ($severity === null) {
- $severity = 'error';
- }
-
- // Add integration-specific context
- $context = array_merge($context, [
- 'component' => 'Integration:' . $this->service_name,
- 'user_id' => $this->userID,
- 'service' => $this->service_name,
- 'timestamp' => current_time('mysql'),
- 'memory_usage' => memory_get_usage(true),
- 'peak_memory' => memory_get_peak_usage(true),
- 'integration_health' => [
- 'is_healthy' => $this->is_healthy,
- 'consecutive_errors' => $this->error_stats['consecutive_errors'],
- 'total_errors' => $this->error_stats['total_errors'],
- 'last_success' => $this->error_stats['last_success']
- ]
- ]);
-
- // Log through ErrorHandler
- $this->errorHandler->log(
- 'Integration:' . $this->service_name,
- $message,
- $context,
- $severity
- );
- }
-
- /**
- * Categorize API errors for better tracking
- */
- protected function categorizeApiError(int $code): string
- {
- return match(true) {
- $code >= 400 && $code < 404 => 'client_error',
- $code === 404 => 'not_found',
- $code === 401 => 'authentication',
- $code === 403 => 'authorization',
- $code === 429 => 'rate_limit',
- $code >= 500 && $code < 600 => 'server_error',
- default => 'unknown'
- };
- }
-
- /**
- * Determine error severity based on HTTP code
- */
- protected function getErrorSeverity(int $code): string
- {
- return match(true) {
- $code === 429 => 'warning', // Rate limiting
- $code >= 400 && $code < 500 => 'error', // Client errors
- $code >= 500 => 'critical', // Server errors
- default => 'error'
- };
- }
-
- /**
- * Extract error details from response
- */
- protected function extractErrorDetails($decoded, string $body): array
- {
- $details = [
- 'message' => 'Unknown error',
- 'code' => null,
- 'details' => null
- ];
-
- if ($decoded && isset($decoded['error'])) {
- if (is_array($decoded['error'])) {
- $details['message'] = $decoded['error']['message'] ?? json_encode($decoded['error']);
- $details['code'] = $decoded['error']['code'] ?? null;
- $details['details'] = $decoded['error']['details'] ?? null;
- } else {
- $details['message'] = $decoded['error'];
- }
- } elseif ($decoded && isset($decoded['message'])) {
- $details['message'] = $decoded['message'];
- } elseif (!empty($body)) {
- $details['message'] = $body;
- }
-
- return $details;
- }
-
- /**
- * Extract rate limit information from response
- */
- protected function extractRateLimitInfo($decoded, string $body): array
- {
- $info = [
- 'retry_after' => null,
- 'limit' => null,
- 'remaining' => null,
- 'reset' => null
- ];
-
- // Try to extract from decoded response
- if ($decoded) {
- $info['retry_after'] = $decoded['retry_after'] ?? $decoded['retry-after'] ?? null;
- $info['limit'] = $decoded['x-rate-limit-limit'] ?? null;
- $info['remaining'] = $decoded['x-rate-limit-remaining'] ?? null;
- $info['reset'] = $decoded['x-rate-limit-reset'] ?? null;
- }
-
- return $info;
- }
-
- /**
- * Update error statistics
- */
- protected function updateErrorStats(int $code, string $endpoint): void
- {
- $this->error_stats['total_errors']++;
- $this->error_stats['consecutive_errors']++;
-
- // Track error types
- $error_type = $this->categorizeApiError($code);
- if (!isset($this->error_stats['error_types'][$error_type])) {
- $this->error_stats['error_types'][$error_type] = 0;
- }
- $this->error_stats['error_types'][$error_type]++;
-
- // Save stats to cache
- $this->saveErrorStats();
- }
-
- /**
- * Reset error statistics on successful request
- */
- protected function resetErrorStats(): void
- {
- $this->error_stats['consecutive_errors'] = 0;
- $this->error_stats['last_success'] = time();
- $this->is_healthy = true;
-
- $this->saveErrorStats();
- }
-
- /**
- * Get integration health status
- */
- public function getHealthStatus(): array
- {
- return [
- 'is_healthy' => $this->is_healthy,
- 'consecutive_errors' => $this->error_stats['consecutive_errors'],
- 'total_errors' => $this->error_stats['total_errors'],
- 'last_success' => $this->error_stats['last_success'],
- 'error_types' => $this->error_stats['error_types'],
- 'threshold' => $this->error_threshold
- ];
- }
-
- /**
- * Manually reset integration health
- */
- public function resetHealth(): void
- {
- $this->error_stats = [
- 'total_errors' => 0,
- 'consecutive_errors' => 0,
- 'last_success' => time(),
- 'error_types' => []
- ];
- $this->is_healthy = true;
- $this->saveErrorStats();
- }
-
- /**
- * Check integration health based on error patterns
- */
- protected function checkIntegrationHealth(): void
- {
- // Mark unhealthy if too many consecutive errors
- if ($this->error_stats['consecutive_errors'] >= $this->error_threshold) {
- $this->is_healthy = false;
-
- // Log critical health issue
- $this->logError(
- "Integration marked unhealthy after {$this->error_stats['consecutive_errors']} consecutive errors",
- [
- 'error_stats' => $this->error_stats,
- 'threshold' => $this->error_threshold
- ],
- 'critical'
- );
- }
- }
-
- /**
- * Load error statistics from cache
- */
- protected function loadErrorStats(): void
- {
- $cache_key = "error_stats_{$this->service_name}_{$this->userID}";
- $cached_stats = $this->cache->get($cache_key);
-
- if ($cached_stats) {
- $this->error_stats = array_merge($this->error_stats, $cached_stats);
-
- // Check if integration was marked unhealthy
- if ($this->error_stats['consecutive_errors'] >= $this->error_threshold) {
- $this->is_healthy = false;
- }
- }
- }
-
- /**
- * Save error statistics to cache
- */
- protected function saveErrorStats(): void
- {
- $cache_key = "error_stats_{$this->service_name}_{$this->userID}";
- $this->cache->set($cache_key, $this->error_stats, 86400); // 24 hours
- }
-
- protected function logDebug(string $message, array $context = []): void
- {
- error_log('[Integration: '.$this->service_name.']'.$message.': '.print_r($context, true));
- }
-
- /**
* Get service name
*/
public function getServiceName(): string
@@ -2703,23 +532,19 @@
return $this->title;
}
- /*********************************************************************
- RENDERING
- *********************************************************************/
-// public function renderAdditionalOptions()
-// {
-// //Default: nothing.
-// }
+ public static function title():string
+ {
+ return static::getInstance()->getTitle();
+ }
+ public static function icon():string
+ {
+ return static::getInstance()->getIcon();
+ }
- /**
- * Render additional action buttons (optional)
- * Override in integration classes that need extra actions
- */
-// public function renderAdditionalActions(): void
-// {
-// // Default: no additional actions
-// // Override in extensions for service-specific actions
-// }
+ public static function hasExtraOptions():bool
+ {
+ return static::getInstance()::$hasExtraOptions;
+ }
/**
* Get service description (optional)
@@ -2730,77 +555,6 @@
return "Manage your {$this->getServiceName()} integration settings.";
}
- /**
- * Get service key for consistency
- */
- public function getServiceKey(): string
- {
- return strtolower($this->service_name);
- }
-
- /**
- * Refresh credentials from storage and re-initialize
- */
- public function refreshCredentials(): void
- {
- $this->credentials = [];
- $this->ensureInitialized();
- }
-
- /**
- * Enhanced webhook handling with common patterns
- */
- public function handleWebhook(array $payload): bool
- {
- // Log incoming webhook for debugging
- $this->logDebug('Webhook received', [
- 'payload_keys' => array_keys($payload),
- 'timestamp' => time()
- ]);
-
- // Validate webhook signature/authenticity
- if (!$this->validateWebhook($payload)) {
- $this->logError('Webhook validation failed', [
- 'webhook_type' => 'validation_error',
- 'payload_sample' => array_slice($payload, 0, 5) // Log partial payload for debugging
- ], 'warning');
- return false;
- }
-
- // Check for duplicate processing (idempotency)
- if ($this->isWebhookProcessed($payload)) {
- return true; // Return true to prevent retries
- }
-
- try {
- // Process the webhook
- $result = $this->processWebhook($payload);
-
- // Mark as processed if successful
- if ($result) {
- $this->markWebhookProcessed($payload);
- $this->resetErrorStats();
- } else {
- $this->logError('Webhook processing returned false', [
- 'webhook_id' => $this->extractWebhookId($payload)
- ], 'warning');
- }
-
- return $result;
-
- } catch (Exception $e) {
- $this->logError('Webhook processing failed', [
- 'error' => $e->getMessage(),
- 'webhook_id' => $this->extractWebhookId($payload),
- 'trace' => $e->getTraceAsString()
- ], 'critical');
-
- // Update error stats
- $this->updateErrorStats($e->getCode() ?: 500, 'webhook');
- $this->checkIntegrationHealth();
- return false;
- }
- }
/**
* Validate webhook signature
@@ -2822,541 +576,14 @@
return hash_equals($expected, $signature);
}
- protected function validateWebhook(array $payload):bool
- {
- //Handled by child classes
- return false;
- }
- protected function processWebhook(array $payload):bool
- {
- //Handled by child classes
- return false;
- }
-
- /**
- * Check if webhook was already processed (prevent duplicates)
- */
- protected function isWebhookProcessed(array $payload): bool
- {
- $webhook_id = $this->extractWebhookId($payload);
- if (!$webhook_id) {
- return false;
- }
-
- $cache_key = "webhook_processed_{$this->service_name}_{$webhook_id}";
- return $this->cache->get($cache_key) !== null;
- }
-
- /**
- * Mark webhook as processed
- */
- protected function markWebhookProcessed(array $payload): void
- {
- $webhook_id = $this->extractWebhookId($payload);
- if ($webhook_id) {
- $cache_key = "webhook_processed_{$this->service_name}_{$webhook_id}";
- $this->cache->set($cache_key, time(), 86400); // Store for 24 hours
- }
- }
-
- /**
- * Extract unique webhook ID (override in child classes)
- */
- protected function extractWebhookId(array $payload): ?string
- {
- // Common patterns - child classes can override
- return $payload['id'] ??
- $payload['event_id'] ??
- $payload['webhook_id'] ??
- md5(serialize($payload));
- }
-
- /**
- * Register webhook endpoint
- */
- protected function registerWebhookEndpoint(): void
- {
- // Register REST API endpoint for webhooks
- add_action('rest_api_init', function() {
- register_rest_route('jvb/v1', '/webhooks/' . $this->service_name, [
- 'methods' => 'POST',
- 'callback' => [$this, 'handleWebhookRequest'],
- 'permission_callback' => '__return_true', // Webhooks come from external services
- ]);
- });
- }
-
- /**
- * Handle webhook REST API request
- */
- public function handleWebhookRequest(WP_REST_Request $request): WP_REST_Response
- {
- $payload = $request->get_params();
- $headers = $request->get_headers();
-
- // Include headers in payload for signature validation
- $payload['_headers'] = $headers;
-
- $success = $this->handleWebhook($payload);
-
- return new WP_REST_Response([
- 'success' => $success
- ], $success ? 200 : 400);
- }
-
- protected function renderWebhookUrl(): string
- {
- if (!$this->handleWebhooks || !$this->isSetUp()) {
- return '';
- }
-
- $webhook_url = rest_url('jvb/v1/webhooks/' . $this->service_name);
-
- return sprintf(
- '<div class="webhook-info">
- <h4>Webhook URL</h4>
- <code id="webhook-url-%s">%s</code>
- <p class="hint">Add this URL to your %s webhook settings</p>
- </div>',
- esc_attr($this->service_name),
- esc_html($webhook_url),
- esc_html($this->title)
- );
- }
-
- public function renderConnection(bool $return = false):string
- {
- if ($this->userID && !JVB()->userCanConnect($this->service_name, $this->userID)) {
- return '';
- }
-
- $meta = new MetaManager($this->userID, 'integrations');
- $is_connected = $this->isSetUp();
- $credentials = $this->getCredentials();
-
- $admin_only = $this->isOAuthService ? [
- 'client_id',
- 'client_secret',
- ] : [];
-
- ob_start();
- ?>
- <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">
- <h3><?php echo esc_html($this->title); ?></h3>
- <div class="setup">
- <?php if ($is_connected): ?>
- <span class="indicator connected">●</span>
- <span class="text">Set Up</span>
- <?php else: ?>
- <span class="indicator disconnected">●</span>
- <span class="text">Not Set Up</span>
- <?php endif; ?>
- </div>
- </div>
-
- <?php if ($is_connected && array_key_exists('updated_at', $credentials) && $credentials['updated_at'] > 0): ?>
- <div class="meta">
- <small>Last updated: <?php echo human_time_diff($credentials['updated_at']) . ' ago'; ?></small>
- </div>
- <?php endif; ?>
-
- <?php
- if (!empty($this->instructions)) {
- ?>
- <details>
- <summary>
- Instructions
- </summary>
- <ol>
- <?php
- foreach ($this->instructions as $instruction) {
- echo '<li>'.$instruction.'</li>';
- }
- ?>
- </ol>
- </details>
- <?php
- }
-?>
- <details class="initial-setup"<?= $is_connected?'' : ' open'?>>
- <summary>Initial Setup</summary>
- <?php
- foreach ($this->fields as $name => $config) {
- if ($is_connected && !empty($credentials[$name])) {
- if (in_array($name, $admin_only) && !current_user_can('manage_options')) {
- continue;
- }
- ?>
- <span class="label"><?=$config['label']?>:</span>
- <code>
- <?php
- if (str_contains($name, 'secret')) {
- for ($i = 1; $i<=strlen($credentials[$name]) - 8; $i++) {
- echo '*';
- }
- echo substr($credentials[$name], -8);
- } else {
- echo $credentials[$name];
- }
- ?>
- </code>
- <?php
- } else {
- $config['value'] = $credentials[$name]??'';
- $config['autocomplete'] = 'off';
- $config['base'] = $this->service_name.'_';
- $meta->render('form', $name, $config);
- }
- }
- if ($this->handleWebhooks) {
- echo $this->renderWebhookUrl();
- }
- ?>
- </details>
- <?php
-
- if ($this->isOAuthService) {
- $this->renderConnectedOAuthStatus();
- }
-
- ?>
-
- <div class="integration-content">
-
- <?php
-
-
-
- if (!empty($this->advanced)) {
- ?>
- <details>
- <summary>Advanced Settings</summary>
- <?php
- foreach ($this->advanced as $name => $config) {
- $config['value'] = $credentials[$name]??'';
- $config['base'] = $this->service_name.'_';
- $config['autocomplete'] = 'off';
- $meta->render('form', $name, $config);
- }
- ?>
- </details>
- <?php
- }
- if (!empty($this->defaults)) {
- ?>
- <a href="<?php echo admin_url('admin.php?page=jvb-integration-' . $this->service_name); ?>"
- class="button">
- More Settings
- </a>
- <?php
- }
- ?>
- </div>
- <div class="actions row btw wrap">
- <?php
- foreach ($this->buttons as $action => $label) {
- if (!$is_connected && $action !== 'save_credentials') {
- continue;
- }
- $title = $confirm = '';
- switch ($action) {
- case 'save_credentials':
- $title = $label;
- $label = jvbIcon('floppy-disk');
- break;
- case 'clear_credentials':
- $title = $label;
- $label = jvbIcon('plugs');
- $confirm = ' data-confirm="Are you sure you want to delete these credentials?"';
- break;
- case 'clear_cache':
- $title = $label;
- $label = jvbIcon('arrows-clockwise');
- break;
- }
- $title = $title === '' ? '' : ' title ="'.$title.'"';
- ?>
- <button type="button" data-action="<?=$action?>"<?=$title?><?=$confirm?>><?=$label?></button>
- <?php
- }
- ?>
- </div>
- </form>
- <?php
- $result = ob_get_clean();
- if(!$return) {
- echo $result;
- }
- return $result;
- }
-
- protected function renderConnectedOAuthStatus(): void
- {
- if (!$this->isSetup()) {
- return;
- }
- $credentials = $this->getCredentials();
- $hasCredentials = $this->hasOAuthCredentials();
- $returnURL = is_admin() ? admin_url('admin.php?page=jvb-integrations') : (get_the_permalink() ?: home_url());
- ?>
-
- <details <?= $hasCredentials?' open':''?>>
- <summary>
- <?php if ($hasCredentials) { ?>
- Connected Account
- <?php } else { ?>
- <div class="oauth-connect">
- <a href="<?php echo esc_url($this->getOAuthUrl($returnURL)); ?>"
- class="button button-primary jvb-oauth-connect"
- data-service="<?php echo esc_attr($this->service_name); ?>">
- <?php echo jvbIcon($this->icon); ?>
- Authorize Connection
- </a>
- </div>
- <?php } ?>
- <div class="connection-status <?= $hasCredentials ? 'connected' : 'disconnected' ?>">
- <span class="status-indicator">●</span>
- <span><?= $hasCredentials ? 'Connected' : 'Not Connected' ?></span>
- </div>
- </summary>
- <label>OAuth Redirect URL:</label>
- <code>
- <?= $this->getRedirectUri(); ?>
- </code>
-
-
- <?php if (!empty($credentials['updated_at'])): ?>
- <div class="oauth-meta">
- <small>Token expires: <?php
- echo isset($credentials['expires_at'])
- ? human_time_diff($credentials['expires_at'])
- : 'Never';
- ?></small>
- </div>
- <?php endif;
- // Allow child classes to add service-specific connected UI
- $this->renderOAuthConnectedOptions();
- ?>
- </details>
- <?php
- }
-
- protected function renderOAuthConnectedOptions():void
- {
-
- }
-
- /**
- * Get last tested timestamp - extends your existing pattern
- */
- protected function getLastTestedTime(): ?int
- {
- $key = $this->userID
- ? "jvb_integration_{$this->service_name}_last_test_user_{$this->userID}"
- : "jvb_integration_{$this->service_name}_last_test";
- return get_option($key, null);
- }
/**
* Update last tested time
*/
public function updateLastTestedTime(): void
{
- $key = $this->userID
- ? "jvb_integration_{$this->service_name}_last_test_user_{$this->userID}"
- : "jvb_integration_{$this->service_name}_last_test";
- update_option($key, time());
- }
-
- public function handleAdminPost(): void
- {
- if (!current_user_can('manage_options')) {
- wp_die('Insufficient permissions');
- }
-
- $service = $this->getServiceName();
-
- // Verify nonce
- $nonce_field = 'jvb_integration_nonce_' . $service;
- $nonce_action = 'jvb_integration_save_' . $service;
- if (!isset($_POST[$nonce_field]) || !wp_verify_nonce($_POST[$nonce_field], $nonce_action)) {
- wp_die('Security check failed');
- }
-
- // Get the action type
- $action_type = sanitize_text_field($_POST['action_type'] ?? 'save');
-
- // Prepare the request in the format handleAjaxRequest expects
- $_REQUEST['action'] = $action_type;
- $_REQUEST['service'] = $service;
-
- // Copy all POST data to REQUEST
- foreach ($_POST as $key => $value) {
- $_REQUEST[$key] = $value;
- }
-
- // Set up for JSON response capture
- ob_start();
-
- try {
- // Call the existing AJAX handler
- $this->handleAjaxRequest();
- $response = ob_get_clean();
-
- // Parse the JSON response
- $result = json_decode($response, true);
-
- if ($result && isset($result['success'])) {
- if ($result['success']) {
- $message = $result['message'] ?? ucfirst($service) . ' settings saved successfully!';
- $this->setAdminNotice($message, 'success');
- } else {
- $message = $result['message'] ?? 'Failed to save ' . ucfirst($service) . ' settings.';
- $this->setAdminNotice($message, 'error');
- }
- } else {
- // If no proper JSON response, check if connection worked
- if ($action_type === 'test') {
- $connected = $this->testConnection();
- $message = $connected ? 'Connection successful!' : 'Connection failed. Please check your credentials.';
- $this->setAdminNotice($message, $connected ? 'success' : 'error');
- }
- }
- } catch (Exception $e) {
- ob_end_clean();
- $this->setAdminNotice('Error: ' . $e->getMessage(), 'error');
- }
-
- // Redirect back to integrations page
- wp_redirect(admin_url('admin.php?page=jvb-integrations'));
- exit;
- }
-
- /**
- * Set admin notice using transients for redirect
- */
- protected function setAdminNotice(string $message, string $type = 'info'): void
- {
- $notices = get_transient('jvb_admin_notices') ?: [];
- $notices[] = [
- 'message' => $message,
- 'type' => $type === 'success' ? 'updated' : 'error'
- ];
- set_transient('jvb_admin_notices', $notices, 30);
- }
-
- /**
- * Display admin notices from transient
- */
- public static function displayAdminNotices(): void
- {
- $notices = get_transient('jvb_admin_notices');
-
- if ($notices) {
- foreach ($notices as $notice) {
- ?>
- <div class="notice notice-<?php echo esc_attr($notice['type']); ?> is-dismissible">
- <p><?php echo esc_html($notice['message']); ?></p>
- </div>
- <?php
- }
- delete_transient('jvb_admin_notices');
- }
- }
-
- public function hasDefaults():bool
- {
- return !empty($this->defaults);
- }
-
- public function renderDefaults():void
- {
- $types = $this->enabledContentTypes();
- if (empty($types)) {
- return;
- }
- $meta = new MetaManager($this->userID, 'integrations');
- ?>
- <form>
- <h1><?= $this->title?> Defaults:</h1>
- <p>Find yourself constantly repeating yourself?</p>
- <p>Set defaults for different content types and <?=$this->title?>!</p>
- <?php
- foreach ($this->defaults as $name => $config) {
- $config['required'] = false;
-
- $config['base'] = $this->service_name.'_';
- $config['autocomplete'] = 'off';
- $meta->render('form', $name, $config);
- }
- foreach ($this->syncPostTypes as $type) {
- $config = JVB_CONTENT[$type];
- ?>
- <details>
- <summary><?= jvbIcon($config['icon']) ?><?= $config['singular']?> Defaults</summary>
- <?php
- $fields = new \JVBase\registry\providers\IntegrationFieldProvider();
- $fields = $fields->getIntegrationFields($this->service_name, $config);
- foreach($fields as $name=>$c) {
- $c['required'] = false;
- if ($c['type'] === 'number') {
- $c['type'] = 'text';
- $c['subtype'] = 'number';
- }
- if (array_key_exists('description', $c)) {
- $c['hint'] = $c['description'];
- unset($c['description']);
- }
- $meta->render('form', $name, $c);
- }
- ?>
- </details>
- <?php
- }
- ?>
- </form>
- <?php
- }
-
- public function hasContent():bool
- {
- return $this->has_content;
- }
- public function getDefaultContentType():string
- {
- return $this->defaultContent;
- }
-
- public function enabledContentTypes():array
- {
- if (!$this->has_content) {
- return [];
- }
-
- $key = BASE.$this->service_name.'_enabled_content_types';
- $enabled = get_option($key);
- if (!$enabled) {
- $enabled = [];
- foreach (array_merge(JVB_CONTENT, JVB_TAXONOMY) as $content => $config) {
- if (!array_key_exists('integrations', $config)) {
- continue;
- }
- if (!array_key_exists($this->service_name, $config['integrations'])) {
- 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;
- }
- }
- update_option($key, $enabled);
- }
- return $enabled;
+ $cred = Auth::getInstance();
+ $cred->updateTested($this->service_name, $this->userID);
}
protected function getSupportedImage(int $imgID):int
@@ -3376,153 +603,29 @@
if ($jpegVersion !== '' && is_int($jpegVersion)) {
return $jpegVersion;
}
-
- $converted = $this->convertWebpToJpeg($imgID);
- if ($converted) {
- update_post_meta($imgID, BASE . 'jpeg_version', $converted);
- return $converted;
+ $uploader = new UploadManager();
+ $converted = $uploader->convertImageTo($imgID, 'jpeg', 80, false);
+ if ($converted && !is_wp_error($converted)) {
+ update_post_meta($imgID, BASE . 'jpeg_version', $converted['attachment_id']);
+ return $converted['attachment_id'];
}
return $imgID;
}
+
/**
- * Convert WebP image to JPEG
- *
- * @param int $webpAttachmentId
- * @return int|false Image ID on success, false on failure.
+ * Used by JVBase\registrar\helpers\AddIntegrationFields.php
+ * @return array
*/
- protected function convertWebpToJpeg(int $webpAttachmentId):int|false
+ public function getAdditionalFields(?string $content_type = null):array
{
- try {
- $webpPath = get_attached_file($webpAttachmentId);
-
- if (!file_exists($webpPath)) {
- throw new Exception('WebP file not found');
- }
-
- // Generate JPEG filename
- $pathInfo = pathinfo($webpPath);
- $jpegPath = $pathInfo['dirname'] . '/' . $pathInfo['filename'] . '_jpeg.jpg';
-
- // Check if already exists (shouldn't happen due to meta check, but safety first)
- if (file_exists($jpegPath)) {
- // Try to find existing attachment
- global $wpdb;
- $existingId = $wpdb->get_var($wpdb->prepare(
- "SELECT ID FROM {$wpdb->posts} WHERE guid = %s",
- str_replace(ABSPATH, site_url('/'), $jpegPath)
- ));
-
- if ($existingId) {
- return (int)$existingId;
- }
- }
-
- // Perform conversion based on available libraries
- if (extension_loaded('imagick')) {
- $this->convertWebpToJpegImagick($webpPath, $jpegPath);
- } else {
- $this->convertWebpToJpegGd($webpPath, $jpegPath);
- }
-
- // Create attachment for the JPEG
- $attachmentData = [
- 'post_mime_type' => 'image/jpeg',
- 'post_title' => get_the_title($webpAttachmentId) . ' (JPEG)',
- 'post_content' => '',
- 'post_status' => 'inherit'
- ];
-
- $jpegAttachmentId = wp_insert_attachment($attachmentData, $jpegPath);
-
- if (is_wp_error($jpegAttachmentId)) {
- throw new Exception('Failed to create JPEG attachment: ' . $jpegAttachmentId->get_error_message());
- }
-
- // Generate metadata
- require_once(ABSPATH . 'wp-admin/includes/image.php');
- $attachData = wp_generate_attachment_metadata($jpegAttachmentId, $jpegPath);
- wp_update_attachment_metadata($jpegAttachmentId, $attachData);
-
- // Copy alt text and other meta
- $altText = get_post_meta($webpAttachmentId, '_wp_attachment_image_alt', true);
- if ($altText) {
- update_post_meta($jpegAttachmentId, '_wp_attachment_image_alt', $altText);
- }
-
- return $jpegAttachmentId;
-
- } catch (Exception $e) {
- $this->logError('Image conversion failed...', [
- 'method' => 'convertWebpToJpeg'
- ]);
- return false;
- }
+ return [];
}
- /**
- * Convert WebP to JPEG using Imagick
- */
- protected function convertWebpToJpegImagick(string $source, string $destination): void
+ public function getIcon():string
{
- $image = new \Imagick($source);
- $image->setImageFormat('jpeg');
-
- // Set quality (using 85 for good balance of quality/size)
- $image->setImageCompressionQuality(85);
-
- // Remove alpha channel and set white background
- $image->setImageBackgroundColor('white');
- $image->setImageAlphaChannel(\Imagick::ALPHACHANNEL_REMOVE);
- $image->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
-
- $image->writeImage($destination);
- $image->clear();
+ return $this->icon;
}
- /**
- * Convert WebP to JPEG using GD
- */
- protected function convertWebpToJpegGd(string $source, string $destination): void
- {
- $image = imagecreatefromwebp($source);
-
- if ($image === false) {
- throw new Exception('Failed to create image from WebP source');
- }
-
- // Get dimensions
- $width = imagesx($image);
- $height = imagesy($image);
-
- // Create new true color image
- $jpegImage = imagecreatetruecolor($width, $height);
-
- // Set white background (for transparency)
- $white = imagecolorallocate($jpegImage, 255, 255, 255);
- imagefill($jpegImage, 0, 0, $white);
-
- // Copy WebP image to JPEG
- imagecopy($jpegImage, $image, 0, 0, 0, 0, $width, $height);
-
- // Save as JPEG (quality 85)
- $result = imagejpeg($jpegImage, $destination, 85);
-
- // Clean up
- imagedestroy($image);
- imagedestroy($jpegImage);
-
- if (!$result) {
- 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;
- }
}
--
Gitblit v1.10.0