diff --git a/includes/class-newspack-newsletters-contacts.php b/includes/class-newspack-newsletters-contacts.php new file mode 100644 index 000000000..c87a526c5 --- /dev/null +++ b/includes/class-newspack-newsletters-contacts.php @@ -0,0 +1,259 @@ +service . '.' ); + } else { + Newspack_Newsletters_Logger::log( 'Adding contact without lists. Provider is ' . $provider->service . '.' ); + } + + $existing_contact = Newspack_Newsletters_Subscription::get_contact_data( $contact['email'], true ); + $contact['existing_contact_data'] = \is_wp_error( $existing_contact ) ? false : $existing_contact; + $is_updating = \is_wp_error( $existing_contact ) ? false : true; + + /** + * Filters the contact before passing on to the API. + * + * @param array $contact { + * Contact information. + * + * @type string $email Contact email address. + * @type string $name Contact name. Optional. + * @type string $existing_contact_data Existing contact data, if updating a contact. The hook will be also called when + * @type string[] $metadata Contact additional metadata. Optional. + * } + * @param string[]|false $selected_list_ids Array of list IDs the contact will be subscribed to, or false. + * @param string $provider The provider name. + */ + $contact = apply_filters( 'newspack_newsletters_contact_data', $contact, $lists, $provider->service ); + + if ( isset( $contact['metadata'] ) ) { + Newspack_Newsletters_Logger::log( 'Adding contact with metadata key(s): ' . implode( ', ', array_keys( $contact['metadata'] ) ) . '.' ); + } + + if ( ! isset( $contact['metadata'] ) ) { + $contact['metadata'] = []; + } + $contact['metadata']['origin_newspack'] = '1'; + + /** + * Filters the contact selected lists before passing on to the API. + * + * @param string[]|false $lists Array of list IDs the contact will be subscribed to, or false. + * @param array $contact { + * Contact information. + * + * @type string $email Contact email address. + * @type string $name Contact name. Optional. + * @type string[] $metadata Contact additional metadata. Optional. + * } + * @param string $provider The provider name. + */ + $lists = apply_filters( 'newspack_newsletters_contact_lists', $lists, $contact, $provider->service ); + + return self::add_to_esp( $contact, $lists, $is_updating ); + } + + /** + * Permanently delete a user subscription. + * + * @param int $user_id User ID. + * + * @return bool|WP_Error Whether the contact was deleted or error. + */ + public static function delete( $user_id ) { + $user = get_user_by( 'id', $user_id ); + if ( ! $user ) { + return new WP_Error( 'newspack_newsletters_invalid_user', __( 'Invalid user.' ) ); + } + /** Only delete if email ownership is verified. */ + if ( ! Newspack_Newsletters_Subscription::is_email_verified( $user_id ) ) { + return new \WP_Error( 'newspack_newsletters_email_not_verified', __( 'Email ownership is not verified.' ) ); + } + $provider = Newspack_Newsletters::get_service_provider(); + if ( empty( $provider ) ) { + return new WP_Error( 'newspack_newsletters_invalid_provider', __( 'Provider is not set.' ) ); + } + if ( ! method_exists( $provider, 'delete_contact' ) ) { + return new WP_Error( 'newspack_newsletters_invalid_provider_method', __( 'Provider does not support deleting user subscriptions.' ) ); + } + return $provider->delete_contact( $user->user_email ); + } + + /** + * Update a contact lists subscription. + * + * This method will remove the contact from all subscription lists and add + * them to the specified lists. + * + * @param string $email Contact email address. + * @param string[] $lists Array of list IDs to subscribe the contact to. + * + * @return bool|WP_Error Whether the contact was updated or error. + */ + public static function update_lists( $email, $lists = [] ) { + if ( ! Newspack_Newsletters_Subscription::has_subscription_management() ) { + return new WP_Error( 'newspack_newsletters_not_supported', __( 'Not supported for this provider', 'newspack-newsletters' ) ); + } + $provider = Newspack_Newsletters::get_service_provider(); + + Newspack_Newsletters_Logger::log( 'Updating lists of a contact. List selection: ' . implode( ', ', $lists ) . '. Provider is ' . $provider->service . '.' ); + + /** Determine lists to add/remove from existing list config. */ + $lists_config = Newspack_Newsletters_Subscription::get_lists_config(); + $lists_to_add = array_intersect( array_keys( $lists_config ), $lists ); + $lists_to_remove = array_diff( array_keys( $lists_config ), $lists ); + + /** Clean up lists to add/remove from contact's existing data. */ + $current_lists = Newspack_Newsletters_Subscription::get_contact_lists( $email ); + $lists_to_add = array_diff( $lists_to_add, $current_lists ); + $lists_to_remove = array_intersect( $current_lists, $lists_to_remove ); + + if ( empty( $lists_to_add ) && empty( $lists_to_remove ) ) { + return false; + } + + $result = $provider->update_contact_lists_handling_local( $email, $lists_to_add, $lists_to_remove ); + + /** + * Fires after a contact's lists are updated. + * + * @param string $provider The provider name. + * @param string $email Contact email address. + * @param string[] $lists_to_add Array of list IDs to subscribe the contact to. + * @param string[] $lists_to_remove Array of list IDs to remove the contact from. + * @param bool|WP_Error $result True if the contact was updated or error if failed. + */ + do_action( 'newspack_newsletters_update_contact_lists', $provider->service, $email, $lists_to_add, $lists_to_remove, $result ); + + return $result; + } + + /** + * Internal method to add a contact to lists. Should be called by the + * `add_contact` method or `handle_async_subscribe` for the async strategy. + * + * @param array $contact Contact information. + * @param array $lists Array of list IDs to subscribe the contact to. + * @param bool $is_updating Whether the contact is being updated. If false, the contact is being created. + * + * @return array|WP_Error Contact data if it was added, or error otherwise. + */ + private static function add_to_esp( $contact, $lists = [], $is_updating = false ) { + $provider = Newspack_Newsletters::get_service_provider(); + $errors = new WP_Error(); + $result = []; + + try { + if ( method_exists( $provider, 'add_contact_with_groups_and_tags' ) ) { + $result = $provider->add_contact_with_groups_and_tags( $contact, $lists ); + } elseif ( empty( $lists ) ) { + $result = $provider->add_contact( $contact ); + } else { + foreach ( $lists as $list_id ) { + $result = $provider->add_contact( $contact, $list_id ); + } + } + } catch ( \Exception $e ) { + $errors->add( 'newspack_newsletters_subscription_add_contact', $e->getMessage() ); + } + + if ( is_wp_error( $result ) ) { + $errors->add( $result->get_error_code(), $result->get_error_message() ); + } + + // Handle local lists feature. + foreach ( $lists as $list_id ) { + try { + $provider->add_contact_handling_local_list( $contact, $list_id ); + } catch ( \Exception $e ) { + $errors->add( 'newspack_newsletters_subscription_handling_local_list', $e->getMessage() ); + } + } + + /** + * Fires after a contact is added. + * + * @param string $provider The provider name. + * @param array $contact { + * Contact information. + * + * @type string $email Contact email address. + * @type string $name Contact name. Optional. + * @type string[] $metadata Contact additional metadata. Optional. + * } + * @param string[]|false $lists Array of list IDs to subscribe the contact to. + * @param array|WP_Error $result Array with data if the contact was added or error if failed. + * @param bool $is_updating Whether the contact is being updated. If false, the contact is being created. + */ + do_action( 'newspack_newsletters_add_contact', $provider->service, $contact, $lists, $result, $is_updating ); + + if ( $errors->has_errors() ) { + return $errors; + } + + return $result; + } +} diff --git a/includes/class-newspack-newsletters-subscription.php b/includes/class-newspack-newsletters-subscription.php index d34163b7f..8720e095c 100644 --- a/includes/class-newspack-newsletters-subscription.php +++ b/includes/class-newspack-newsletters-subscription.php @@ -34,7 +34,6 @@ class Newspack_Newsletters_Subscription { public static function init() { add_action( 'rest_api_init', [ __CLASS__, 'register_api_endpoints' ] ); add_action( 'newspack_registered_reader', [ __CLASS__, 'newspack_registered_reader' ], 10, 5 ); - add_action( 'delete_user', [ __CLASS__, 'delete_user' ], 10, 3 ); /** User email verification for subscription management. */ add_action( 'resetpass_form', [ __CLASS__, 'set_current_user_email_verified' ] ); @@ -357,125 +356,48 @@ public static function get_contact_data( $email_address, $return_details = false * @return array|WP_Error|true Contact data if it was added, or error otherwise. True if async. */ public static function add_contact( $contact, $lists = false, $async = false ) { - if ( ! is_array( $lists ) && false !== $lists ) { - $lists = [ $lists ]; - } - - /** - * Trigger an action before contact adding. - * - * @param string[]|false $lists Array of list IDs the contact will be subscribed to, or false. - * @param array $contact { - * Contact information. - * - * @type string $email Contact email address. - * @type string $name Contact name. Optional. - * @type string[] $metadata Contact additional metadata. Optional. - * } - */ - do_action( 'newspack_newsletters_pre_add_contact', $lists, $contact ); - - $provider = Newspack_Newsletters::get_service_provider(); - if ( empty( $provider ) ) { - return new WP_Error( 'newspack_newsletters_invalid_provider', __( 'Provider is not set.' ) ); - } - - if ( false !== $lists ) { - Newspack_Newsletters_Logger::log( 'Adding contact to list(s): ' . implode( ', ', $lists ) . '. Provider is ' . $provider->service . '.' ); - } else { - Newspack_Newsletters_Logger::log( 'Adding contact without lists. Provider is ' . $provider->service . '.' ); - } - - $existing_contact = self::get_contact_data( $contact['email'], true ); - $contact['existing_contact_data'] = \is_wp_error( $existing_contact ) ? false : $existing_contact; - $is_updating = \is_wp_error( $existing_contact ) ? false : true; - - /** - * Filters the contact before passing on to the API. - * - * @param array $contact { - * Contact information. - * - * @type string $email Contact email address. - * @type string $name Contact name. Optional. - * @type string $existing_contact_data Existing contact data, if updating a contact. The hook will be also called when - * @type string[] $metadata Contact additional metadata. Optional. - * } - * @param string[]|false $selected_list_ids Array of list IDs the contact will be subscribed to, or false. - * @param string $provider The provider name. - */ - $contact = apply_filters( 'newspack_newsletters_contact_data', $contact, $lists, $provider->service ); - - if ( isset( $contact['metadata'] ) ) { - Newspack_Newsletters_Logger::log( 'Adding contact with metadata key(s): ' . implode( ', ', array_keys( $contact['metadata'] ) ) . '.' ); - } - - if ( ! isset( $contact['metadata'] ) ) { - $contact['metadata'] = []; - } - $contact['metadata']['origin_newspack'] = '1'; - - /** - * Filters the contact selected lists before passing on to the API. - * - * @param string[]|false $lists Array of list IDs the contact will be subscribed to, or false. - * @param array $contact { - * Contact information. - * - * @type string $email Contact email address. - * @type string $name Contact name. Optional. - * @type string[] $metadata Contact additional metadata. Optional. - * } - * @param string $provider The provider name. - */ - $lists = apply_filters( 'newspack_newsletters_contact_lists', $lists, $contact, $provider->service ); - - if ( true !== $async ) { - return self::add_contact_to_provider( $contact, $lists, $is_updating ); - } else { - $intent_id = self::add_subscription_intent( $contact, $lists, $is_updating ); - $nonce = wp_create_nonce( self::ASYNC_ACTION ); - $url = admin_url( 'admin-ajax.php?action=' . self::ASYNC_ACTION . '&nonce=' . $nonce ); - $args = [ - 'timeout' => 0.01, - 'blocking' => false, - 'cookies' => $_COOKIE, // phpcs:ignore - 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), - 'body' => [ - 'action_name' => self::ASYNC_ACTION, - 'intent_id' => $intent_id, - ], - ]; - wp_remote_post( $url, $args ); - return true; - } + _deprecated_function( __METHOD__, '2.21', 'Newspack_Newsletters_Contacts::upsert' ); + return Newspack_Newsletters_Contacts::upsert( $contact, $lists, $async ); } /** - * Register a subscription intent. + * Register a subscription intent and dispatches a async request to process it. * * @param array $contact Contact information. * @param array $lists Array of list IDs to subscribe the contact to. - * @param bool $is_updating Whether the contact is being updated. If false, the contact is being created. * * @return int|WP_Error Subscription intent ID or error. */ - private static function add_subscription_intent( $contact, $lists, $is_updating ) { + public static function add_subscription_intent( $contact, $lists ) { $intent_id = \wp_insert_post( [ 'post_type' => self::SUBSCRIPTION_INTENT_CPT, 'post_status' => 'publish', 'meta_input' => [ - 'contact' => $contact, - 'lists' => $lists, - 'is_updating' => $is_updating, - 'errors' => [], + 'contact' => $contact, + 'lists' => $lists, + 'errors' => [], ], ] ); if ( is_wp_error( $intent_id ) ) { Newspack_Newsletters_Logger::log( 'Error adding subscription intent: ' . $intent_id->get_error_message() ); } + + $nonce = wp_create_nonce( self::ASYNC_ACTION ); + $url = admin_url( 'admin-ajax.php?action=' . self::ASYNC_ACTION . '&nonce=' . $nonce ); + $args = [ + 'timeout' => 0.01, + 'blocking' => false, + 'cookies' => $_COOKIE, // phpcs:ignore + 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), + 'body' => [ + 'action_name' => self::ASYNC_ACTION, + 'intent_id' => $intent_id, + ], + ]; + wp_remote_post( $url, $args ); + return $intent_id; } @@ -496,11 +418,10 @@ private static function get_subscription_intent( $intent_id_or_post ) { return false; } return [ - 'id' => $intent->ID, - 'contact' => get_post_meta( $intent->ID, 'contact', true ), - 'lists' => get_post_meta( $intent->ID, 'lists', true ), - 'is_updating' => get_post_meta( $intent->ID, 'is_updating', true ), - 'errors' => get_post_meta( $intent->ID, 'errors', true ), + 'id' => $intent->ID, + 'contact' => get_post_meta( $intent->ID, 'contact', true ), + 'lists' => get_post_meta( $intent->ID, 'lists', true ), + 'errors' => get_post_meta( $intent->ID, 'errors', true ), ]; } @@ -552,8 +473,7 @@ public static function process_subscription_intents( $intent_id = null ) { $contact = $intent['contact']; $email = $contact['email']; $lists = $intent['lists']; - $is_updating = $intent['is_updating']; - $result = self::add_contact_to_provider( $contact, $lists, $is_updating ); + $result = Newspack_Newsletters_Contacts::upsert( $contact, $lists, false ); $user = get_user_by( 'email', $email ); if ( \is_wp_error( $result ) ) { @@ -618,78 +538,6 @@ public static function handle_async_subscribe() { \wp_die( 'OK', '', 200 ); } - /** - * Internal method to add a contact to lists. Should be called by the - * `add_contact` method or `handle_async_subscribe` for the async strategy. - * - * @param array $contact Contact information. - * @param array $lists Array of list IDs to subscribe the contact to. - * @param bool $is_updating Whether the contact is being updated. If false, the contact is being created. - * - * @return array|WP_Error Contact data if it was added, or error otherwise. - */ - private static function add_contact_to_provider( $contact, $lists = [], $is_updating = false ) { - $provider = Newspack_Newsletters::get_service_provider(); - $errors = new WP_Error(); - $result = []; - - try { - if ( method_exists( $provider, 'add_contact_with_groups_and_tags' ) ) { - $result = $provider->add_contact_with_groups_and_tags( $contact, $lists ); - } elseif ( empty( $lists ) ) { - $result = $provider->add_contact( $contact ); - } else { - foreach ( $lists as $list_id ) { - $result = $provider->add_contact( $contact, $list_id ); - } - } - } catch ( \Exception $e ) { - $errors->add( 'newspack_newsletters_subscription_add_contact', $e->getMessage() ); - } - - if ( is_wp_error( $result ) ) { - $errors->add( $result->get_error_code(), $result->get_error_message() ); - } - - // Handle local lists feature. - foreach ( $lists as $list_id ) { - try { - $provider->add_contact_handling_local_list( $contact, $list_id ); - } catch ( \Exception $e ) { - $errors->add( 'newspack_newsletters_subscription_handling_local_list', $e->getMessage() ); - } - } - - /** - * Fires after a contact is added. - * - * @param string $provider The provider name. - * @param array $contact { - * Contact information. - * - * @type string $email Contact email address. - * @type string $name Contact name. Optional. - * @type string[] $metadata Contact additional metadata. Optional. - * } - * @param string[]|false $lists Array of list IDs to subscribe the contact to. - * @param array|WP_Error $result Array with data if the contact was added or error if failed. - * @param bool $is_updating Whether the contact is being updated. If false, the contact is being created. - */ - do_action( 'newspack_newsletters_add_contact', $provider->service, $contact, $lists, $result, $is_updating ); - - // Remove any existing subscription error message. - if ( ! $errors->has_errors() ) { - $user = get_user_by( 'email', $contact['email'] ); - if ( $user ) { - delete_user_meta( $user->ID, 'newspack_newsletters_subscription_error' ); - } - } else { - return $errors; - } - - return $result; - } - /** * Permanently delete a user subscription. * @@ -698,22 +546,8 @@ private static function add_contact_to_provider( $contact, $lists = [], $is_upda * @return bool|WP_Error Whether the contact was deleted or error. */ public static function delete_user_subscription( $user_id ) { - $user = get_user_by( 'id', $user_id ); - if ( ! $user ) { - return new WP_Error( 'newspack_newsletters_invalid_user', __( 'Invalid user.' ) ); - } - /** Only delete if email ownership is verified. */ - if ( ! self::is_email_verified( $user_id ) ) { - return new \WP_Error( 'newspack_newsletters_email_not_verified', __( 'Email ownership is not verified.' ) ); - } - $provider = Newspack_Newsletters::get_service_provider(); - if ( empty( $provider ) ) { - return new WP_Error( 'newspack_newsletters_invalid_provider', __( 'Provider is not set.' ) ); - } - if ( ! method_exists( $provider, 'delete_contact' ) ) { - return new WP_Error( 'newspack_newsletters_invalid_provider_method', __( 'Provider does not support deleting user subscriptions.' ) ); - } - return $provider->delete_contact( $user->user_email ); + _deprecated_function( __METHOD__, '2.21', 'Newspack_Newsletters_Contacts::delete' ); + return Newspack_Newsletters_Contacts::delete( $user_id ); } /** @@ -756,31 +590,6 @@ public static function newspack_registered_reader( $email, $authenticate, $user_ } } - /** - * Delete a contact from ESP when a reader is deleted. - * - * @param int $user_id ID of the user to delete. - * @param int|null $reassign ID of the user to reassign posts and links to. - * Default null, for no reassignment. - * @param WP_User $user WP_User object of the user to delete. - * - * @return bool|WP_Error Whether the contact was deleted or error. - */ - public static function delete_user( $user_id, $reassign, $user ) { - if ( ! class_exists( '\Newspack\Reader_Activation' ) || ! \Newspack\Reader_Activation::is_user_reader( $user ) ) { - return; - } - $sync = \Newspack\Reader_Activation::get_setting( 'sync_esp' ); - if ( ! $sync ) { - return; - } - $sync_delete = \Newspack\Reader_Activation::get_setting( 'sync_esp_delete' ); - if ( ! $sync_delete ) { - return; - } - return self::delete_user_subscription( $user_id ); - } - /** * Update a contact lists subscription. * @@ -793,41 +602,8 @@ public static function delete_user( $user_id, $reassign, $user ) { * @return bool|WP_Error Whether the contact was updated or error. */ private static function update_contact_lists( $email, $lists = [] ) { - if ( ! self::has_subscription_management() ) { - return new WP_Error( 'newspack_newsletters_not_supported', __( 'Not supported for this provider', 'newspack-newsletters' ) ); - } - $provider = Newspack_Newsletters::get_service_provider(); - - Newspack_Newsletters_Logger::log( 'Updating lists of a contact. List selection: ' . implode( ', ', $lists ) . '. Provider is ' . $provider->service . '.' ); - - /** Determine lists to add/remove from existing list config. */ - $lists_config = self::get_lists_config(); - $lists_to_add = array_intersect( array_keys( $lists_config ), $lists ); - $lists_to_remove = array_diff( array_keys( $lists_config ), $lists ); - - /** Clean up lists to add/remove from contact's existing data. */ - $current_lists = self::get_contact_lists( $email ); - $lists_to_add = array_diff( $lists_to_add, $current_lists ); - $lists_to_remove = array_intersect( $current_lists, $lists_to_remove ); - - if ( empty( $lists_to_add ) && empty( $lists_to_remove ) ) { - return false; - } - - $result = $provider->update_contact_lists_handling_local( $email, $lists_to_add, $lists_to_remove ); - - /** - * Fires after a contact's lists are updated. - * - * @param string $provider The provider name. - * @param string $email Contact email address. - * @param string[] $lists_to_add Array of list IDs to subscribe the contact to. - * @param string[] $lists_to_remove Array of list IDs to remove the contact from. - * @param bool|WP_Error $result True if the contact was updated or error if failed. - */ - do_action( 'newspack_newsletters_update_contact_lists', $provider->service, $email, $lists_to_add, $lists_to_remove, $result ); - - return $result; + _deprecated_function( __METHOD__, '2.21', 'Newspack_Newsletters_Contacts::update_lists' ); + return Newspack_Newsletters_Contacts::update_lists( $email, $lists ); } /** @@ -1225,7 +1001,7 @@ public static function process_subscription_update() { } else { $email = get_userdata( get_current_user_id() )->user_email; $lists = isset( $_POST['lists'] ) ? array_map( 'sanitize_text_field', $_POST['lists'] ) : []; - $result = self::update_contact_lists( $email, $lists ); + $result = Newspack_Newsletters_Contacts::update_lists( $email, $lists ); if ( is_wp_error( $result ) ) { wc_add_notice( $result->get_error_message(), 'error' ); } elseif ( false === $result ) { diff --git a/includes/service-providers/active_campaign/class-newspack-newsletters-active-campaign.php b/includes/service-providers/active_campaign/class-newspack-newsletters-active-campaign.php index 517c6057e..6c229702b 100644 --- a/includes/service-providers/active_campaign/class-newspack-newsletters-active-campaign.php +++ b/includes/service-providers/active_campaign/class-newspack-newsletters-active-campaign.php @@ -1157,9 +1157,9 @@ public function update_contact_lists( $email, $lists_to_add = [], $lists_to_remo $existing_contact = $this->get_contact_data( $email ); if ( is_wp_error( $existing_contact ) ) { /** Create contact */ - // Call Newspack_Newsletters_Subscription's method (not the provider's directly), + // Call Newspack_Newsletters_Contacts's method (not the provider's directly), // so the appropriate hooks are called. - $contact_data = Newspack_Newsletters_Subscription::add_contact( [ 'email' => $email ] ); + $contact_data = Newspack_Newsletters_Contacts::upsert( [ 'email' => $email ] ); if ( is_wp_error( $contact_data ) ) { return $contact_data; } diff --git a/includes/service-providers/constant_contact/class-newspack-newsletters-constant-contact.php b/includes/service-providers/constant_contact/class-newspack-newsletters-constant-contact.php index 41c771317..66aba7cbc 100644 --- a/includes/service-providers/constant_contact/class-newspack-newsletters-constant-contact.php +++ b/includes/service-providers/constant_contact/class-newspack-newsletters-constant-contact.php @@ -940,9 +940,9 @@ public function update_contact_lists( $email, $lists_to_add = [], $lists_to_remo $contact_data = $this->get_contact_data( $email ); if ( is_wp_error( $contact_data ) ) { /** Create contact */ - // Call Newspack_Newsletters_Subscription's method (not the provider's directly), + // Call Newspack_Newsletters_Contacts's method (not the provider's directly), // so the appropriate hooks are called. - $contact_data = Newspack_Newsletters_Subscription::add_contact( [ 'email' => $email ] ); + $contact_data = Newspack_Newsletters_Contacts::upsert( [ 'email' => $email ] ); if ( is_wp_error( $contact_data ) ) { return $contact_data; } diff --git a/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp.php b/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp.php index e76ffd9f4..5eb1f099c 100644 --- a/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp.php +++ b/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp.php @@ -1214,6 +1214,129 @@ public function add_contact_with_groups_and_tags( $contact, $lists ) { return $results; } + /** + * Get merge field type. + * + * @param mixed $value Value to check. + * + * @return string Merge field type. + */ + private function get_merge_field_type( $value ) { + if ( is_numeric( $value ) ) { + return 'number'; + } + if ( is_bool( $value ) ) { + return 'boolean'; + } + return 'text'; + } + + /** + * Given a contact metadata array, build the `merge_fields` array to be sent to Mailchimp + * by sarching for existing merge fields and creating new ones as needed. + * + * @param string $audience_id Audience ID. + * @param array $data The contact metadata. + * + * @return array Merge fields. + */ + private function prepare_merge_fields( $audience_id, $data ) { + $merge_fields = []; + + // Strip arrays. + $data = array_filter( + $data, + function( $value ) { + return ! is_array( $value ); + } + ); + + // Get and match existing merge fields. + try { + $existing_fields = Newspack_Newsletters_Mailchimp_Cached_Data::get_merge_fields( $audience_id ); + } catch ( \Exception $e ) { + Newspack_Newsletters_Logger::log( + sprintf( + // Translators: %1$s is the error message. + __( 'Error getting merge fields: %1$s', 'newspack-newsletters' ), + $existing_fields->get_error_message() + ) + ); + return []; + } + if ( empty( $existing_fields ) ) { + $existing_fields = []; + } + + usort( + $existing_fields, + function( $a, $b ) { + return $a['merge_id'] - $b['merge_id']; + } + ); + + $list_merge_fields = []; + + // Handle duplicate fields. + foreach ( $existing_fields as $field ) { + if ( ! isset( $list_merge_fields[ $field['name'] ] ) ) { + $list_merge_fields[ $field['name'] ] = $field['tag']; + } else { + Newspack_Newsletters_Logger::log( + sprintf( + // Translators: %1$s is the merge field name, %2$s is the field's unique tag. + __( 'Warning: Duplicate merge field %1$s found with tag %2$s.', 'newspack-newsletters' ), + $field['name'], + $field['tag'] + ) + ); + } + } + + foreach ( $data as $field_name => $field_value ) { + // If field already exists, add it to the payload. + if ( isset( $list_merge_fields[ $field_name ] ) ) { + $merge_fields[ $list_merge_fields[ $field_name ] ] = $data[ $field_name ]; + unset( $data[ $field_name ] ); + } + } + + // Create remaining fields. + $remaining_fields = array_keys( $data ); + $mc = new Mailchimp( $this->api_key() ); + foreach ( $remaining_fields as $field_name ) { + $created_field = $mc->post( + "lists/$audience_id/merge-fields", + [ + 'name' => $field_name, + 'type' => $this->get_merge_field_type( $data[ $field_name ] ), + ] + ); + // Skip field if it failed to create. + if ( empty( $created_field['merge_id'] ) ) { + Newspack_Newsletters_Logger::log( + sprintf( + // Translators: %1$s is the merge field key, %2$s is the error message. + __( 'Failed to create merge field %1$s. Error response: %2$s', 'newspack-newsletters' ), + $field_name, + $created_field['detail'] ?? 'Unknown error' + ) + ); + continue; + } + Newspack_Newsletters_Logger::log( + sprintf( + // Translators: %1$s is the merge field key, %2$s is the error message. + __( 'Created merge field %1$s.', 'newspack-newsletters' ), + $field_name + ) + ); + $merge_fields[ $created_field['tag'] ] = $data[ $field_name ]; + } + + return $merge_fields; + } + /** * Add contact to a list. * @@ -1252,69 +1375,9 @@ public function add_contact( $contact, $list_id = false, $sublists = [] ) { $update_payload = [ 'email_address' => $email_address ]; if ( isset( $contact['metadata'] ) && is_array( $contact['metadata'] ) && ! empty( $contact['metadata'] ) ) { - $update_payload['merge_fields'] = []; - - $merge_fields_res = $mc->get( "lists/$list_id/merge-fields", [ 'count' => 1000 ] ); - if ( ! isset( $merge_fields_res['merge_fields'] ) ) { - return new \WP_Error( - 'newspack_newsletters_mailchimp_add_contact_failed', - sprintf( - // Translators: %1$s is the error message. - __( 'Error getting merge fields: %1$s', 'newspack-newsletters' ), - $merge_fields_res['detail'] ?? __( 'Unable to fetch merge fields.' ) - ) - ); - } - $existing_merge_fields = $merge_fields_res['merge_fields']; - usort( - $existing_merge_fields, - function ( $a, $b ) { - return $a['merge_id'] - $b['merge_id']; - } - ); - - $list_merge_fields = []; - - // Handle duplicate fields. - foreach ( $existing_merge_fields as $key => $field ) { - if ( ! isset( $list_merge_fields[ $field['name'] ] ) ) { - $list_merge_fields[ $field['name'] ] = $field['tag']; - } else { - Newspack_Newsletters_Logger::log( - sprintf( - // Translators: %1$s is the merge field name, %2$s is the unique tag. - __( 'Warning: Duplicate merge field %1$s found with tag %2$s.', 'newspack-newsletters' ), - $field['name'], - $field['tag'] - ) - ); - } - } - - foreach ( $contact['metadata'] as $key => $value ) { - if ( isset( $list_merge_fields[ $key ] ) ) { - $update_payload['merge_fields'][ $list_merge_fields[ $key ] ] = (string) $value; - } else { - $created_merge_field = $mc->post( - "lists/$list_id/merge-fields", - [ - 'name' => $key, - 'type' => 'text', - ] - ); - if ( isset( $created_merge_field['status'] ) && '4' === substr( $created_merge_field['status'], 0, 1 ) ) { - Newspack_Newsletters_Logger::log( - sprintf( - // Translators: %1$s is the merge field key, %2$s is the error message. - __( 'Failed to create merge field %1$s. Reason: %2$s', 'newspack-newsletters' ), - $key, - $created_merge_field['detail'] ?? $created_merge_field['title'] - ) - ); - continue; - } - $update_payload['merge_fields'][ $created_merge_field['tag'] ] = (string) $value; - } + $merge_fields = $this->prepare_merge_fields( $list_id, $contact['metadata'] ); + if ( ! empty( $merge_fields ) ) { + $update_payload['merge_fields'] = $merge_fields; } } @@ -1474,7 +1537,7 @@ public function update_contact_lists( $email, $lists_to_add = [], $lists_to_remo $contact = $this->get_contact_data( $email ); if ( is_wp_error( $contact ) ) { /** Create contact */ - $result = Newspack_Newsletters_Subscription::add_contact( [ 'email' => $email ], $lists_to_add ); + $result = Newspack_Newsletters_Contacts::upsert( [ 'email' => $email ], $lists_to_add ); if ( is_wp_error( $result ) ) { return $result; } diff --git a/phpcs.xml b/phpcs.xml index 6a6550f75..d3c35d007 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -6,6 +6,9 @@ + + + diff --git a/phpcsSniffs/Sniffs/NewspackNewslettersContactsMethodsSniff.php b/phpcsSniffs/Sniffs/NewspackNewslettersContactsMethodsSniff.php new file mode 100644 index 000000000..1c78b0705 --- /dev/null +++ b/phpcsSniffs/Sniffs/NewspackNewslettersContactsMethodsSniff.php @@ -0,0 +1,122 @@ +methods and check if they are called from the allowed classes. + * They are also allowed to be called from within the service-providers directory. + * + * @param PHP_CodeSniffer_File $phpcs_file The file where the token was found. + * @param int $stack_ptr The position in the stack where the token was found. + */ + public function process( PHP_CodeSniffer_File $phpcs_file, $stack_ptr ) { + + $path_parts = explode( DIRECTORY_SEPARATOR, $phpcs_file->path ); + + $possible_provider_dirs = [ + $path_parts[ count( $path_parts ) - 2 ], + $path_parts[ count( $path_parts ) - 3 ], + ]; + + if ( in_array( 'service-providers', $possible_provider_dirs, true ) ) { + return; + } + + $tokens = $phpcs_file->getTokens(); + $token = $tokens[ $stack_ptr ]; + + if ( $token['code'] === T_CLASS ) { + $this->current_class = $tokens[ $stack_ptr + 2 ]['content']; + return; + } + + if ( in_array( $token['content'], $this->methods, true ) ) { + $operator = $tokens[ $stack_ptr - 1 ]; + if ( $operator['type'] === 'T_DOUBLE_COLON' || $operator['type'] === 'T_OBJECT_OPERATOR' ) { + + if ( ! in_array( $this->current_class, $this->allowed_classes, true ) ) { + + $method_name = $tokens[ $stack_ptr - 2 ]['content'] . $tokens[ $stack_ptr - 1 ]['content'] . $token['content'] . '()'; + + $phpcs_file->addError( + sprintf( self::ERROR_MESSAGE, $method_name ), + $stack_ptr, + self::ERROR_CODE + ); + } + } + } + } +} diff --git a/phpcsSniffs/ruleset.xml b/phpcsSniffs/ruleset.xml new file mode 100644 index 000000000..9e78e9282 --- /dev/null +++ b/phpcsSniffs/ruleset.xml @@ -0,0 +1,4 @@ + + + Add Newspack Newsletters specific rules. + diff --git a/src/blocks/subscribe/index.php b/src/blocks/subscribe/index.php index dbed4a359..298f6b88c 100644 --- a/src/blocks/subscribe/index.php +++ b/src/blocks/subscribe/index.php @@ -498,7 +498,7 @@ function process_form() { \Newspack\Reader_Activation::register_reader( $email, $name, true, $metadata ); } - $result = \Newspack_Newsletters_Subscription::add_contact( + $result = \Newspack_Newsletters_Contacts::upsert( [ 'name' => $name ?? null, 'email' => $email, diff --git a/tests/test-esp-add-contact.php b/tests/test-esp-add-contact.php index 89f97348b..601bb441d 100644 --- a/tests/test-esp-add-contact.php +++ b/tests/test-esp-add-contact.php @@ -22,7 +22,7 @@ public static function set_up_before_class() { * Add a contact to a single list. */ public function test_add_contact_to_single_list() { - $result = Newspack_Newsletters_Subscription::add_contact( + $result = Newspack_Newsletters_Contacts::upsert( [ 'email' => 'test@example.com', ], @@ -43,7 +43,7 @@ public function test_add_contact_to_single_list() { * Add a contact to multiple lists. */ public function test_add_contact_to_multiple_lists() { - $result = Newspack_Newsletters_Subscription::add_contact( + $result = Newspack_Newsletters_Contacts::upsert( [ 'email' => 'test@example.com', ], @@ -68,7 +68,7 @@ public function test_add_contact_to_multiple_lists() { * Add a contact to lists and sublists. */ public function test_add_contact_to_lists_and_sublists() { - $result = Newspack_Newsletters_Subscription::add_contact( + $result = Newspack_Newsletters_Contacts::upsert( [ 'email' => 'test@example.com', ], @@ -96,7 +96,7 @@ public function test_add_contact_to_lists_and_sublists() { * Add a contact to multiple lists and sublists. */ public function test_add_contact_to_multiple_lists_and_sublists() { - $result = Newspack_Newsletters_Subscription::add_contact( + $result = Newspack_Newsletters_Contacts::upsert( [ 'email' => 'test@example.com', ],