| inc/integrations/CredentialsManager.php | ●●●●● patch | view | raw | blame | history | |
| inc/integrations/Integrations.php | ●●●●● patch | view | raw | blame | history | |
| inc/registrar/Fields.php | ●●●●● 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, ]); } } }