diff --git a/includes/class-newspack-newsletters-ads.php b/includes/class-newspack-newsletters-ads.php index 2ace23217..53b435154 100644 --- a/includes/class-newspack-newsletters-ads.php +++ b/includes/class-newspack-newsletters-ads.php @@ -75,21 +75,11 @@ public static function rest_api_init() { [ 'callback' => [ __CLASS__, 'get_ads_config' ], 'methods' => 'GET', - 'permission_callback' => [ __CLASS__, 'permission_callback' ], + 'permission_callback' => [ 'Newspack_Newsletters', 'api_authoring_permissions_check' ], ] ); } - /** - * Check capabilities for using the API for authoring tasks. - * - * @param WP_REST_Request $request API request object. - * @return bool|WP_Error - */ - public static function permission_callback( $request ) { - return current_user_can( 'edit_posts' ); - } - /** * Register custom fields. */ diff --git a/includes/class-newspack-newsletters-editor.php b/includes/class-newspack-newsletters-editor.php index 711edc7d7..526966095 100644 --- a/includes/class-newspack-newsletters-editor.php +++ b/includes/class-newspack-newsletters-editor.php @@ -338,6 +338,7 @@ public static function enqueue_block_editor_assets() { 'byline_connector_label' => __( 'and ', 'newspack-newsletters' ), ], 'supported_social_icon_services' => Newspack_Newsletters_Renderer::get_supported_social_icons_services(), + 'supported_esps' => Newspack_Newsletters::get_supported_providers(), ]; if ( self::is_editing_email() ) { @@ -381,6 +382,7 @@ public static function enqueue_block_editor_assets() { 'is_service_provider_configured' => Newspack_Newsletters::is_service_provider_configured(), 'service_provider' => Newspack_Newsletters::service_provider(), 'user_test_emails' => self::get_current_user_test_emails(), + 'labels' => $provider ? $provider::get_labels() : [], ] ); wp_register_style( diff --git a/includes/class-newspack-newsletters-subscription.php b/includes/class-newspack-newsletters-subscription.php index 2867e6271..865d5f4f6 100644 --- a/includes/class-newspack-newsletters-subscription.php +++ b/includes/class-newspack-newsletters-subscription.php @@ -73,7 +73,7 @@ public static function register_api_endpoints() { [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ __CLASS__, 'api_get_lists' ], - 'permission_callback' => [ 'Newspack_Newsletters', 'api_permission_callback' ], + 'permission_callback' => [ 'Newspack_Newsletters', 'api_administration_permissions_check' ], ] ); register_rest_route( @@ -82,7 +82,7 @@ public static function register_api_endpoints() { [ 'methods' => \WP_REST_Server::EDITABLE, 'callback' => [ __CLASS__, 'api_update_lists' ], - 'permission_callback' => [ 'Newspack_Newsletters', 'api_permission_callback' ], + 'permission_callback' => [ 'Newspack_Newsletters', 'api_administration_permissions_check' ], 'args' => [ 'lists' => [ 'type' => 'array', diff --git a/includes/class-newspack-newsletters.php b/includes/class-newspack-newsletters.php index 78d3f83c2..6e333f498 100644 --- a/includes/class-newspack-newsletters.php +++ b/includes/class-newspack-newsletters.php @@ -267,6 +267,7 @@ public static function register_meta() { 'type' => 'string', 'single' => true, 'auth_callback' => '__return_true', + 'default' => '', ] ); \register_meta( @@ -285,6 +286,70 @@ public static function register_meta() { 'default' => -1, ] ); + \register_meta( + 'post', + 'send_list_id', + [ + 'object_subtype' => self::NEWSPACK_NEWSLETTERS_CPT, + 'show_in_rest' => [ + 'schema' => [ + 'context' => [ 'edit' ], + ], + ], + 'type' => 'string', + 'single' => true, + 'auth_callback' => '__return_true', + 'default' => '', + ] + ); + \register_meta( + 'post', + 'send_sublist_id', + [ + 'object_subtype' => self::NEWSPACK_NEWSLETTERS_CPT, + 'show_in_rest' => [ + 'schema' => [ + 'context' => [ 'edit' ], + ], + ], + 'type' => 'string', + 'single' => true, + 'auth_callback' => '__return_true', + 'default' => '', + ] + ); + \register_meta( + 'post', + 'senderName', + [ + 'object_subtype' => self::NEWSPACK_NEWSLETTERS_CPT, + 'show_in_rest' => [ + 'schema' => [ + 'context' => [ 'edit' ], + ], + ], + 'type' => 'string', + 'single' => true, + 'auth_callback' => '__return_true', + 'default' => '', + ] + ); + \register_meta( + 'post', + 'senderEmail', + [ + 'object_subtype' => self::NEWSPACK_NEWSLETTERS_CPT, + 'show_in_rest' => [ + 'schema' => [ + 'context' => [ 'edit' ], + ], + ], + 'type' => 'string', + 'single' => true, + 'auth_callback' => '__return_true', + 'default' => '', + ] + ); \register_meta( 'post', 'newsletter_sent', @@ -314,6 +379,7 @@ public static function register_meta() { 'type' => 'string', 'single' => true, 'auth_callback' => '__return_true', + 'default' => '', ] ); \register_meta( @@ -329,6 +395,7 @@ public static function register_meta() { 'type' => 'string', 'single' => true, 'auth_callback' => '__return_true', + 'default' => '', ] ); \register_meta( @@ -344,6 +411,7 @@ public static function register_meta() { 'type' => 'string', 'single' => true, 'auth_callback' => '__return_true', + 'default' => '', ] ); \register_meta( @@ -359,6 +427,7 @@ public static function register_meta() { 'type' => 'string', 'single' => true, 'auth_callback' => '__return_true', + 'default' => '', ] ); \register_meta( @@ -374,6 +443,7 @@ public static function register_meta() { 'type' => 'boolean', 'single' => true, 'auth_callback' => '__return_true', + 'default' => false, ] ); \register_meta( @@ -835,15 +905,6 @@ public static function api_set_settings( $request ) { return $wp_error->has_errors() ? $wp_error : self::api_get_settings(); } - /** - * Whether the current user can manage admin settings. - * - * @return bool Whether the current user can manage admin settings. - */ - public static function api_permission_callback() { - return current_user_can( 'manage_options' ); - } - /** * Retrieve settings. */ diff --git a/includes/class-send-lists.php b/includes/class-send-lists.php index f93037d46..388772b1e 100644 --- a/includes/class-send-lists.php +++ b/includes/class-send-lists.php @@ -8,10 +8,7 @@ namespace Newspack\Newsletters; use Newspack_Newsletters; -use Newspack_Newsletters_Settings; -use Newspack_Newsletters_Subscription; use WP_Error; -use WP_Post; defined( 'ABSPATH' ) || exit; @@ -34,7 +31,7 @@ public static function init() { return; } - add_action( 'rest_api_init', [ __CLASS__, 'register_api_endpoints' ] ); + \add_action( 'rest_api_init', [ __CLASS__, 'register_api_endpoints' ] ); } /** @@ -55,13 +52,13 @@ public static function should_initialize_send_lists() { * Register the endpoints needed to fetch send lists. */ public static function register_api_endpoints() { - register_rest_route( + \register_rest_route( Newspack_Newsletters::API_NAMESPACE, '/send-lists', [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ __CLASS__, 'api_get_send_lists' ], - 'permission_callback' => [ 'Newspack_Newsletters', 'api_permission_callback' ], + 'permission_callback' => [ 'Newspack_Newsletters', 'api_administration_permissions_check' ], 'args' => [ 'ids' => [ 'type' => [ 'array', 'string' ], @@ -171,9 +168,15 @@ public static function api_get_send_lists( $request ) { foreach ( $defaults as $key => $value ) { $args[ $key ] = $request[ $key ] ?? $value; } - + $send_lists = $provider->get_send_lists( $args ); return \rest_ensure_response( - $provider->get_send_lists( $args ) + \is_wp_error( $send_lists ) ? $send_lists : + array_map( + function( $send_list ) { + return $send_list->to_array(); + }, + $send_lists + ) ); } } diff --git a/includes/service-providers/active_campaign/class-newspack-newsletters-active-campaign-controller.php b/includes/service-providers/active_campaign/class-newspack-newsletters-active-campaign-controller.php index 3c8172c93..64baaf2d3 100644 --- a/includes/service-providers/active_campaign/class-newspack-newsletters-active-campaign-controller.php +++ b/includes/service-providers/active_campaign/class-newspack-newsletters-active-campaign-controller.php @@ -104,7 +104,7 @@ public function register_routes() { [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'api_retrieve' ], - 'permission_callback' => [ $this->service_provider, 'api_authoring_permissions_check' ], + 'permission_callback' => [ 'Newspack_Newsletters', 'api_authoring_permissions_check' ], 'args' => [ 'id' => [ 'sanitize_callback' => 'absint', 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 c00dca0a2..c958081ee 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 @@ -7,11 +7,21 @@ defined( 'ABSPATH' ) || exit; +use Newspack\Newsletters\Send_Lists; +use Newspack\Newsletters\Send_List; + /** * ActiveCampaign ESP Class. */ final class Newspack_Newsletters_Active_Campaign extends \Newspack_Newsletters_Service_Provider { + /** + * Provider name. + * + * @var string + */ + public $name = 'ActiveCampaign'; + /** * Cached fields. * @@ -26,6 +36,13 @@ final class Newspack_Newsletters_Active_Campaign extends \Newspack_Newsletters_S */ private $lists = null; + /** + * Cached segments. + * + * @var array + */ + private $segments = null; + /** * Cached contact data. * @@ -40,13 +57,6 @@ final class Newspack_Newsletters_Active_Campaign extends \Newspack_Newsletters_S */ public static $support_local_lists = true; - /** - * Provider name. - * - * @var string - */ - public $name = 'ActiveCampaign'; - /** * Class constructor. */ @@ -495,13 +505,39 @@ public function set_api_credentials( $credentials ) { /** * Get lists. * + * @param array $args Query args to pass to the lists_lists endpoint. + * For supported args, see: https://www.activecampaign.com/api/example.php?call=list_list. + * * @return array|WP_Error List of existing lists or error. */ - public function get_lists() { + public function get_lists( $args = [] ) { if ( null !== $this->lists ) { + if ( ! empty( $args['ids'] ) ) { + return array_values( + array_filter( + $this->lists, + function ( $list ) use ( $args ) { + return Send_Lists::matches_id( $args['ids'], $list['id'] ); + } + ) + ); + } + if ( ! empty( $args['filters[name]'] ) ) { + return array_values( + array_filter( + $this->lists, + function ( $list ) use ( $args ) { + return Send_Lists::matches_search( $args['filters[name]'], [ $list['name'] ] ); + } + ) + ); + } return $this->lists; } - $lists = $this->api_v1_request( 'list_list', 'GET', [ 'query' => [ 'ids' => 'all' ] ] ); + if ( empty( $args['ids'] ) && empty( $args['filters[name]'] ) ) { + $args['ids'] = 'all'; + } + $lists = $this->api_v1_request( 'list_list', 'GET', [ 'query' => $args ] ); if ( is_wp_error( $lists ) ) { return $lists; } @@ -509,43 +545,159 @@ public function get_lists() { unset( $lists['result_code'] ); unset( $lists['result_message'] ); unset( $lists['result_output'] ); - $this->lists = array_values( $lists ); - return $this->lists; + + if ( ! empty( $args['ids'] ) && 'all' === $args['ids'] ) { + $this->lists = array_values( $lists ); + } + return array_values( $lists ); + } + + /** + * Get all applicable lists and segments as Send_List objects. + * + * @param array $args Array of search args. See Send_Lists::get_default_args() for supported params and default values. + * + * @return Send_List[]|WP_Error Array of Send_List objects on success, or WP_Error object on failure. + */ + public function get_send_lists( $args = [] ) { + $send_lists = []; + if ( empty( $args['type'] ) || 'list' === $args['type'] ) { + $list_args = [ + 'limit' => ! empty( $args['limit'] ) ? intval( $args['limit'] ) : 100, + ]; + + // Search by IDs. + if ( ! empty( $args['ids'] ) ) { + $list_args['ids'] = implode( ',', $args['ids'] ); + } + + // Search by name. + if ( ! empty( $args['search'] ) ) { + if ( is_array( $args['search'] ) ) { + return new WP_Error( + 'newspack_newsletters_active_campaign_fetch_send_lists', + __( 'ActiveCampaign supports searching by a single search term only.', 'newspack-newsletters' ) + ); + } + $list_args['filters[name]'] = $args['search']; + } + + $lists = $this->get_lists( $list_args ); + if ( is_wp_error( $lists ) ) { + return $lists; + } + foreach ( $lists as $list ) { + $send_lists[] = new Send_List( + [ + 'provider' => $this->service, + 'type' => 'list', + 'id' => $list['id'], + 'name' => $list['name'], + 'entity_type' => 'list', + 'count' => $list['subscriber_count'] ?? 0, + ] + ); + } + } + + if ( empty( $args['type'] ) || 'sublist' === $args['type'] ) { + $segment_args = []; + if ( ! empty( $args['ids'] ) ) { + $segment_args['ids'] = $args['ids']; + } + if ( ! empty( $args['search'] ) ) { + $segment_args['search'] = $args['search']; + } + $segments = $this->get_segments( $segment_args ); + if ( is_wp_error( $segments ) ) { + return $segments; + } + foreach ( $segments as $segment ) { + $segment_name = ! empty( $segment['name'] ) ? + $segment['name'] . ' (ID ' . $segment['id'] . ')' : + sprintf( + // Translators: %s is the segment ID. + __( 'Untitled %s', 'newspack-newsletters' ), + $segment['id'] + ); + $send_lists[] = new Send_List( + [ + 'provider' => $this->service, + 'type' => 'sublist', + 'id' => $segment['id'], + 'parent' => $args['parent'] ?? null, + 'name' => $segment_name, + 'entity_type' => 'segment', + 'count' => $segment['subscriber_count'] ?? null, + ] + ); + } + } + + return $send_lists; } /** * Get segments. * + * @param array $args Array of search args. + * * @return array|WP_Error List os existing segments or error. */ - public function get_segments() { - $limit = 100; - $offset = 0; + public function get_segments( $args = [] ) { + if ( null !== $this->segments ) { + if ( ! empty( $args['ids'] ) ) { + $filtered = array_values( + array_filter( + $this->segments, + function ( $segment ) use ( $args ) { + return Send_Lists::matches_id( $args['ids'], $segment['id'] ); + } + ) + ); + return array_slice( $filtered, 0, $args['limit'] ?? count( $filtered ) ); + } + if ( ! empty( $args['search'] ) ) { + $filtered = array_values( + array_filter( + $this->segments, + function ( $segment ) use ( $args ) { + return Send_Lists::matches_search( $args['search'], [ $segment['name'] ] ); + } + ) + ); + return array_slice( $filtered, 0, $args['limit'] ?? count( $filtered ) ); + } + return $this->segments; + } + + $query_args = $args; + $query_args['limit'] = $args['limit'] ?? 100; + $query_args['offset'] = 0; $result = $this->api_v3_request( 'segments', 'GET', [ - 'query' => [ - 'limit' => $limit, - 'offset' => $offset, - ], + 'query' => $query_args, ] ); if ( is_wp_error( $result ) ) { return $result; } $segments = $result['segments']; - $total = $result['meta']['total']; - while ( $total > $offset + $limit ) { - $offset = $offset + $limit; + if ( isset( $args['limit'] ) ) { + return $segments; + } + + // If not passed a limit, get all the segments. + $total = $result['meta']['total']; + while ( $total > $query_args['offset'] + $query_args['limit'] ) { + $query_args['offset'] = $query_args['offset'] + $query_args['limit']; $result = $this->api_v3_request( 'segments', 'GET', [ - 'query' => [ - 'limit' => $limit, - 'offset' => $offset, - ], + 'query' => $query_args, ] ); if ( is_wp_error( $result ) ) { @@ -553,7 +705,13 @@ public function get_segments() { } $segments = array_merge( $segments, $result['segments'] ); } - return $segments; + + $this->segments = $segments; + if ( ! empty( $args['ids'] ) || ! empty( $args['search'] ) ) { + return $this->get_segments( $args ); + } + + return $this->segments; } /** @@ -578,39 +736,81 @@ public function retrieve( $post_id, $skip_sync = false ) { if ( ! $this->has_api_credentials() ) { return []; } - $lists = $this->get_lists(); - if ( is_wp_error( $lists ) ) { - return $lists; + + $campaign_id = get_post_meta( $post_id, 'ac_campaign_id', true ); + $send_list_id = get_post_meta( $post_id, 'send_list_id', true ); + $send_sublist_id = get_post_meta( $post_id, 'send_sublist_id', true ); + + // Handle legacy send-to meta. + if ( ! $send_list_id ) { + $legacy_list_id = get_post_meta( $post_id, 'ac_list_id', true ); + if ( $legacy_list_id ) { + $newsletter_data['list_id'] = $legacy_list_id; + $send_list_id = $legacy_list_id; + } } - $segments = $this->get_segments(); - if ( is_wp_error( $segments ) ) { - return $segments; + if ( ! $send_sublist_id ) { + $legacy_sublist_id = get_post_meta( $post_id, 'ac_segment_id', true ); + if ( $legacy_sublist_id ) { + $newsletter_data['sublist_id'] = $legacy_sublist_id; + $send_sublist_id = $legacy_sublist_id; + } } - $campaign_id = get_post_meta( $post_id, 'ac_campaign_id', true ); - $from_name = get_post_meta( $post_id, 'ac_from_name', true ); - $from_email = get_post_meta( $post_id, 'ac_from_email', true ); - $list_id = get_post_meta( $post_id, 'ac_list_id', true ); - $segment_id = get_post_meta( $post_id, 'ac_segment_id', true ); - $result = [ - 'campaign' => true, // Satisfy the JS API. - 'campaign_id' => $campaign_id, - 'from_name' => $from_name, - 'from_email' => $from_email, - 'list_id' => $list_id, - 'segment_id' => $segment_id, - 'lists' => $lists, - 'segments' => $segments, + $send_lists = $this->get_send_lists( // Get first 10 top-level send lists for autocomplete. + [ + 'ids' => $send_list_id ? [ $send_list_id ] : null, // If we have a selected list, make sure to fetch it. + 'type' => 'list', + ] + ); + if ( is_wp_error( $send_lists ) ) { + return $send_lists; + } + $send_sublists = $send_list_id || $send_sublist_id ? + $this->get_send_lists( + [ + 'ids' => [ $send_sublist_id ], // If we have a selected sublist, make sure to fetch it. Otherwise, we'll populate sublists later. + 'parent_id' => $send_list_id, + 'type' => 'sublist', + ] + ) : + []; + if ( is_wp_error( $send_sublists ) ) { + return $send_sublists; + } + $newsletter_data = [ + 'campaign' => true, // Satisfy the JS API. + 'campaign_id' => $campaign_id, + 'supports_multiple_test_recipients' => true, + 'lists' => $send_lists, + 'sublists' => $send_sublists, ]; + + // Handle legacy sender meta. + $from_name = get_post_meta( $post_id, 'senderName', true ); + $from_email = get_post_meta( $post_id, 'senderEmail', true ); + if ( ! $from_name ) { + $legacy_from_name = get_post_meta( $post_id, 'ac_from_name', true ); + if ( $legacy_from_name ) { + $newsletter_data['senderName'] = $legacy_from_name; + } + } + if ( ! $from_email ) { + $legacy_from_email = get_post_meta( $post_id, 'ac_from_email', true ); + if ( $legacy_from_email ) { + $newsletter_data['senderEmail'] = $legacy_from_email; + } + } + if ( ! $campaign_id && true !== $skip_sync ) { $sync_result = $this->sync( get_post( $post_id ) ); if ( ! is_wp_error( $sync_result ) ) { - $result = wp_parse_args( + $newsletter_data = wp_parse_args( $sync_result, - $result + $newsletter_data ); } } - return $result; + return $newsletter_data; } /** @@ -714,17 +914,19 @@ public function sync( $post ) { $transient_name = $this->get_transient_name( $post->ID ); delete_transient( $transient_name ); - $from_name = get_post_meta( $post->ID, 'ac_from_name', true ); - $from_email = get_post_meta( $post->ID, 'ac_from_email', true ); - $list_id = get_post_meta( $post->ID, 'ac_list_id', true ); - $is_public = get_post_meta( $post->ID, 'is_public', true ); - $message_id = get_post_meta( $post->ID, 'ac_message_id', true ); + $from_name = get_post_meta( $post->ID, 'senderName', true ); + $from_email = get_post_meta( $post->ID, 'senderEmail', true ); + $send_list_id = get_post_meta( $post->ID, 'send_list_id', true ); + $message_id = get_post_meta( $post->ID, 'ac_message_id', true ); $renderer = new Newspack_Newsletters_Renderer(); $content = $renderer->retrieve_email_html( $post ); $message_action = 'message_add'; $message_data = []; + $sync_data = [ + 'campaign' => true, // Satisfy JS API. + ]; if ( $message_id ) { $message = $this->api_v1_request( 'message_view', 'GET', [ 'query' => [ 'id' => $message_id ] ] ); @@ -736,10 +938,8 @@ public function sync( $post ) { // If sender data is not available locally, update from ESP. if ( ! $from_name || ! $from_email ) { - $from_name = $message['fromname']; - $from_email = $message['fromemail']; - update_post_meta( $post->ID, 'ac_from_name', $from_name ); - update_post_meta( $post->ID, 'ac_from_email', $from_email ); + $sync_data['senderName'] = $message['fromname']; + $sync_data['senderEmail'] = $message['fromemail']; } } else { // Validate required meta if campaign and message are not yet created. @@ -749,7 +949,7 @@ public function sync( $post ) { __( 'Please input sender name and email address.', 'newspack-newsletters' ) ); } - if ( empty( $list_id ) ) { + if ( empty( $send_list_id ) ) { return new \WP_Error( 'newspack_newsletters_active_campaign_invalid_list', __( 'Please select a list.', 'newspack-newsletters' ) @@ -759,13 +959,13 @@ public function sync( $post ) { $message_data = wp_parse_args( [ - 'format' => 'html', - 'htmlconstructor' => 'editor', - 'html' => $content, - 'p[' . $list_id . ']' => 1, - 'fromemail' => $from_email, - 'fromname' => $from_name, - 'subject' => $post->post_title, + 'format' => 'html', + 'htmlconstructor' => 'editor', + 'html' => $content, + 'p[' . $send_list_id . ']' => 1, + 'fromemail' => $from_email, + 'fromname' => $from_name, + 'subject' => $post->post_title, ], $message_data ); @@ -776,14 +976,7 @@ public function sync( $post ) { } update_post_meta( $post->ID, 'ac_message_id', $message['id'] ); - - $sync_data = [ - 'campaign' => true, // Satisfy JS API. - 'message_id' => $message['id'], - 'list_id' => $list_id, - 'from_email' => $from_email, - 'from_name' => $from_name, - ]; + $sync_data['message_id'] = $message['id']; // Retrieve and store campaign data. $data = $this->retrieve( $post->ID, true ); @@ -792,7 +985,6 @@ public function sync( $post ) { return $data; } else { $data = array_merge( $data, $sync_data ); - update_post_meta( $post->ID, 'newsletterData', $data ); } return $sync_data; @@ -811,8 +1003,13 @@ private function create_campaign( $post, $campaign_name = '' ) { if ( is_wp_error( $sync_result ) ) { return $sync_result; } - $segment_id = get_post_meta( $post->ID, 'ac_segment_id', true ); - $is_public = get_post_meta( $post->ID, 'is_public', true ); + + $from_name = get_post_meta( $post->ID, 'senderName', true ); + $from_email = get_post_meta( $post->ID, 'senderEmail', true ); + $send_list_id = get_post_meta( $post->ID, 'send_list_id', true ); + $send_sublist_id = get_post_meta( $post->ID, 'send_sublist_id', true ); + + $is_public = get_post_meta( $post->ID, 'is_public', true ); if ( empty( $campaign_name ) ) { $campaign_name = $this->get_campaign_name( $post ); } @@ -821,10 +1018,10 @@ private function create_campaign( $post, $campaign_name = '' ) { 'status' => 0, // 0 = Draft; 1 = Scheduled. 'public' => (int) $is_public, 'name' => $campaign_name, - 'fromname' => $sync_result['from_name'], - 'fromemail' => $sync_result['from_email'], - 'segmentid' => $segment_id ?? 0, // 0 = No segment. - 'p[' . $sync_result['list_id'] . ']' => $sync_result['list_id'], + 'fromname' => $from_name, + 'fromemail' => $from_email, + 'segmentid' => $send_sublist_id ?? 0, // 0 = No segment. + 'p[' . $send_list_id . ']' => $send_list_id, 'm[' . $sync_result['message_id'] . ']' => 100, // 100 = 100% of contacts will receive this. ]; if ( defined( 'NEWSPACK_NEWSLETTERS_AC_DISABLE_LINK_TRACKING' ) && NEWSPACK_NEWSLETTERS_AC_DISABLE_LINK_TRACKING ) { @@ -1335,6 +1532,12 @@ public static function get_labels( $context = '' ) { 'name' => 'Active Campaign', 'list_explanation' => __( 'Active Campaign List', 'newspack-newsletters' ), 'local_list_explanation' => __( 'Active Campaign Tag', 'newspack-newsletters' ), + 'list' => __( 'list', 'newspack-newsletters' ), // "list" in lower case singular format. + 'lists' => __( 'lists', 'newspack-newsletters' ), // "list" in lower case plural format. + 'sublist' => __( 'segment', 'newspack-newsletters' ), // Sublist entities in lowercase singular format. + 'List' => __( 'List', 'newspack-newsletters' ), // "list" in uppercase case singular format. + 'Lists' => __( 'Lists', 'newspack-newsletters' ), // "list" in uppercase case plural format. + 'Sublist' => __( 'Segments', 'newspack-newsletters' ), // Sublist entities in uppercase singular format. ] ); } diff --git a/includes/service-providers/campaign_monitor/class-newspack-newsletters-campaign-monitor-controller.php b/includes/service-providers/campaign_monitor/class-newspack-newsletters-campaign-monitor-controller.php index dabce22ea..ba25966fe 100644 --- a/includes/service-providers/campaign_monitor/class-newspack-newsletters-campaign-monitor-controller.php +++ b/includes/service-providers/campaign_monitor/class-newspack-newsletters-campaign-monitor-controller.php @@ -18,107 +18,10 @@ class Newspack_Newsletters_Campaign_Monitor_Controller extends Newspack_Newslett */ public function __construct( $campaign_monitor ) { $this->service_provider = $campaign_monitor; - add_action( 'init', [ __CLASS__, 'register_meta' ] ); add_action( 'rest_api_init', [ $this, 'register_routes' ] ); parent::__construct( $campaign_monitor ); } - /** - * Register custom fields. - */ - public static function register_meta() { - \register_meta( - 'post', - 'cm_list_id', - [ - 'object_subtype' => Newspack_Newsletters::NEWSPACK_NEWSLETTERS_CPT, - 'show_in_rest' => [ - 'schema' => [ - 'context' => [ 'edit' ], - ], - ], - 'type' => 'string', - 'single' => true, - 'auth_callback' => '__return_true', - ] - ); - \register_meta( - 'post', - 'cm_segment_id', - [ - 'object_subtype' => Newspack_Newsletters::NEWSPACK_NEWSLETTERS_CPT, - 'show_in_rest' => [ - 'schema' => [ - 'context' => [ 'edit' ], - ], - ], - 'type' => 'string', - 'single' => true, - 'auth_callback' => '__return_true', - ] - ); - \register_meta( - 'post', - 'cm_send_mode', - [ - 'object_subtype' => Newspack_Newsletters::NEWSPACK_NEWSLETTERS_CPT, - 'show_in_rest' => [ - 'schema' => [ - 'context' => [ 'edit' ], - ], - ], - 'type' => 'string', - 'single' => true, - 'auth_callback' => '__return_true', - ] - ); - \register_meta( - 'post', - 'cm_from_name', - [ - 'object_subtype' => Newspack_Newsletters::NEWSPACK_NEWSLETTERS_CPT, - 'show_in_rest' => [ - 'schema' => [ - 'context' => [ 'edit' ], - ], - ], - 'type' => 'string', - 'single' => true, - 'auth_callback' => '__return_true', - ] - ); - \register_meta( - 'post', - 'cm_from_email', - [ - 'object_subtype' => Newspack_Newsletters::NEWSPACK_NEWSLETTERS_CPT, - 'show_in_rest' => [ - 'schema' => [ - 'context' => [ 'edit' ], - ], - ], - 'type' => 'string', - 'single' => true, - 'auth_callback' => '__return_true', - ] - ); - \register_meta( - 'post', - 'cm_preview_text', - [ - 'object_subtype' => Newspack_Newsletters::NEWSPACK_NEWSLETTERS_CPT, - 'show_in_rest' => [ - 'schema' => [ - 'context' => [ 'edit' ], - ], - ], - 'type' => 'string', - 'single' => true, - 'auth_callback' => '__return_true', - ] - ); - } - /** * Register API endpoints unique to Campaign Monitor. */ @@ -134,7 +37,7 @@ public function register_routes() { [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'api_retrieve' ], - 'permission_callback' => [ $this->service_provider, 'api_authoring_permissions_check' ], + 'permission_callback' => [ 'Newspack_Newsletters', 'api_authoring_permissions_check' ], 'args' => [ 'id' => [ 'sanitize_callback' => 'absint', @@ -143,22 +46,13 @@ public function register_routes() { ], ] ); - \register_rest_route( - $this->service_provider::BASE_NAMESPACE . $this->service_provider->service, - '(?P[\a-z]+)/content', - [ - 'methods' => \WP_REST_Server::READABLE, - 'callback' => [ $this, 'api_content' ], - 'permission_callback' => '__return_true', - ] - ); \register_rest_route( $this->service_provider::BASE_NAMESPACE . $this->service_provider->service, '(?P[\a-z]+)/test', [ 'methods' => \WP_REST_Server::EDITABLE, 'callback' => [ $this, 'api_test' ], - 'permission_callback' => [ $this->service_provider, 'api_authoring_permissions_check' ], + 'permission_callback' => [ 'Newspack_Newsletters', 'api_authoring_permissions_check' ], 'args' => [ 'id' => [ 'sanitize_callback' => 'absint', @@ -201,25 +95,4 @@ public function api_test( $request ) { ); return self::get_api_response( $response ); } - - /** - * Get raw HTML for a campaign. Required for the Campaign Monitor API. - * - * @param WP_REST_Request $request API request object. - * @return void|WP_Error - */ - public function api_content( $request ) { - $response = $this->service_provider->content( - $request['public_id'] - ); - - if ( is_wp_error( $response ) ) { - return self::get_api_response( $response ); - } - - header( 'Content-Type: text/html; charset=UTF-8' ); - - echo $response; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - exit(); - } } diff --git a/includes/service-providers/campaign_monitor/class-newspack-newsletters-campaign-monitor.php b/includes/service-providers/campaign_monitor/class-newspack-newsletters-campaign-monitor.php index ada28dfe9..ddd901811 100644 --- a/includes/service-providers/campaign_monitor/class-newspack-newsletters-campaign-monitor.php +++ b/includes/service-providers/campaign_monitor/class-newspack-newsletters-campaign-monitor.php @@ -7,6 +7,9 @@ defined( 'ABSPATH' ) || exit; +use Newspack\Newsletters\Send_Lists; +use Newspack\Newsletters\Send_List; + // Increase default timeout for 3rd-party API requests to 30s. define( 'CS_REST_CALL_TIMEOUT', 30 ); @@ -22,6 +25,21 @@ final class Newspack_Newsletters_Campaign_Monitor extends \Newspack_Newsletters_ */ public $name = 'Campaign Monitor'; + /** + * Cached lists. + * + * @var array + */ + private $lists = null; + + /** + * Cached segments. + * + * @var array + */ + private $segments = null; + + /** * Class constructor. */ @@ -115,9 +133,13 @@ public function set_api_credentials( $credentials ) { /** * Get lists for a client iD. * - * @return object|WP_Error API API Response or error. + * @return array|WP_Error Array of lists, or error. */ public function get_lists() { + if ( null !== $this->lists ) { + return $this->lists; + } + $api_key = $this->api_key(); $client_id = $this->client_id(); @@ -145,7 +167,7 @@ public function get_lists() { ); } - return array_map( + $lists = array_map( function ( $item ) { return [ 'id' => $item->ListID, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase @@ -154,14 +176,21 @@ function ( $item ) { }, $lists->response ); + + $this->lists = $lists; + return $this->lists; } /** * Get segments for a client iD. * - * @return object|WP_Error API API Response or error. + * @return array|WP_Error Array of segments, or error. */ public function get_segments() { + if ( null !== $this->segments ) { + return $this->segments; + } + $api_key = $this->api_key(); $client_id = $this->client_id(); @@ -189,46 +218,182 @@ public function get_segments() { ); } - return $segments->response; + $segments = array_map( + function ( $item ) { + return [ + 'id' => $item->SegmentID, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + 'name' => $item->Title, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + 'parent' => $item->ListID, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + ]; + }, + $segments->response + ); + + $this->segments = $segments; + return $this->segments; + } + + /** + * Get all applicable lists and segments as Send_List objects. + * Note that in CM, campaigns can be sent to either lists or segments, not both, + * so both entity types should be treated as top-level send lists. + * + * @param array $args Array of search args. See Send_Lists::get_default_args() for supported params and default values. + * + * @return Send_List[]|WP_Error Array of Send_List objects on success, or WP_Error object on failure. + */ + public function get_send_lists( $args = [] ) { + $api_key = $this->api_key(); + $client_id = $this->client_id(); + + if ( ! $api_key ) { + return new WP_Error( + 'newspack_newsletters_missing_api_key', + __( 'No Campaign Monitor API key available.', 'newspack-newsletters' ) + ); + } + if ( ! $client_id ) { + return new WP_Error( + 'newspack_newsletters_missing_client_id', + __( 'No Campaign Monitor Client ID available.', 'newspack-newsletters' ) + ); + } + + $lists = $this->get_lists(); + if ( is_wp_error( $lists ) ) { + return $lists; + } + $send_lists = array_map( + function( $list ) use ( $api_key ) { + $config = [ + 'provider' => $this->service, + 'type' => 'list', + 'id' => $list['id'], + 'name' => $list['name'], + 'entity_type' => 'list', + ]; + + $list_details = new CS_REST_Lists( $list['id'], [ 'api_key' => $api_key ] ); + $list_stats = $list_details->get_stats(); + if ( ! empty( $list_stats->response->TotalActiveSubscribers ) ) { + $config['count'] = $list_stats->response->TotalActiveSubscribers; + } + + return new Send_List( $config ); + }, + $lists + ); + $segments = $this->get_segments(); + if ( is_wp_error( $segments ) ) { + return $segments; + } + $send_segments = array_map( + function( $segment ) { + $segment_id = (string) $segment['id']; + $config = [ + 'provider' => $this->service, + 'type' => 'list', // In CM, segments and lists have the same hierarchy. + 'id' => $segment_id, + 'name' => $segment['name'], + 'entity_type' => 'segment', + ]; + + return new Send_List( $config ); + }, + $segments + ); + $send_lists = array_merge( $send_lists, $send_segments ); + $filtered_lists = $send_lists; + if ( ! empty( $args['ids'] ) ) { + $ids = ! is_array( $args['ids'] ) ? [ $args['ids'] ] : $args['ids']; + $filtered_lists = array_values( + array_filter( + $send_lists, + function ( $list ) use ( $ids ) { + return Send_Lists::matches_id( $ids, $list->get_id() ); + } + ) + ); + } + if ( ! empty( $args['search'] ) ) { + $search = ! is_array( $args['search'] ) ? [ $args['search'] ] : $args['search']; + $filtered_lists = array_values( + array_filter( + $send_lists, + function ( $list ) use ( $search ) { + return Send_Lists::matches_search( + $search, + [ + $list->get_id(), + $list->get_name(), + $list->get_entity_type(), + ] + ); + } + ) + ); + } + + if ( ! empty( $args['limit'] ) ) { + $filtered_lists = array_slice( $filtered_lists, 0, $args['limit'] ); + } + + return $filtered_lists; } /** * Retrieve campaign details. * * @param integer $post_id Numeric ID of the Newsletter post. - * @param boolean $fetch_all If true, returns all campaign data, even those stored in WP. * @return object|WP_Error API Response or error. + * @throws Exception Error message. */ - public function retrieve( $post_id, $fetch_all = false ) { + public function retrieve( $post_id ) { if ( ! $this->has_api_credentials() ) { return []; } try { - $cm = new CS_REST_General( $this->api_key() ); - $response = []; - - $lists = $this->get_lists(); - $segments = $this->get_segments(); - - $response['lists'] = ! empty( $lists ) ? $lists : []; - $response['segments'] = ! empty( $segments ) ? $segments : []; - - if ( $fetch_all ) { - $cm_send_mode = $this->retrieve_send_mode( $post_id ); - $cm_list_id = $this->retrieve_list_id( $post_id ); - $cm_segment_id = $this->retrieve_segment_id( $post_id ); - $cm_from_name = $this->retrieve_from_name( $post_id ); - $cm_from_email = $this->retrieve_from_email( $post_id ); - - $response['send_mode'] = $cm_send_mode; - $response['list_id'] = $cm_list_id; - $response['segment_id'] = $cm_segment_id; - $response['from_name'] = $cm_from_name; - $response['from_email'] = $cm_from_email; - $response['campaign'] = true; + $send_list_id = get_post_meta( $post_id, 'send_list_id', true ); + $send_lists = $this->get_send_lists( // Get first 10 top-level send lists for autocomplete. + [ + 'ids' => $send_list_id ? [ $send_list_id ] : null, // If we have a selected list, make sure to fetch it. + 'type' => 'list', + ] + ); + if ( is_wp_error( $send_lists ) ) { + throw new Exception( wp_kses_post( $send_lists->get_error_message() ) ); + } + $newsletter_data = [ + 'campaign' => true, // Satisfy the JS API. + 'supports_multiple_test_recipients' => true, + 'lists' => $send_lists, + ]; + + // Handle legacy sender meta. + $from_name = get_post_meta( $post_id, 'senderName', true ); + $from_email = get_post_meta( $post_id, 'senderEmail', true ); + if ( ! $from_name ) { + $legacy_from_name = get_post_meta( $post_id, 'cm_from_name', true ); + if ( $legacy_from_name ) { + $newsletter_data['senderName'] = $legacy_from_name; + } + } + if ( ! $from_email ) { + $legacy_from_email = get_post_meta( $post_id, 'cm_from_email', true ); + if ( $legacy_from_email ) { + $newsletter_data['senderEmail'] = $legacy_from_email; + } } - return $response; + // Handle legacy send-to meta. + if ( ! $send_list_id ) { + $legacy_list_id = get_post_meta( $post_id, 'cm_list_id', true ) ?? get_post_meta( $post_id, 'cm_segment_id', true ); + if ( $legacy_list_id ) { + $newsletter_data['list_id'] = $legacy_list_id; + } + } + + return $newsletter_data; } catch ( Exception $e ) { return new WP_Error( 'newspack_newsletters_campaign_monitor_error', @@ -276,20 +441,20 @@ public function test( $post_id, $emails ) { } // Use the temporary test campaign ID to send a preview. - $preview = new CS_REST_Campaigns( $test_campaign->response, [ 'api_key' => $api_key ] ); - $preview->send_preview( $emails ); + $preview = new CS_REST_Campaigns( $test_campaign->response, [ 'api_key' => $api_key ] ); + $preview_response = $preview->send_preview( $emails ); // After sending a preview, delete the temporary test campaign. We must do this because the API doesn't support updating campaigns. - $delete = $preview->delete(); - - $data['result'] = $test_campaign->response; - $data['message'] = sprintf( - // translators: Message after successful test email. - __( 'Campaign Monitor test sent successfully to %s.', 'newspack-newsletters' ), - implode( ', ', $emails ) - ); - - return \rest_ensure_response( $data ); + $deleted = $preview->delete(); + $response = [ + 'result' => $preview_response->response, + 'message' => sprintf( + // translators: Message after successful test email. + __( 'Campaign Monitor test sent successfully to %s.', 'newspack-newsletters' ), + implode( ', ', $emails ) + ), + ]; + return \rest_ensure_response( $response ); } catch ( Exception $e ) { return new WP_Error( 'newspack_newsletters_campaign_monitor_error', @@ -305,7 +470,6 @@ public function test( $post_id, $emails ) { * @return object Args for sending a campaign or campaign preview. */ public function format_campaign_args( $post_id ) { - $data = $this->validate( $this->retrieve( $post_id, true ) ); $public_id = get_post_meta( $post_id, Newspack_Newsletters::PUBLIC_POST_ID_META, true ); // If we don't have a public ID, generate one and save it. @@ -317,18 +481,25 @@ public function format_campaign_args( $post_id ) { $args = [ 'Subject' => get_the_title( $post_id ), 'Name' => get_the_title( $post_id ) . ' ' . gmdate( 'h:i:s A' ), // Name must be unique. - 'FromName' => $data['from_name'], - 'FromEmail' => $data['from_email'], - 'ReplyTo' => $data['from_email'], + 'FromName' => get_post_meta( $post_id, 'senderName', true ), + 'FromEmail' => get_post_meta( $post_id, 'senderEmail', true ), + 'ReplyTo' => get_post_meta( $post_id, 'senderEmail', true ), 'HtmlUrl' => rest_url( $this::BASE_NAMESPACE . $this->service . '/' . $public_id . '/content' ), ]; - if ( 'list' === $data['send_mode'] ) { - $args['ListIDs'] = [ $data['list_id'] ]; - } else { - $args['SegmentIDs'] = [ $data['segment_id'] ]; + $send_list_id = get_post_meta( $post_id, 'send_list_id', true ); + if ( $send_list_id ) { + $send_list = $this->get_send_lists( [ 'ids' => $send_list_id ] ); + if ( ! empty( $send_list[0] ) ) { + $send_mode = $send_list[0]->get_entity_type(); + if ( 'list' === $send_mode ) { + $args['ListIDs'] = [ $send_list_id ]; + } elseif ( 'segment' === $send_mode ) { + $args['SegmentIDs'] = [ $send_list_id ]; + } + } } return $args; @@ -688,7 +859,11 @@ public static function get_labels( $context = '' ) { return array_merge( parent::get_labels(), [ - 'name' => 'Campaign Monitor', + 'name' => 'Campaign Monitor', + 'list' => __( 'list or segment', 'newspack-newsletters' ), // "list" in lower case singular format. + 'lists' => __( 'lists or segments', 'newspack-newsletters' ), // "list" in lower case plural format. + 'List' => __( 'List or Segment', 'newspack-newsletters' ), // "list" in uppercase case singular format. + 'Lists' => __( 'Lists or Segments', 'newspack-newsletters' ), // "list" in uppercase case plural format. ] ); } diff --git a/includes/service-providers/class-newspack-newsletters-service-provider-controller.php b/includes/service-providers/class-newspack-newsletters-service-provider-controller.php index dd3ebd95d..6ddbca6d5 100644 --- a/includes/service-providers/class-newspack-newsletters-service-provider-controller.php +++ b/includes/service-providers/class-newspack-newsletters-service-provider-controller.php @@ -17,7 +17,7 @@ abstract class Newspack_Newsletters_Service_Provider_Controller extends \WP_REST * * @var Newspack_Newsletters_Service_Provider $service_provider */ - private $service_provider; + protected $service_provider; /** * Newspack_Newsletters_Service_Provider_Controller constructor. @@ -33,12 +33,12 @@ public function __construct( $service_provider ) { */ public function register_routes() { \register_rest_route( - $this->service_provider::BASE_NAMESPACE, + Newspack_Newsletters::API_NAMESPACE, '(?P[\a-z]+)/sync-error', [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'api_get_sync_error' ], - 'permission_callback' => [ $this->service_provider, 'api_authoring_permissions_check' ], + 'permission_callback' => [ 'Newspack_Newsletters', 'api_authoring_permissions_check' ], 'args' => [ 'id' => [ 'sanitize_callback' => 'absint', diff --git a/includes/service-providers/class-newspack-newsletters-service-provider.php b/includes/service-providers/class-newspack-newsletters-service-provider.php index 236687cd6..ef659aa13 100644 --- a/includes/service-providers/class-newspack-newsletters-service-provider.php +++ b/includes/service-providers/class-newspack-newsletters-service-provider.php @@ -94,25 +94,6 @@ public static function instance() { return self::$instances[ static::class ]; } - /** - * Check capabilities for using the API for authoring tasks. - * - * @param WP_REST_Request $request API request object. - * @return bool|WP_Error - */ - public function api_authoring_permissions_check( $request ) { - if ( ! current_user_can( 'edit_others_posts' ) ) { - return new \WP_Error( - 'newspack_rest_forbidden', - esc_html__( 'You cannot use this resource.', 'newspack-newsletters' ), - [ - 'status' => 403, - ] - ); - } - return true; - } - /** * Handle newsletter post status changes. * @@ -448,8 +429,10 @@ public static function get_labels( $context = '' ) { 'name' => '', // The provider name. 'list' => __( 'list', 'newspack-newsletters' ), // "list" in lower case singular format. 'lists' => __( 'lists', 'newspack-newsletters' ), // "list" in lower case plural format. + 'sublist' => __( 'sublist', 'newspack-newsletters' ), // Sublist entities in lowercase singular format. 'List' => __( 'List', 'newspack-newsletters' ), // "list" in uppercase case singular format. 'Lists' => __( 'Lists', 'newspack-newsletters' ), // "list" in uppercase case plural format. + 'Sublist' => __( 'Sublist', 'newspack-newsletters' ), // Sublist entities in uppercase singular format. 'tag_prefix' => 'Newspack: ', // The prefix to be used in tags. 'tag_metabox_before_save' => __( 'Once this list is saved, a tag will be created for it.', 'newspack-newsletters' ), 'tag_metabox_after_save' => __( 'Tag created for this list', 'newspack-newsletters' ), diff --git a/includes/service-providers/constant_contact/class-newspack-newsletters-constant-contact-controller.php b/includes/service-providers/constant_contact/class-newspack-newsletters-constant-contact-controller.php index 6c6cd326a..02c76b877 100644 --- a/includes/service-providers/constant_contact/class-newspack-newsletters-constant-contact-controller.php +++ b/includes/service-providers/constant_contact/class-newspack-newsletters-constant-contact-controller.php @@ -54,7 +54,7 @@ public function register_routes() { [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'verify_token' ], - 'permission_callback' => [ $this->service_provider, 'api_authoring_permissions_check' ], + 'permission_callback' => [ 'Newspack_Newsletters', 'api_authoring_permissions_check' ], ] ); \register_rest_route( @@ -63,7 +63,7 @@ public function register_routes() { [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'api_retrieve' ], - 'permission_callback' => [ $this->service_provider, 'api_authoring_permissions_check' ], + 'permission_callback' => [ 'Newspack_Newsletters', 'api_authoring_permissions_check' ], 'args' => [ 'id' => [ 'sanitize_callback' => 'absint', @@ -78,7 +78,7 @@ public function register_routes() { [ 'methods' => \WP_REST_Server::EDITABLE, 'callback' => [ $this, 'api_test' ], - 'permission_callback' => [ $this->service_provider, 'api_authoring_permissions_check' ], + 'permission_callback' => [ 'Newspack_Newsletters', 'api_authoring_permissions_check' ], 'args' => [ 'id' => [ 'sanitize_callback' => 'absint', @@ -90,99 +90,6 @@ public function register_routes() { ], ] ); - \register_rest_route( - $this->service_provider::BASE_NAMESPACE . $this->service_provider->service, - '(?P[\a-z]+)/sender', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_sender' ], - 'permission_callback' => [ $this->service_provider, 'api_authoring_permissions_check' ], - 'args' => [ - 'id' => [ - 'sanitize_callback' => 'absint', - 'validate_callback' => [ 'Newspack_Newsletters', 'validate_newsletter_id' ], - ], - 'from_name' => [ - 'sanitize_callback' => 'sanitize_text_field', - ], - 'reply_to' => [ - 'sanitize_callback' => 'sanitize_email', - ], - ], - ] - ); - \register_rest_route( - $this->service_provider::BASE_NAMESPACE . $this->service_provider->service, - '(?P[\a-z]+)/list/(?P[\a-z]+)', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_list' ], - 'permission_callback' => [ $this->service_provider, 'api_authoring_permissions_check' ], - 'args' => [ - 'id' => [ - 'sanitize_callback' => 'absint', - 'validate_callback' => [ 'Newspack_Newsletters', 'validate_newsletter_id' ], - ], - 'list_id' => [ - 'sanitize_callback' => 'esc_attr', - ], - ], - ] - ); - \register_rest_route( - $this->service_provider::BASE_NAMESPACE . $this->service_provider->service, - '(?P[\a-z]+)/list/(?P[\a-z]+)', - [ - 'methods' => \WP_REST_Server::DELETABLE, - 'callback' => [ $this, 'api_list' ], - 'permission_callback' => [ $this->service_provider, 'api_authoring_permissions_check' ], - 'args' => [ - 'id' => [ - 'sanitize_callback' => 'absint', - 'validate_callback' => [ 'Newspack_Newsletters', 'validate_newsletter_id' ], - ], - 'list_id' => [ - 'sanitize_callback' => 'esc_attr', - ], - ], - ] - ); - \register_rest_route( - $this->service_provider::BASE_NAMESPACE . $this->service_provider->service, - '(?P[\a-z]+)/segment/(?P[\a-z]+)', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_segment' ], - 'permission_callback' => [ $this->service_provider, 'api_authoring_permissions_check' ], - 'args' => [ - 'id' => [ - 'sanitize_callback' => 'absint', - 'validate_callback' => [ 'Newspack_Newsletters', 'validate_newsletter_id' ], - ], - 'segment_id' => [ - 'sanitize_callback' => 'esc_attr', - ], - ], - ] - ); - \register_rest_route( - $this->service_provider::BASE_NAMESPACE . $this->service_provider->service, - '(?P[\a-z]+)/segment/', - [ - 'methods' => \WP_REST_Server::DELETABLE, - 'callback' => [ $this, 'api_segment' ], - 'permission_callback' => [ $this->service_provider, 'api_authoring_permissions_check' ], - 'args' => [ - 'id' => [ - 'sanitize_callback' => 'absint', - 'validate_callback' => [ 'Newspack_Newsletters', 'validate_newsletter_id' ], - ], - 'segment_id' => [ - 'sanitize_callback' => 'esc_attr', - ], - ], - ] - ); } /** @@ -224,60 +131,4 @@ public function api_test( $request ) { ); return self::get_api_response( $response ); } - - /** - * Set the sender name and email for the campaign. - * - * @param WP_REST_Request $request API request object. - * @return WP_REST_Response|mixed API response or error. - */ - public function api_sender( $request ) { - $response = $this->service_provider->sender( - $request['id'], - $request['from_name'], - $request['reply_to'] - ); - return self::get_api_response( $response ); - } - - /** - * Set list for a campaign. - * - * @param WP_REST_Request $request API request object. - * @return WP_REST_Response|mixed API response or error. - */ - public function api_list( $request ) { - if ( 'DELETE' === $request->get_method() ) { - $response = $this->service_provider->unset_list( - $request['id'], - $request['list_id'] - ); - } else { - $response = $this->service_provider->list( - $request['id'], - $request['list_id'] - ); - } - return self::get_api_response( $response ); - } - - /** - * Set segment for a campaign. - * - * @param WP_REST_Request $request API request object. - * @return WP_REST_Response|mixed API response or error. - */ - public function api_segment( $request ) { - if ( 'DELETE' === $request->get_method() ) { - $response = $this->service_provider->unset_segment( - $request['id'] - ); - } else { - $response = $this->service_provider->set_segment( - $request['id'], - $request['segment_id'] - ); - } - return self::get_api_response( $response ); - } } diff --git a/includes/service-providers/constant_contact/class-newspack-newsletters-constant-contact-sdk.php b/includes/service-providers/constant_contact/class-newspack-newsletters-constant-contact-sdk.php index 99a3b88f7..83c86aab8 100644 --- a/includes/service-providers/constant_contact/class-newspack-newsletters-constant-contact-sdk.php +++ b/includes/service-providers/constant_contact/class-newspack-newsletters-constant-contact-sdk.php @@ -294,10 +294,12 @@ public function get_account_info() { /** * Get account email addresses * + * @param array $args Array of query args. + * * @return object Email addresses. */ - public function get_email_addresses() { - return $this->request( 'GET', 'account/emails' ); + public function get_email_addresses( $args = [] ) { + return $this->request( 'GET', 'account/emails', [ 'query' => $args ] ); } /** @@ -306,26 +308,68 @@ public function get_email_addresses() { * @return object Contact lists. */ public function get_contact_lists() { + $args = [ + 'include_count' => 'true', + 'include_membership_count' => 'active', + 'limit' => 1000, + 'status' => 'active', + ]; return $this->request( 'GET', 'contact_lists', - [ 'query' => [ 'include_count' => 'true' ] ] + [ 'query' => $args ] )->lists; } + /** + * Get a Contact List by ID + * + * @param string $id Contact List ID. + * + * @return object Contact list. + */ + public function get_contact_list( $id ) { + $args = [ + 'include_membership_count' => 'active', + ]; + return $this->request( + 'GET', + 'contact_lists/' . $id, + [ 'query' => $args ] + ); + } + /** * Get segments * * @return array */ public function get_segments() { + $args = [ + 'limit' => 1000, + 'sort_by' => 'date', + ]; return $this->request( 'GET', 'segments', - [ 'query' => [ 'sort_by' => 'name' ] ] + [ 'query' => $args ] )->segments; } + /** + * Get a segment by ID + * + * @param string $id Segment ID. + * + * @return object Segment. + */ + public function get_segment( $id ) { + return $this->request( + 'GET', + 'segments/' . $id + ); + } + /** * Get v3 campaign UUID if matches v2 format. * 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 86ecd266c..a7b6e404d 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 @@ -7,11 +7,28 @@ defined( 'ABSPATH' ) || exit; +use Newspack\Newsletters\Send_Lists; +use Newspack\Newsletters\Send_List; + /** * Main Newspack Newsletters Class for Constant Contact ESP. */ final class Newspack_Newsletters_Constant_Contact extends \Newspack_Newsletters_Service_Provider { + /** + * Provider name. + * + * @var string + */ + public $name = 'Contant Constact'; + + /** + * Cached instance of the CC SDK. + * + * @var Newspack_Newsletters_Constant_Contact_SDK + */ + private $cc = null; + /** * Cached lists. * @@ -20,18 +37,18 @@ final class Newspack_Newsletters_Constant_Contact extends \Newspack_Newsletters_ private $lists = null; /** - * Cached contact data. + * Cached segments. * * @var array */ - private $contact_data = []; + private $segments = null; /** - * Provider name. + * Cached contact data. * - * @var string + * @var array */ - public $name = 'Contant Constact'; + private $contact_data = []; /** * Whether the provider has support to tags and tags based Subscription Lists. @@ -46,6 +63,7 @@ final class Newspack_Newsletters_Constant_Contact extends \Newspack_Newsletters_ public function __construct() { $this->service = 'constant_contact'; $this->controller = new Newspack_Newsletters_Constant_Contact_Controller( $this ); + $credentials = $this->api_credentials(); add_action( 'admin_init', [ $this, 'oauth_callback' ] ); add_action( 'update_option_newspack_newsletters_constant_contact_api_key', [ $this, 'clear_tokens' ], 10, 2 ); @@ -79,6 +97,22 @@ public function has_api_credentials() { return ! empty( $this->api_key() ) && ! empty( $this->api_secret() ); } + /** + * Get or create a cached instance of the Constant Contact SDK. + */ + public function get_sdk() { + if ( $this->cc ) { + return $this->cc; + } + $credentials = $this->api_credentials(); + $this->cc = new Newspack_Newsletters_Constant_Contact_SDK( + $credentials['api_key'], + $credentials['api_secret'], + $credentials['access_token'] + ); + return $this->cc; + } + /** * Verify service provider connection. * @@ -88,15 +122,9 @@ public function has_api_credentials() { */ public function verify_token( $refresh = true ) { try { - $credentials = $this->api_credentials(); $redirect_uri = $this->get_oauth_redirect_uri(); - $cc = new Newspack_Newsletters_Constant_Contact_SDK( - $credentials['api_key'], - $credentials['api_secret'], - $credentials['access_token'] - ); - - $response = [ + $cc = $this->get_sdk(); + $response = [ 'error' => null, 'valid' => false, 'auth_url' => $cc->get_auth_code_url( wp_create_nonce( 'constant_contact_oauth2' ), $redirect_uri ), @@ -106,7 +134,9 @@ public function verify_token( $refresh = true ) { $response['valid'] = true; return $response; } + // If we have a refresh token, we can get a new access token. + $credentials = $this->api_credentials(); if ( $refresh && ! empty( $credentials['refresh_token'] ) ) { $token = $cc->refresh_token( $credentials['refresh_token'] ); $response['valid'] = $this->set_access_token( $token->access_token, $token->refresh_token ); @@ -189,7 +219,7 @@ public function oauth_callback() { * @return boolean Whether we are connected. */ private function connect( $redirect_uri, $code ) { - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret() ); + $cc = $this->get_sdk(); $token = $cc->get_access_token( $redirect_uri, $code ); if ( ! $token || ! isset( $token->access_token ) ) { return false; @@ -306,7 +336,7 @@ public function list( $post_id, $list_id ) { } try { $cc_campaign_id = $this->retrieve_campaign_id( $post_id ); - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); + $cc = $this->get_sdk(); $campaign = $cc->get_campaign( $cc_campaign_id ); $activity = $campaign->activity; @@ -344,7 +374,7 @@ public function unset_list( $post_id, $list_id ) { } try { $cc_campaign_id = $this->retrieve_campaign_id( $post_id ); - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); + $cc = $this->get_sdk(); $campaign = $cc->get_campaign( $cc_campaign_id ); $activity = $campaign->activity; @@ -381,7 +411,7 @@ public function set_segment( $post_id, $segment_id ) { } try { $cc_campaign_id = $this->retrieve_campaign_id( $post_id ); - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); + $cc = $this->get_sdk(); $campaign = $cc->get_campaign( $cc_campaign_id ); $activity = $campaign->activity; @@ -418,7 +448,7 @@ public function unset_segment( $post_id ) { } try { $cc_campaign_id = $this->retrieve_campaign_id( $post_id ); - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); + $cc = $this->get_sdk(); $campaign = $cc->get_campaign( $cc_campaign_id ); $activity = $campaign->activity; @@ -442,13 +472,14 @@ public function unset_segment( $post_id ) { * * @param integer $post_id Numeric ID of the Newsletter post. * @return object|WP_Error API Response or error. + * @throws Exception Error message. */ public function retrieve( $post_id ) { if ( ! $this->has_api_credentials() || ! $this->has_valid_connection() ) { return []; } try { - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); + $cc = $this->get_sdk(); $cc_campaign_id = get_post_meta( $post_id, 'cc_campaign_id', true ); if ( ! $cc_campaign_id ) { @@ -458,21 +489,46 @@ public function retrieve( $post_id ) { $campaign = $cc->get_campaign( $cc_campaign_id ); } - $lists = $cc->get_contact_lists(); - $segments = $cc->get_segments(); - - $data = [ - 'lists' => $lists, - 'campaign' => $campaign, - 'campaign_id' => $cc_campaign_id, - 'segments' => $segments, + $list_id = $campaign->activity->contact_list_ids[0] ?? null; + $segment_id = $campaign->activity->segment_ids[0] ?? null; + $send_list_id = get_post_meta( $post_id, 'send_list_id', true ); + $newsletter_data = [ + 'campaign' => $campaign, + 'campaign_id' => $cc_campaign_id, + 'allowed_sender_emails' => $this->get_verified_email_addresses(), // Get allowed email addresses for sender UI. + 'email_settings_url' => 'https://app.constantcontact.com/pages/myaccount/settings/emails', ]; - // Store retrieved campaign data. - update_post_meta( $post_id, 'newsletterData', $data ); + // Reconcile campaign settings with info fetched from the ESP for a true two-way sync. + if ( ! empty( $campaign->activity->from_name ) && $campaign->activity->from_name !== get_post_meta( $post_id, 'senderName', true ) ) { + $newsletter_data['senderName'] = $campaign->activity->from_name; // If campaign has different sender info set, update ours. + } + if ( ! empty( $campaign->activity->from_email ) && $campaign->activity->from_email !== get_post_meta( $post_id, 'senderEmail', true ) ) { + $newsletter_data['senderEmail'] = $campaign->activity->from_email; // If campaign has different sender info set, update ours. + } + if ( ( $list_id || $segment_id ) && $list_id !== $send_list_id && $segment_id !== $send_list_id ) { + $newsletter_data['send_list_id'] = strval( $list_id ?? $segment_id ); // If campaign has different list or segment set, update ours. + $send_list_id = $newsletter_data['send_list_id']; + } + + // Prefetch send list info if we have a selected list and/or sublist. + $send_lists = $this->get_send_lists( + [ + 'ids' => $send_list_id ? [ $send_list_id ] : null, // If we have a selected list, make sure to fetch it. + 'type' => 'list', + ] + ); + if ( is_wp_error( $send_lists ) ) { + throw new Exception( wp_kses_post( $send_lists->get_error_message() ) ); + } + $newsletter_data['lists'] = $send_lists; - return $data; + return $newsletter_data; } catch ( Exception $e ) { + // If we couldn't get the campaign, delete the cc_campaign_id so it gets recreated on the next sync. + delete_post_meta( $post_id, 'cc_campaign_id' ); + $this->retrieve( $post_id ); + return new WP_Error( 'newspack_newsletters_constant_contact_error', $e->getMessage() @@ -498,7 +554,7 @@ public function sender( $post_id, $from_name, $reply_to ) { try { $post = get_post( $post_id ); $cc_campaign_id = $this->retrieve_campaign_id( $post_id ); - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); + $cc = $this->get_sdk(); $renderer = new Newspack_Newsletters_Renderer(); $content = $renderer->retrieve_email_html( $post ); @@ -540,8 +596,7 @@ public function test( $post_id, $emails ) { ); } try { - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); - + $cc = $this->get_sdk(); $data = $this->retrieve( $post_id ); if ( is_wp_error( $data ) ) { @@ -567,6 +622,86 @@ public function test( $post_id, $emails ) { } } + /** + * Get all of the verified email addresses associated with the CC account. + * See: https://developer.constantcontact.com/api_reference/index.html#!/Account_Services/retrieveEmailAddresses. + */ + public function get_verified_email_addresses() { + $cc = $this->get_sdk(); + $email_addresses = (array) $cc->get_email_addresses( [ 'confirm_status' => 'CONFIRMED' ] ); + + return array_map( + function( $email ) { + return $email->email_address; + }, + $email_addresses + ); + } + + /** + * Get a payload for syncing post data to the ESP campaign. + * + * @param WP_Post|int $post Post object or ID. + * @return object Payload for syncing. + */ + public function get_sync_payload( $post ) { + $cc = $this->get_sdk(); + $renderer = new Newspack_Newsletters_Renderer(); + $content = $renderer->retrieve_email_html( $post ); + $auto_draft_html = '[[trackingImage]]

Auto draft

'; + $account_info = $cc->get_account_info(); + $sender_name = get_post_meta( $post->ID, 'senderName', true ); + $sender_email = get_post_meta( $post->ID, 'senderEmail', true ); + + // If we don't have a sender name or email, set default values. + if ( ! $sender_name && $account_info->organization_name ) { + $sender_name = $account_info->organization_name; + } elseif ( ! $sender_name && $account_info->first_name && $account_info->last_name ) { + $sender_name = $account_info->first_name . ' ' . $account_info->last_name; + } + + $verified_email_addresses = $this->get_verified_email_addresses(); + if ( empty( $verified_email_addresses ) ) { + return new WP_Error( + 'newspack_newsletters_constant_contact_error', + __( 'There are no verified email addresses in the Constant Contact account.', 'newspack-newsletters' ) + ); + } + if ( ! $sender_email ) { + $sender_email = $verified_email_addresses[0]; + } + if ( ! in_array( $sender_email, $verified_email_addresses, true ) ) { + return new WP_Error( + 'newspack_newsletters_constant_contact_error', + __( 'Sender email must be a verified Constant Contact account email address.', 'newspack-newsletters' ) + ); + } + $payload = [ + 'format_type' => 5, // https://v3.developer.constantcontact.com/api_guide/email_campaigns_overview.html#collapse-format-types . + 'html_content' => empty( $content ) ? $auto_draft_html : $content, + 'subject' => $post->post_title, + 'from_name' => $sender_name ?? __( 'Sender Name', 'newspack-newsletters' ), + 'from_email' => $sender_email, + 'reply_to_email' => $sender_email, + ]; + if ( $account_info->physical_address ) { + $payload['physical_address_in_footer'] = $account_info->physical_address; + } + + // Sync send-to selections. + $send_lists = $this->get_send_lists( [ 'ids' => get_post_meta( $post->ID, 'send_list_id', true ) ] ); + if ( ! empty( $send_lists[0] ) ) { + $send_list = $send_lists[0]; + if ( 'list' === $send_list->get_entity_type() ) { + $payload['contact_list_ids'] = [ $send_list->get_id() ]; + } elseif ( 'segment' === $send_list->get_entity_type() ) { + $payload['segment_ids'] = [ $send_list->get_id() ]; + } + } + + return $payload; + } + /** * Synchronize post with corresponding ESP campaign. * @@ -598,21 +733,24 @@ public function sync( $post ) { return; } - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); + $cc = $this->get_sdk(); $cc_campaign_id = get_post_meta( $post->ID, 'cc_campaign_id', true ); - $renderer = new Newspack_Newsletters_Renderer(); - $content = $renderer->retrieve_email_html( $post ); - $auto_draft_html = '[[trackingImage]]

Auto draft

'; - $account_info = $cc->get_account_info(); - - $activity_data = [ - 'format_type' => 5, // https://v3.developer.constantcontact.com/api_guide/email_campaigns_overview.html#collapse-format-types . - 'html_content' => empty( $content ) ? $auto_draft_html : $content, - 'subject' => $post->post_title, - ]; - - if ( $account_info->physical_address ) { - $activity_data['physical_address_in_footer'] = $account_info->physical_address; + $payload = $this->get_sync_payload( $post ); + + /** + * Filter the metadata payload sent to CC when syncing. + * + * Allows custom tracking codes to be sent. + * + * @param array $payload CC payload. + * @param object $post Post object. + * @param string $cc_campaign_id CC campaign ID, if defined. + */ + $payload = apply_filters( 'newspack_newsletters_cc_payload_sync', $payload, $post, $cc_campaign_id ); + + // If we have any errors in the payload, throw an exception. + if ( is_wp_error( $payload ) ) { + throw new Exception( esc_html( $payload->get_error_message() ) ); } if ( $cc_campaign_id ) { @@ -620,72 +758,31 @@ public function sync( $post ) { // Constant Constact only allow updates on DRAFT or SENT status. if ( ! in_array( $campaign->current_status, [ 'DRAFT', 'SENT' ], true ) ) { - return; + throw new Exception( + __( 'The newsletter campaign must have a DRAFT or SENT status.', 'newspack-newsletters' ) + ); } - $activity = array_merge( - $activity_data, - [ - 'contact_list_ids' => $campaign->activity->contact_list_ids, - 'from_name' => $campaign->activity->from_name, - 'from_email' => $campaign->activity->from_email, - 'reply_to_email' => $campaign->activity->reply_to_email, - ] - ); + $cc->update_campaign_activity( $campaign->activity->campaign_activity_id, $payload ); - $activity_result = $cc->update_campaign_activity( $campaign->activity->campaign_activity_id, $activity ); - $name_result = $cc->update_campaign_name( $cc_campaign_id, $this->get_campaign_name( $post ) ); + // Update campaign name. + $campaign_name = $this->get_campaign_name( $post ); + if ( $campaign->name !== $campaign_name ) { + $cc->update_campaign_name( $cc_campaign_id, $campaign_name ); + } $campaign_result = $cc->get_campaign( $cc_campaign_id ); } else { - - $initial_sender = __( 'Sender Name', 'newspack-newsletters' ); - if ( $account_info->organization_name ) { - $initial_sender = $account_info->organization_name; - } elseif ( $account_info->first_name && $account_info->last_name ) { - $initial_sender = $account_info->first_name . ' ' . $account_info->last_name; - } - - $email_addresses = (array) $cc->get_email_addresses(); - $verified_email_addresses = array_values( - array_filter( - $email_addresses, - function ( $email ) { - return 'CONFIRMED' === $email->confirm_status; - } - ) - ); - - if ( empty( $verified_email_addresses ) ) { - throw new Exception( __( 'There are no verified email addresses in the Constant Contact account.', 'newspack-newsletters' ) ); - } - - $initial_email_address = $verified_email_addresses[0]->email_address; - $campaign = [ 'name' => $this->get_campaign_name( $post ), - 'email_campaign_activities' => [ - array_merge( - $activity_data, - [ - 'subject' => $post->post_title, - 'from_name' => $initial_sender, - 'from_email' => $initial_email_address, - 'reply_to_email' => $initial_email_address, - ] - ), - ], + 'email_campaign_activities' => [ $payload ], ]; $campaign_result = $cc->create_campaign( $campaign ); } update_post_meta( $post->ID, 'cc_campaign_id', $campaign_result->campaign_id ); - // Retrieve and store campaign data. - $this->retrieve( $post->ID ); - return $campaign_result; - } catch ( Exception $e ) { set_transient( $transient_name, __( 'Error syncing with ESP. ', 'newspack-newsletters' ) . $e->getMessage(), 45 ); return new WP_Error( 'newspack_newsletters_constant_contact_error', $e->getMessage() ); @@ -748,7 +845,7 @@ public function send( $post ) { } try { - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); + $cc = $this->get_sdk(); $cc->create_schedule( $sync_result->activity->campaign_activity_id ); } catch ( Exception $e ) { return new WP_Error( @@ -784,7 +881,7 @@ public function trash( $post_id ) { return; } - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); + $cc = $this->get_sdk(); $campaign = $cc->get_campaign( $cc_campaign_id ); if ( $campaign && 'DRAFT' === $campaign->current_status ) { @@ -821,23 +918,144 @@ public function get_lists() { return $this->lists; } try { - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); - $this->lists = array_map( - function ( $list ) { - return [ - 'id' => $list->list_id, - 'name' => $list->name, - 'membership_count' => $list->membership_count, - ]; - }, - $cc->get_contact_lists() - ); + $cc = $this->get_sdk(); + if ( ! $this->lists ) { + $this->lists = array_map( + function ( $list ) { + return [ + 'id' => $list->list_id, + 'name' => $list->name, + 'membership_count' => $list->membership_count, + ]; + }, + $cc->get_contact_lists() + ); + } + return $this->lists; } catch ( Exception $e ) { return new WP_Error( 'newspack_newsletters_error', $e->getMessage() ); } } + /** + * Get segments. + * + * @return array|WP_Error List of existing segments or error. + */ + public function get_segments() { + if ( null !== $this->segments ) { + return $this->segments; + } + try { + $cc = $this->get_sdk(); + if ( ! $this->segments ) { + $this->segments = array_map( + function ( $segment ) { + return [ + 'id' => $segment->segment_id, + 'name' => $segment->name, + ]; + }, + $cc->get_segments() + ); + } + + return $this->segments; + } catch ( Exception $e ) { + return new WP_Error( 'newspack_newsletters_error', $e->getMessage() ); + } + } + + /** + * Get all applicable lists and segments as Send_List objects. + * Note that in CC, campaigns can be sent to either lists or segments, not both, + * so both entity types should be treated as top-level send lists. + * + * @param array $args Array of search args. See Send_Lists::get_default_args() for supported params and default values. + * + * @return Send_List[]|WP_Error Array of Send_List objects on success, or WP_Error object on failure. + */ + public function get_send_lists( $args = [] ) { + $lists = $this->get_lists(); + if ( is_wp_error( $lists ) ) { + return $lists; + } + $send_lists = array_map( + function( $list ) { + $config = [ + 'provider' => $this->service, + 'type' => 'list', + 'id' => $list['id'], + 'name' => $list['name'], + 'entity_type' => 'list', + 'count' => $list['membership_count'], + 'edit_link' => 'https://app.constantcontact.com/pages/contacts/ui#contacts/' . $list['id'], + ]; + + return new Send_List( $config ); + }, + $lists + ); + $segments = $this->get_segments(); + if ( is_wp_error( $segments ) ) { + return $segments; + } + $send_segments = array_map( + function( $segment ) { + $segment_id = (string) $segment['id']; + $config = [ + 'provider' => $this->service, + 'type' => 'list', // In CC, segments and lists have the same hierarchy. + 'id' => $segment_id, + 'name' => $segment['name'], + 'entity_type' => 'segment', + 'edit_link' => "https://app.constantcontact.com/pages/contacts/ui#segments/$segment_id/preview", + ]; + + return new Send_List( $config ); + }, + $segments + ); + $send_lists = array_merge( $send_lists, $send_segments ); + $filtered_lists = $send_lists; + if ( ! empty( $args['ids'] ) ) { + $ids = ! is_array( $args['ids'] ) ? [ $args['ids'] ] : $args['ids']; + $filtered_lists = array_values( + array_filter( + $send_lists, + function ( $list ) use ( $ids ) { + return Send_Lists::matches_id( $ids, $list->get_id() ); + } + ) + ); + } + if ( ! empty( $args['search'] ) ) { + $search = ! is_array( $args['search'] ) ? [ $args['search'] ] : $args['search']; + $filtered_lists = array_values( + array_filter( + $send_lists, + function ( $list ) use ( $search ) { + return Send_Lists::matches_search( + $search, + [ + $list->get_id(), + $list->get_name(), + $list->get_entity_type(), + ] + ); + } + ) + ); + } + + if ( ! empty( $args['limit'] ) ) { + $filtered_lists = array_slice( $filtered_lists, 0, $args['limit'] ); + } + + return $filtered_lists; + } + /** * Add contact to a list or update an existing contact. * @@ -853,7 +1071,7 @@ function ( $list ) { * @return array|WP_Error Contact data if the contact was added or error if failed. */ public function add_contact( $contact, $list_id = false ) { - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); + $cc = $this->get_sdk(); $data = []; if ( $list_id ) { $data['list_ids'] = [ $list_id ]; @@ -895,7 +1113,7 @@ public function add_contact( $contact, $list_id = false ) { * @return array|WP_Error Response or error if contact was not found. */ public function get_contact_data( $email, $return_details = false ) { - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); + $cc = $this->get_sdk(); $contact = $cc->get_contact( $email ); if ( ! $contact || is_wp_error( $contact ) ) { return new WP_Error( @@ -939,7 +1157,7 @@ public function get_contact_lists( $email ) { * @return true|WP_Error True if the contact was updated or error. */ public function update_contact_lists( $email, $lists_to_add = [], $lists_to_remove = [] ) { - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); + $cc = $this->get_sdk(); $contact_data = $this->get_contact_data( $email ); if ( is_wp_error( $contact_data ) ) { /** Create contact */ @@ -986,6 +1204,10 @@ public static function get_labels( $context = '' ) { 'name' => 'Constant Contact', 'list_explanation' => __( 'Constant Contact List', 'newspack-newsletters' ), 'local_list_explanation' => __( 'Constant Contact Tag', 'newspack-newsletters' ), + 'list' => __( 'list or segment', 'newspack-newsletters' ), // "list" in lower case singular format. + 'lists' => __( 'lists or segments', 'newspack-newsletters' ), // "list" in lower case plural format. + 'List' => __( 'List or Segment', 'newspack-newsletters' ), // "list" in uppercase case singular format. + 'Lists' => __( 'Lists or Segments', 'newspack-newsletters' ), // "list" in uppercase case plural format. ] ); } @@ -999,7 +1221,7 @@ public static function get_labels( $context = '' ) { * @return int|WP_Error The tag ID on success. WP_Error on failure. */ public function get_tag_id( $tag_name, $create_if_not_found = true, $list_id = null ) { - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); + $cc = $this->get_sdk(); $tag = $cc->get_tag_by_name( $tag_name ); if ( is_wp_error( $tag ) && $create_if_not_found ) { $tag = $this->create_tag( $tag_name ); @@ -1018,7 +1240,7 @@ public function get_tag_id( $tag_name, $create_if_not_found = true, $list_id = n * @return string|WP_Error The tag name on success. WP_Error on failure. */ public function get_tag_by_id( $tag_id, $list_id = null ) { - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); + $cc = $this->get_sdk(); $tag = $cc->get_tag_by_id( $tag_id ); if ( is_wp_error( $tag ) ) { return $tag; @@ -1034,7 +1256,7 @@ public function get_tag_by_id( $tag_id, $list_id = null ) { * @return array|WP_Error The tag representation with at least 'id' and 'name' keys on succes. WP_Error on failure. */ public function create_tag( $tag, $list_id = null ) { - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); + $cc = $this->get_sdk(); $tag = $cc->create_tag( $tag ); if ( is_wp_error( $tag ) ) { return $tag; @@ -1056,7 +1278,7 @@ public function create_tag( $tag, $list_id = null ) { * @return array|WP_Error The tag representation with at least 'id' and 'name' keys on succes. WP_Error on failure. */ public function update_tag( $tag_id, $tag, $list_id = null ) { - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); + $cc = $this->get_sdk(); $tag = $cc->update_tag( $tag_id, $tag ); if ( is_wp_error( $tag ) ) { return $tag; @@ -1084,7 +1306,7 @@ public function add_tag_to_contact( $email, $tag, $list_id = null ) { return true; } $new_tags = array_merge( $tags, [ $tag ] ); - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); + $cc = $this->get_sdk(); return $cc->upsert_contact( $email, [ 'taggings' => $new_tags ] ); } @@ -1102,7 +1324,7 @@ public function remove_tag_from_contact( $email, $tag, $list_id = null ) { if ( count( $new_tags ) === count( $tags ) ) { return true; } - $cc = new Newspack_Newsletters_Constant_Contact_SDK( $this->api_key(), $this->api_secret(), $this->access_token() ); + $cc = $this->get_sdk(); return $cc->upsert_contact( $email, [ 'taggings' => $new_tags ] ); } diff --git a/includes/service-providers/interface-newspack-newsletters-esp-service.php b/includes/service-providers/interface-newspack-newsletters-esp-service.php index bec6ad564..c0d1efc1d 100644 --- a/includes/service-providers/interface-newspack-newsletters-esp-service.php +++ b/includes/service-providers/interface-newspack-newsletters-esp-service.php @@ -50,17 +50,6 @@ public function list( $post_id, $list_id ); */ public function retrieve( $post_id ); - /** - * Set sender data. - * - * @param string $post_id Numeric ID of the campaign. - * @param string $from_name Sender name. - * @param string $reply_to Reply to email address. - * - * @return array|WP_Error API Response or error. - */ - public function sender( $post_id, $from_name, $reply_to ); - /** * Send test email or emails. * @@ -87,6 +76,15 @@ public function sync( $post ); */ public function get_lists(); + /** + * Get the ESP's available lists and sublists, reformatted as Send_List items. + * + * @param array $args Array of search args. See Send_Lists::get_default_args() for supported params and default values. + * + * @return array|WP_Error API Response or error. + */ + public function get_send_lists( $args ); + /** * Add contact to a list. * diff --git a/includes/service-providers/letterhead/class-newspack-newsletters-letterhead.php b/includes/service-providers/letterhead/class-newspack-newsletters-letterhead.php index 3c2a7e25d..84904680d 100644 --- a/includes/service-providers/letterhead/class-newspack-newsletters-letterhead.php +++ b/includes/service-providers/letterhead/class-newspack-newsletters-letterhead.php @@ -70,6 +70,15 @@ public function get_lists() { // TODO: Implement get_lists() method. } + /** + * Get send lists + * + * @param array $args Array of search args. See Send_Lists::get_default_args() for supported params and default values. + * + * @return object|void|null API Response or Error + */ + public function get_send_lists( $args = [] ) {} // Not used. + /** * This will call the Letterhead promotions API with the specific date passed as the * argument and the appropriate credentials. It will return an array. diff --git a/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp-cached-data.php b/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp-cached-data.php index 811c200d2..ef6b4d768 100644 --- a/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp-cached-data.php +++ b/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp-cached-data.php @@ -130,6 +130,35 @@ private static function get_mc_api() { } } + /** + * Get audiences (lists). + * + * @param int|null $limit (Optional) The maximum number of items to return. If not given, will get all items. + * + * @throws Exception In case of errors while fetching data from the server. + * @return array|WP_Error The audiences, or WP_Error if there was an error. + */ + public static function get_lists( $limit = null ) { + // If we've already gotten or fetched lists in this request, return those. + if ( ! empty( self::$memoized_data['lists'] ) ) { + return self::$memoized_data['lists']; + } + + $data = get_option( self::get_lists_cache_key() ); + if ( ! $data || self::is_cache_expired() ) { + Newspack_Newsletters_Logger::log( 'Mailchimp cache: No data found. Fetching lists from ESP.' ); + $data = self::fetch_lists( $limit ); + } else { + Newspack_Newsletters_Logger::log( 'Mailchimp cache: serving from cache' ); + } + + self::$memoized_data['lists'] = $data; + if ( $limit ) { + $data = array_slice( $data, 0, $limit ); + } + return $data; + } + /** * Get segments of a given audience (list) * @@ -200,6 +229,13 @@ private static function get_mc_instance() { return Newspack_Newsletters_Mailchimp::instance(); } + /** + * Get the cache key for the cached lists data. + */ + private static function get_lists_cache_key() { + return self::OPTION_PREFIX . '_lists'; + } + /** * Get the cache key for a given list * @@ -211,12 +247,12 @@ private static function get_cache_key( $list_id ) { } /** - * Get the cache date key for a given list + * Get the cache date key for a given list or all lists * - * @param string $list_id The List ID. + * @param string $list_id The List ID, or 'lists' for the cached lists data. * @return string The cache key */ - private static function get_cache_date_key( $list_id ) { + private static function get_cache_date_key( $list_id = 'lists' ) { return self::OPTION_PREFIX . '_date_' . $list_id; } @@ -226,7 +262,7 @@ private static function get_cache_date_key( $list_id ) { * @param string $list_id The List ID. * @return boolean */ - private static function is_cache_expired( $list_id ) { + private static function is_cache_expired( $list_id = null ) { $cache_date = get_option( self::get_cache_date_key( $list_id ) ); return $cache_date && ( time() - $cache_date ) > 20 * MINUTE_IN_SECONDS; } @@ -390,10 +426,15 @@ private static function get_data( $list_id ) { /** * Dispatches a new request to refresh the cache * - * @param string $list_id The List ID. + * @param string $list_id The List ID or null for the cache for all lists. * @return void */ - private static function dispatch_refresh( $list_id ) { + private static function dispatch_refresh( $list_id = null ) { + // If no list_id is provided, refresh the lists cache. + if ( ! $list_id ) { + self::fetch_lists(); + return; + } if ( ! function_exists( 'wp_create_nonce' ) ) { require_once ABSPATH . WPINC . '/pluggable.php'; @@ -484,38 +525,89 @@ private static function refresh_cached_data( $list_id ) { */ public static function handle_cron() { Newspack_Newsletters_Logger::log( 'Mailchimp cache: Handling cron request to refresh cache' ); + $lists = self::get_lists(); + + foreach ( $lists as $list ) { + Newspack_Newsletters_Logger::log( 'Mailchimp cache: Dispatching request to refresh cache for list ' . $list['id'] ); + self::dispatch_refresh( $list['id'] ); + } + } + + /** + * Fetches all audiences (lists) from the Mailchimp server + * + * @param int|null $limit (Optional) The maximum number of items to return. If not given, will get all items. + * + * @throws Exception In case of errors while fetching data from the server. + * @return array|WP_Error The audiences, or WP_Error if there was an error. + */ + public static function fetch_lists( $limit = null ) { $mc = self::get_mc_api(); if ( \is_wp_error( $mc ) ) { - return; + return []; } $lists_response = ( self::get_mc_instance() )->validate( $mc->get( 'lists', [ - 'count' => 1000, - 'fields' => [ 'id' ], + 'count' => $limit ?? 1000, + 'fields' => 'lists.name,lists.id,lists.web_id,lists.stats.member_count', ] ), __( 'Error retrieving Mailchimp lists.', 'newspack_newsletters' ) ); if ( is_wp_error( $lists_response ) || empty( $lists_response['lists'] ) ) { - return; + Newspack_Newsletters_Logger::log( 'Mailchimp cache: Error refreshing cache: ' . ( $lists_response->getMessage() ?? __( 'Error retrieving Mailchimp lists.', 'newspack_newsletters' ) ) ); + return is_wp_error( $lists_response ) ? $lists_response : []; } - foreach ( $lists_response['lists'] as $list ) { - Newspack_Newsletters_Logger::log( 'Mailchimp cache: Dispatching request to refresh cache for list ' . $list['id'] ); - self::dispatch_refresh( $list['id'] ); + // Cache the lists (only if we got them all). + if ( ! $limit ) { + update_option( self::get_lists_cache_key(), $lists_response['lists'], false ); // auto-load false. + update_option( self::get_cache_date_key(), time(), false ); // auto-load false. } + + return $lists_response['lists']; } /** - * Fetches the segments for a given List from the Mailchimp server + * Fetches a single segment by segment ID + list ID. * + * @param string $segment_id The segment ID. * @param string $list_id The audience (list) ID. + * + * @throws Exception In case of errors while fetching data from the server. + * @return array The audience segment + */ + public static function fetch_segment( $segment_id, $list_id ) { + $mc = self::get_mc_api(); + if ( \is_wp_error( $mc ) ) { + return $mc; + } + $response = ( self::get_mc_instance() )->validate( + $mc->get( + "lists/$list_id/segment/$segment_id", + [ + 'fields' => 'id,name,member_count,type,options,list_id', + ], + 60 + ), + __( 'Error retrieving Mailchimp segment with ID: ', 'newspack_newsletters' ) . $segment_id + ); + + return $response; + } + + /** + * Fetches the segments for a given List from the Mailchimp server + * + * @param string $list_id The audience (list) ID. + * @param int|null $limit (Optional) The maximum number of items to return. If not given, will get all items. + * * @throws Exception In case of errors while fetching data from the server. * @return array The audience segments */ - public static function fetch_segments( $list_id ) { + public static function fetch_segments( $list_id, $limit = null ) { $segments = []; $mc = self::get_mc_api(); @@ -528,7 +620,7 @@ public static function fetch_segments( $list_id ) { "lists/$list_id/segments", [ 'type' => 'saved', // 'saved' or 'static' segments. 'static' segments are actually the same thing as tags, so we can exclude them from this request as we fetch tags separately. - 'count' => 1000, + 'count' => $limit ?? 1000, ], 60 ), @@ -542,17 +634,19 @@ public static function fetch_segments( $list_id ) { /** * Fetches the interest_categories (aka Groups) for a given List from the Mailchimp server * - * @param string $list_id The audience (list) ID. + * @param string $list_id The audience (list) ID. + * @param int|null $limit (Optional) The maximum number of items to return. If not given, will get all items. + * * @throws Exception In case of errors while fetching data from the server. * @return array The audience interest_categories */ - private static function fetch_interest_categories( $list_id ) { + private static function fetch_interest_categories( $list_id, $limit = null ) { $mc = self::get_mc_api(); if ( \is_wp_error( $mc ) ) { return []; } $interest_categories = $list_id ? ( self::get_mc_instance() )->validate( - $mc->get( "lists/$list_id/interest-categories", [ 'count' => 1000 ], 60 ), + $mc->get( "lists/$list_id/interest-categories", [ 'count' => $limit ?? 1000 ], 60 ), __( 'Error retrieving Mailchimp groups.', 'newspack_newsletters' ) ) : null; @@ -560,7 +654,7 @@ private static function fetch_interest_categories( $list_id ) { foreach ( $interest_categories['categories'] as &$category ) { $category_id = $category['id']; $category['interests'] = ( self::get_mc_instance() )->validate( - $mc->get( "lists/$list_id/interest-categories/$category_id/interests", [ 'count' => 1000 ], 60 ), + $mc->get( "lists/$list_id/interest-categories/$category_id/interests", [ 'count' => $limit ?? 1000 ], 60 ), __( 'Error retrieving Mailchimp groups.', 'newspack_newsletters' ) ); } @@ -572,11 +666,13 @@ private static function fetch_interest_categories( $list_id ) { /** * Fetches the tags for a given audience (list) from the Mailchimp server * - * @param string $list_id The audience (list) ID. + * @param string $list_id The audience (list) ID. + * @param int|null $limit (Optional) The maximum number of items to return. If not given, will get all items. + * * @throws Exception In case of errors while fetching data from the server. * @return array The audience tags */ - public static function fetch_tags( $list_id ) { + public static function fetch_tags( $list_id, $limit = null ) { $mc = self::get_mc_api(); if ( \is_wp_error( $mc ) ) { return []; @@ -586,7 +682,7 @@ public static function fetch_tags( $list_id ) { "lists/$list_id/segments", [ 'type' => 'static', // 'saved' or 'static' segments. Tags are called 'static' segments in Mailchimp's API. - 'count' => 1000, + 'count' => $limit ?? 1000, ], 60 ), diff --git a/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp-controller.php b/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp-controller.php index 48deb7c7a..44ebbd7c0 100644 --- a/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp-controller.php +++ b/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp-controller.php @@ -42,14 +42,10 @@ public static function register_meta() { ); \register_meta( 'post', - 'mc_list_id', + 'mc_folder_id', [ 'object_subtype' => Newspack_Newsletters::NEWSPACK_NEWSLETTERS_CPT, - 'show_in_rest' => [ - 'schema' => [ - 'context' => [ 'edit' ], - ], - ], + 'show_in_rest' => true, 'type' => 'string', 'single' => true, 'auth_callback' => '__return_true', @@ -67,11 +63,11 @@ public function register_routes() { \register_rest_route( $this->service_provider::BASE_NAMESPACE . $this->service_provider->service, - '(?P[\a-z]+)', + '(?P[\a-z]+)/retrieve', [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'api_retrieve' ], - 'permission_callback' => [ $this->service_provider, 'api_authoring_permissions_check' ], + 'permission_callback' => [ 'Newspack_Newsletters', 'api_authoring_permissions_check' ], 'args' => [ 'id' => [ 'sanitize_callback' => 'absint', @@ -86,7 +82,7 @@ public function register_routes() { [ 'methods' => \WP_REST_Server::EDITABLE, 'callback' => [ $this, 'api_test' ], - 'permission_callback' => [ $this->service_provider, 'api_authoring_permissions_check' ], + 'permission_callback' => [ 'Newspack_Newsletters', 'api_authoring_permissions_check' ], 'args' => [ 'id' => [ 'sanitize_callback' => 'absint', @@ -98,84 +94,6 @@ public function register_routes() { ], ] ); - \register_rest_route( - $this->service_provider::BASE_NAMESPACE . $this->service_provider->service, - '(?P[\a-z]+)/sender', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_sender' ], - 'permission_callback' => [ $this->service_provider, 'api_authoring_permissions_check' ], - 'args' => [ - 'id' => [ - 'sanitize_callback' => 'absint', - 'validate_callback' => [ 'Newspack_Newsletters', 'validate_newsletter_id' ], - ], - 'from_name' => [ - 'sanitize_callback' => 'sanitize_text_field', - ], - 'reply_to' => [ - 'sanitize_callback' => 'sanitize_email', - ], - ], - ] - ); - \register_rest_route( - $this->service_provider::BASE_NAMESPACE . $this->service_provider->service, - '(?P[\a-z]+)/folder', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_folder' ], - 'permission_callback' => [ $this->service_provider, 'api_authoring_permissions_check' ], - 'args' => [ - 'id' => [ - 'sanitize_callback' => 'absint', - 'validate_callback' => [ 'Newspack_Newsletters', 'validate_newsletter_id' ], - ], - 'list_id' => [ - 'sanitize_callback' => 'esc_attr', - ], - 'folder_id' => [ - 'sanitize_callback' => 'esc_attr', - ], - ], - ] - ); - \register_rest_route( - $this->service_provider::BASE_NAMESPACE . $this->service_provider->service, - '(?P[\a-z]+)/list/(?P[\a-z]+)', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_list' ], - 'permission_callback' => [ $this->service_provider, 'api_authoring_permissions_check' ], - 'args' => [ - 'id' => [ - 'sanitize_callback' => 'absint', - 'validate_callback' => [ 'Newspack_Newsletters', 'validate_newsletter_id' ], - ], - 'list_id' => [ - 'sanitize_callback' => 'esc_attr', - ], - ], - ] - ); - \register_rest_route( - $this->service_provider::BASE_NAMESPACE . $this->service_provider->service, - '(?P[\a-z]+)/segments', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_segments' ], - 'permission_callback' => [ $this->service_provider, 'api_authoring_permissions_check' ], - 'args' => [ - 'id' => [ - 'sanitize_callback' => 'absint', - 'validate_callback' => [ 'Newspack_Newsletters', 'validate_newsletter_id' ], - ], - 'target_id' => [ - 'sanitize_callback' => 'esc_attr', - ], - ], - ] - ); } /** @@ -207,61 +125,4 @@ public function api_test( $request ) { ); return self::get_api_response( $response ); } - - /** - * Set the sender name and email for the campaign. - * - * @param WP_REST_Request $request API request object. - * @return WP_REST_Response|mixed API response or error. - */ - public function api_sender( $request ) { - $response = $this->service_provider->sender( - $request['id'], - $request['from_name'], - $request['reply_to'] - ); - return self::get_api_response( $response ); - } - - /** - * Set folder for a campaign. - * - * @param WP_REST_Request $request API request object. - * @return WP_REST_Response|mixed API response or error. - */ - public function api_folder( $request ) { - $response = $this->service_provider->folder( - $request['id'], - $request['folder_id'] - ); - return self::get_api_response( $response ); - } - - /** - * Set list for a campaign. - * - * @param WP_REST_Request $request API request object. - * @return WP_REST_Response|mixed API response or error. - */ - public function api_list( $request ) { - $response = $this->service_provider->list( - $request['id'], - $request['list_id'] - ); - return self::get_api_response( $response ); - } - - /** - * Set Mailchimp audience segments for a campaign. - * - * @param WP_REST_Request $request API request object. - * @return WP_REST_Response|mixed API response or error. - */ - public function api_segments( $request ) { - $response = $this->service_provider->audience_segments( - $request['id'], - $request['target_id'] - ); - return self::get_api_response( $response ); - } } diff --git a/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp.php b/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp.php index 21a64df6e..8c4dd067f 100644 --- a/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp.php +++ b/includes/service-providers/mailchimp/class-newspack-newsletters-mailchimp.php @@ -10,6 +10,8 @@ use DrewM\MailChimp\MailChimp; use Newspack\Newsletters\Subscription_List; use Newspack\Newsletters\Subscription_Lists; +use Newspack\Newsletters\Send_Lists; +use Newspack\Newsletters\Send_List; /** * Main Newspack Newsletters Class. @@ -19,18 +21,18 @@ final class Newspack_Newsletters_Mailchimp extends \Newspack_Newsletters_Service use Newspack_Newsletters_Mailchimp_Groups; /** - * Whether the provider has support to tags and tags based Subscription Lists. + * Provider name. * - * @var boolean + * @var string */ - public static $support_local_lists = true; + public $name = 'Mailchimp'; /** - * Provider name. + * Whether the provider has support to tags and tags based Subscription Lists. * - * @var string + * @var boolean */ - public $name = 'Mailchimp'; + public static $support_local_lists = true; /** * Cache of contact added on execution. Control to avoid adding the same @@ -111,6 +113,20 @@ public function api_key() { return $credentials['api_key']; } + /** + * Get the base URL for the Mailchimp admin dashboard. + * + * @return string|boolean The URL on success. False on failure. + */ + public function get_admin_url() { + $api_key = $this->api_key(); + if ( strpos( $api_key, '-' ) === false ) { + return false; + } + list(, $data_center) = explode( '-', $api_key ); + return 'https://' . $data_center . '.admin.mailchimp.com/'; + } + /** * Set the API credentials for the service provider. * @@ -315,6 +331,15 @@ public function remove_tag_from_contact( $email, $tag, $list_id = null ) { ); } + /** + * Get available campaign folders. + * + * @return array|WP_Error List of folders or error. + */ + public function get_folders() { + return Newspack_Newsletters_Mailchimp_Cached_Data::get_folders(); + } + /** * Set folder for a campaign. * @@ -322,7 +347,7 @@ public function remove_tag_from_contact( $email, $tag, $list_id = null ) { * @param string $folder_id ID of the folder. * @return object|WP_Error API API Response or error. */ - public function folder( $post_id, $folder_id ) { + public function folder( $post_id, $folder_id = '' ) { $mc_campaign_id = get_post_meta( $post_id, 'mc_campaign_id', true ); if ( ! $mc_campaign_id ) { return new WP_Error( @@ -411,34 +436,94 @@ public function retrieve( $post_id ) { } try { $mc_campaign_id = get_post_meta( $post_id, 'mc_campaign_id', true ); - if ( ! $mc_campaign_id ) { - $this->sync( get_post( $post_id ) ); - } - $mc = new Mailchimp( $this->api_key() ); - $campaign = $this->validate( - $mc->get( "campaigns/$mc_campaign_id" ), - __( 'Error retrieving Mailchimp campaign.', 'newspack_newsletters' ) - ); - $list_id = $campaign && isset( $campaign['recipients']['list_id'] ) ? $campaign['recipients']['list_id'] : null; - $lists = $this->get_lists( true ); - if ( \is_wp_error( $lists ) ) { - return $lists; + // If there's no synced campaign ID yet, create it. + if ( ! $mc_campaign_id ) { + Newspack_Newsletters_Logger::log( 'Creating new campaign for post ID ' . $post_id ); + $sync_result = $this->sync( get_post( $post_id ) ); + if ( is_wp_error( $sync_result ) ) { + return $sync_result; + } + $campaign = $sync_result['campaign_result']; + $mc_campaign_id = $campaign['id']; + } else { + Newspack_Newsletters_Logger::log( 'Retrieving campaign ' . $mc_campaign_id . ' for post ID ' . $post_id ); + $mc = new Mailchimp( $this->api_key() ); + $campaign = $this->validate( + $mc->get( + "campaigns/$mc_campaign_id", + [ + 'fields' => 'id,type,status,emails_sent,content_type,recipients,settings', + ] + ), + __( 'Error retrieving Mailchimp campaign.', 'newspack_newsletters' ) + ); } + $list_id = $campaign['recipients']['list_id'] ?? null; + $send_list_id = get_post_meta( $post_id, 'send_list_id', true ); + $send_sublist_id = get_post_meta( $post_id, 'send_sublist_id', true ); $newsletter_data = [ - 'campaign' => $campaign, - 'campaign_id' => $mc_campaign_id, - 'folders' => Newspack_Newsletters_Mailchimp_Cached_Data::get_folders(), - 'interest_categories' => $this->get_interest_categories( $list_id ), - 'lists' => $lists, - 'merge_fields' => $list_id ? Newspack_Newsletters_Mailchimp_Cached_Data::get_merge_fields( $list_id ) : [], - 'segments' => $list_id ? Newspack_Newsletters_Mailchimp_Cached_Data::get_segments( $list_id ) : [], - 'tags' => $this->get_tags( $list_id ), + 'campaign' => $campaign, + 'campaign_id' => $mc_campaign_id, + 'folders' => Newspack_Newsletters_Mailchimp_Cached_Data::get_folders(), + 'allowed_sender_domains' => $this->get_verified_domains(), + 'merge_fields' => $list_id ? Newspack_Newsletters_Mailchimp_Cached_Data::get_merge_fields( $list_id ) : [], ]; + // Reconcile campaign settings with info fetched from the ESP for a true two-way sync. + if ( ! empty( $campaign['settings']['from_name'] ) && $campaign['settings']['from_name'] !== get_post_meta( $post_id, 'senderName', true ) ) { + $newsletter_data['senderName'] = $campaign['settings']['from_name']; // If campaign has different sender info set, update ours. + } + if ( ! empty( $campaign['settings']['reply_to'] ) && $campaign['settings']['reply_to'] !== get_post_meta( $post_id, 'senderEmail', true ) ) { + $newsletter_data['senderEmail'] = $campaign['settings']['reply_to']; // If campaign has different sender info set, update ours. + } + if ( $list_id && $list_id !== $send_list_id ) { + $newsletter_data['list_id'] = $list_id; // If campaign has a different list selected, update ours. + $send_list_id = $list_id; + + if ( ! empty( $campaign['recipients']['segment_opts'] ) ) { + $segment_opts = $campaign['recipients']['segment_opts']; + $target_id_raw = $segment_opts['saved_segment_id'] ?? null; + if ( ! $target_id_raw ) { + $target_id_raw = $segment_opts['conditions'][0]['value'] ?? null; + } + if ( $target_id_raw ) { + $target_id = strval( is_array( $target_id_raw ) && ! empty( $target_id_raw[0] ) ? $target_id_raw[0] : $target_id_raw ); + if ( ! $target_id ) { + $target_id = (string) $target_id_raw; + } + if ( $target_id && $target_id !== $send_sublist_id ) { + $newsletter_data['sublist_id'] = $target_id; // If campaign has a different sublist selected, update ours. + $send_sublist_id = $target_id; + } + } + } + } + + // Prefetch send list info if we have a selected list and/or sublist. + $newsletter_data['lists'] = $this->get_send_lists( + [ + 'ids' => $send_list_id ? [ $send_list_id ] : null, // If we have a selected list, make sure to fetch it. + 'type' => 'list', + ] + ); + $newsletter_data['sublists'] = $send_list_id || $send_sublist_id ? // Prefetch send lists only if we have something selected already. + $this->get_send_lists( + [ + 'ids' => [ $send_sublist_id ], // If we have a selected sublist, make sure to fetch it. Otherwise, we'll populate sublists later. + 'parent_id' => $send_list_id, + 'type' => 'sublist', + ] + ) : + []; + return $newsletter_data; } catch ( Exception $e ) { + // If we couldn't get the campaign, delete the mc_campaign_id so it gets recreated on the next sync. + delete_post_meta( $post_id, 'mc_campaign_id' ); + $this->retrieve( $post_id ); + return new WP_Error( 'newspack_newsletters_mailchimp_error', $e->getMessage() @@ -455,92 +540,171 @@ public function retrieve( $post_id ) { * @return array|WP_Error List of subscription lists or error. */ public function get_lists( $audiences_only = false ) { - try { - $mc = new Mailchimp( $this->api_key() ); - $lists_response = $this->validate( - $mc->get( - 'lists', - [ - 'count' => 1000, - ] - ), - __( 'Error retrieving Mailchimp lists.', 'newspack_newsletters' ) - ); - if ( is_wp_error( $lists_response ) ) { - return new WP_Error( - 'newspack_newsletters_mailchimp_error', - $lists_response->getMessage() - ); - } + $lists = Newspack_Newsletters_Mailchimp_Cached_Data::get_lists(); + if ( $audiences_only || is_wp_error( $lists ) ) { + return $lists; + } - if ( ! isset( $lists_response['lists'] ) ) { - $error_message = __( 'Error retrieving Mailchimp lists.', 'newspack_newsletters' ); - $error_message .= ! empty( $lists_response['title'] ) ? ' ' . $lists_response['title'] : ''; - return new WP_Error( - 'newspack_newsletters_mailchimp_error', - $error_message + // In addition to Audiences, we also automatically fetch all groups and tags and offer them as Subscription Lists. + // Build the final list inside the loop so groups are added after the list they belong to and we can then represent the hierarchy in the UI. + foreach ( $lists as $list ) { + + $lists[] = $list; + $all_categories = Newspack_Newsletters_Mailchimp_Cached_Data::get_interest_categories( $list['id'] ); + $all_categories = $all_categories['categories'] ?? []; + $all_tags = Newspack_Newsletters_Mailchimp_Cached_Data::get_tags( $list['id'] ) ?? []; + + foreach ( $all_categories as $found_category ) { + + // Do not include groups under the category we use to store "Local" lists. + if ( $this->get_group_category_name() === $found_category['title'] ) { + continue; + } + + $all_groups = $found_category['interests'] ?? []; + + $groups = array_map( + function ( $group ) use ( $list ) { + $group['id'] = Subscription_List::mailchimp_generate_public_id( $group['id'], $list['id'] ); + $group['type'] = 'mailchimp-group'; + return $group; + }, + $all_groups['interests'] ?? [] // Yes, two levels of 'interests'. ); + $lists = array_merge( $lists, $groups ); } - if ( $audiences_only ) { - return $lists_response['lists']; + foreach ( $all_tags as $tag ) { + $tag['id'] = Subscription_List::mailchimp_generate_public_id( $tag['id'], $list['id'], 'tag' ); + $tag['type'] = 'mailchimp-tag'; + $lists[] = $tag; } + } - $lists = []; + // Reconcile edited names for locally-configured lists. + $configured_lists = Newspack_Newsletters_Subscription::get_lists_config(); + if ( ! empty( $configured_lists ) ) { + foreach ( $lists as &$list ) { + if ( ! empty( $configured_lists[ $list['id'] ]['name'] ) ) { + $list['local_name'] = $configured_lists[ $list['id'] ]['name']; + } + } + } - // In addition to Audiences, we also automatically fetch all groups and tags and offer them as Subscription Lists. - // Build the final list inside the loop so groups are added after the list they belong to and we can then represent the hierarchy in the UI. - foreach ( $lists_response['lists'] as $list ) { + return $lists; + } - $lists[] = $list; - $all_categories = Newspack_Newsletters_Mailchimp_Cached_Data::get_interest_categories( $list['id'] ); - $all_categories = $all_categories['categories'] ?? []; - $all_tags = Newspack_Newsletters_Mailchimp_Cached_Data::get_tags( $list['id'] ) ?? []; + /** + * Get all applicable audiences, groups, tags, and segments as Send_List objects. + * + * @param array $args Array of search args. See Send_Lists::get_default_args() for supported params and default values. + * + * @return Send_List[]|WP_Error Array of Send_List objects on success, or WP_Error object on failure. + */ + public function get_send_lists( $args = [] ) { + $defaults = Send_Lists::get_default_args(); + $args = wp_parse_args( $args, $defaults ); + $by_id = ! empty( $args['ids'] ); + $admin_url = self::get_admin_url(); + $audiences = Newspack_Newsletters_Mailchimp_Cached_Data::get_lists( $args['limit'] ); + $send_lists = []; + + $entity_type = 'audience'; + foreach ( $audiences as $audience ) { + if ( ! empty( $args['parent_id'] ) && $audience['id'] !== $args['parent_id'] ) { + continue; + } + $matches = $by_id ? Send_Lists::matches_id( $args['ids'], $audience['id'] ) : Send_Lists::matches_search( $args['search'], [ $audience['id'], $audience['name'], $entity_type ] ); + if ( ( ! $args['type'] || 'list' === $args['type'] ) && $matches ) { + $config = [ + 'provider' => $this->service, + 'type' => 'list', + 'id' => $audience['id'], + 'name' => $audience['name'], + 'entity_type' => $entity_type, + 'count' => $audience['stats']['member_count'] ?? 0, + ]; + if ( $admin_url && ! empty( $audience['web_id'] ) ) { + $config['edit_link'] = $admin_url . 'audience/contacts/?id=' . $audience['web_id']; + } + $send_lists[] = new Send_List( $config ); + } - foreach ( $all_categories as $found_category ) { + if ( 'list' === $args['type'] ) { + continue; + } - // Do not include groups under the category we use to store "Local" lists. - if ( $this->get_group_category_name() === $found_category['title'] ) { - continue; + $groups = Newspack_Newsletters_Mailchimp_Cached_Data::get_interest_categories( $audience['id'], $args['limit'] ); + $entity_type = 'group'; + if ( isset( $groups['categories'] ) ) { + foreach ( $groups['categories'] as $category ) { + if ( isset( $category['interests']['interests'] ) ) { + foreach ( $category['interests']['interests'] as $interest ) { + $matches = $by_id ? Send_Lists::matches_id( $args['ids'], $interest['id'] ) : Send_Lists::matches_search( $args['search'], [ $interest['id'], $interest['name'], $entity_type ] ); + if ( $matches ) { + $config = [ + 'provider' => $this->service, + 'type' => 'sublist', + 'id' => $interest['id'], + 'name' => $interest['name'], + 'entity_type' => $entity_type, + 'parent' => $interest['list_id'], + 'count' => $interest['subscriber_count'], + ]; + if ( $admin_url && $audience['web_id'] ) { + $config['edit_link'] = $admin_url . 'audience/groups/?id=' . $audience['web_id']; + } + $send_lists[] = new Send_List( $config ); + } + } } - - $all_groups = $found_category['interests'] ?? []; - - $groups = array_map( - function ( $group ) use ( $list ) { - $group['id'] = Subscription_List::mailchimp_generate_public_id( $group['id'], $list['id'] ); - $group['type'] = 'mailchimp-group'; - return $group; - }, - $all_groups['interests'] ?? [] // Yes, two levels of 'interests'. - ); - $lists = array_merge( $lists, $groups ); } + } - foreach ( $all_tags as $tag ) { - $tag['id'] = Subscription_List::mailchimp_generate_public_id( $tag['id'], $list['id'], 'tag' ); - $tag['type'] = 'mailchimp-tag'; - $lists[] = $tag; + $tags = Newspack_Newsletters_Mailchimp_Cached_Data::get_tags( $audience['id'], $args['limit'] ); + $entity_type = 'tag'; + foreach ( $tags as $tag ) { + $matches = $by_id ? Send_Lists::matches_id( $args['ids'], $tag['id'] ) : Send_Lists::matches_search( $args['search'], [ $tag['id'], $tag['name'], $entity_type ] ); + if ( $matches ) { + $config = [ + 'provider' => $this->service, + 'type' => 'sublist', + 'id' => $tag['id'], + 'name' => $tag['name'], + 'entity_type' => $entity_type, + 'parent' => $tag['list_id'], + 'count' => $tag['member_count'], + ]; + if ( $admin_url && $audience['web_id'] ) { + $config['edit_link'] = $admin_url . 'audience/tags/?id=' . $audience['web_id']; + } + $send_lists[] = new Send_List( $config ); } } - // Reconcile edited names for locally-configured lists. - $configured_lists = Newspack_Newsletters_Subscription::get_lists_config(); - if ( ! empty( $configured_lists ) ) { - foreach ( $lists as &$list ) { - if ( ! empty( $configured_lists[ $list['id'] ]['name'] ) ) { - $list['local_name'] = $configured_lists[ $list['id'] ]['name']; + $segments = Newspack_Newsletters_Mailchimp_Cached_Data::get_segments( ( $parent_id ?? $audience['id'] ), $args['limit'] ); + $entity_type = 'segment'; + foreach ( $segments as $segment ) { + $matches = $by_id ? Send_Lists::matches_id( $args['ids'], $segment['id'] ) : Send_Lists::matches_search( $args['search'], [ $segment['id'], $segment['name'], $entity_type ] ); + if ( $matches ) { + $config = [ + 'provider' => $this->service, + 'type' => 'sublist', + 'id' => $segment['id'], + 'name' => $segment['name'], + 'entity_type' => $entity_type, + 'parent' => $segment['list_id'], + 'count' => $segment['member_count'], + ]; + if ( $admin_url && $audience['web_id'] ) { + $config['edit_link'] = $admin_url . 'audience/segments/?id=' . $audience['web_id']; } + $send_lists[] = new Send_List( $config ); } } - - return $lists; - } catch ( Exception $e ) { - return new WP_Error( - 'newspack_newsletters_mailchimp_error', - $e->getMessage() - ); } + + return $send_lists; } /** @@ -631,30 +795,19 @@ public function get_list_merge_fields( $list_id ) { } /** - * Set sender data. + * Get verified domains from the MC account. * - * @param string $post_id Numeric ID of the campaign. - * @param string $from_name Sender name. - * @param string $reply_to Reply to email address. - * @return object|WP_Error API Response or error. + * @return array List of verified domains. */ - public function sender( $post_id, $from_name, $reply_to ) { - $mc_campaign_id = get_post_meta( $post_id, 'mc_campaign_id', true ); - if ( ! $mc_campaign_id ) { - return new WP_Error( - 'newspack_newsletters_no_campaign_id', - __( 'Mailchimp campaign ID not found.', 'newspack-newsletters' ) - ); - } - try { - $mc = new Mailchimp( $this->api_key() ); - - $result = $this->validate( - $mc->get( 'verified-domains', [ 'count' => 1000 ] ), - __( 'Error retrieving verified domains from Mailchimp.', 'newspack-newsletters' ) - ); + public function get_verified_domains() { + $mc = new Mailchimp( $this->api_key() ); + $result = $this->validate( + $mc->get( 'verified-domains', [ 'count' => 1000 ] ), + __( 'Error retrieving verified domains from Mailchimp.', 'newspack-newsletters' ) + ); - $verified_domains = array_filter( + return array_values( + array_filter( array_map( function ( $domain ) { return $domain['verified'] ? strtolower( trim( $domain['domain'] ) ) : null; @@ -664,10 +817,21 @@ function ( $domain ) { function ( $domain ) { return ! empty( $domain ); } - ); + ) + ); + } - $explode = explode( '@', $reply_to ); - $domain = strtolower( trim( array_pop( $explode ) ) ); + /** + * Set sender data. + * + * @param string $email Reply to email address. + * @return boolean|WP_Error True if the email address is valid, otherwise error. + */ + public function validate_sender_email( $email ) { + try { + $verified_domains = $this->get_verified_domains(); + $explode = explode( '@', $email ); + $domain = strtolower( trim( array_pop( $explode ) ) ); if ( ! in_array( $domain, $verified_domains ) ) { return new WP_Error( @@ -681,25 +845,7 @@ function ( $domain ) { ); } - $settings = []; - if ( $from_name ) { - $settings['from_name'] = $from_name; - } - if ( $reply_to ) { - $settings['reply_to'] = $reply_to; - } - $payload = [ - 'settings' => $settings, - ]; - $result = $this->validate( - $mc->patch( "campaigns/$mc_campaign_id", $payload ), - __( 'Error setting sender name and email.', 'newspack_newsletters' ) - ); - - $data = $this->retrieve( $post_id ); - $data['result'] = $result; - - return \rest_ensure_response( $data ); + return true; } catch ( Exception $e ) { return new WP_Error( 'newspack_newsletters_mailchimp_error', @@ -767,6 +913,110 @@ public function test( $post_id, $emails ) { } } + /** + * Get a payload for syncing post data to the ESP campaign. + * + * @param WP_Post|int $post Post object or ID. + * @return object Payload for syncing. + */ + public function get_sync_payload( $post ) { + if ( is_int( $post ) ) { + $post = get_post( $post ); + } + $payload = [ + 'type' => 'regular', + 'content_type' => 'template', + 'settings' => [ + 'subject_line' => $post->post_title, + 'title' => $this->get_campaign_name( $post ), + ], + ]; + + // Sync sender name + email. + $sender_name = get_post_meta( $post->ID, 'senderName', true ); + $sender_email = get_post_meta( $post->ID, 'senderEmail', true ); + if ( ! empty( $sender_name ) ) { + $payload['settings']['from_name'] = $sender_name; + } + if ( ! empty( $sender_email ) ) { + $is_valid_email = $this->validate_sender_email( $sender_email ); + if ( is_wp_error( $is_valid_email ) ) { + delete_post_meta( $post->ID, 'senderEmail' ); // Delete invalid email so we can't accidentally attempt to send with it. + return $is_valid_email; + } + $payload['settings']['reply_to'] = $sender_email; + } + + // Sync send-to selections. + $send_list_id = get_post_meta( $post->ID, 'send_list_id', true ); + if ( ! empty( $send_list_id ) ) { + $payload['recipients'] = [ + 'list_id' => $send_list_id, + ]; + $send_sublist_id = get_post_meta( $post->ID, 'send_sublist_id', true ); + if ( ! empty( $send_sublist_id ) ) { + $sublist = $this->get_send_lists( + [ + 'ids' => [ $send_sublist_id ], + 'limit' => 1, + 'parent_id' => $send_list_id, + 'type' => 'sublist', + ] + ); + if ( ! empty( $sublist[0]->get_entity_type() ) ) { + $sublist_type = $sublist[0]->get_entity_type(); + switch ( $sublist_type ) { + case 'group': + $payload['recipients']['segment_opts'] = [ + 'match' => 'all', + 'conditions' => [ + [ + 'condition_type' => 'Interests', + 'field' => 'interests-' . $send_sublist_id, + 'op' => 'interestcontains', + 'value' => [ $send_sublist_id ], + ], + ], + ]; + break; + case 'tag': + $payload['recipients']['segment_opts'] = [ + 'match' => 'all', + 'conditions' => [ + [ + 'condition_type' => 'StaticSegment', + 'field' => 'static_segment', + 'op' => 'static_is', + 'value' => $send_sublist_id, + ], + ], + ]; + break; + case 'segment': + $segment_data = Newspack_Newsletters_Mailchimp_Cached_Data::fetch_segment( $send_sublist_id, $send_list_id ); + if ( is_wp_error( $segment_data ) ) { + return $segment_data; + } + if ( ! empty( $segment_data['options'] ) ) { + $payload['recipients']['segment_opts'] = $segment_data['options']; + } else { + return new WP_Error( 'newspack_newsletters_mailchimp_error', __( 'Could not fetch segment criteria for segment ', 'newspack-newsletters' ) . $sublist['name'] ); + } + break; + } + } + } + } + + // Sync folder selection. + $folder_id = get_post_meta( $post->ID, 'mc_folder_id', true ); + if ( $folder_id ) { + $payload['settings']['folder_id'] = $folder_id; + } + + return $payload; + } + /** * Synchronize post with corresponding ESP campaign. * @@ -789,15 +1039,8 @@ public function sync( $post ) { throw new Exception( __( 'The newsletter subject cannot be empty.', 'newspack-newsletters' ) ); } $mc = new Mailchimp( $api_key ); - $payload = [ - 'type' => 'regular', - 'content_type' => 'template', - 'settings' => [ - 'subject_line' => $post->post_title, - 'title' => $this->get_campaign_name( $post ), - ], - ]; $mc_campaign_id = get_post_meta( $post->ID, 'mc_campaign_id', true ); + $payload = $this->get_sync_payload( $post ); /** * Filter the metadata payload sent to Mailchimp when syncing. @@ -810,6 +1053,11 @@ public function sync( $post ) { */ $payload = apply_filters( 'newspack_newsletters_mc_payload_sync', $payload, $post, $mc_campaign_id ); + // If we have any errors in the payload, throw an exception. + if ( is_wp_error( $payload ) ) { + throw new Exception( esc_html( $payload->get_error_message() ) ); + } + if ( $mc_campaign_id ) { $campaign_result = $this->validate( $mc->patch( "campaigns/$mc_campaign_id", $payload ), @@ -838,13 +1086,6 @@ public function sync( $post ) { $mc->put( "campaigns/$mc_campaign_id/content", $content_payload ), __( 'Error updating campaign content.', 'newspack_newsletters' ) ); - - // Retrieve and store campaign data. - $data = $this->retrieve( $post->ID ); - if ( ! is_wp_error( $data ) ) { - update_post_meta( $post->ID, 'newsletterData', $data ); - } - return [ 'campaign_result' => $campaign_result, 'content_result' => $content_result, @@ -1705,8 +1946,10 @@ public static function get_labels( $context = '' ) { 'name' => 'Mailchimp', // The provider name. 'list' => __( 'audience', 'newspack-newsletters' ), // "list" in lower case singular format. 'lists' => __( 'audiences', 'newspack-newsletters' ), // "list" in lower case plural format. + 'sublist' => __( 'group, segment, or tag', 'newspack-newsletters' ), // Sublist entities in lowercase singular format. 'List' => __( 'Audience', 'newspack-newsletters' ), // "list" in uppercase case singular format. 'Lists' => __( 'Audiences', 'newspack-newsletters' ), // "list" in uppercase case plural format. + 'Sublist' => __( 'Group, Segment, or Tag', 'newspack-newsletters' ), // Sublist entities in uppercase singular format. 'list_explanation' => __( 'Mailchimp Audience', 'newspack-newsletters' ), // translators: %s is the name of the group category. "Newspack newsletters" by default. 'local_list_explanation' => sprintf( __( 'Mailchimp Group under the %s category', 'newspack-newsletters' ), self::get_group_category_name() ), diff --git a/tests/test-labels.php b/tests/test-labels.php index 0a1fdc3a0..87ccb41ee 100644 --- a/tests/test-labels.php +++ b/tests/test-labels.php @@ -20,11 +20,11 @@ public function test_labels_inheritance() { $ac_labels = Newspack_Newsletters_Campaign_Monitor::get_labels(); $this->assertSame( 'Campaign Monitor', $ac_labels['name'] ); - $this->assertSame( 'list', $ac_labels['list'] ); + $this->assertSame( 'list or segment', $ac_labels['list'] ); $ac_labels = Newspack_Newsletters_Constant_Contact::get_labels(); $this->assertSame( 'Constant Contact', $ac_labels['name'] ); - $this->assertSame( 'list', $ac_labels['list'] ); + $this->assertSame( 'list or segment', $ac_labels['list'] ); } /**