| | |
| | | namespace JVBase\integrations; |
| | | |
| | | use Exception; |
| | | use JVBase\managers\CacheManager; |
| | | use JVBase\managers\UploadManager; |
| | | use JVBase\meta\MetaManager; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\queue\executors\IntegrationExecutor; |
| | | use JVBase\managers\queue\mergers\DefaultMerger; |
| | | use JVBase\managers\queue\TypeConfig; |
| | | use JVBase\meta\Form; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\managers\ErrorHandler; |
| | | use JVBase\registrar\helpers\AddIntegrationFields; |
| | | use JVBase\registrar\Registrar; |
| | | use WP_Error; |
| | | use WP_Post; |
| | | use WP_Query; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | | use WP_Term; |
| | | use WP_User; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | |
| | | */ |
| | | abstract class Integrations |
| | | { |
| | | //Flag to allow for custom settings (defaults, etc) for an integration in the dashboard |
| | | public static bool $hasExtraOptions = false; |
| | | public bool $hasBatchCreate = false; |
| | | public bool $hasBatchUpdate = false; |
| | | public bool $hasBatchDelete = false; |
| | | public bool $canCreateOnUpdate = false; |
| | | /** |
| | | * Queue types |
| | | * These types match with IntegrationExecutor |
| | | */ |
| | | protected static string $syncTo; |
| | | protected static string $deleteFrom; |
| | | protected static string $syncFrom; |
| | | protected static string $syncCustomer; |
| | | protected static string $import; |
| | | /** |
| | | * API Configuration |
| | | * These properties define how the integration connects to external services |
| | |
| | | * 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 |
| | | 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 |
| | |
| | | 'test_connection' => 'Test Connection', |
| | | ]; |
| | | |
| | | protected array $allowedContent = []; |
| | | |
| | | /** |
| | | * Caching Configuration |
| | | */ |
| | | protected ?string $cacheName = null; |
| | | protected CacheManager $cache; |
| | | protected Cache $cache; |
| | | protected array $cacheStrategy = [ |
| | | 'aggressive' => 3600, // 1 hour for stable data (e.g., profile info) |
| | | 'moderate' => 300, // 5 minutes for semi-dynamic data (e.g., posts) |
| | |
| | | 'update' => false, // Can update already-shared posts |
| | | 'delete' => false, // Can remove posts from the service |
| | | ]; |
| | | protected array $syncPostTypes = []; // Post types that can be synced (e.g., ['artwork', 'tattoo']): usually built by querying the global JVB_CONTENT if the integration name exists as a key in [ 'integrations' => []] |
| | | protected array $syncTaxonomies = []; // Post types that can be synced (e.g., ['artwork', 'tattoo']): usually built by querying the global JVB_CONTENT if the integration name exists as a key in [ 'integrations' => []] |
| | | protected array $syncPostTypes = []; // Post types that can be synced (e.g., ['artwork', 'tattoo']): usually built by Registrar.php if the integration name exists as a key in [ 'integrations' => []] |
| | | protected array $syncTaxonomies = []; // Post types that can be synced (e.g., ['artwork', 'tattoo']): usually built by Registrar.php if the integration name exists as a key in [ 'integrations' => []] |
| | | protected array $contentTypes = []; // Integration's available content types. Set by child classes' getContentTypes |
| | | protected bool $has_content = false; // Whether integration has content that can sync |
| | | /** |
| | |
| | | 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 = CacheManager::for('integrations_' . $this->cacheName, $this->ttl); |
| | | $this->cache = Cache::for('integrations_' . $this->cacheName, $this->ttl); |
| | | |
| | | // Load error stats from cache |
| | | $this->loadErrorStats(); |
| | |
| | | ]; |
| | | } |
| | | |
| | | self::$syncTo = $this->service_name.'_sync_to'; |
| | | self::$deleteFrom = $this->service_name.'_delete_from'; |
| | | self::$syncFrom = $this->service_name.'_sync_from'; |
| | | //TODO: What is the difference between sync_from and import? |
| | | self::$import = $this->service_name.'import_from'; |
| | | |
| | | add_filter('jvbShouldRenderMeta', [$this, 'checkRenderField'], 10, 4); |
| | | } |
| | | |
| | | protected function addFilters():bool |
| | | { |
| | | return is_null($this->userID); |
| | | } |
| | | |
| | | protected function setContentTypes():void |
| | | { |
| | | |
| | |
| | | if (!empty($this->credentials['expires_at'])) { |
| | | $expires_at = intval($this->credentials['expires_at']); |
| | | if ($expires_at <= time()) { |
| | | // Token expired, try to refresh |
| | | if (!empty($this->credentials['refresh_token'])) { |
| | | return $this->refreshOAuthToken(); |
| | | } |
| | | return false; |
| | | } |
| | | } |
| | |
| | | |
| | | protected function getPostTypes(): void |
| | | { |
| | | $key = BASE . $this->service_name . '_sync_post_types'; |
| | | $postTypes = get_option($key, false); |
| | | if (!$postTypes) { |
| | | $postTypes = []; |
| | | |
| | | // Get from JVB_CONTENT |
| | | |
| | | foreach (JVB_CONTENT as $type => $config) { |
| | | if (array_key_exists('integrations', $config) && array_key_exists($this->service_name, $config['integrations'])) { |
| | | $postTypes[] = $type; |
| | | } |
| | | } |
| | | |
| | | update_option($key, $postTypes); |
| | | } |
| | | |
| | | $this->syncPostTypes = $postTypes; |
| | | $this->syncPostTypes = Registrar::withIntegration($this->service_name); |
| | | } |
| | | |
| | | protected function getTaxonomies():void |
| | |
| | | if (!$taxonomies) { |
| | | // Combine both content and taxonomy filtering |
| | | $taxonomies = []; |
| | | // Get from JVB_TAXONOMY (content taxonomies) |
| | | foreach (JVB_TAXONOMY as $type => $config) { |
| | | if (jvbCheck('is_content', $config) && |
| | | isset($config['integrations'][$this->service_name])) { |
| | | $taxonomies[] = $type; |
| | | foreach (Registrar::withFeature('is_content', 'term') as $type) { |
| | | $registrar = Registrar::getInstance($type); |
| | | if ($registrar->hasIntegration($this->service_name)) { |
| | | $taxonomies[] = $registrar->getSlug(); |
| | | } |
| | | } |
| | | |
| | |
| | | } else { |
| | | $result = $this->$method(); |
| | | } |
| | | error_log('Action result: '.print_r($result, true)); |
| | | if (is_wp_error($result)) { |
| | | return [ |
| | | 'success' => false, |
| | |
| | | } |
| | | |
| | | try { |
| | | error_log('Credentials to save: '.print_r($this->credentials, true)); |
| | | if ($this->isOAuthService && !$this->hasOAuthCredentials()){ |
| | | error_log('Just saving credentials, we don\'t have OAuth setup yet...'); |
| | | //If this is an OAuth service, we might only be saving the app credentials first |
| | | $result = true; |
| | | } else { |
| | |
| | | |
| | | protected function clearCache():array |
| | | { |
| | | $success = $this->cache->clear(); |
| | | $this->cache->flush(); |
| | | return [ |
| | | 'success' => $success, |
| | | 'success' => true, |
| | | ]; |
| | | } |
| | | |
| | |
| | | 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, [ |
| | |
| | | return new WP_Error('rate_limit', 'Rate limit exceeded. Please try again later.'); |
| | | } |
| | | |
| | | // Debug: Check if credentials are loaded |
| | | error_log('['.$this->service_name.'] Make Request - Credentials loaded: ' . (!empty($this->credentials) ? 'Yes' : 'No')); |
| | | error_log('With Credentials: '.print_r($this->credentials, true)); |
| | | |
| | | $attempt = 0; |
| | | $lastError = null; |
| | | |
| | | while ($attempt < $this->maxRetries) { |
| | | try { |
| | | $this->logDebug('[Integrations] Making request to '.$this->service_name); |
| | | // $this->logDebug('[Integrations] Making request to '.$this->service_name); |
| | | $result = $this->executeRequest($method, $endpoint, $data, $baseKey, $options); |
| | | if (!$result) { |
| | | return new WP_Error('Request Error'); |
| | | } |
| | | $this->recordRequest($method, $endpoint); |
| | | $this->logDebug('[Integrations]', [ |
| | | 'response' => $result |
| | | ]); |
| | | // $this->logDebug('[Integrations]', [ |
| | | // 'response' => $result |
| | | // ]); |
| | | // Reset error stats on success |
| | | $this->resetErrorStats(); |
| | | return $result; |
| | |
| | | return null; |
| | | } |
| | | |
| | | $this->logDebug("$method request to: $url: ".print_r($args, true)); |
| | | // $this->logDebug("$method request to: $url: ".print_r($args, true)); |
| | | |
| | | // Make the request |
| | | $response = match($method) { |
| | |
| | | if ($retry_count === 0) { |
| | | $retry_count++; |
| | | |
| | | $this->logDebug('Got 401, attempting token refresh...'); |
| | | // $this->logDebug('Got 401, attempting token refresh...'); |
| | | if ($this->refreshOAuthToken()) { |
| | | $this->logDebug('Token refreshed successfully, retrying request...'); |
| | | // $this->logDebug('Token refreshed successfully, retrying request...'); |
| | | |
| | | // Rebuild request args with new token |
| | | $args = $this->buildRequestArgs($method, $data, $options); |
| | |
| | | bool $force = false |
| | | ): ?array |
| | | { |
| | | $cacheKey = $this->buildCacheKey('GET', $endpoint, $params); |
| | | $ttl = $this->cacheStrategy[$cacheStrategy] ?? $this->ttl; |
| | | $cacheKey = $this->buildCacheKey('GET', $endpoint, $params, $baseKey); |
| | | |
| | | $ttl = is_int($cacheStrategy) |
| | | ? max(0, $cacheStrategy) |
| | | : ($this->cacheStrategy[$cacheStrategy] ?? $this->ttl); |
| | | |
| | | if (!$force && $ttl > 0) { |
| | | $cached = $this->cache->get($cacheKey); |
| | | if ($cached !== false) { |
| | | $this->logDebug("Cache hit for: $cacheKey"); |
| | | |
| | | // $this->logDebug("Cache hit for: $cacheKey"); |
| | | return $cached; |
| | | } |
| | | } |
| | |
| | | |
| | | return $result; |
| | | } |
| | | |
| | | /** |
| | | * Check if response contains an error |
| | | * Override in child classes for service-specific error detection |
| | |
| | | ):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([ |
| | |
| | | */ |
| | | 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 (is_null($this->userID)) { |
| | | 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 && is_null($this->userID)){ |
| | | $this->registerWebhookEndpoint(); |
| | | } |
| | | // Let child classes register additional hooks if needed |
| | | $this->registerAdditionalHooks(); |
| | | |
| | | if (!empty($this->syncPostTypes)) { |
| | | add_action('save_post', [$this, 'handleSavePost'], 20, 3); |
| | | if (!empty($this->syncPostTypes) && is_null($this->userID)) { |
| | | $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)) { |
| | | if (!empty($this->syncTaxonomies) && is_null($this->userID)) { |
| | | add_action('saved_term', [$this, 'handleSaveTerm'], 20, 5); |
| | | if ($this->canSync['delete']) { |
| | | add_action('pre_delete_term', [$this, 'handleDeleteTerm'], 10, 2); |
| | | } |
| | | } |
| | | } |
| | | |
| | | add_action('init', [$this, 'registerQueueTypes'], 10); |
| | | } |
| | | public function addSavePost():void |
| | | { |
| | | if (!has_action('save_post', [$this, 'handleSavePost'])) { |
| | | add_action('save_post', [$this, 'handleSavePost'], 20, 3); |
| | | } |
| | | } |
| | | public function removeSavePost():void |
| | | { |
| | | remove_action('save_post', [$this, 'handleSavePost'], 20, 3); |
| | | } |
| | | |
| | | |
| | | public function registerQueueTypes():void |
| | | { |
| | | if (empty($this->syncTaxonomies) && empty($this->syncPostTypes)) { |
| | | return; |
| | | } |
| | | $queue = JVB()->queue(); |
| | | $executor = new IntegrationExecutor(); |
| | | |
| | | $queue->registry()->register(self::$syncTo, new TypeConfig( |
| | | mergeable: new DefaultMerger('items'), |
| | | executor: $executor, |
| | | chunkKey: 'items', |
| | | chunkSize: 50, |
| | | maxRetries: 3, |
| | | )); |
| | | |
| | | if ($this->canSync['delete']) { |
| | | $queue->registry()->register(self::$deleteFrom, new TypeConfig( |
| | | mergeable: new DefaultMerger('external_ids'), |
| | | executor: $executor, |
| | | chunkKey: 'external_ids', |
| | | chunkSize: 200, |
| | | maxRetries: 2 |
| | | )); |
| | | } |
| | | |
| | | $queue->registry()->register(self::$syncFrom, new TypeConfig( |
| | | executor: $executor, |
| | | maxRetries: 3 |
| | | )); |
| | | |
| | | $this->registerAdditionalQueueTypes($executor); |
| | | } |
| | | protected function registerAdditionalQueueTypes(IntegrationExecutor $executor):void |
| | | { |
| | | //Empty. Integration extensions can register additional operation types from here. |
| | | } |
| | | public function handleAjaxResponse() |
| | | { |
| | | error_log('Ajax Response: '.print_r($_GET, true)); |
| | | |
| | | $code = $_GET['code']; |
| | | $state = $_GET['state']; |
| | | |
| | | error_log('OAuth Callback - Code: ' . $code); |
| | | error_log('OAuth Callback - State: ' . $state); |
| | | |
| | | |
| | | $state_parts = explode('|', $state); |
| | | $state_key = $state_parts[0] ?? ''; |
| | |
| | | $user_id = ($user_id === 0) ? null : $user_id; |
| | | $return_url = isset($state_parts[2]) ? base64_decode($state_parts[2]) : admin_url('admin.php?page=jvb-integrations'); |
| | | |
| | | error_log('Service: '.print_r($this->service_name, true)); |
| | | $state_data = get_transient('oauth_state_' . $state_key); |
| | | error_log('State Data: '.print_r($state_data, true)); |
| | | if (!$state_data || $state_data['service'] !== $this->service_name) { |
| | | wp_die('Invalid state parameter', 'OAuth Error'); |
| | | } |
| | | |
| | | // Delete the transient to prevent reuse |
| | | delete_transient('oauth_state_' . $state_key); |
| | | error_log('Return URL: '.print_r($return_url, true)); |
| | | // Handle error from OAuth provider |
| | | if (array_key_exists('error', $_GET)) { |
| | | $error_description = $_GET['error_description'] ?? 'Authorization denied'; |
| | |
| | | if ($this->isOAuthService && $this->hasOAuthCredentials()) { |
| | | // Check if token is expired first |
| | | if (!$this->isOAuthValid()) { |
| | | $this->logDebug('OAuth token expired, attempting refresh'); |
| | | if (!$this->refreshOAuthToken()) { |
| | | $this->logError('Failed to refresh expired OAuth token'); |
| | | // Only attempt refresh once per request |
| | | if (!$this->token_refresh_attempted) { |
| | | $this->token_refresh_attempted = true; |
| | | // $this->logDebug('OAuth token expired, attempting refresh'); |
| | | |
| | | if (!$this->refreshOAuthToken()) { |
| | | $this->logError('Failed to refresh expired OAuth token - stopping execution'); |
| | | // Token refresh failed - DO NOT continue making API requests |
| | | return; |
| | | } |
| | | } else { |
| | | // Already attempted refresh in this request |
| | | // $this->logDebug('Token refresh already attempted, skipping'); |
| | | return; |
| | | } |
| | | } |
| | | // Check if we should proactively refresh (before expiry) |
| | | elseif ($this->shouldRefreshToken()) { |
| | | $this->logDebug('OAuth token should be refreshed proactively'); |
| | | elseif ($this->shouldRefreshToken() && !$this->token_refresh_attempted) { |
| | | $this->token_refresh_attempted = true; |
| | | // $this->logDebug('OAuth token should be refreshed proactively'); |
| | | if (!$this->refreshOAuthToken()) { |
| | | $this->logError('Failed to proactively refresh OAuth token'); |
| | | // Not critical - token is still valid |
| | | // Not critical - token is still valid, so continue |
| | | } |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 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->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 |
| | | ***************************************************************/ |
| | |
| | | $params = $this->addOAuthParams($params); |
| | | } |
| | | |
| | | $auth_url = $this->oauth['authorize'] . '?' . http_build_query($params); |
| | | |
| | | // Debug log for troubleshooting |
| | | error_log("Generated OAuth URL for {$this->service_name}: " . $auth_url); |
| | | |
| | | return $auth_url; |
| | | return $this->oauth['authorize'] . '?' . http_build_query($params); |
| | | } |
| | | |
| | | /** |
| | |
| | | return false; |
| | | } |
| | | |
| | | // Build refresh request data |
| | | $request_data = [ |
| | | 'client_id' => $this->credentials['client_id'], |
| | | 'client_secret' => $this->credentials['client_secret'], |
| | |
| | | 'grant_type' => 'refresh_token' |
| | | ]; |
| | | |
| | | // Use centralized OAuth request method |
| | | $response = $this->makeOAuthRequest('POST', $this->oauth['token'], $request_data); |
| | | |
| | | if (is_wp_error($response)) { |
| | | $this->logError('Failed to refresh Square token', [ |
| | | 'error' => $response->get_error_message() |
| | | $error_message = $response->get_error_message(); |
| | | |
| | | if (str_contains($error_message, 'invalid_grant')) { |
| | | $this->logError('OAuth refresh token is invalid - user must re-authorize', [ |
| | | 'error' => $error_message |
| | | ], 'critical'); |
| | | |
| | | // Mark unhealthy immediately |
| | | $this->error_stats['consecutive_errors'] = $this->error_threshold; |
| | | $this->is_healthy = false; |
| | | $this->saveErrorStats(); |
| | | } |
| | | |
| | | $this->logError('Failed to refresh OAuth token for '.$this->service_name, [ |
| | | 'error' => $error_message |
| | | ]); |
| | | return false; |
| | | } |
| | |
| | | $this->credentials['access_token'] = $response['access_token']; |
| | | $this->credentials['expires_at'] = time() + ($response['expires_in'] ?? 2592000); // 30 days |
| | | |
| | | // Note: Square returns the SAME refresh token |
| | | // Note: Some services return the SAME refresh token |
| | | if (isset($response['refresh_token'])) { |
| | | $this->credentials['refresh_token'] = $response['refresh_token']; |
| | | } |
| | |
| | | */ |
| | | protected function mapFieldsToService(int $postID, array $mapping): array |
| | | { |
| | | $meta_manager = new MetaManager($postID, 'post'); |
| | | $post = get_post($postID); |
| | | $meta_manager = Meta::forPost($postID); |
| | | $service_data = []; |
| | | |
| | | foreach ($mapping as $wp_field => $service_field) { |
| | | $value = null; |
| | | |
| | | // Check if it's a post field |
| | | if (property_exists($post, $wp_field)) { |
| | | $value = $post->$wp_field; |
| | | } else { |
| | | // It's a meta field |
| | | $value = $meta_manager->getValue($wp_field); |
| | | } |
| | | $value = $meta_manager->get($wp_field); |
| | | |
| | | if ($value !== null && $value !== '') { |
| | | $this->setNestedValue($service_data, $service_field, $value); |
| | |
| | | */ |
| | | public function handleSavePost(int $postID, WP_Post $post, bool $update): void |
| | | { |
| | | error_log('Testing For Save Post'); |
| | | if (jvbNoSaveIt($postID, $post)) { |
| | | error_log('Excluded by jvbNoSaveIt'); |
| | | if (!is_null($this->userID)) { |
| | | return; |
| | | } |
| | | if (empty($this->syncPostTypes)) { |
| | | error_log('No Syncable post types'); |
| | | error_log('=== ['.$this->service_name.']::handleSavePost called'); |
| | | |
| | | if (!$postID || $postID === 0) { |
| | | return; |
| | | } |
| | | $postType = jvbNoBase($post->post_type); |
| | | |
| | | if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; |
| | | if (wp_is_post_revision($postID)) return; |
| | | |
| | | |
| | | if (empty($this->syncPostTypes) || !in_array(jvbNoBase($postType), $this->syncPostTypes)) { |
| | | error_log('Not handling save for '.$this->service_name.' because there are no syncPostTypes: '.print_r($this->syncPostTypes, true)); |
| | | return; |
| | | } |
| | | |
| | | $config = JVB_CONTENT[jvbNoBase($post->post_type)]??null; |
| | | if (!$config) { |
| | | error_log('No Config set'); |
| | | $registrar = Registrar::getInstance($postType); |
| | | if (!$registrar){ |
| | | return; |
| | | } |
| | | |
| | | $settings = $config['integrations'][$this->service_name]??null; |
| | | $settings = $registrar->hasIntegration($this->service_name)??null; |
| | | if (!$settings) { |
| | | error_log('No settings'); |
| | | error_log('Not handling save for '.$this->service_name.' because of no registrar settings'); |
| | | return; |
| | | } |
| | | |
| | | $settings = $registrar->getIntegrationConfig($this->service_name); |
| | | if (!$settings){ |
| | | error_log('Not handling save for '.$this->service_name.' because of no integration config '.print_r($settings, true)); |
| | | return; |
| | | } |
| | | |
| | | |
| | | $fields = $this->getSyncFields($postID, 'post', ['schedule_'.$this->service_name]); |
| | | error_log('Fields to check: '.print_r($fields, true)); |
| | | if (!$fields['share_to_'.$this->service_name]) { |
| | | error_log('Not handling save for '.$this->service_name.' because of no share_to_'.$this->service_name.' '.print_r($fields, true)); |
| | | return; |
| | | } |
| | | |
| | | $isShared = isset($fields["_{$this->service_name}_item_id"]); |
| | | if ($update && $isShared && !$fields['_keep_synced_'.$this->service_name]) { |
| | | error_log('Do not keep synced, not syncing with '.$this->service_name); |
| | | error_log('Not handling save for '.$this->service_name.' because it is already shared, and not set to keep synced. '); |
| | | return; |
| | | } |
| | | |
| | | if ($post->post_status !== 'publish' && !$isShared) { |
| | | error_log('Not published and not already shared'); |
| | | error_log('Not handling save for '.$this->service_name.' because post status is not publish, and it is not already shared.'); |
| | | return; |
| | | } |
| | | error_log('Sending to integration for processing...'); |
| | | error_log('==== Sending to integration\'s handleTheSavePost '.$this->service_name.' ===='); |
| | | $this->removeSavePost(); |
| | | $this->handleTheSavePost($postID, $post, $update, $settings); |
| | | $this->addSavePost(); |
| | | } |
| | | |
| | | |
| | | protected function getSyncFields(int $postID, string $type, array $additional = []):array |
| | | { |
| | | $meta = new MetaManager($postID, $type); |
| | | $meta = new Meta($postID, $type); |
| | | $fieldsToCheck = [ |
| | | 'share_to_' . $this->service_name, |
| | | '_keep_synced_' . $this->service_name, |
| | |
| | | if (!in_array($noBase, $this->syncTaxonomies)) { |
| | | return; |
| | | } |
| | | // Check if taxonomy is content-type |
| | | $config = JVB_TAXONOMY[$noBase] ?? null; |
| | | if (!$config || !($config['is_content'] ?? false)) { |
| | | $registrar = Registrar::getInstance($noBase); |
| | | if (!$registrar->hasFeature('is_content')) { |
| | | return; |
| | | } |
| | | |
| | | $settings = $config['integrations'][$this->service_name] ?? null; |
| | | |
| | | $settings = $registrar->getIntegrationConfig($this->service_name); |
| | | if (!$settings) { |
| | | return; |
| | | } |
| | |
| | | |
| | | } |
| | | |
| | | 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 |
| | |
| | | ]; |
| | | $this->is_healthy = true; |
| | | $this->saveErrorStats(); |
| | | |
| | | $this->logDebug('Integration health manually reset', [ |
| | | 'reset_by' => get_current_user_id(), |
| | | 'reset_time' => time() |
| | | ]); |
| | | } |
| | | |
| | | /** |
| | |
| | | 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; |
| | | } |
| | | |
| | | /********************************************************************* |
| | | RENDERING |
| | | *********************************************************************/ |
| | |
| | | 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 |
| | |
| | | |
| | | // Check for duplicate processing (idempotency) |
| | | if ($this->isWebhookProcessed($payload)) { |
| | | $this->logDebug('Webhook already processed', [ |
| | | 'webhook_id' => $this->extractWebhookId($payload) |
| | | ]); |
| | | return true; // Return true to prevent retries |
| | | } |
| | | |
| | |
| | | return ''; |
| | | } |
| | | |
| | | $meta = new MetaManager($this->userID, 'integrations'); |
| | | $meta = Meta::forOptions($this->userID.'_integrations'); |
| | | $is_connected = $this->isSetUp(); |
| | | $credentials = $this->getCredentials(); |
| | | |
| | |
| | | ?> |
| | | <form id="<?=$this->service_name?>" class="integration <?php echo $is_connected ? 'connected' : 'disconnected'; ?>" |
| | | data-service="<?php echo esc_attr($this->service_name); ?>"> |
| | | <div class="header row btw"> |
| | | <div class="header row x-btw"> |
| | | <h3><?php echo esc_html($this->title); ?></h3> |
| | | <div class="setup"> |
| | | <?php if ($is_connected): ?> |
| | |
| | | $config['value'] = $credentials[$name]??''; |
| | | $config['autocomplete'] = 'off'; |
| | | $config['base'] = $this->service_name.'_'; |
| | | $meta->render('form', $name, $config); |
| | | echo Form::render($name, '', $config); |
| | | } |
| | | } |
| | | if ($this->handleWebhooks) { |
| | |
| | | $config['value'] = $credentials[$name]??''; |
| | | $config['base'] = $this->service_name.'_'; |
| | | $config['autocomplete'] = 'off'; |
| | | $meta->render('form', $name, $config); |
| | | Form::render($name,null, $config); |
| | | } |
| | | ?> |
| | | </details> |
| | |
| | | } |
| | | ?> |
| | | </div> |
| | | <div class="actions row btw wrap"> |
| | | <div class="actions row x-btw wrap"> |
| | | <?php |
| | | foreach ($this->buttons as $action => $label) { |
| | | if (!$is_connected && $action !== 'save_credentials') { |
| | |
| | | switch ($action) { |
| | | case 'save_credentials': |
| | | $title = $label; |
| | | $label = jvbIcon('save'); |
| | | $label = jvbIcon('floppy-disk'); |
| | | break; |
| | | case 'clear_credentials': |
| | | $title = $label; |
| | |
| | | // Verify nonce |
| | | $nonce_field = 'jvb_integration_nonce_' . $service; |
| | | $nonce_action = 'jvb_integration_save_' . $service; |
| | | error_log('handleAdminPost: '.print_r($_POST, true)); |
| | | if (!isset($_POST[$nonce_field]) || !wp_verify_nonce($_POST[$nonce_field], $nonce_action)) { |
| | | wp_die('Security check failed'); |
| | | } |
| | |
| | | if (empty($types)) { |
| | | return; |
| | | } |
| | | $meta = new MetaManager($this->userID, 'integrations'); |
| | | $meta = Meta::forOptions($this->userID.'_integrations'); |
| | | ?> |
| | | <form> |
| | | <h1><?= $this->title?> Defaults:</h1> |
| | |
| | | |
| | | $config['base'] = $this->service_name.'_'; |
| | | $config['autocomplete'] = 'off'; |
| | | $meta->render('form', $name, $config); |
| | | echo Form::render($name, null, $config); |
| | | } |
| | | foreach ($this->syncPostTypes as $type) { |
| | | $config = JVB_CONTENT[$type]; |
| | | $registrar = Registrar::getInstance($type); |
| | | |
| | | $icon = $registrar->getIcon(); |
| | | $icon = $icon === '' ? jvbDefaultIcon() : $icon; |
| | | ?> |
| | | <details> |
| | | <summary><?= jvbIcon($config['icon']) ?><?= $config['singular']?> Defaults</summary> |
| | | <summary><?= jvbIcon($icon) ?><?= $registrar->getSingular()?> Defaults</summary> |
| | | <?php |
| | | $fields = new \JVBase\registry\providers\IntegrationFieldProvider(); |
| | | $fields = $fields->getIntegrationFields($this->service_name, $config); |
| | | $fields = new AddIntegrationFields($this->service_name); |
| | | $fields = $fields->getIntegrationFields(); |
| | | foreach($fields as $name=>$c) { |
| | | $c['required'] = false; |
| | | if ($c['type'] === 'number') { |
| | |
| | | $c['hint'] = $c['description']; |
| | | unset($c['description']); |
| | | } |
| | | $meta->render('form', $name, $c); |
| | | echo Form::render($name, null, $c); |
| | | } |
| | | ?> |
| | | </details> |
| | |
| | | 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; |
| | | return array_filter(array_map(function($registrar) { |
| | | $registrar = Registrar::getInstance($registrar); |
| | | return $registrar->getIntegration($this->service_name)->getContentType(); |
| | | }, Registrar::withIntegration($this->service_name))); |
| | | } |
| | | |
| | | protected function getSupportedImage(int $imgID):int |
| | |
| | | throw new Exception('Failed to save JPEG image'); |
| | | } |
| | | } |
| | | /** |
| | | * Reset token refresh attempt flag |
| | | * Called automatically when switching users |
| | | */ |
| | | protected function resetTokenRefreshFlag(): void |
| | | { |
| | | $this->token_refresh_attempted = false; |
| | | } |
| | | |
| | | public function getAllowedContent():array |
| | | { |
| | | return $this->allowedContent; |
| | | } |
| | | |
| | | /** |
| | | * Used by JVBase\registrar\helpers\AddIntegrationFields.php |
| | | * @return array |
| | | */ |
| | | public function getAdditionalFields(?string $content_type = null):array |
| | | { |
| | | return []; |
| | | } |
| | | |
| | | public function getIcon():string |
| | | { |
| | | return $this->icon; |
| | | } |
| | | |
| | | public function getServiceItemID(int $itemID, string $type = 'post'):string |
| | | { |
| | | return match($type) { |
| | | 'post' => Meta::forPost($itemID)->get("_{$this->service_name}_item_id"), |
| | | 'term','taxonomy' => Meta::forTerm($itemID)->get("_{$this->service_name}_item_id"), |
| | | 'user' => Meta::forUser($itemID)->get("_{$this->service_name}_item_id"), |
| | | default => '' |
| | | }; |
| | | } |
| | | |
| | | public function canBatchUpdate():bool |
| | | { |
| | | return $this->hasBatchUpdate; |
| | | } |
| | | public function canBatchCreate():bool |
| | | { |
| | | return $this->hasBatchCreate; |
| | | } |
| | | public function canBatchDelete():bool |
| | | { |
| | | return $this->hasBatchDelete; |
| | | } |
| | | |
| | | protected function updateItemStatus(int|array $items, string $status = 'pending', string $type = 'post'):void |
| | | { |
| | | if (!is_array($items)) { |
| | | $items = [$items]; |
| | | } |
| | | foreach ($items as $ID) { |
| | | match ($type) { |
| | | 'post' => Meta::forPost($ID)->set('_'.$this->service_name.'_sync_status', $status), |
| | | 'term' => Meta::forTerm($ID)->set('_'.$this->service_name.'_sync_status', $status), |
| | | 'user' => Meta::forUser($ID)->set('_'.$this->service_name.'_sync_status', $status), |
| | | default => false |
| | | }; |
| | | } |
| | | } |
| | | |
| | | public function updateBatchToService(array $data):array |
| | | { |
| | | $type = $data['type']??'post'; |
| | | |
| | | $newlyCreated = []; |
| | | if (!$this->canCreateOnUpdate) { |
| | | $created = array_filter($data['items'], function($ID) use ($type) { |
| | | return !empty($this->getServiceItemID($ID, $type)); |
| | | }); |
| | | |
| | | //Test to see if we have any that haven't been created yet. |
| | | //For some services, items may have to be created before they can be updated |
| | | if (count($created) !== count($data['items'])) { |
| | | $notCreated = array_filter($data['items'], function($ID) use ($type) { |
| | | return empty($this->getServiceItemID($ID, $type)); |
| | | }); |
| | | $newData = $data; |
| | | $newData['items'] = $notCreated; |
| | | $newlyCreated = $this->createBatchToService($newData); |
| | | } |
| | | |
| | | // If we don't have any that are created, just send the noUpdatedItems response, with any newly created items added |
| | | if (empty($created)) { |
| | | return $this->noUpdatedItems($newlyCreated); |
| | | } |
| | | $data['items'] = $created; |
| | | } |
| | | |
| | | |
| | | $items = $this->formatItems($data['items'], $type); |
| | | |
| | | if (empty($items)) { |
| | | return $this->noUpdatedItems($newlyCreated); |
| | | } |
| | | |
| | | if ($this->hasBatchUpdate) { |
| | | $response = $this->sendBatchUpdate($items); |
| | | if (!is_wp_error($response)) { |
| | | $result = $this->processBatchUpdateResponse($data, $response); |
| | | } else { |
| | | $this->logError('Batch update failed',[ |
| | | 'method' => 'updateBatchToService', |
| | | 'item_ids' => $data['items'], |
| | | 'error' => $response |
| | | ]); |
| | | $this->updateItemStatus($data['items'], 'error'); |
| | | |
| | | $result = [ |
| | | 'outcome' => 'failed_permanent', |
| | | 'result' => 'Could not update items' |
| | | ]; |
| | | } |
| | | } else { |
| | | $success = $errors = []; |
| | | //Does not have batch update, manually update each one |
| | | foreach ($items as $item) { |
| | | $itemResult = $this->updateOne($item); |
| | | if (!is_wp_error($itemResult)) { |
| | | $success[] = $itemResult; |
| | | } else { |
| | | $errors[] = $itemResult; |
| | | } |
| | | } |
| | | |
| | | $result = [ |
| | | 'outcome' => empty($errors) ? 'success' : (empty($success) ? 'failed' : 'partial'), |
| | | 'result' => [ |
| | | 'success' => $success, |
| | | 'errors' => $errors |
| | | ] |
| | | ]; |
| | | } |
| | | |
| | | return array_merge($result, $newlyCreated); |
| | | } |
| | | |
| | | /** |
| | | * To be implemented by integration extension. Updates a single item |
| | | * @param array $item |
| | | * @return array|WP_Error |
| | | */ |
| | | protected function updateOne(array $item):array|WP_Error |
| | | { |
| | | return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own updateOne method'); |
| | | } |
| | | |
| | | /** |
| | | * Overridden by child classes |
| | | * @param array $items |
| | | * @return array|WP_Error |
| | | */ |
| | | protected function sendBatchUpdate(array $items):array|WP_Error |
| | | { |
| | | return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own sendBatchUpdate'); |
| | | } |
| | | /** |
| | | * Overridden by child classes |
| | | * @param array $items |
| | | * @return array|WP_Error |
| | | */ |
| | | protected function sendBatchCreate(array $items):array|WP_Error |
| | | { |
| | | return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own sendBatchCreate'); |
| | | } |
| | | |
| | | /** |
| | | * To be implemented by extensions |
| | | * @param array $data |
| | | * @param array $response |
| | | * @return array |
| | | */ |
| | | protected function processBatchUpdateResponse(array $data, array $response):array |
| | | { |
| | | return [ |
| | | 'outcome' => 'failed', |
| | | 'result' => [ |
| | | 'message' => $this->service_name.' should implement processBatchUpdateResponse.' |
| | | ] |
| | | ]; |
| | | } |
| | | /** |
| | | * To be implemented by extensions |
| | | * @param array $data |
| | | * @param array $response |
| | | * @return array |
| | | */ |
| | | protected function processBatchCreateResponse(array $data, array $response):array |
| | | { |
| | | return [ |
| | | 'outcome' => 'failed', |
| | | 'result' => [ |
| | | 'message' => $this->service_name.' should implement processBatchCreateResponse.' |
| | | ] |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Defaults to an array of formatted items. Can be overridden in child classes |
| | | * @param array $itemIDs |
| | | * @param string $type |
| | | * @return array |
| | | */ |
| | | protected function formatItems(array $itemIDs, string $type):array |
| | | { |
| | | $items = []; |
| | | foreach ($itemIDs as $ID) { |
| | | try { |
| | | $items[] = $this->formatForService($ID, $type); |
| | | } catch (Exception $e){ |
| | | $this->logError('Could not format Item for service',[ |
| | | 'itemID' => $ID, |
| | | 'type' => $type, |
| | | 'message' => $e->getMessage(), |
| | | ]); |
| | | } |
| | | } |
| | | return $items; |
| | | } |
| | | |
| | | /** |
| | | * Must be defined according to how each service needs it to be |
| | | * @param int $itemID |
| | | * @param string $type |
| | | * @return array |
| | | * @throws Exception |
| | | */ |
| | | protected function formatForService(int $itemID, string $type = 'post'):array |
| | | { |
| | | throw new Exception('formatForService must be implemented by child class'); |
| | | } |
| | | protected function noUpdatedItems(array $created):array |
| | | { |
| | | $result = [ |
| | | 'outcome' => 'success', |
| | | 'result' => [ |
| | | 'message' => 'No items to update', |
| | | 'updated' => [], |
| | | 'errors' => [], |
| | | ] |
| | | ]; |
| | | |
| | | if (!empty($created)) { |
| | | error_log('Result before: '.print_r($result, true)); |
| | | $result = array_merge($result, $created); |
| | | error_log('Result after merge: '.print_r($result, true)); |
| | | } |
| | | return $result; |
| | | } |
| | | |
| | | protected function noCreatedItems(array $updated):array |
| | | { |
| | | $result = [ |
| | | 'outcome' => 'success', |
| | | 'result' => [ |
| | | 'message' => 'No items to create', |
| | | 'created' => [], |
| | | 'errors' => [], |
| | | ] |
| | | ]; |
| | | if (!empty($updated)) { |
| | | $result = array_merge($result, $updated); |
| | | } |
| | | return $result; |
| | | } |
| | | |
| | | public function createBatchToService($data):array |
| | | { |
| | | $type = $data['type']??'post'; |
| | | |
| | | //Check if any of the submitted ids are already created |
| | | $created = array_filter($data['items'], |
| | | function($ID) use ($type) { |
| | | return !empty($this->getServiceItemID($ID, $type)); |
| | | }); |
| | | |
| | | $updated = []; |
| | | if (!empty($created)) { |
| | | $updateData = $data; |
| | | $updateData['items'] = $created; |
| | | $updated = $this->updateBatchToService($data); |
| | | |
| | | //remove any updated items from the original items to process |
| | | $data['items'] = array_filter($data['items'], function ($ID) use ($created) { |
| | | return !in_array($ID, $created); |
| | | }); |
| | | } |
| | | |
| | | $items = $this->formatItems($data['items'], $type); |
| | | if (empty($items)) { |
| | | return $this->noCreatedItems($updated); |
| | | } |
| | | |
| | | $response = [ |
| | | 'outcome' => 'failed', |
| | | 'result' => [ |
| | | 'message' => 'No result' |
| | | ] |
| | | ]; |
| | | if ($this->hasBatchCreate) { |
| | | $response = $this->sendBatchCreate($items); |
| | | if (!is_wp_error($response)) { |
| | | $result = $this->processBatchCreateResponse($data, $response); |
| | | } else { |
| | | $this->logError('Batch create failed',[ |
| | | 'method' => 'createBatchToService', |
| | | 'item_ids' => $data['items'], |
| | | 'error' => $response |
| | | ]); |
| | | $this->updateItemStatus($data['items'], 'error'); |
| | | |
| | | $result = [ |
| | | 'outcome' => 'failed_permanent', |
| | | 'result' => 'Could not update items' |
| | | ]; |
| | | } |
| | | } else { |
| | | $success = $errors = []; |
| | | foreach ($items as $item) { |
| | | $itemResult = $this->createOne($item); |
| | | if (!is_wp_error($itemResult)) { |
| | | $success[] = $itemResult; |
| | | } else { |
| | | $errors[] = $itemResult; |
| | | } |
| | | } |
| | | $result = [ |
| | | 'outcome' => empty($errors) ? 'success' : (empty($success) ? 'failed' : 'partial'), |
| | | 'result' => [ |
| | | 'success' => $success, |
| | | 'errors' => $errors |
| | | ] |
| | | ]; |
| | | } |
| | | |
| | | return array_merge($result, $updated); |
| | | } |
| | | |
| | | /** |
| | | * To be implemented by integration extension. Updates a single item |
| | | * @param array $item |
| | | * @return array|WP_Error |
| | | */ |
| | | protected function createOne(array $item):array|WP_Error |
| | | { |
| | | return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own createOne'); |
| | | } |
| | | |
| | | public function findWPItem(string $integrationID, ?string $type = null):array|false |
| | | { |
| | | if (empty($integrationID)) { |
| | | error_log('Integrations::findWPItem No Integration ID received.'); |
| | | return false; |
| | | } |
| | | $types = ['post', 'term', 'user']; |
| | | if (!is_null($type) && !in_array($type, $types)) { |
| | | error_log('Integrations::findWPItem invalid type sent: '.$type.'; defaulting to checking all'); |
| | | $type = null; |
| | | } |
| | | if (!is_null($type)) { |
| | | return $this->queryWPType($integrationID, $type); |
| | | } else { |
| | | foreach ($types as $type) { |
| | | $check = $this->queryWPType($integrationID, $type); |
| | | if ($check) { |
| | | return $check; |
| | | } |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * @param string $integrationID |
| | | * @param string|null $type |
| | | * @return array|false if success, an array of $ID and $type |
| | | */ |
| | | public function queryWPType(string $integrationID, ?string $type = null):array|false |
| | | { |
| | | if (!in_array($type, ['post', 'term', 'user'])) { |
| | | return $this->findWPItem($integrationID); |
| | | } |
| | | return match($type) { |
| | | 'post' => $this->findWPPost($integrationID), |
| | | 'term' => $this->findWPTerm($integrationID), |
| | | 'user' => $this->findWPUser($integrationID), |
| | | default => false |
| | | }; |
| | | } |
| | | public function findWPPost(string $integrationID):array|false |
| | | { |
| | | if (empty($integrationID)) { |
| | | return false; |
| | | } |
| | | $get = new WP_Query([ |
| | | 'meta_query' => [ |
| | | 'key' => BASE."_{$this->service_name}_item_id", |
| | | 'value' => $integrationID, |
| | | ], |
| | | 'fields' => 'ids', |
| | | 'posts_per_page' => 1, |
| | | ]); |
| | | return $get->have_posts() && !empty($get->posts) ? [$get->posts[0], 'post'] : false; |
| | | } |
| | | public function findWPTerm(string $integrationID):array|false |
| | | { |
| | | if (empty($integrationID)) { |
| | | return false; |
| | | } |
| | | $get = get_terms([ |
| | | 'meta_query' => [ |
| | | 'key' => BASE."_{$this->service_name}_item_id", |
| | | 'value' => $integrationID, |
| | | ], |
| | | 'fields' => 'ids', |
| | | 'hide_empty'=> true, |
| | | 'number' => 1 |
| | | ]); |
| | | return is_wp_error($get) || empty($get) ? false : [$get[0], 'term']; |
| | | } |
| | | public function findWPUser(string $integrationID):array|false |
| | | { |
| | | if (empty($integrationID)) { |
| | | return false; |
| | | } |
| | | $get = get_users([ |
| | | 'meta_query' => [ |
| | | 'key' => BASE."_{$this->service_name}_item_id", |
| | | 'value' => $integrationID, |
| | | ], |
| | | 'fields' => 'ids', |
| | | 'number' => 1, |
| | | ]); |
| | | return is_wp_error($get) || empty($get) ? false : [$get[0], 'user']; |
| | | } |
| | | |
| | | /********************************************************************************* |
| | | * Syncing WP items from Service |
| | | *********************************************************************************/ |
| | | /** |
| | | * @param array $itemData |
| | | * @return bool |
| | | * @throws Exception |
| | | */ |
| | | protected function createItemFromService(array $itemData):bool |
| | | { |
| | | throw new Exception('createItemFromService must be implemented by child class '.$this->service_name); |
| | | } |
| | | |
| | | /** |
| | | * @param array $itemData |
| | | * @return bool |
| | | * @throws Exception |
| | | */ |
| | | protected function updateItemFromService(array $itemData):bool |
| | | { |
| | | throw new Exception('updateItemFromService must be implemented by child class '.$this->service_name); |
| | | } |
| | | |
| | | /** |
| | | * @param array $itemData |
| | | * @return bool |
| | | * @throws Exception |
| | | */ |
| | | protected function deleteItemFromService(array $itemData):bool |
| | | { |
| | | throw new Exception('deleteItemFromService must be implemented by child class '.$this->service_name); |
| | | } |
| | | |
| | | /**************************************************************** |
| | | * CONNECT to a user's integration |
| | | ****************************************************************/ |
| | | protected array $connections = []; |
| | | public function connectAs(?int $userID = null):bool |
| | | { |
| | | if (!$this->userCanConnect($userID)) { |
| | | return false; |
| | | } |
| | | $this->cleanup(); |
| | | $this->userID = $userID; |
| | | $this->ensureInitialized(); |
| | | return true; |
| | | } |
| | | |
| | | public function userCanConnect(?int $userID):bool |
| | | { |
| | | $user = get_userdata($userID); |
| | | if (!$user || is_wp_error($user)) { |
| | | return false; |
| | | } |
| | | if (current_user_can('manage_options')) { |
| | | return true; |
| | | } |
| | | $role = jvbUserRole($userID); |
| | | $registrar = Registrar::getInstance($role); |
| | | if (!$registrar || !$registrar->hasIntegration($this->service_name)) { |
| | | return false; |
| | | } |
| | | |
| | | return (bool) Meta::forUser($userID)->get('integration_'.$this->service_name.'_connected'); |
| | | } |
| | | |
| | | protected function cleanup():void |
| | | { |
| | | $this->userID = false; |
| | | $this->credentials = []; |
| | | $this->request_history = []; |
| | | $this->resetTokenRefreshFlag(); |
| | | } |
| | | } |