From ed57a66d6cd8eb52efee42a6ff471b91f3ae3b88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Droz?= Date: Mon, 20 Mar 2023 11:37:21 -0300 Subject: [PATCH] 5.2 --- change_log.txt | 23 +- class-gf-mailchimp.php | 566 ++++++++++++++++++++++++---- css/form_settings.css | 20 +- css/form_settings.min.css | 2 +- includes/class-gf-mailchimp-api.php | 100 +++-- mailchimp.php | 6 +- 6 files changed, 608 insertions(+), 109 deletions(-) diff --git a/change_log.txt b/change_log.txt index b2708aa..2c644fb 100644 --- a/change_log.txt +++ b/change_log.txt @@ -1,3 +1,24 @@ +### 5.2 | 2023-02-15 +- Added Gravity Forms license key to oAuth process. +- Added support for async (background) feed processing to improve form submission performance. +- Fixed an issue that prevents the Marketing Permission setting from being applied to users in certain situations. +- Fixed an issue where the opt-in email is not sending to already pending members when they resubscribe. + +### 5.1 | 2022-05-11 +- Fixed an issue where the save settings button isn't visible when creating or editing a feed. +- Fixed a display issue with some conditional logic feed settings. +- Fixed an issue where API calls are being made on all admin pages when checking for deprecated keys. + + +### 5.0 | 2021-09-29 +- Updated the authorization flow to connect to the Mailchimp API via Oauth. + + +### 4.9 | 2021-04-28 +- Fixed an issue where conditional logic is not correctly identifying matching selections when forms contain a multi-select field. +- Fixed an issue where the add-on icon is missing on the form settings screen in Gravity Forms 2.5. + + ### 4.8 | 2020-09-09 - Added support for Gravity Forms 2.5. - Fixed birthday merge fields no longer being sent in the correct format expected by the Mailchimp API. @@ -298,4 +319,4 @@ - Split logic from Feeds Add-On. - Implemented automatic upgrade. - Implemented list page checkboxes (for bulk actions). -- Implemented active/inactive icons on list page. \ No newline at end of file +- Implemented active/inactive icons on list page. diff --git a/class-gf-mailchimp.php b/class-gf-mailchimp.php index 588f972..0fc5856 100755 --- a/class-gf-mailchimp.php +++ b/class-gf-mailchimp.php @@ -17,6 +17,8 @@ */ class GFMailChimp extends GFFeedAddOn { + const POST_ACTION = 'gravityformsmailchimp_disconnect'; + /** * Contains an instance of this class, if available. * @@ -26,6 +28,15 @@ class GFMailChimp extends GFFeedAddOn { */ private static $_instance = null; + /** + * Enabling background feed processing. + * + * @since 5.1.1 + * + * @var bool + */ + protected $_async_feed_processing = true; + /** * Defines the version of the Mailchimp Add-On. * @@ -231,6 +242,23 @@ public function init() { } + /** + * Add actions for admin_init + * + * @since 4.10 + * + * @return void + */ + public function init_admin() { + parent::init_admin(); + + add_action( 'admin_init', array( $this, 'maybe_update_auth_creds' ) ); + if ( GFForms::is_gravity_page() ) { + add_action( 'admin_init', array( $this, 'warn_for_deprecated_key' ) ); + } + add_action( 'admin_post_' . self::POST_ACTION, array( $this, 'handle_disconnection' ) ); + } + /** * Remove unneeded settings. * @@ -260,11 +288,14 @@ public function styles() { $styles = array( array( - 'handle' => $this->_slug . '_form_settings', + 'handle' => 'gravityformsmailchimp_form_settings', 'src' => $this->get_base_url() . "/css/form_settings{$min}.css", 'version' => $this->_version, 'enqueue' => array( - array( 'admin_page' => array( 'form_settings' ) ), + array( + 'admin_page' => array( 'plugin_settings', 'form_settings' ), + 'tab' => $this->_slug, + ), ), ), ); @@ -282,13 +313,119 @@ public function styles() { */ public function get_menu_icon() { - return file_get_contents( $this->get_base_path() . '/images/menu-icon.svg' ); + return $this->is_gravityforms_supported( '2.5-beta-4' ) ? 'gform-icon--mailchimp' : 'dashicons-admin-generic'; + + } + + + // # OAUTH SETTINGS ----------------------------------------------------------------------------------------------- + + /** + * Get the authorization payload data. + * + * Returns the auth POST request if it's present, otherwise attempts to return a recent transient cache. + * + * @since 3.10 + * + * @return array + */ + private function get_oauth_payload() { + $payload = array_filter( + array( + 'auth_payload' => rgpost( 'auth_payload' ), + 'state' => rgpost( 'state' ), + 'auth_error' => rgpost( 'auth_error' ), + ) + ); + + if ( count( $payload ) === 2 || isset( $payload['auth_error'] ) ) { + return $payload; + } + + $payload = get_transient( "gravityapi_response_{$this->_slug}" ); + if ( ! is_array( $payload ) ) { + return array(); + } + + delete_transient( "gravityapi_response_{$this->_slug}" ); + + return $payload; } + /** + * Update Auth Creds if they have changed. + * + * @since 4.10 + * + * @return void + */ + public function maybe_update_auth_creds() { + $payload = $this->get_oauth_payload(); + // No payload, bail. + if ( empty( $payload ) ) { + return; + } + + // Auth Error form API - log and bail. + if ( isset( $payload['auth_error'] ) ) { + $this->add_error_notice( __METHOD__, 'error authenticating with the API Server' ); + + return; + } + + $state = $payload['state']; + $auth_payload = json_decode( $payload['auth_payload'], true ); + // State didn't pass our nonce - log and bail. + if ( $state !== get_transient( "gravityapi_request_{$this->_slug}" ) ) { + $this->add_error_notice( __METHOD__, 'could not verify the state value from the API Server.' ); + return; + } + + // Incorrect/missing auth data - log and bail. + if ( ! isset( $auth_payload['access_token'] ) || ! isset( $auth_payload['server_prefix'] ) ) { + $this->add_error_notice( __METHOD__, 'missing access_token or server_prefix in API response.' ); + + return; + } + + // Store the auth payload. + $this->update_plugin_settings( $auth_payload ); + } + + /** + * Add an error notice to admin if something goes awry. Also logs error to error_log. + * + * @since 4.10 + * + * @param string $method The method being called. + * @param string $message The message to display. + * + * @return void + */ + private function add_error_notice( $method, $message ) { + add_action( 'admin_notices', function () { + $message = __( 'Could not authenticate with Mailchimp.', 'gravityformsmailchimp' ); + + printf( '

%1$s

', esc_html( $message ) ); + } ); + + $this->log_error( $method . ': ' . $message ); + } + + /** + * Get the authentication state, which was created from a wp nonce. + * + * @since 4.10 + * + * @return string + */ + private function get_authentication_state_action() { + return 'gform_mailchimp_authentication_state'; + } // # PLUGIN SETTINGS ----------------------------------------------------------------------------------------------- @@ -304,19 +441,20 @@ public function plugin_settings_fields() { return array( array( - 'description' => '

' . - sprintf( - esc_html__( 'Mailchimp makes it easy to send email newsletters to your customers, manage your subscriber audiences, and track campaign performance. Use Gravity Forms to collect customer information and automatically add it to your Mailchimp subscriber audience. If you don\'t have a Mailchimp account, you can %1$ssign up for one here.%2$s', 'gravityformsmailchimp' ), - '', '' - ) - . '

', + // translators: %1 is an opening tag, and %2 is a closing tag. + 'description' => '

' . sprintf( esc_html__( 'Mailchimp makes it easy to send email newsletters to your customers, manage your subscriber audiences, and track campaign performance. Use Gravity Forms to collect customer information and automatically add it to your Mailchimp subscriber audience. If you don\'t have a Mailchimp account, you can %1$ssign up for one here.%2$s', 'gravityformsmailchimp' ), '', '' ) . '

', 'fields' => array( + array( - 'name' => 'apiKey', - 'label' => esc_html__( 'Mailchimp API Key', 'gravityformsmailchimp' ), - 'type' => 'text', - 'class' => 'medium', + 'name' => 'connection', + 'type' => 'html', 'feedback_callback' => array( $this, 'initialize_api' ), + 'html' => array( $this, 'render_connection_button' ), + 'callback' => array( $this, 'render_connection_button' ), + ), + array( + 'type' => 'save', + 'class' => 'hidden', ), ), ), @@ -324,8 +462,169 @@ public function plugin_settings_fields() { } + /** + * Render the Connection Button on the Settings Page. + * + * @since 4.10 + * + * @return string + */ + public function render_connection_button() { + $valid = $this->is_valid_connection(); + + if ( ! $valid ) { + $nonce = wp_create_nonce( $this->get_authentication_state_action() ); + $transient_name = 'gravityapi_request_' . $this->_slug; + if ( get_transient( $transient_name ) ) { + delete_transient( $transient_name ); + } + set_transient( $transient_name, $nonce, 10 * MINUTE_IN_SECONDS ); + } + + $before = $this->get_before_button_content( $valid ); + $button = $this->get_button_content( $valid ); + $after = $this->get_after_button_content( $valid ); + + if ( version_compare( GFForms::$version, '2.5', '<' ) ) { + echo $before . $button . $after; + + return; + } + + return $before . $button . $after; + } + + /** + * Get the markup to display before the connect button. + * + * @since 4.10 + * + * @param bool $valid Whether the current connection is valid. + * + * @return string + */ + private function get_before_button_content( $valid ) { + $html = ''; + + if ( ! $valid ) { + return ''; + } + + $account = $this->api->account_details(); + $name = isset( $account['account_name'] ) ? $account['account_name'] : false; + $html .= '

'; + + if ( $name ) { + $html .= esc_html__( 'Connected to Mailchimp as: ', 'gravityformsmailchimp' ); + $html .= esc_html( $name ) . '

'; + } else { + $html .= esc_html__( 'Connected to Mailchimp.', 'gravityformsmailchimp' ); + $html .= '

'; + } + + /** + * Allows third-party code to modify the HTML content which appears before the Connect button. + * + * @since 4.10 + * + * @param string $html The current HTML markup. + * @param bool $valid Whether the current API connection is valid (connected and using oAuth). + * + * @return string + */ + return apply_filters( 'gform_mailchimp_before_connect_button', $html, $valid ); + } + + /** + * Get the markup to display the connect button. + * + * @since 4.10 + * + * @param bool $valid Whether the current connection is valid. + * + * @return string + */ + private function get_button_content( $valid ) { + $html = sprintf( + '%2$s', + $valid ? $this->get_disconnect_url() : $this->get_connect_url(), + $valid ? __( 'Disconnect from Mailchimp', 'gravityformsmailchimp' ) : __( 'Connect to Mailchimp', 'gravityformsmailchimp' ), + '_self', + $valid ? 'gform-button--secondary' : 'gform-button--primary' + ); + + /** + * Allows third-party code to modify the Connect button HTML markup. + * + * @since 4.10 + * + * @param string $html The current button HTML markup. + * @param bool $valid Whether the current API connection is valid (connected and using oAuth). + * + * @return string + */ + return apply_filters( 'gform_mailchimp_connect_button', $html, $valid ); + } + + /** + * Get the markup to display after the connect button. + * + * @since 4.10 + * + * @param bool $valid Whether the current connection is valid. + * + * @return string + */ + private function get_after_button_content( $valid ) { + if ( ! $valid ) { + return ''; + } + + $html = '

'; + // translators: %1 is an opening tag, and %2 is a closing tag. + $html .= sprintf( __( 'In order to remove this site from your Mailchimp account, you\'ll need to remove it from your Mailchimp Account. %1$sLearn More.%2$s' ), '', '' ); + $html .= '

'; + + /** + * Allows third-party code to modify the HTML content which appears after the Connect button. + * + * @since 4.10 + * + * @param string $html The current HTML markup. + * @param bool $valid Whether the current API connection is valid (connected and using oAuth). + * + * @return string + */ + return apply_filters( 'gform_mailchimp_after_connect_button', $html, $valid ); + } + + /** + * Get the correct disconnect URL + * + * @since 4.10 + * + * @return string + */ + private function get_disconnect_url() { + return add_query_arg( array( 'action' => self::POST_ACTION ), admin_url( 'admin-post.php' ) ); + } + + /** + * Get the correct connect URL + * + * @since 4.10 + * + * @return string + */ + private function get_connect_url() { + $settings_url = urlencode( admin_url( 'admin.php?page=gf_settings&subview=' . $this->_slug ) ); + $connect_url = sprintf( '%1$s/auth/mailchimp', GRAVITY_API_URL ); + $nonce = wp_create_nonce( $this->get_authentication_state_action() ); + + return add_query_arg( array( 'redirect_to' => $settings_url, 'state' => $nonce, 'license' => GFCommon::get_key() ), $connect_url ); + } // # FEED SETTINGS ------------------------------------------------------------------------------------------------- @@ -417,11 +716,11 @@ public function feed_settings_fields() { ), ), array( - 'name' => 'tags', - 'type' => 'text', - 'class' => 'medium merge-tag-support mt-position-right mt-hide_all_fields', - 'label' => esc_html__( 'Tags', 'gravityformsmailchimp' ), - 'tooltip' => sprintf( + 'name' => 'tags', + 'type' => 'text', + 'class' => 'medium merge-tag-support mt-position-right mt-hide_all_fields', + 'label' => esc_html__( 'Tags', 'gravityformsmailchimp' ), + 'tooltip' => sprintf( '
%s
%s', esc_html__( 'Tags', 'gravityformsmailchimp' ), esc_html__( 'Associate tags to your Mailchimp contacts with a comma separated list (e.g. new lead, Gravity Forms, web source). Commas within a merge tag value will be created as a single tag.', 'gravityformsmailchimp' ) @@ -443,7 +742,10 @@ public function feed_settings_fields() { esc_html__( 'When conditional logic is enabled, form submissions will only be exported to Mailchimp when the conditions are met. When disabled all form submissions will be exported.', 'gravityformsmailchimp' ) ), ), - array( 'type' => 'save' ), + array( + 'type' => 'save', + 'dependency' => 'mailchimpList', + ), ), ), ); @@ -769,6 +1071,7 @@ public function settings_interest_categories( $field, $echo = true ) { // If no categories are found, return. if ( empty( $categories ) ) { $this->log_debug( __METHOD__ . '(): No categories found.' ); + return ''; } @@ -848,7 +1151,7 @@ public function interest_category_condition( $setting_name_root ) { $is_enabled = $this->get_setting( $condition_enabled_setting ) == '1'; $container_style = ! $is_enabled ? "style='display:none;'" : ''; - $str = "
" . + $str = "
" . esc_html__( 'Assign to group:', 'gravityformsmailchimp' ) . ' '; $str .= $this->settings_select( @@ -876,7 +1179,7 @@ public function interest_category_condition( $setting_name_root ) { $conditional_style = $decision == 'always' ? "style='display:none;'" : ''; - $str .= '
' . + $str .= '
' . $this->simple_condition( $setting_name_root, $is_enabled ) . '
' . @@ -982,7 +1285,7 @@ public function marketing_permissions_condition( $setting_name_root ) { $container_style = ! $is_enabled ? "style='display:none;'" : ''; $str = sprintf( - '
%s', + '
%s', $setting_name_root, $container_style, $setting_name_root, @@ -990,11 +1293,11 @@ public function marketing_permissions_condition( $setting_name_root ) { ); - $str .= '
' . - $this->simple_condition( $setting_name_root, $is_enabled ) . - '
' . + $str .= '
' . + $this->simple_condition( $setting_name_root, $is_enabled ) . + '
' . - '
'; + '
'; return $str; @@ -1127,6 +1430,7 @@ public function process_feed( $feed, $entry, $form ) { // If unable to initialize API, log error and return. if ( ! $this->initialize_api() ) { $this->add_feed_error( esc_html__( 'Unable to process feed because API could not be initialized.', 'gravityformsmailchimp' ), $feed, $entry, $form ); + return $entry; } @@ -1142,6 +1446,7 @@ public function process_feed( $feed, $entry, $form ) { // If email address is invalid, log error and return. if ( GFCommon::is_invalid_or_empty_email( $email ) ) { $this->add_feed_error( esc_html__( 'A valid Email address must be provided.', 'gravityformsmailchimp' ), $feed, $entry, $form ); + return $entry; } @@ -1204,7 +1509,7 @@ public function process_feed( $feed, $entry, $form ) { $field_value_timestamp = strtotime( $field_value ); // Format date. - switch( $date_format ) { + switch ( $date_format ) { case 'DD/MM': case 'MM/DD': @@ -1277,6 +1582,7 @@ public function process_feed( $feed, $entry, $form ) { // If member is unsubscribed and resubscription is not allowed, exit. if ( 'unsubscribed' == $member_status && ! $allow_resubscription ) { $this->log_debug( __METHOD__ . '(): User is unsubscribed and resubscription is not allowed.' ); + return; } @@ -1285,10 +1591,10 @@ public function process_feed( $feed, $entry, $form ) { * * @since 1.9 * - * @param bool $keep_existing_interests Should user keep existing interest categories? - * @param array $form The form object. - * @param array $entry The entry object. - * @param array $feed The feed object. + * @param bool $keep_existing_interests Should user keep existing interest categories? + * @param array $form The form object. + * @param array $entry The entry object. + * @param array $feed The feed object. */ $keep_existing_interests = gf_apply_filters( array( 'gform_mailchimp_keep_existing_groups', $form['id'] ), true, $form, $entry, $feed ); @@ -1349,7 +1655,7 @@ public function process_feed( $feed, $entry, $form ) { } // Get tags. - $tags = explode(',', rgars( $feed, 'meta/tags' ) ); + $tags = explode( ',', rgars( $feed, 'meta/tags' ) ); $tags = array_map( 'trim', $tags ); // Prepare tags. @@ -1428,6 +1734,28 @@ public function process_feed( $feed, $entry, $form ) { $subscription['email_address'] = $subscription['email']['email']; unset( $subscription['email'] ); + // If member exists, status is pending, and is double opt-in, then update member status to unsubscribed first. + if ( $member && rgar( $member, 'status' ) === 'pending' && rgars( $feed, 'meta/double_optin' ) ) { + try { + // Log that we are patching member status. + $this->log_debug( __METHOD__ . '(): Patching member status for opt-in.' ); + + // Update member status to unsubscribed. + $this->api->update_list_member( $list_id, $subscription['email_address'], array( 'status' => 'unsubscribed' ), 'PATCH' ); + + // Log that the subscription was successfully updated. + $this->log_debug( __METHOD__ . '(): Member status successfully updated.' ); + } catch ( Exception $e ) { + // Log that member status could not be updated. + $this->add_feed_error( sprintf( esc_html__( __METHOD__ . '(): Unable to update member status: %s', 'gravityformsmailchimp' ), $e->getMessage() ), $feed, $entry, $form ); + + // Log field errors. + if ( $e->hasErrors() ) { + $this->log_error( __METHOD__ . '(): Error when attempting to update member status: ' . print_r( $e->getErrors(), true ) ); + } + } + } + /** * Modify the subscription object before it is executed. * @@ -1486,7 +1814,7 @@ public function process_feed( $feed, $entry, $form ) { // Check condition and add to subscription. $subscription['marketing_permissions'][] = array( - 'marketing_permission_id' => $existing_permission['marketing_permission_id'], + 'marketing_permission_id' => (string) $existing_permission['marketing_permission_id'], 'enabled' => $this->is_marketing_permission_condition_met( $permissions[ $existing_permission['marketing_permission_id'] ], $form, $entry ), ); @@ -1499,7 +1827,7 @@ public function process_feed( $feed, $entry, $form ) { // Add to subscription. $subscription['marketing_permissions'][] = array( - 'marketing_permission_id' => $permission_id, + 'marketing_permission_id' => (string) $permission_id, 'enabled' => $this->is_marketing_permission_condition_met( $permission, $form, $entry ), ); @@ -1722,18 +2050,60 @@ public function maybe_override_field_value( $field_value, $form, $entry, $field_ } + // # HELPERS ------------------------------------------------------------------------------------------------------- + + /** + * Returns the currently saved plugin settings + * + * @since Unknown + * + * @return array|false + */ + public function get_plugin_settings() { + $settings = get_option( 'gravityformsaddon_' . $this->_slug . '_settings' ); + + if ( $this->is_connection_legacy() ) { + $settings['access_token'] = $settings['apiKey']; + $exploded_key = explode( '-', $settings['apiKey'] ); + $settings['server_prefix'] = isset( $exploded_key[1] ) ? $exploded_key[1] : 'us1'; + } + return $settings; + } + /** + * Determine whether a currently-existing connection to Mailchimp is using the legacy + * API Key paradigm. + * + * @since 4.10 + * + * @return bool + */ + private function is_connection_legacy() { + $settings = get_option( 'gravityformsaddon_' . $this->_slug . '_settings' ); - // # HELPERS ------------------------------------------------------------------------------------------------------- + return ( ! isset( $settings['access_token'] ) && isset( $settings['apiKey'] ) ); + } + + /** + * Determine if the current connection to Mailchimp is valid (it connects without error and + * uses OAuth instead of an API Key) + * + * @since 4.10 + * + * @return bool + */ + private function is_valid_connection() { + return $this->initialize_api() && ! $this->is_connection_legacy(); + } /** * Initializes Mailchimp API if credentials are valid. * * @since 4.0 - * @access public + * @since 4.10 - Deprecated API Key param. * - * @param string $api_key Mailchimp API key. + * @access public * * @uses GFAddOn::get_plugin_setting() * @uses GFAddOn::log_debug() @@ -1742,28 +2112,30 @@ public function maybe_override_field_value( $field_value, $form, $entry, $field_ * * @return bool|null */ - public function initialize_api( $api_key = null ) { + public function initialize_api( $deprecated = null ) { + + if ( ! empty( $deprecated ) ) { + _deprecated_argument( __METHOD__, '4.10' ); + } // If API is already initialized, return true. if ( ! is_null( $this->api ) ) { return true; } - // Get the API key. - if ( rgblank( $api_key ) ) { - $api_key = $this->get_plugin_setting( 'apiKey' ); - } - - // If the API key is blank, do not run a validation check. - if ( rgblank( $api_key ) ) { - return null; - } - // Log validation step. $this->log_debug( __METHOD__ . '(): Validating API Info.' ); + $this->maybe_update_auth_creds(); + + $settings = $this->get_plugin_settings(); + + if ( ! isset( $settings['access_token'] ) || ! isset( $settings['server_prefix'] ) ) { + return false; + } + // Setup a new Mailchimp object with the API credentials. - $mc = new GF_MailChimp_API( $api_key ); + $mc = new GF_MailChimp_API( $settings['access_token'], $settings['server_prefix'] ); try { @@ -1781,7 +2153,7 @@ public function initialize_api( $api_key = null ) { } catch ( Exception $e ) { // Log that authentication test failed. - $this->log_error( __METHOD__ . '(): Unable to authenticate with Mailchimp; '. $e->getMessage() ); + $this->log_error( __METHOD__ . '(): Unable to authenticate with Mailchimp; ' . $e->getMessage() ); return false; @@ -1842,7 +2214,7 @@ private function get_interest_categories( $list_id = null ) { /** * Get available marketing permissions for a list/audience. * - * @since 4.6 + * @since 4.6 * @access public * * @param string $list_id Mailchimp List/Audience ID. @@ -2025,40 +2397,44 @@ public function get_feed_setting_conditions( $feed, $name = 'interestCategory', * @return bool */ public function is_category_condition_met( $category, $form, $entry ) { - - if ( ! $category['enabled'] ) { - + if ( ! rgar( $category, 'enabled' ) ) { $this->log_debug( __METHOD__ . '(): Interest category not enabled. Returning false.' ); return false; + } - } else if ( $category['decision'] == 'always' ) { - + if ( rgar( $category, 'decision' ) == 'always' ) { $this->log_debug( __METHOD__ . '(): Interest category decision is always. Returning true.' ); return true; - } - $field = GFFormsModel::get_field( $form, $category['field'] ); + $category_field = rgar( $category, 'field' ); + $field = GFFormsModel::get_field( $form, $category_field ); if ( ! is_object( $field ) ) { - - $this->log_debug( __METHOD__ . "(): Field #{$category['field']} not found. Returning true." ); + $this->log_debug( __METHOD__ . "(): Field #{$category_field} not found. Returning true." ); return true; + } - } else { - - $field_value = GFFormsModel::get_lead_field_value( $entry, $field ); - $is_value_match = GFFormsModel::is_value_match( $field_value, $category['value'], $category['operator'] ); - - $this->log_debug( __METHOD__ . "(): Add to interest category if field #{$category['field']} value {$category['operator']} '{$category['value']}'. Is value match? " . var_export( $is_value_match, 1 ) ); + // Prepare values for field matching and log output. + $category_value = rgar( $category, 'value' ); + $category_operator = rgar( $category, 'operator' ); + $rule = array_merge( $category, array( 'fieldId' => $field->id ) ); - return $is_value_match; + // Check for the value match. + $is_value_match = GFFormsModel::is_value_match( + GFFormsModel::get_lead_field_value( $entry, $field ), + $category_value, + $category_operator, + $field, + $rule + ); - } + $this->log_debug( __METHOD__ . "(): Add to interest category if field #{$category_field} value {$category_operator} '{$category_value}'. Is value match? " . var_export( $is_value_match, 1 ) ); + return $is_value_match; } @@ -2078,6 +2454,7 @@ public function is_marketing_permission_condition_met( $permission, $form, $entr if ( ! $permission['enabled'] ) { $this->log_debug( __METHOD__ . '(): Marketing Permission not enabled. Returning false.' ); + return false; } @@ -2087,6 +2464,7 @@ public function is_marketing_permission_condition_met( $permission, $form, $entr if ( ! is_object( $field ) ) { $this->log_debug( __METHOD__ . "(): Field #{$permission['field']} not found. Returning true." ); + return true; } else { @@ -2286,6 +2664,7 @@ public function convert_groups_to_categories() { // If API cannot be initialized, exit. if ( ! $this->initialize_api() ) { $this->log_error( __METHOD__ . '(): Unable to convert Mailchimp groups to interest categories because API could not be initialized.' ); + return; } @@ -2669,7 +3048,7 @@ public function get_old_feeds() { * Retrieve the group setting key. * * @param string $grouping_id The group ID. - * @param string $group_name The group name. + * @param string $group_name The group name. * * @return string */ @@ -2693,4 +3072,53 @@ public function get_group_setting_key( $grouping_id, $group_name ) { return $plugin_settings[ $key ]; } + /** + * Add a warning if the current connection uses the (deprecated) API Key connection method. + * + * @since 4.10 + * + * @return void + */ + public function warn_for_deprecated_key() { + $api_key = $this->get_plugin_setting( 'apiKey' ); + if ( empty( $api_key ) ) { + return; + } + + $initialized = $this->initialize_api(); + + if ( ! $initialized ) { + return; + } + + add_action( + 'admin_notices', + function () { + $settings_url = admin_url( 'admin.php?page=gf_settings&subview=' . $this->_slug ); + + // translators: %1 is an opening tag, and %2 is a closing tag. + $message = sprintf( __( 'It looks like you\'re using an API Key to connect to Mailchimp. Please visit the %1$sMailchimp settings page%2$s in order to connect to the Mailchimp API.', 'gravityformsmailchimp' ), "", '' ); + + printf( '

%1$s

', $message ); + } + ); + + $this->log_error( __METHOD__ . ': user has API Key but has not connected to oAuth.' ); + } + + /** + * Removes the stored API settings when disconnecting. + * + * @since 4.10 + * + * @action admin_post_{self::POST_ACTION} + * + * @return void + */ + public function handle_disconnection() { + delete_option( 'gravityformsaddon_' . $this->_slug . '_settings' ); + $redirect_url = admin_url( 'admin.php?page=gf_settings&subview=' . $this->_slug ); + wp_safe_redirect( $redirect_url ); + } + } diff --git a/css/form_settings.css b/css/form_settings.css index a1e3400..3b60650 100644 --- a/css/form_settings.css +++ b/css/form_settings.css @@ -51,7 +51,15 @@ padding-top: .5rem; } -.condition_container > select { + +.gaddon-mailchimp-category .gform-settings-field__conditional-logic .gf_conditional_logic_rules_container, +.gaddon-mailchimp-permission .gform-settings-field__conditional-logic .gf_conditional_logic_rules_container { + align-items: center; + display: flex; + flex-direction: row; +} + +.condition_container select { width: auto !important; @@ -64,3 +72,13 @@ padding: .5rem 2.4375rem .5rem .8125rem; } + + #tab_gravityformsmailchimp #gaddon-setting-row-connection > th { + display: none; + } + + + + html[dir=rtl] .gform-settings-field__conditional-logic .gf_conditional_logic_rules_container select:first-child { + margin-left: 2px; + } diff --git a/css/form_settings.min.css b/css/form_settings.min.css index e437398..f09b732 100644 --- a/css/form_settings.min.css +++ b/css/form_settings.min.css @@ -1 +1 @@ -.gaddon-mailchimp-categoryname{font-weight:700}.gaddon-setting-checkbox{margin:5px 0 0 0}.gaddon-mailchimp-permission-toggle{margin-bottom:5px}.form-table .gaddon-mailchimp-category .gf_animate_sub_settings,.form-table .gaddon-mailchimp-permission .gf_animate_sub_settings{padding-left:10px}.gaddon-mailchimp-permission:not(:first-child){margin-top:5px}.gaddon-mailchimp-permission .gaddon-setting-checkbox:first-child{margin-top:0}.form-table .condition_container{margin-bottom:10px;margin-top:5px}.gaddon-mailchimp-permission .condition_label{display:inline-block;margin-bottom:5px}.form-table .gaddon-mailchimp-category{padding-bottom:12px}.gform-settings-panel__content .condition_container{padding-bottom:1rem}.gform-settings-panel__content .gaddon-mailchimp-permission .condition_container{margin-bottom:1rem;padding-top:.5rem}.condition_container>select{width:auto!important}.gform-settings-panel__content .condition_container>select{line-height:1.0625rem;margin-bottom:.5rem;padding:.5rem 2.4375rem .5rem .8125rem} \ No newline at end of file +.gaddon-mailchimp-categoryname{font-weight:700}.gaddon-setting-checkbox{margin:5px 0 0}.gaddon-mailchimp-permission-toggle{margin-bottom:5px}.form-table .gaddon-mailchimp-category .gf_animate_sub_settings,.form-table .gaddon-mailchimp-permission .gf_animate_sub_settings{padding-left:10px}.gaddon-mailchimp-permission:not(:first-child){margin-top:5px}.gaddon-mailchimp-permission .gaddon-setting-checkbox:first-child{margin-top:0}.form-table .condition_container{margin-bottom:10px;margin-top:5px}.gaddon-mailchimp-permission .condition_label{display:inline-block;margin-bottom:5px}.form-table .gaddon-mailchimp-category{padding-bottom:12px}.gform-settings-panel__content .condition_container{padding-bottom:1rem}.gform-settings-panel__content .gaddon-mailchimp-permission .condition_container{margin-bottom:1rem;padding-top:.5rem}.gaddon-mailchimp-category .gform-settings-field__conditional-logic .gf_conditional_logic_rules_container,.gaddon-mailchimp-permission .gform-settings-field__conditional-logic .gf_conditional_logic_rules_container{align-items:center;display:flex;flex-direction:row}.condition_container select{width:auto!important}.gform-settings-panel__content .condition_container>select{line-height:1.0625rem;margin-bottom:.5rem;padding:.5rem 2.4375rem .5rem .8125rem}#tab_gravityformsmailchimp #gaddon-setting-row-connection>th{display:none}html[dir=rtl] .gform-settings-field__conditional-logic .gf_conditional_logic_rules_container select:first-child{margin-left:2px} \ No newline at end of file diff --git a/includes/class-gf-mailchimp-api.php b/includes/class-gf-mailchimp-api.php index 27daae0..2e3ce3d 100644 --- a/includes/class-gf-mailchimp-api.php +++ b/includes/class-gf-mailchimp-api.php @@ -31,21 +31,20 @@ class GF_MailChimp_API { /** * Initialize API library. * - * @since 4.0 - * @access public + * @since 4.0 + * @since 4.10 - Transitioned to oAuth2 Connections with Access Tokens and Server Prefixes. * - * @param string $api_key (default: '') Mailchimp API key. + * @access public * - * @uses GF_MailChimp_API::set_data_center() + * @param string $access_token Mailchimp oAuth2 Access Token. + * @param string $server_prefix Mailchimp oAuth2 Server Prefix (Used as data center). */ - public function __construct( $api_key = '' ) { - - // Assign API key to object. - $this->api_key = $api_key; - - // Set data center. - $this->set_data_center(); + public function __construct( $access_token, $server_prefix = '' ) { + $this->api_key = $access_token; + if ( ! empty( $server_prefix ) ) { + $this->data_center = $server_prefix; + } } /** @@ -71,7 +70,7 @@ public function account_details() { * @since 4.6 * @access public * - * @param string $list_id Mailchimp list/audience ID. + * @param string $list_id Mailchimp list/audience ID. * @param string $email_address Email address. * * @uses GF_MailChimp_API::process_request() @@ -94,7 +93,7 @@ public function delete_list_member( $list_id, $email_address ) { * @since 4.0 * @access public * - * @param string $list_id Mailchimp list/audience ID. + * @param string $list_id Mailchimp list/audience ID. * @param string $category_id Interest category ID. * * @uses GF_MailChimp_API::process_request() @@ -171,7 +170,7 @@ public function get_list_interest_categories( $list_id ) { * @since 4.0 * @access public * - * @param string $list_id Mailchimp list/audience ID. + * @param string $list_id Mailchimp list/audience ID. * @param string $email_address Email address. * * @uses GF_MailChimp_API::process_request() @@ -195,7 +194,7 @@ public function get_list_member( $list_id, $email_address ) { * @access public * * @param string $list_id Mailchimp list/audience ID. - * @param array $options Additional settings. + * @param array $options Additional settings. * * @uses GF_MailChimp_API::process_request() * @@ -231,23 +230,29 @@ public function get_list_merge_fields( $list_id ) { * Add or update a Mailchimp list/audience member. * * @since 4.0 + * @since 5.2 - Add support for request method to allow PATCH requests. * @access public * - * @param string $list_id Mailchimp list/audience ID. + * @param string $list_id Mailchimp list/audience ID. * @param string $email_address Email address. - * @param array $subscription Subscription details. + * @param array $subscription Subscription details. + * @param string $method Request method. Defaults to PUT. * * @uses GF_MailChimp_API::process_request() * * @return array * @throws GF_MailChimp_Exception|Exception */ - public function update_list_member( $list_id, $email_address, $subscription ) { + public function update_list_member( $list_id, $email_address, $subscription, $method = 'PUT' ) { + // Make sure that method is either PUT or PATCH. + if ( ! in_array( $method, array( 'PUT', 'PATCH' ) ) ) { + throw Exception( __METHOD__ . '(): Method must be one of PUT or PATCH.' ); + } // Prepare subscriber hash. $subscriber_hash = md5( strtolower( $email_address ) ); - return $this->process_request( 'lists/' . $list_id . '/members/' . $subscriber_hash, $subscription, 'PUT' ); + return $this->process_request( 'lists/' . $list_id . '/members/' . $subscriber_hash, $subscription, $method ); } @@ -257,9 +262,9 @@ public function update_list_member( $list_id, $email_address, $subscription ) { * @since Unknown * @access public * - * @param string $list_id Mailchimp list/audience ID. + * @param string $list_id Mailchimp list/audience ID. * @param string $email_address Email address. - * @param array $tags Member tags. + * @param array $tags Member tags. * * @uses GF_MailChimp_API::process_request() * @@ -281,9 +286,9 @@ public function update_member_tags( $list_id, $email_address, $tags ) { * @since 4.0.10 * @access public * - * @param string $list_id Mailchimp list/audience ID. + * @param string $list_id Mailchimp list/audience ID. * @param string $email_address Email address. - * @param string $note The note to be added to the member. + * @param string $note The note to be added to the member. * * @uses GF_MailChimp_API::process_request() * @@ -305,9 +310,9 @@ public function add_member_note( $list_id, $email_address, $note ) { * @since 4.0 * @access private * - * @param string $path Request path. - * @param array $data Request data. - * @param string $method Request method. Defaults to GET. + * @param string $path Request path. + * @param array $data Request data. + * @param string $method Request method. Defaults to GET. * @param string $return_key Array key from response to return. Defaults to null (return full response). * * @throws GF_MailChimp_Exception|Exception If API request returns an error, exception is thrown. @@ -318,23 +323,30 @@ private function process_request( $path = '', $data = array(), $method = 'GET', // If API key is not set, throw exception. if ( rgblank( $this->api_key ) ) { - throw new Exception( 'API key must be defined to process an API request.' ); + throw new Exception( 'Access Token must be defined to process an API request.' ); } // Build base request URL. - $request_url = 'https://' . $this->data_center . '.api.mailchimp.com/3.0/' . $path; + $request_url = 'https://' . $this->get_data_center() . '.api.mailchimp.com/3.0/' . $path; // Add request URL parameters if needed. if ( 'GET' === $method && ! empty( $data ) ) { $request_url = add_query_arg( $data, $request_url ); } + $auth = 'Bearer ' . $this->api_key; + + // Deprecated API Key method detected - use that for auth to prevent breakage. + if ( $this->get_data_center_from_api_key() ) { + $auth = 'Basic ' . base64_encode( ':' . $this->api_key ); + } + // Build base request arguments. $args = array( - 'method' => $method, - 'headers' => array( + 'method' => $method, + 'headers' => array( 'Accept' => 'application/json', - 'Authorization' => 'Basic ' . base64_encode( ':' . $this->api_key ), + 'Authorization' => $auth, 'Content-Type' => 'application/json', ), /** @@ -364,7 +376,7 @@ private function process_request( $path = '', $data = array(), $method = 'GET', /** * Filters the Mailchimp request arguments. * - * @param array $args The request arguments sent to Mailchimp. + * @param array $args The request arguments sent to Mailchimp. * @param string $path The request path. * * @return array @@ -427,19 +439,39 @@ private function process_request( $path = '', $data = array(), $method = 'GET', * @since 4.0 * @access private */ - private function set_data_center() { + private function get_data_center() { // If API key is empty, return. if ( empty( $this->api_key ) ) { return; } + if ( ! empty( $this->data_center ) ) { + return $this->data_center; + } + + $data_center = $this->get_data_center_from_api_key(); + + return $data_center ? $data_center : 'us1'; + } + + private function get_data_center_from_api_key() { // Explode API key. $exploded_key = explode( '-', $this->api_key ); // Set data center from API key. - $this->data_center = isset( $exploded_key[1] ) ? $exploded_key[1] : 'us1'; + return isset( $exploded_key[1] ) ? $exploded_key[1] : false; + } + /** + * Get disconnect link. + * + * @since 4.10 + * + * @return string + */ + public function get_disconnect_url() { + return sprintf( 'https://%s.admin.mailchimp.com/account/api/', $this->data_center ); } } diff --git a/mailchimp.php b/mailchimp.php index e99465a..c5df8ef 100644 --- a/mailchimp.php +++ b/mailchimp.php @@ -9,7 +9,7 @@ Plugin Name: Gravity Forms Mailchimp Add-On Plugin URI: https://gravityforms.com Description: Integrates Gravity Forms with Mailchimp, allowing form submissions to be automatically sent to your Mailchimp account. -Version: 4.8 +Version: 5.2.0 Author: Gravity Forms Author URI: https://gravityforms.com License: GPL-2.0+ @@ -17,7 +17,7 @@ Domain Path: /languages ------------------------------------------------------------------------ -Copyright 2009-2020 rocketgenius +Copyright 2009-2023 Rocketgenius This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -34,7 +34,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA **/ -define( 'GF_MAILCHIMP_VERSION', '4.8' ); +define( 'GF_MAILCHIMP_VERSION', '5.2.0' ); // If Gravity Forms is loaded, bootstrap the Mailchimp Add-On. add_action( 'gform_loaded', array( 'GF_MailChimp_Bootstrap', 'load' ), 5 );