| | |
| | | namespace JVBase\integrations; |
| | | |
| | | use Exception; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\meta\Sanitizer; |
| | | use JVBase\registrar\Registrar; |
| | | use WP_Error; |
| | | use WP_Post; |
| | | use WP_Term; |
| | | use WP_User; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | trait SyncTo { |
| | | use SyncHelpers; |
| | | use SyncHelpers, UserConnection; |
| | | /** |
| | | * Must be defined according to how each service needs it to be |
| | | * @param int $itemID |
| | |
| | | * @return array |
| | | * @throws Exception |
| | | */ |
| | | protected function formatForService(int $itemID, string $type = 'post'):array |
| | | { |
| | | throw new Exception('formatForService must be implemented by child class'); |
| | | } |
| | | abstract protected function formatForService(int $itemID, string $type = 'post'):array; |
| | | |
| | | /** |
| | | * Defaults to an array of formatted items. Can be overridden in child classes |
| | |
| | | try { |
| | | $items[] = $this->formatForService($ID, $type); |
| | | } catch (Exception $e){ |
| | | $this->logError('Could not format Item for service',[ |
| | | $this->logError('formatItems', 'Could not format Item for service',[ |
| | | 'itemID' => $ID, |
| | | 'type' => $type, |
| | | 'message' => $e->getMessage(), |
| | |
| | | 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',[ |
| | | $this->logError('createBatchToService','Batch create failed',[ |
| | | 'method' => 'createBatchToService', |
| | | 'item_ids' => $data['items'], |
| | | 'error' => $response |
| | |
| | | if (!is_wp_error($response)) { |
| | | $result = $this->processBatchUpdateResponse($data, $response); |
| | | } else { |
| | | $this->logError('Batch update failed',[ |
| | | $this->logError('updateBatchToService','Batch update failed',[ |
| | | 'method' => 'updateBatchToService', |
| | | 'item_ids' => $data['items'], |
| | | 'error' => $response |
| | |
| | | } |
| | | return $result; |
| | | } |
| | | |
| | | /**************************************************************** |
| | | * UTILITY |
| | | ****************************************************************/ |
| | | protected function getSyncFields(int $itemID, string $type, array $additionalFields = []):array |
| | | { |
| | | $meta = match ($type) { |
| | | 'post' => Meta::forPost($itemID), |
| | | 'term' => Meta::forTerm($itemID), |
| | | 'user' => Meta::forUser($itemID), |
| | | default => false |
| | | }; |
| | | if (!$meta) { |
| | | return []; |
| | | } |
| | | |
| | | $fields = [ |
| | | 'share_to_' . $this->service_name, |
| | | '_keep_synced_' . $this->service_name, |
| | | "_{$this->service_name}_item_id", |
| | | "_{$this->service_name}_last_sync", |
| | | "_{$this->service_name}_shared_at", |
| | | "_{$this->service_name}_sync_status", |
| | | "_{$this->service_name}_scheduled_at", |
| | | ... $additionalFields |
| | | ]; |
| | | return $meta->getAll($fields); |
| | | } |
| | | /**************************************************************** |
| | | * POST SYNC |
| | | ****************************************************************/ |
| | | 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 handleSavePost(int $postID, WP_Post $post, bool $update):void |
| | | { |
| | | if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; |
| | | if (wp_is_post_revision($postID)) return; |
| | | |
| | | error_log('=== ['.$this->service_name.']::handleSavePost called'); |
| | | |
| | | $postType = jvbNoBase($post->post_type); |
| | | if (!in_array($postType, $this->syncPostTypes)) { |
| | | error_log('Not handling save for '.$this->service_name.' because there are no syncPostTypes: '.print_r($this->syncPostTypes, true)); |
| | | return; |
| | | } |
| | | |
| | | $registrar = Registrar::getInstance($postType); |
| | | //Should not happen, as syncPostTypes is defined by Registrar instances |
| | | if (!$registrar) { |
| | | 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'); |
| | | 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 = array_key_exists("_{$this->service_name}_item_id", $fields) && !empty($fields["_{$this->service_name}_item_id"]); |
| | | |
| | | if ($post->post_status !== 'publish' && !$isShared) { |
| | | error_log('Not handling save for '.$this->service_name.' because post status is not publish, and it is not already shared.'); |
| | | return; |
| | | } |
| | | |
| | | if ($isShared && $update && !$fields['_keep_synced_'.$this->service_name]) { |
| | | error_log('Not handling save for '.$this->service_name.' because it is already shared, and not set to keep synced. '); |
| | | return; |
| | | } |
| | | error_log('==== Sending to integration\'s handleTheSavePost '.$this->service_name.' ===='); |
| | | $this->removeSavePost(); |
| | | $this->handleTheSavePost($postID, $post, $update, $settings); |
| | | $this->addSavePost(); |
| | | } |
| | | /** |
| | | * Handle post save for syncing |
| | | * |
| | | * Override to implement custom sync logic when posts are saved. |
| | | * Check the $settings array for post type specific configuration. |
| | | * |
| | | * @param int $postID The post ID |
| | | * @param WP_Post $post The post object |
| | | * @param bool $update Whether this is an update |
| | | * @param array $settings Post type integration settings |
| | | * @return void |
| | | */ |
| | | protected function handleTheSavePost(int $postID, WP_Post $post, bool $update, array $settings):void |
| | | { |
| | | error_log('==== ['.$this->title.']::handleTheSavePost ===='); |
| | | $this->queueOperation(self::$syncTo, [ |
| | | 'posts' => [$postID], |
| | | 'user' => user_can($post->post_author, 'manage_options') ? null : $post->post_author |
| | | ], [ |
| | | 'priority' => 'high', |
| | | 'delay' => 30, |
| | | ]); |
| | | Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued'); |
| | | } |
| | | |
| | | public function addDeletePost():void |
| | | { |
| | | if (!has_action('before_delete_post', [$this, 'handleDeletePost'])) { |
| | | add_action('before_delete_post', [$this, 'handleDeletePost'], 20, 3); |
| | | } |
| | | } |
| | | public function removeDeletePost():void |
| | | { |
| | | remove_action('before_delete_post', [$this, 'handleSavePost'], 20, 3); |
| | | } |
| | | public function handleDeletePost(int $postID):void |
| | | { |
| | | if (!$this->canSync['delete']) { |
| | | return; |
| | | } |
| | | $postType = get_post_type($postID); |
| | | if (!in_array(jvbNoBase($postType), $this->syncPostTypes)) { |
| | | return; |
| | | } |
| | | $fields = $this->getSyncFields($postID, 'post'); |
| | | if (empty($fields["_{$this->service_name}_item_id"])) { |
| | | return; |
| | | } |
| | | $post = get_post($postID); |
| | | if (!$post) { |
| | | return; |
| | | } |
| | | $userID = $this->determineUserID($post->post_author); |
| | | if (!$userID) { |
| | | return; |
| | | } |
| | | |
| | | JVB()->queue()->add( |
| | | self::$deleteFrom, |
| | | $userID, |
| | | [ |
| | | 'fields' => [$postID => $fields], |
| | | 'service' => $this->service_name, |
| | | 'type' => 'post' |
| | | ] |
| | | ); |
| | | } |
| | | /**************************************************************** |
| | | * TERM SYNC |
| | | ****************************************************************/ |
| | | public function addSaveTerm():void |
| | | { |
| | | if (empty($this->syncTaxonomies)) { |
| | | return; |
| | | } |
| | | if (!has_action('saved_term', [$this, 'handleSaveTerm'])) { |
| | | add_action('saved_term', [$this, 'handleSaveTerm'], 20, 3); |
| | | } |
| | | } |
| | | public function removeSaveTerm():void |
| | | { |
| | | if (empty($this->syncTaxonomies)) { |
| | | return; |
| | | } |
| | | remove_action('saved_term', [$this, 'handleSaveTerm'], 20, 3); |
| | | } |
| | | protected function handleSaveTerm(int $termID, int $tt_id, string $taxonomy, bool $update, array $args):void |
| | | { |
| | | $tax = jvbNoBase($taxonomy); |
| | | if (!in_array($tax, $this->syncTaxonomies)) { |
| | | return; |
| | | } |
| | | $registrar = Registrar::getInstance($tax); |
| | | if (!$registrar) { |
| | | return; |
| | | } |
| | | |
| | | $settings = $registrar->getIntegrationConfig($this->service_name); |
| | | if (!$settings) { |
| | | return; |
| | | } |
| | | |
| | | $fields = $this->getSyncFields($termID, 'term'); |
| | | if (!$fields['share_to_'.$this->service_name]) { |
| | | return; |
| | | } |
| | | |
| | | $isShared = array_key_exists("_{$this->service_name}_item_id", $fields) && !empty($fields["_{$this->service_name}_item_id"]); |
| | | |
| | | if ($isShared && $update && !$fields['_keep_synced_'.$this->service_name]) { |
| | | return; |
| | | } |
| | | $this->removeSaveTerm(); |
| | | $this->handleTheSaveTerm($termID, $update, $taxonomy, $settings); |
| | | $this->addSaveTerm(); |
| | | } |
| | | |
| | | /** |
| | | * @param int $termID |
| | | * @param bool $update |
| | | * @param array $settings |
| | | * @return void |
| | | */ |
| | | protected function handleTheSaveTerm(int $termID, bool $update, string $taxonomy, array $settings):void |
| | | { |
| | | error_log('==== ['.$this->title.']::handleTheSaveTerm ===='); |
| | | //TODO: Figure out some sort of permissions for if the user can share this term, particularly for content types |
| | | //TODO: If this is a content type, it likely has its own integration. This is an edmonton.ink problem, so I'm offloading it for now |
| | | $this->queueOperation(self::$syncTo, [ |
| | | 'terms' => [$termID], |
| | | 'user' => get_current_user_id(), |
| | | ], [ |
| | | 'priority' => 'high', |
| | | 'delay' => 30, |
| | | ]); |
| | | Meta::forTerm($termID)->set('_'.$this->service_name.'_sync_status', 'queued'); |
| | | } |
| | | |
| | | public function addDeleteTerm():void |
| | | { |
| | | if (!has_action('pre_delete_term', [$this, 'handleDeleteTerm'])) { |
| | | add_action('pre_delete_term', [$this, 'handleDeleteTerm']); |
| | | } |
| | | } |
| | | public function removeDeleteTerm():void |
| | | { |
| | | remove_action('pre_delete_term', [$this, 'handleDeleteTerm']); |
| | | } |
| | | public function handleDeleteTerm(int $termID, string $taxonomy):void |
| | | { |
| | | if (!$this->canSync['delete']) { |
| | | return; |
| | | } |
| | | $tax = jvbNoBase($taxonomy); |
| | | if (!in_array($tax, $this->syncTaxonomies)) { |
| | | return; |
| | | } |
| | | $fields = $this->getSyncFields($termID, 'term'); |
| | | if (empty($fields["_{$this->service_name}_item_id"])) { |
| | | return; |
| | | } |
| | | JVB()->queue()->add( |
| | | self::$deleteFrom, |
| | | 0, |
| | | [ |
| | | 'fields' => [$termID => $fields], |
| | | 'service' => $this->service_name, |
| | | 'type' => 'term' |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | /**************************************************************** |
| | | * USER SYNC |
| | | ****************************************************************/ |
| | | public function addSaveUser():void |
| | | { |
| | | if (empty($this->syncUsers)) { |
| | | return; |
| | | } |
| | | if (!has_action('profile_update', [$this, 'handleUpdateUser'])) { |
| | | add_action('profile_update', [$this, 'handleUpdateUser'], 20, 1); |
| | | } |
| | | if (!has_action('user_register', [$this, 'handleUpdateUser'])) { |
| | | add_action('user_register', [$this, 'handleUpdateUser'], 20, 1); |
| | | } |
| | | } |
| | | public function removeSaveUser():void |
| | | { |
| | | if (empty($this->syncUsers)) { |
| | | return; |
| | | } |
| | | remove_action('profile_update', [$this, 'handleUpdateUser'], 20); |
| | | remove_action('user_register', [$this, 'handleUpdateUser'], 20); |
| | | } |
| | | protected function handleUpdateUser(int $userID):void |
| | | { |
| | | $user = $this->getOrCreateUser($userID); |
| | | if (!$user) { |
| | | return; |
| | | } |
| | | |
| | | $fields = $this->getSyncFields($userID, 'user'); |
| | | if (!empty($fields["_{$this->service_name}_item_id"])) { |
| | | return; |
| | | } |
| | | |
| | | $this->removeSaveUser(); |
| | | $this->handleTheSaveUser($userID); |
| | | $this->addSaveTerm(); |
| | | } |
| | | |
| | | protected function getOrCreateUser(int $userID):string|false |
| | | { |
| | | $user = get_userdata($userID); |
| | | if (!$user || is_wp_error($user)) { |
| | | return false; |
| | | } |
| | | $role = jvbUserRole($userID); |
| | | if (!in_array(jvbNoBase($role), $this->syncUsers)) { |
| | | return false; |
| | | } |
| | | $fields = $this->getSyncFields($userID, 'user'); |
| | | if (!empty($fields["_{$this->service_name}_item_id"])) { |
| | | return $fields["_{$this->service_name}_item_id"]; |
| | | } |
| | | |
| | | $meta = Meta::forUser($userID); |
| | | $serviceUserID = $this->searchServiceForUser($user->user_email??''); |
| | | if ($serviceUserID) { |
| | | $meta->set("{$this->service_name}_item_id", $serviceUserID); |
| | | return $serviceUserID; |
| | | } |
| | | |
| | | $created = $this->createServiceUser($userID, $fields); |
| | | if ($created) { |
| | | return $created; |
| | | } |
| | | return false; |
| | | } |
| | | public function searchServiceForUser(string $email):string|false |
| | | { |
| | | if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { |
| | | return false; |
| | | } |
| | | return $this->handleEmailSearch($email); |
| | | } |
| | | |
| | | /** |
| | | * Searches for existing user from sanitized email |
| | | * @param string $email |
| | | * @return string|false The found service's User ID or false on failure |
| | | */ |
| | | abstract public function handleEmailSearch(string $email):string|false; |
| | | |
| | | public function createServiceUser(int $userID, array $fields):string|false |
| | | { |
| | | $checked = $this->validateUserFields($userID, $fields); |
| | | $response = $this->handleCreateUser($checked); |
| | | if ($response['success']) { |
| | | $meta = Meta::forUser($userID); |
| | | $meta->set("{$this->service_name}_item_id", $response['result']['id']); |
| | | } |
| | | return $response['success'] ? $response['customer']??false : false; |
| | | } |
| | | |
| | | protected function validateUserFields(int $userID, array $fields):array|false |
| | | { |
| | | foreach ($fields as $f => $v) { |
| | | $v = match ($f) { |
| | | 'email' => filter_var($v, FILTER_SANITIZE_EMAIL), |
| | | 'phone' => Sanitizer::sanitizePhone($v), |
| | | default => sanitize_text_field($v), |
| | | }; |
| | | if (empty($v) && in_array($f, $this->requiredUserFields())) { |
| | | return false; |
| | | } |
| | | $fields[$f] = $v; |
| | | } |
| | | return $fields; |
| | | } |
| | | protected function requiredUserFields():array |
| | | { |
| | | return []; |
| | | } |
| | | |
| | | /** |
| | | * @param array $data User Data |
| | | * @return array The created user ID or false on failure |
| | | */ |
| | | abstract protected function handleCreateUser(array $data):array; |
| | | |
| | | /** |
| | | * @param int $userID |
| | | * @return void |
| | | */ |
| | | protected function handleTheSaveUser(int $userID):void |
| | | { |
| | | error_log('==== ['.$this->title.']::handleTheSaveUser ===='); |
| | | //TODO: This is likely only for stuff like customers. |
| | | //If we have multiple stores connected, we may have to queue operations to update every connection's customer account if they made an order with that store |
| | | $role = jvbUserRole($userID); |
| | | $registrar = Registrar::getInstance($role); |
| | | if (!$registrar || !$registrar->hasIntegration($this->service_name)) { |
| | | return; |
| | | } |
| | | |
| | | $this->queueOperation(self::$syncTo, [ |
| | | 'users' => [$userID], |
| | | ]); |
| | | } |
| | | |
| | | public function addDeleteUser():void |
| | | { |
| | | if (!has_action('delete_user', [$this, 'handleDeleteTerm'])) { |
| | | add_action('delete_user', [$this, 'handleDeleteTerm']); |
| | | } |
| | | } |
| | | public function removeDeleteUser():void |
| | | { |
| | | remove_action('delete_user', [$this, 'handleDeleteTerm']); |
| | | } |
| | | public function handleDeleteUser(int $userID):void |
| | | { |
| | | if (!$this->canSync['delete']) { |
| | | return; |
| | | } |
| | | |
| | | $fields = $this->getSyncFields($userID, 'user'); |
| | | if (empty($fields["_{$this->service_name}_item_id"])) { |
| | | return; |
| | | } |
| | | JVB()->queue()->add( |
| | | self::$deleteFrom, |
| | | 0, |
| | | [ |
| | | 'fields' => [$userID => $fields], |
| | | 'service' => $this->service_name, |
| | | 'type' => 'user' |
| | | ] |
| | | ); |
| | | } |
| | | } |