'', 'auth' => ''] /** * @var array An array of endpoints objects, with optional create, update, delete, batch, and batchImport */ protected array $apiEndpoints = []; // Valid endpoint paths for this service protected int $refresh_interval = 0; //seconds before expiry to refresh tokens. 0 to disable /** * Credentials & State */ protected array $allowedContent = []; /** * Caching Configuration */ 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 function __construct(?int $userID = null) { $this->cacheName = $this->cacheName ?: $this->service_name; $this->userID = $userID; $this->cache = Cache::for('integrations_' . $this->cacheName); $this->getPostTypes(); $this->getTaxonomies(); $this->getUsers(); if (method_exists($this, 'setContentTypes')) { $this->setContentTypes(); } $this->registerHooks(); if (method_exists($this, 'setQueueTypes')) { $this->setQueueTypes(); } $this->initializeRateLimiters(); if (method_exists($this, 'initializeActions')) { $this->initializeActions(); } if (method_exists($this, 'addOAuthActions')) { $this->addOAuthActions(); } } protected function getPostTypes(): void { $this->syncPostTypes = Registrar::withIntegration($this->service_name, 'post'); } protected function getUsers():void { $this->syncUsers = Registrar::withIntegration($this->service_name, 'user'); } protected function getTaxonomies():void { $this->syncTaxonomies = Registrar::withIntegration($this->service_name, 'term'); } /********************************************************************* * ABSTRACT METHODS - MUST BE IMPLEMENTED BY CHILD CLASSES *********************************************************************/ /** * Initialize the integration with loaded credentials * * Called after credentials are loaded. Use this to: * - Set up API endpoints with dynamic values (e.g., account IDs) * - Initialize service-specific configurations * - Validate credentials format * * @return void */ abstract protected function initialize(): void; /** * Render the connection settings form * * Output HTML form fields for configuring this integration. * Use the provided $credentials array to populate existing values. * * Guidelines: * - Use proper escaping (esc_attr, esc_html, etc.) * - Include helpful descriptions for each field * - Mark required fields clearly * - Use appropriate input types (password for secrets, url for endpoints) * * @param array $credentials Current credentials (may be empty) * @return void */ // abstract public function renderConnectionSettings(): void; /********************************************************************* * OPTIONAL OVERRIDE METHODS - IMPLEMENT AS NEEDED *********************************************************************/ /** * Save credentials using Auth */ public function saveCredentials(array $credentials, bool $test = true):array { try { // Process and validate credentials if (!$this->validateCredentials($credentials)) { $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); // Test the connection before saving if ($test) { if (!$this->testConnection(true)) { $this->logError('saveCredentials', 'Connection test failed'); return $this->response(false, 'Connection failed. Please check your credentials'); } } // Connection successful, save credentials $stored = Auth::getInstance()->storeCredentials( $this->service_name, $credentials, $this->userID ); if ($stored) { $this->updateLastTestedTime(); $this->clearCache(); return $this->response(true, 'Credentials validated and saved successfully'); } return $this->response(false, 'Failed to save credentials to database'); } catch (\Exception $e) { // Revert to old credentials on any error $old = Auth::getInstance()->getCredentials( $this->service_name, $this->userID ); return $this->response(false, 'Validation error: '.print_r($e->getMessage(), true)); } } /** * Delete credentials */ public function deleteCredentials():array { $success = Auth::getInstance()->deleteCredentials($this->service_name, $this->userID); return $this->response(true, 'Deleted successfully'); } /** * Validate credentials before storing * * Override to add service-specific validation logic. * Check for required fields, format validation, etc. * * @param array $credentials Credentials to validate * @return bool True if valid */ protected function validateCredentials(array $credentials):bool { // Default: check that credentials is not empty if (empty($credentials)) { return false; } return true; } /** * Sanitize credentials before storing * * Override to clean/format credentials before storage. * Remove whitespace, normalize URLs, etc. * * @param array $credentials Raw credentials from form * @return array Sanitized credentials */ protected function sanitizeCredentials(array $credentials): array { $sanitized = []; foreach ($credentials as $key => $value) { if (is_string($value)) { $sanitized[$key] = sanitize_text_field($value); } else { $sanitized[$key] = $value; } } return $sanitized; } /** * Test connection to the external service * * Override to implement service-specific connection testing. * This method includes caching by default. * * @return bool True if connection successful */ public function testConnection(bool $force = false): bool { if (!$this->isSetUp()) { return false; } $this->ensureInitialized(); if (empty($this->credentials)) { return false; } // Cache test results to avoid excessive API calls $cacheKey = "connection_test_$this->service_name" . ($this->userID ? "_$this->userID" : ''); $cached = $this->cache->get($cacheKey); if ($cached !== false && !$force) { return (bool)$cached; } try { if ($this->isOAuthService && !$this->hasOAuthCredentials()){ //If this is an OAuth service, we might only be saving the app credentials first $result = true; } else { $result = $this->performConnectionTest(); } $this->updateLastTestedTime(); $this->cache->set($cacheKey, $result, 300); // Cache for 5 minutes return $result; } catch (Exception $e) { $this->logError('Connection test failed', ['error' => $e->getMessage()]); $this->cache->set($cacheKey, false, 60); // Cache failure for 1 minute return false; } } protected function clearCache():array { $this->cache->flush(); return [ 'success' => true, ]; } /** * Perform actual connection test * * Override this method to implement the actual connection test logic. * Default implementation returns true if credentials exist. * * @return bool True if connection successful * @throws Exception If connection fails */ protected function performConnectionTest(): bool { // Override in child class with actual test // Example: make a simple API call to verify credentials return !empty($this->credentials); } /** * Register additional WordPress hooks * * Override to register service-specific hooks beyond the default ones. * Called during construction after base hooks are registered. * * @return void */ protected function registerAdditionalHooks(): void { // Override in child classes to add service-specific hooks } /****************************************************************** POST SYNC ******************************************************************/ /********************************************************************* * SYNC METHODS *********************************************************************/ /** * Queue a sync operation for processing, utilizing the OperationQueue.php * * @param string $type Operation type (sync_post, delete_post, etc.) * @param array $data Operation data * @param array $options Operation options (scheduled time, priority, etc.) * @return bool True if queued successfully */ public function queueOperation( string $type, array $data, array $options = [] ):bool { $queue = JVB()->queue(); $queued = $queue->queueOperation( $type, $this->userID ?? 0, array_merge($data, ['service' => $this->service_name, 'user' => $this->userID??0]), array_merge([ 'priority' => 'normal', 'chunk_key' => $options['batch_field'] ?? null, 'chunk_size' => $options['batch_size'] ?? 10 ], $options) ); return (!is_wp_error($queued)); } /** * The filter called by OperationQueue.php processOperation * 1) Test if the operation type is the type we set in queueOperation * I usually do a switch ($operation->type) { * case strtolower($this->service_name. '_update_post'): * return $this->processPostUpdate($data); * default: * return $result; * } * 2) Process the data * 3) Return an array in this format: * [ * 'success' => true|false, * 'result' => []//anything we should pass to the operation queue. If we have any dependent operations, it will refer to this data to proceed * ] * @param WP_Error|array $result * @param object $operation * @param array $data * @return WP_Error|array */ public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array { return $result; } /********************************************************************* * UTILITY METHODS *********************************************************************/ /** * Register WordPress hooks based on capabilities */ protected function registerHooks(): void { //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)) { $this->addSavePost(); add_action('transition_post_status', [$this, 'handlePostStatusTransition'], 10, 3); if ($this->canSync['delete']) { add_action('before_delete_post', [$this, 'handleDeletePost'], 10, 1); } } if (!empty($this->syncTaxonomies)) { add_action('saved_term', [$this, 'handleSaveTerm'], 20, 5); if ($this->canSync['delete']) { add_action('pre_delete_term', [$this, 'handleDeleteTerm'], 10, 2); } } if (method_exists($this, 'registerQueueTypes')) { add_action('init', [$this, 'registerQueueTypes'], 10); } } /** * Handle connection setup and credential storage * * @param array $credentials The credentials to save * @return array Result with 'success' and 'message' keys */ public function handleConnection(array $credentials): array { try { // Sanitize credentials $sanitized = $this->sanitizeCredentials($credentials); // Validate if needed if (method_exists($this, 'validateCredentials')) { if (!$this->validateCredentials($sanitized)) { return ['success' => false, 'message' => 'Invalid Credentials']; } } // Store credentials $credentials = array_merge($this->loadCredentials() ?? [], $sanitized); $credentials['last_updated'] = time(); // Save to database $saved = $this->saveCredentials($credentials); if (!$saved) { return [ 'success' => false, 'message' => 'Failed to save credentials to database' ]; } // Test connection if method exists if (method_exists($this, 'testConnection')) { if (!$this->testConnection()) { // Still save but warn about connection return [ 'success' => true, 'message' => 'Credentials saved but connection test failed:' ]; } } return [ 'success' => true, 'message' => 'Connection established successfully' ]; } catch (\Exception $e) { return [ 'success' => false, 'message' => 'Error: ' . $e->getMessage() ]; } } public function getCredentials(): array { return $this->loadCredentials(); } public function hasOAuthCredentials(?int $userID = null): bool { if ($userID !== $this->userID) { $this->switchUser($userID); } 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; } $this->initialize(); } /** * Generate webhook signature key for services that require it * @return string */ protected function generateWebhookSignature(): string { return wp_generate_password(32, false); } /******************************************************************* * UTILITIES *******************************************************************/ /** * Get service name */ public function getServiceName(): string { return $this->service_name; } public function getTitle():string { return $this->title; } public static function title():string { return static::getInstance()->getTitle(); } public static function icon():string { return static::getInstance()->getIcon(); } public static function hasExtraOptions():bool { return static::getInstance()::$hasExtraOptions; } /** * Get service description (optional) * Override in integration classes for custom descriptions */ public function getServiceDescription(): string { return "Manage your {$this->getServiceName()} integration settings."; } /** * Validate webhook signature * @param string $payload Raw payload body * @param string $signature Signature from headers * @param string $secret Secret key * @param string $algorithm Algorithm used (default: sha256) * @return bool */ protected function verifyWebhookSignature(string $payload, string $signature, string $secret, string $algorithm = 'sha256'): bool { if (empty($signature) || empty($secret)) { return false; } $expected = hash_hmac($algorithm, $payload, $secret); // Use hash_equals for timing-safe comparison return hash_equals($expected, $signature); } /** * Update last tested time */ public function updateLastTestedTime(): void { $cred = Auth::getInstance(); $cred->updateTested($this->service_name, $this->userID); } protected function getSupportedImage(int $imgID):int { //If this integration supports webp, we can just send the original image id if ($this->supportsWebp) { return $imgID; } //Test if it is in webp format $mimeType = get_post_mime_type($imgID); if ($mimeType !== 'image/webp') { return $imgID; } //Test if we already have converted this image $jpegVersion = get_post_meta($imgID, BASE.'jpeg_version', true); if ($jpegVersion !== '' && is_int($jpegVersion)) { return $jpegVersion; } $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; } /** * Used by JVBase\registrar\helpers\AddIntegrationFields.php * @return array */ public function getAdditionalFields(?string $content_type = null):array { return []; } public function getIcon():string { return $this->icon; } }