<?php
|
namespace JVBase\integrations;
|
|
use Exception;
|
use JVBase\managers\Cache;
|
use JVBase\managers\UploadManager;
|
use JVBase\registrar\Registrar;
|
use WP_Error;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* Base Integration Class
|
*
|
* This abstract class provides the foundation for all external service integrations.
|
* Child classes should extend this to implement specific service integrations.
|
*
|
* @abstract
|
* @since 1.0.0
|
*/
|
abstract class Integrations
|
{
|
use _Base;
|
|
/**
|
* API Configuration
|
* These properties define how the integration connects to external services
|
*/
|
protected string|array $apiBase = ''; // Base URL(s) for API endpoints. Array format: ['base' => '', 'auth' => '']
|
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;
|
}
|
|
}
|