Jake Vanderwerf
2026-07-10 f94860aacd6200fb24c9e7431eb379a368cb392d
=Refactored CredentialsManager.php to utilize a CustomTable instance instead
3 files modified
302 ■■■■■ changed files
inc/integrations/CredentialsManager.php 125 ●●●●● patch | view | raw | blame | history
inc/integrations/Integrations.php 161 ●●●●● patch | view | raw | blame | history
inc/registrar/Fields.php 16 ●●●●● patch | view | raw | blame | history
inc/integrations/CredentialsManager.php
@@ -1,6 +1,8 @@
<?php
namespace JVBase\integrations;
use JVBase\managers\CustomTable;
if (!defined('ABSPATH')) {
    exit;
}
@@ -9,12 +11,39 @@
{
    private static ?CredentialsManager $instance = null;
    private string $encryption_key;
    private string $option_prefix = 'jvb_integration_';
    private string $user_meta_prefix = 'jvb_integration_';
    private CustomTable $table;
    private function __construct()
    {
        $this->encryption_key = $this->getEncryptionKey();
        $this->getEncryptionKey();
        $this->defineTable();
    }
    protected function defineTable():void
    {
        $table = CustomTable::for('integrations');
        $table->setColumns([
            'id'            => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
            'user_id'       => $table->getUserIDType().' NOT NULL',
            'integration'   => "ENUM('bluesky','cloudflare','facebook','google-maps','gmb','helcim','instagram','postmark','square','umami') NOT NULL",
            'credentials'   => 'varchar(255) DEFAULT NULL',
            'created_at'    => 'datetime DEFAULT CURRENT_TIMESTAMP',
            'updated_at'    => 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
        ]);
        $table->setKeys([
            ['key' => 'PRIMARY', 'value' => '(`id`)'],
            ['key' => 'UNIQUE', 'value' => '`user_integration` (`user_id`, `integration`)'],
            '`user` (`user_id`)',
        ]);
        $base = BASE;
        $table->setConstraints([
            "CONSTRAINT `{$base}integrations_user` FOREIGN KEY (`user_id`)
            REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE"
        ]);
        $table->defineTable();
        $this->table = $table;
    }
    public static function getInstance(): CredentialsManager
@@ -30,18 +59,11 @@
     */
    public function storeCredentials(string $service, array $credentials, ?int $userID = null): bool
    {
        $credentials['updated_at'] = time();
        if ($userID) {
            return $this->storeUserCredentials($service, $credentials, $userID);
        }
        $encrypted_data = $this->encrypt(json_encode($credentials));
        return update_option($this->option_prefix . $service, $encrypted_data);
    }
    public function storeUserCredentials(string $service, array $credentials, int $userID): bool
    {
        $encrypted_data = $this->encrypt(json_encode($credentials));
        return update_user_meta($userID, $this->user_meta_prefix . $service, $encrypted_data);
        $find = $this->findBy($service, $userID);
        $update = [
            'credentials'   => $this->encrypt(json_encode($credentials))
        ];
        return (bool)$this->table->findOrCreate($find, $update);
    }
    /**
@@ -49,30 +71,13 @@
     */
    public function getCredentials(string $service, ?int $userID = null): array
    {
        if ($userID) {
            return $this->getUserCredentials($service, $userID);
        }
        $encrypted_data = get_option($this->option_prefix . $service, '');
        if (empty($encrypted_data)) {
        $find = $this->findBy($service, $userID);
        $credentials = $this->table->pluck('credentials', $find);
        if (empty($credentials)) {
            return [];
        }
        $decrypted_data = $this->decrypt($encrypted_data);
        return $decrypted_data ? json_decode($decrypted_data, true) : [];
    }
    public function getUserCredentials(string $service, int $userID): array
    {
        $encrypted_data = get_user_meta($userID, $this->user_meta_prefix . $service, true);
        if (empty($encrypted_data)) {
            return [];
        }
        $decrypted_data = $this->decrypt($encrypted_data);
        return $decrypted_data ? json_decode($decrypted_data, true) : [];
        $decrypted_data = $this->decrypt($credentials[0]);
        return empty($decrypted_data) ? [] : json_decode($decrypted_data, true);
    }
    /**
@@ -80,16 +85,16 @@
     */
    public function deleteCredentials(string $service, ?int $userID = null): bool
    {
        if ($userID) {
            return $this->deleteUserCredentials($service, $userID);
        }
        return delete_option($this->option_prefix . $service);
        $find = $this->findBy($service, $userID);
        return $this->table->delete($find);
    }
    public function deleteUserCredentials(string $service, int $userID): bool
    protected function findBy(string $service, ?int $userID = null):array
    {
        return delete_user_meta($userID, $this->user_meta_prefix . $service);
        return [
            'user_id'   => is_null($userID) ? 0 : $userID,
            'integration'=> $service
        ];
    }
    /**
@@ -97,31 +102,15 @@
     */
    public function hasCredentials(string $service, ?int $userID = null): bool
    {
        if ($userID) {
            return $this->hasUserCredentials($service, $userID);
        }
        return !empty(get_option($this->option_prefix . $service, ''));
    }
    public function hasUserCredentials(string $service, int $userID): bool
    {
        $encrypted_data = get_user_meta($userID, $this->user_meta_prefix . $service, true);
        return !empty($encrypted_data);
        return !empty($this->getCredentials($service, $userID));
    }
    /**
     * Get or create encryption key
     */
    private function getEncryptionKey(): string
    private function getEncryptionKey():void
    {
        $key = get_option('jvb_encryption_key');
        if (!$key) {
            $key = base64_encode(random_bytes(32));
            update_option('jvb_encryption_key', $key);
        }
        return base64_decode($key);
        $this->encryption_key = JVB_KEY;
    }
    /**
@@ -151,14 +140,6 @@
     */
    public function getUsersWithCredentials(string $service): array
    {
        global $wpdb;
        $meta_key = $this->user_meta_prefix . $service;
        $results = $wpdb->get_results($wpdb->prepare(
            "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s",
            $meta_key
        ));
        return wp_list_pluck($results, 'user_id');
        return $this->table->pluck('user_id', ['integration' => $service]);
    }
}
inc/integrations/Integrations.php
@@ -2312,93 +2312,6 @@
    }
    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
@@ -2793,14 +2706,6 @@
        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
@@ -4001,8 +3906,74 @@
    /*********************************************************************************
     * 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();
    }
}
inc/registrar/Fields.php
@@ -36,6 +36,9 @@
                break;
            case 'user':
                $this->addUserFields();
                if ($registrar->hasAnyIntegrations()) {
                    $this->addIntegrationSetupField();
                }
                break;
        }
    }
@@ -458,4 +461,17 @@
    {
        return (new self())->defaultUserFields();
    }
    protected function addIntegrationSetupField():void
    {
        $integrations = $this->registrar->getIntegrations();
        foreach (array_keys($integrations) as $integration) {
            $this->addField('integration_'.$integration.'_connected', [
                'type'  => 'true_false',
                'hidden'    => true,
                'label'     => 'Connected to '.$integration,
                'default'   => false,
            ]);
        }
    }
}