Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
372 changes: 372 additions & 0 deletions src/email-providers/class-campaign-monitor-segment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,372 @@
<?php
/**
* WP_Newsletter_Builder class file
*
* @package wp-newsletter-builder
*/

namespace WP_Newsletter_Builder\Email_Providers;

/**
* Campaign Monitor Client class for implementations which use a single list with segments instead of individual lists.
*/
class Campaign_Monitor_Segment implements Email_Provider {
/**
* Settings key.
*
* @var string
*/
public const SETTINGS_KEY = 'nb_campaign_monitor_segment_settings';

/**
* Sets things up.
*
* @return void
*/
public function setup(): void {
add_action( 'init', [ $this, 'maybe_register_settings_page' ] );
}

/**
* Registers the submenu settings page for the Campaign Monitor options.
*
* @return void
*/
public function maybe_register_settings_page(): void {
if ( function_exists( 'fm_register_submenu_page' ) && \current_user_can( 'manage_options' ) ) {
\fm_register_submenu_page( static::SETTINGS_KEY, 'edit.php?post_type=nb_newsletter', __( 'Campaign Monitor Settings', 'wp-newsletter-builder' ), __( 'Campaign Monitor Settings', 'wp-newsletter-builder' ) );
\add_action( 'fm_submenu_' . static::SETTINGS_KEY, [ $this, 'register_fields' ] );
}
}

/**
* Registers the fields on the settings page for the Campaign Monitor options.
*
* @return void
*/
public function register_fields(): void {
$settings = new \Fieldmanager_Group(
[
'name' => static::SETTINGS_KEY,
'children' => [
'api_key' => new \Fieldmanager_Password( __( 'API Key', 'wp-newsletter-builder' ) ),
'client_id' => new \Fieldmanager_TextField( __( 'Client ID', 'wp-newsletter-builder' ) ),
'confirmation_email' => new \Fieldmanager_TextField( __( 'Confirmation Email', 'wp-newsletter-builder' ) ),
'list_id' => new \Fieldmanager_TextField( __( 'List ID', 'wp-newsletter-builder' ) ),
],
]
);

$settings->activate_submenu_page();
}

/**
* Get the API key and instantiate a client using the API key.
*
* @return \CS_REST_General|false
*/
public function get_client(): \CS_REST_General|false {
$settings = get_option( static::SETTINGS_KEY );
if ( empty( $settings ) || ! is_array( $settings ) || empty( $settings['api_key'] ) ) {
return false;
}
$auth = [ 'api_key' => $settings['api_key'] ];
$wrap = new \CS_REST_General( $auth );

return $wrap;
Comment on lines +74 to +76
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just return the new instance directly?

Suggested change
$wrap = new \CS_REST_General( $auth );
return $wrap;
return new \CS_REST_General( $auth );

}

/**
* Gets the lists for the client.
*
* @TODO: Add caching that works on Pantheon and WordPress VIP.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A wp_cache_get/wp_cache_set would work fine on both. Can probably cache lists for 5 minutes?

*
* @return mixed
*/
public function get_lists(): mixed {
$settings = get_option( static::SETTINGS_KEY );
if ( empty( $settings ) || ! is_array( $settings ) || empty( $settings['api_key'] ) || empty( $settings['client_id'] ) ) {
return false;
}
$auth = [ 'api_key' => $settings['api_key'] ];

$wrap = new \CS_REST_Lists(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo a better variable name would be $lists

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call! In a previous iteration I was using the Clients Wrapper, copied over from the existing Campaign Monitor provider. Forgot to change the variable when I switched to the more direct class.

$settings['list_id'],
$auth
);

$default_segments = [
'Active',
'Engaged',
'Unengaged',
'Dormant',
'Zombies',
'Ghosts',
];

// Get all segments for the client.
$segments = $wrap->get_segments()->response;
$lists = [];

if ( empty( $segments ) || ! is_array( $segments ) ) {
return false;
}

foreach ( $segments as $segment ) {
// Filter segments to only include segments for the list we're using, and exclude default segments.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the list ID filter already handled by providing the list ID to CS_REST_Lists above? Why do we need to filter for it again?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup! This comment was left over from an earlier implementation (got all segments and then filtered them by lists) before I switched to using the Lists class/endpoint. Thanks for catching.

if ( $segment->ListID === $settings['list_id'] && ! in_array( $segment->Title, $default_segments, true ) ) {
// Reshape the segments into the list format the rest of the plugin expects.
$lists[] = (object) [
'ListID' => $segment->SegmentID,
'Name' => $segment->Title,
];
}
}

return $lists;
}

/**
* Creates an email campaign.
*
* @param int $newsletter_id The id of the nb_newsletter post.
* @param array<string> $list_ids The list ids to send the campaign to.
* @param string $campaign_id Optional campaign id to update.
* @param string $from_name The from name.
* @return array{
* response: mixed,
* http_status_code: int,
* }|false The response from the API.
*/
public function create_campaign( int $newsletter_id, array $list_ids, string $campaign_id = null, string $from_name ): array|false {
// TODO: Move non-email provider code to the core plugin.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this TODO still valid?

$settings = get_option( static::SETTINGS_KEY );
if ( empty( $settings ) || ! is_array( $settings ) || empty( $settings['api_key'] ) || empty( $settings['client_id'] ) ) {
return false;
}
$auth = [ 'api_key' => $settings['api_key'] ];
Comment on lines +143 to +147
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pattern exists frequently in this file - can it be abstracted?


$wrap = new \CS_REST_Campaigns( $campaign_id, $auth );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, let's call this $campaigns


$newsletter = get_post( $newsletter_id );
if ( ! $newsletter instanceof \WP_Post ) {
return false;
}

$url = add_query_arg(
[
'post_type' => 'nb_newsletter',
'p' => $newsletter->ID,
],
home_url(),
);

$segment_id = get_post_meta( $newsletter->ID, 'nb_newsletter_list', true ); // This meta field is used to store the segment id instead of the list id in this provider.

if ( empty( $segment_id ) ) {
return false;
}

if ( is_array( $segment_id ) ) {
$segment_id = reset( $segment_id );
}

/**
* Filter the URL for the HTML version of the newsletter.
*
* @param string $url The URL.
*/
$url = apply_filters( 'wp_newsletter_builder_html_url', $url );

$params = [
'Subject' => get_post_meta( $newsletter->ID, 'nb_newsletter_subject', true ),
'Name' => sprintf( '%s - Post %d - %s', $newsletter->post_title, $newsletter->ID, get_post_modified_time( 'Y-m-d H:i:s', false, $newsletter->ID ) ),
'FromName' => $from_name,
'FromEmail' => $settings['from_email'],
'ReplyTo' => $settings['reply_to_email'],
'HtmlUrl' => $url,
'SegmentIDs' => [ (string) $segment_id ],
];

$result = $wrap->create( $settings['client_id'], $params );

return [
'response' => $result->response,
'http_status_code' => $result->http_status_code,
];
}

/**
* Sends a campaign.
*
* @param string $campaign_id The campaign id.
* @return array{
* response: mixed,
* success: boolean,
* }|false The response from the API.
*/
public function send_campaign( string $campaign_id ): array|false {
$settings = get_option( static::SETTINGS_KEY );
if ( empty( $settings ) || ! is_array( $settings ) || empty( $settings['api_key'] ) || empty( $settings['client_id'] ) ) {
return false;
}
$auth = [ 'api_key' => $settings['api_key'] ];

$wrap = new \CS_REST_Campaigns( $campaign_id, $auth );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$campaigns


$result = $wrap->send(
[
'ConfirmationEmail' => $settings['confirmation_email'],
'SendDate' => 'immediately',
]
);

return [
'response' => $result->response,
'success' => 200 === $result->http_status_code,
];
}

/**
* Gets campaign summary.
*
* @param string $campaign_id The campaign id.
* @return array{
* response: mixed,
* http_status_code: int,
* }|false The response from the API.
*/
public function get_campaign_summary( string $campaign_id ): array|false {
$settings = get_option( static::SETTINGS_KEY );
if ( empty( $settings ) || ! is_array( $settings ) || empty( $settings['api_key'] ) || empty( $settings['client_id'] ) ) {
return false;
}
$auth = [ 'api_key' => $settings['api_key'] ];

$wrap = new \CS_REST_Campaigns( $campaign_id, $auth );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here


$result = $wrap->get_summary();
return [
'response' => $result->response,
'http_status_code' => $result->http_status_code,
];
}

/**
* Determine if the campaign was created successfully.
*
* @param array|false $result {.
* @type mixed $response The deserialised result of the API call.
* @type int $http_status_code The http status code of the API call.
* } The response from the creation request.
* @phpstan-param array{response: mixed, http_status_code: int}|false $result
* @return bool
*/
public function campaign_created_successfully( array|false $result ): bool {
return ! empty( $result['http_status_code'] ) ? 201 === $result['http_status_code'] : false;
}

/**
* Gets the campaign id from the result.
*
* @param array|false $result {.
* @type mixed $response The deserialised result of the API call.
* @type int $http_status_code The http status code of the API call.
* } The response from the creation request.
* @phpstan-param array{response: mixed, http_status_code: int}|false $result
* @return mixed
*/
public function get_campaign_id_from_create_result( array|false $result ): mixed {
return $result['response'] ?? false;
}


/**
* Add subscriber to list
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's worth asking around to other teams that use this plugin whether this functionality is actually needed. I'd rather that Newsletter Builder be responsible for managing campaigns, and not be responsible for managing list members. If implementers need to do list management, they can do so separately, and this plugin should be focused on sending campaigns to existing lists/segments with existing subscriber bases to avoid trying to do too much.

*
* @param string $list_id The list id.
* @param string $email The email address.
* @param array<array<string, string>> $custom_fields The custom fields.
* @return array{
* response: mixed,
* http_status_code: int,
* }|false The response from the API.
*/
public function add_subscriber( string $list_id, string $email, array $custom_fields = [] ): array|false {
$settings = get_option( static::SETTINGS_KEY );
if ( empty( $settings ) || ! is_array( $settings ) || empty( $settings['api_key'] ) || empty( $settings['client_id'] ) ) {
return false;
}
$auth = [ 'api_key' => $settings['api_key'] ];

$wrap = new \CS_REST_Subscribers( $list_id, $auth );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$subscribers


$result = $wrap->add(
[
'EmailAddress' => $email,
'Resubscribe' => true,
'ConsentToTrack' => 'yes',
'CustomFields' => $custom_fields,
]
);

return [
'response' => $result->response,
'http_status_code' => $result->http_status_code,
];
}

/**
* Remove subscriber from list.
*
* @param string $list_id The list id.
* @param string $email The email address.
* @return array{
* response: mixed,
* http_status_code: int,
* }|false The response from the API.
*/
public function remove_subscriber( string $list_id, string $email ): array|false {
$settings = get_option( static::SETTINGS_KEY );
if ( empty( $settings ) || ! is_array( $settings ) || empty( $settings['api_key'] ) || empty( $settings['client_id'] ) ) {
return false;
}
$auth = [ 'api_key' => $settings['api_key'] ];

$wrap = new \CS_REST_Subscribers( $list_id, $auth );

$result = $wrap->unsubscribe( $email );

return [
'response' => $result->response,
'http_status_code' => $result->http_status_code,
];
}

/**
* Whether the provider manages from names.
*
* @return boolean
*/
public function provider_manages_from_names(): bool {
return false;
}

/**
* Whether or not the provider uses suppression lists.
*
* @return boolean
*/
public function uses_suppression_lists(): bool {
return false;
}

/**
* Gets the suppression lists.
*
* @return mixed
*/
public function get_suppression_lists(): mixed {
return [];
}
}
Loading