diff --git a/change_log.txt b/change_log.txt new file mode 100644 index 0000000..f2f031f --- /dev/null +++ b/change_log.txt @@ -0,0 +1,47 @@ +### 1.7 | 2022-08-09 +- Added the [gform_getresponse_limit_pre_get_campaigns](https://docs.gravityforms.com/gform_getresponse_limit_pre_get_campaigns/) filter allowing the maximum number of campaigns which are retrieved to be overridden. + + +### 1.6 | 2021-05-06 +- Added additional logging information for update and create contact API requests. +- Fixed an issue where the add-on icon is missing on the Form Settings page for Gravity Forms 2.5. +- Fixed an issue where contact is not created when Preventing IP Address Storage is enabled under Personal Data settings. + + +### 1.5 | 2020-09-03 +- Added support for Gravity Forms 2.5. +- Added the [gform_getresponse_limit_pre_get_custom_fields](https://docs.gravityforms.com/gform_getresponse_limit_pre_get_custom_fields/) filter allowing the maximum number of custom fields which are retrieved to be overridden. +- Added support for MAX (360/Enterprise) accounts using the .pl endpoint. +- Fixed an issue where values are not added to a multi_select type custom field when multiple choices are selected for the Multi Select field. + + +### 1.4 | 2020-03-19 +- Added translations for Hebrew, Hindi, Japanese, and Turkish. +- Added the [gform_getresponse_contact](https://docs.gravityforms.com/gform_getresponse_contact/) filter. +- Fixed a PHP 7.4 notice which occur when the API is initializing and the settings are empty. + + +### 1.3 | 2020-01-22 +- Added support for GetResponse API version 3. +- Added support for GetResponse Enterprise. +- Updated labels on the add-on settings page to reflect current GetResponse account types. + + +### 1.2 | 2018-04-02 +- Added security enhancements. +- Added GPL to plugin header. +- Updated Plugin URI and Author URI to use https. +- Fixed strings for translations. + + +### 1.1 | 2016-08-31 +- Added feed duplication support. +- Added support for delaying feed processing until payment by PayPal Standard is successfully completed. +- Added gf_getresponse() for easily getting a GetResponse instance. +- Added Spanish (es_ES) translation. +- Updated feed processing to abort if email is empty or invalid. +- Fixed fatal error when GetResponse API is loaded by another plugin. + + +### 1.0 | 2015-05-14 +- It's all new! diff --git a/class-gf-getresponse.php b/class-gf-getresponse.php new file mode 100644 index 0000000..5048a8e --- /dev/null +++ b/class-gf-getresponse.php @@ -0,0 +1,1416 @@ +add_delayed_payment_support( + array( + 'option_label' => esc_html__( 'Subscribe contact to GetResponse only when payment is received.', 'gravityformsgetresponse' ), + ) + ); + + } + + /** + * Return the plugin's icon for the plugin/form settings menu. + * + * @since 1.3 + * + * @return string + */ + public function get_menu_icon() { + + return $this->is_gravityforms_supported( '2.5-beta-4' ) ? 'gform-icon--get-response' : 'dashicons-admin-generic'; + + } + + + + + + // # PLUGIN SETTINGS ----------------------------------------------------------------------------------------------- + + /** + * Configures the settings which should be rendered on the add-on settings tab. + * + * @since 1.0 + * + * @return array + */ + public function plugin_settings_fields() { + + return array( + array( + 'title' => esc_html__( 'GetResponse Account Information', 'gravityformsgetresponse' ), + 'description' => $this->plugin_settings_description(), + 'fields' => array( + array( + 'name' => 'account_type', + 'label' => esc_html__( 'Account Type', 'gravityformsgetresponse' ), + 'type' => 'radio', + 'default_value' => 'standard', + 'onchange' => "jQuery( this ).parents( 'form' ).submit()", + 'horizontal' => true, + 'choices' => array( + array( + 'label' => esc_html__( 'Standard', 'gravityformsgetresponse' ), + 'value' => 'standard', + ), + array( + 'label' => esc_html__( 'MAX', 'gravityformsgetresponse' ), + 'value' => '360', + ), + ), + ), + array( + 'name' => 'api_key', + 'label' => esc_html__( 'API Key', 'gravityformsgetresponse' ), + 'type' => 'text', + 'class' => 'medium', + 'feedback_callback' => array( $this, 'initialize_api' ), + ), + array( + 'name' => 'domain', + 'label' => esc_html__( 'Domain', 'gravityformsgetresponse' ), + 'type' => 'text', + 'class' => 'medium', + 'dependency' => array( 'field' => 'account_type', 'values' => array( '360' ) ), + 'feedback_callback' => array( $this, 'initialize_api' ), + ), + array( + 'name' => 'max_tld', + 'label' => esc_html__( 'MAX Endpoint', 'gravityformsgetresponse' ), + 'type' => 'radio', + 'default_value' => '.com', + 'horizontal' => true, + 'choices' => array( + array( + 'label' => esc_html__( 'Standard (.com)', 'gravityformsgetresponse' ), + 'value' => '.com', + ), + array( + 'label' => esc_html__( 'Europe (.pl)', 'gravityformsgetresponse' ), + 'value' => '.pl', + ), + ), + 'dependency' => array( 'field' => 'account_type', 'values' => array( '360' ) ), + ), + array( + 'type' => 'save', + 'messages' => array( + 'success' => esc_html__( 'GetResponse settings have been updated.', 'gravityformsgetresponse' ), + ), + ), + ), + ), + ); + + } + + /** + * Prepare plugin settings description. + * + * @since 1.0 + * + * @return string + */ + public function plugin_settings_description() { + + // Prepare plugin description. + $description = sprintf( + '

%s

', + sprintf( + esc_html__( 'GetResponse makes it easy to send email newsletters to your customers, manage your subscriber lists, and track campaign performance. Use Gravity Forms to collect customer information and automatically add it to your GetResponse subscriber list. If you don\'t have a GetResponse account, you can %1$s sign up for one here.%2$s', 'gravityformsgetresponse' ), + '', '' + ) + ); + + // If API is not initialized, add instructions on how to retrieve API key. + if ( ! $this->initialize_api() ) { + + $description .= sprintf( + '

%s

', + sprintf( + esc_html__( 'Gravity Forms GetResponse Add-On requires your GetResponse API key, which can be found in the %1$sGetResponse API tab%2$s under your account details.', 'gravityformsgetresponse' ), + '', '' + ) + ); + + } + + return $description; + + } + + + + + + // # FEED SETTINGS ------------------------------------------------------------------------------------------------- + + /** + * Configures the settings which should be rendered on the feed edit page. + * + * @since 1.0 + * + * @return array + */ + public function feed_settings_fields() { + + return array( + array( + 'fields' => array( + array( + 'name' => 'feed_name', + 'label' => esc_html__( 'Name', 'gravityformsgetresponse' ), + 'type' => 'text', + 'class' => 'medium', + 'required' => true, + 'default_value' => $this->get_default_feed_name(), + 'tooltip' => sprintf( + '
%s
%s', + esc_html__( 'Name', 'gravityformsgetresponse' ), + esc_html__( 'Enter a feed name to uniquely identify this setup.', 'gravityformsgetresponse' ) + ), + ), + array( + 'name' => 'campaign', + 'label' => esc_html__( 'GetResponse Campaign', 'gravityformsgetresponse' ), + 'type' => 'select', + 'required' => true, + 'choices' => $this->get_campaigns_for_feed_setting(), + 'onchange' => "jQuery( this ).parents( 'form' ).submit();", + 'no_choices' => esc_html__( 'Please create a GetResponse Campaign to continue setup.', 'gravityformsgetresponse' ), + 'tooltip' => sprintf( + '
%s
%s', + esc_html__( 'GetResponse Campaign', 'gravityformsgetresponse' ), + esc_html__( 'Select which GetResponse campaign this feed will add contacts to.', 'gravityformsgetresponse' ) + ), + ), + array( + 'name' => 'fields', + 'label' => esc_html__( 'Map Fields', 'gravityformsgetresponse' ), + 'type' => 'field_map', + 'dependency' => 'campaign', + 'field_map' => $this->get_standard_fields_for_field_map(), + 'tooltip' => sprintf( + '
%s
%s', + esc_html__( 'Map Fields', 'gravityformsgetresponse' ), + esc_html__( 'Select which Gravity Form fields pair with their respective GetResponse field.', 'gravityformsgetresponse' ) + ), + ), + array( + 'name' => 'custom_fields', + 'label' => esc_html__( 'Custom Fields', 'gravityformsgetresponse' ), + 'type' => 'dynamic_field_map', + 'dependency' => 'campaign', + 'field_map' => $this->get_custom_fields_for_field_map(), + 'save_callback' => array( $this, 'save_custom_fields' ), + 'tooltip' => sprintf( + '
%s
%s', + esc_html__( 'Custom Fields', 'gravityformsgetresponse' ), + esc_html__( 'Select or create a new custom GetResponse field to pair with Gravity Forms fields. Custom field names can only contain up to 32 lowercase alphanumeric characters and underscores.', 'gravityformsgetresponse' ) + ), + ), + array( + 'name' => 'feed_condition', + 'label' => esc_html__( 'Conditional Logic', 'gravityformsgetresponse' ), + 'type' => 'feed_condition', + 'dependency' => 'campaign', + 'checkbox_label' => esc_html__( 'Enable', 'gravityformsgetresponse' ), + 'instructions' => esc_html__( 'Export to GetResponse if', 'gravityformsgetresponse' ), + 'tooltip' => sprintf( + '
%s
%s', + esc_html__( 'Conditional Logic', 'gravityformsgetresponse' ), + esc_html__( 'When conditional logic is enabled, form submissions will only be exported to GetResponse when the condition is met. When disabled, all form submissions will be exported.', 'gravityformsgetresponse' ) + ), + ), + array( + 'type' => 'save', + 'dependency' => 'campaign', + ), + ), + ), + ); + + } + + /** + * Prepare campaigns for feed field. + * + * @since 1.0 + * + * @return array + */ + public function get_campaigns_for_feed_setting() { + + // If GetResponse API instance is not initialized, return choices. + if ( ! $this->initialize_api() ) { + return array(); + } + + // Get GetResponse campaigns. + $campaigns = $this->get_campaigns(); + + // If campaigns could not be retrieved, return. + if ( is_wp_error( $campaigns ) || empty( $campaigns ) ) { + + // Log that campaigns could not be retrieved. + $this->log_error( __METHOD__ . '(): Could not retrieve campaigns;' . ( is_wp_error( $campaigns ) ? ' ' . $campaigns->get_error_message() : '' ) ); + + return array(); + + } + + // Initialize choices array. + $choices = array( + array( + 'label' => esc_html__( 'Select a Campaign', 'gravityformsgetresponse' ), + 'value' => '', + ), + ); + + // Loop through campaigns. + foreach ( $campaigns as $campaign ) { + + // Add campaign as choice. + $choices[] = array( + 'label' => esc_html( $campaign['name'] ), + 'value' => esc_attr( $campaign['campaignId'] ), + ); + + } + + GFCache::delete( $this->get_slug() . '_campaigns_' . $this->get_campaigns_limit() ); + + return $choices; + + } + + /** + * Prepare fields for feed field mapping. + * + * @since 1.0 + * + * @return array + */ + public function get_standard_fields_for_field_map() { + + return array( + array( + 'name' => 'name', + 'label' => esc_html__( 'Name', 'gravityformsgetresponse' ), + 'required' => true, + ), + array( + 'name' => 'email', + 'label' => esc_html__( 'Email Address', 'gravityformsgetresponse' ), + 'required' => true, + 'field_type' => array( 'email' ), + ), + ); + + } + + /** + * Renders and initializes a dynamic field map field based on the $field array whose choices are populated by the fields to be mapped. + * (Forked to refresh field map.) + * + * @since 1.3 + * + * @param array $field Field array containing the configuration options of this field. + * @param bool $echo Determines if field contents should automatically be displayed. Defaults to true. + * + * @return string + */ + public function settings_dynamic_field_map( $field, $echo = true ) { + + // If feed was saved, refresh field map. + if ( $this->is_save_postback() ) { + $field['field_map'] = $this->get_custom_fields_for_field_map(); + } + + return parent::settings_dynamic_field_map( $field, $echo ); + + } + + /** + * Prepare custom fields for feed field mapping. + * + * @since 1.0 + * + * @return array + */ + public function get_custom_fields_for_field_map() { + + // Initialize field map array. + $field_map = array( + array( + 'label' => esc_html__( 'Select a Custom Field', 'gravityformsgetresponse' ), + 'value' => '', + ), + ); + + // If GetResponse API instance is not initialized, return field map. + if ( ! $this->initialize_api() ) { + return $field_map; + } + + // Get GetResponse custom fields. + $custom_fields = $this->get_custom_fields(); + + // If custom fields could not be retrieved, return. + if ( is_wp_error( $custom_fields ) ) { + + // Log that custom fields could not be retrieved. + $this->log_error( __METHOD__ . '(): Could not retrieve custom fields; ' . $custom_fields->get_error_message() ); + + return $field_map; + + } + + // Mapped field types. + $mapped_field_types = array( 'text', 'textarea' ); + + // Get mapped fields. + $mapped_fields = $this->get_setting( 'custom_fields' ); + + // If fields are mapped, add currently mapped field types to array. + if ( $mapped_fields ) { + + // Get the mapped field IDs. + $field_ids = wp_list_pluck( $mapped_fields, 'key' ); + $field_ids = array_map( function( $key ) { return str_replace( 'custom_', '', $key ); }, $field_ids ); + + // Loop through custom fields. + foreach ( $custom_fields as $custom_field ) { + + // If this is not one of the mapped fields, skip. + if ( ! in_array( $custom_field['customFieldId'], $field_ids ) ) { + continue; + } + + // Add field type. + if ( ! in_array( $custom_field['type'], $mapped_field_types ) ) { + $mapped_field_types[] = $custom_field['type']; + } + + } + + } + + // Loop through custom fields. + foreach ( $custom_fields as $custom_field ) { + + // Add custom field to field map. + $field_map[] = array( + 'label' => esc_html( $custom_field['name'] ), + 'value' => 'custom_' . esc_attr( $custom_field['customFieldId'] ), + ); + + } + + return $field_map; + + } + + /** + * Create new GetResponse custom fields. + * + * @since 1.3 + * + * @param array $field Field settings. + * @param array $field_value Field value. + * + * @return array + */ + public function save_custom_fields( $field = array(), $field_value = array() ) { + + global $_gaddon_posted_settings; + + // If API is not initialized, return. + if ( ! $this->initialize_api() || empty( $field_value ) ) { + return $field_value; + } + + // Get existing GetResponse custom fields. + $custom_fields = $this->get_custom_fields(); + + // If custom fields could not be retrieved, return. + if ( is_wp_error( $custom_fields ) ) { + + // Log that existing fields could not be retrieved. + $this->log_error( __METHOD__ . '(): Unable to retrieve existing custom fields, not saving new custom fields; ' . $custom_fields->get_error_message() ); + + return $field_value; + + } + + // Get existing custom field names. + $custom_field_names = wp_list_pluck( $custom_fields, 'name' ); + + // Loop through custom fields; create new field if using custom key. + foreach ( $field_value as $i => $custom_field ) { + + // If this is not a new custom field, skip. + if ( 'gf_custom' !== $custom_field['key'] ) { + continue; + } + + // Prepare custom field name. + $field_name = trim( $custom_field['custom_key'] ); // Set shortcut name to custom key. + $field_name = str_replace( ' ', '_', $field_name ); // Remove all spaces. + $field_name = preg_replace( '([^\w\d])', '', $field_name ); // Strip all custom characters. + $field_name = strtolower( $field_name ); // Set to lowercase. + $field_name = substr( $field_name, 0, 32 ); // Limit field name to 32 characters. + + // Ensure field name is unique. + $field_name_i = 1; + $start_field_name = $field_name; + while ( in_array( $field_name, $custom_field_names ) ) { + $field_name = $start_field_name . $field_name_i; + $field_name_i++; + } + + // Prepare custom field object. + $field_object = array( + 'name' => $field_name, + 'type' => 'textarea', + 'hidden' => false, + 'values' => array(), + ); + + // Log field being created. + $this->log_debug( __METHOD__ . '(): Creating field: ' . print_r( $field_object, true ) ); + + // Create custom field. + $field_object = $this->api->create_custom_field( $field_object ); + + // If custom field could not be created, remove from field map. + if ( is_wp_error( $field_object ) ) { + + // Log that custom field could not be created. + $this->log_error( __METHOD__ . '(): Unable to create custom field; ' . $field_object->get_error_message() ); + + // Remove field. + unset( $field_value[ $i ], $_gaddon_posted_settings[ $field['name'] ][ $i ] ); + + continue; + + } + + // Update field value. + $field_value[ $i ]['key'] = 'custom_' . $field_object['customFieldId']; + $field_value[ $i ]['custom_key'] = ''; + + // Update posted settings. + $_gaddon_posted_settings[ $field['name'] ][ $i ]['key'] = 'custom_' . $field_object['customFieldId']; + $_gaddon_posted_settings[ $field['name'] ][ $i ]['custom_key'] = ''; + + } + + GFCache::delete( $this->get_slug() . '_custom_fields_' . $this->get_custom_fields_limit() ); + + return $field_value; + + } + + + + + + // # FEED LIST ----------------------------------------------------------------------------------------------------- + + /** + * Set feed creation control. + * + * @since 1.0 + * + * @return bool + */ + public function can_create_feed() { + + return $this->initialize_api(); + + } + + /** + * Enable feed duplication. + * + * @since 1.1 + * + * @param int|array $id The ID of the feed to be duplicated or the feed object when duplicating a form. + * + * @return bool + */ + public function can_duplicate_feed( $id ) { + + return true; + + } + + /** + * Configures which columns should be displayed on the feed list page. + * + * @since 1.0 + * + * @return array + */ + public function feed_list_columns() { + + return array( + 'feed_name' => esc_html__( 'Name', 'gravityformsgetresponse' ), + 'campaign' => esc_html__( 'GetResponse Campaign', 'gravityformsgetresponse' ), + ); + + } + + /** + * Returns the value to be displayed in the campaign name column. + * + * @since 1.0 + * + * @param array $feed Feed being displayed in the feed list. + * + * @return string + */ + public function get_column_value_campaign( $feed ) { + + // Get campaign ID. + $campaign_id = rgars( $feed, 'meta/campaign' ); + + // If GetResponse instance is not initialized, return campaign ID. + if ( ! $this->initialize_api() ) { + return $campaign_id; + } + + // Get campaign. + $campaign = $this->api->get_campaign( $campaign_id ); + + // If campaign could not be retrieved, return campaign ID. + if ( is_wp_error( $campaign ) ) { + + // Log that could not be found. + $this->log_error( __METHOD__ . '(): Unable to get campaign for feed list; ' . $campaign->get_error_message() ); + + return $campaign_id; + + } else { + + return esc_html( $campaign['name'] ); + + } + + } + + + + + + // # FEED PROCESSING ----------------------------------------------------------------------------------------------- + + /** + * Subscribe the user to the campaign. + * + * @since 1.0 + * + * @param array $feed Feed object. + * @param array $entry Entry object. + * @param array $form Form object. + * + * @return array + */ + public function process_feed( $feed, $entry, $form ) { + + // If API is not initialized, return error. + if ( ! $this->initialize_api() ) { + $this->add_feed_error( esc_html__( 'Unable to subscribe user to campaign because API was not initialized.', 'gravityformsgetresponse' ), $feed, $entry, $form ); + return $entry; + } + + // Initialize contact object. + $contact = array( + 'name' => $this->get_field_value( $form, $entry, $feed['meta']['fields_name'] ), + 'email' => $this->get_field_value( $form, $entry, $feed['meta']['fields_email'] ), + 'campaign' => array( 'campaignId' => $feed['meta']['campaign'] ), + 'customFieldValues' => array(), + 'ipAddress' => $this->get_field_value( $form, $entry, 'ip' ), + ); + + // If email address is invalid, return. + if ( GFCommon::is_invalid_or_empty_email( $contact['email'] ) ) { + $this->add_feed_error( esc_html__( 'Unable to subscribe user to campaign because an invalid or empty email address was provided.', 'gravityformsgetresponse' ), $feed, $entry, $form ); + return $entry; + } + + // If no name is provided, return. + if ( rgblank( $contact['name'] ) ) { + $this->add_feed_error( esc_html__( 'Unable to subscribe user to campaign because no name was provided.', 'gravityformsgetresponse' ), $feed, $entry, $form ); + return $entry; + } + + // If IP Address is empty, unset it. + if ( rgblank( $contact['ipAddress'] ) ) { + unset( $contact['ipAddress'] ); + } + + // Set custom field values. + $contact['customFieldValues'] = $this->prepare_custom_field_values( $feed, $entry, $form ); + + // Log that we are checking to see if contact already exists. + $this->log_debug( __METHOD__ . "(): Checking to see if {$contact['email']} is already on the list." ); + + // Get contact. + $existing_contact = $this->get_contact_by_email( $contact['email'], $contact['campaign']['campaignId'] ); + + if ( $existing_contact ) { + $this->log_debug( __METHOD__ . '(): Found existing contact; updating name, email address, custom fields.' ); + + // Set contact ID, custom fields. + $contact['contactId'] = $existing_contact['contactId']; + $contact['customFieldValues'] = rgar( $existing_contact, 'customFieldValues' ) && is_array( $existing_contact['customFieldValues'] ) ? array_merge( $existing_contact['customFieldValues'], $contact['customFieldValues'] ) : $contact['customFieldValues']; + } + + /** + * Allows the contact properties to be overridden before they are sent to GetResponse. + * + * @since 1.4 + * + * @param array $contact The contact properties. + * @param bool|array $existing_contact False or the existing contact properties. + * @param array $feed The feed currently being processed. + * @param array $entry The entry currently being processed. + * @param array $form The form currently being processed. + */ + $contact = gf_apply_filters( + array( + 'gform_getresponse_contact', + $form['id'], + ), + $contact, + $existing_contact, + $feed, + $entry, + $form + ); + + // If contact exists, updated it. Otherwise, create it. + if ( $existing_contact ) { + + // Log the contact to be updated. + $this->log_debug( __METHOD__ . '(): Contact that will be updated => ' . print_r( $contact, true ) ); + + // Update contact. + $updated_contact = $this->api->update_contact( $contact['contactId'], $contact ); + + // If contact could not be created, add feed error. + if ( is_wp_error( $updated_contact ) ) { + // Log that contact could not be created. + $error_message = $updated_contact->get_error_message(); + $error_data = $updated_contact->get_error_data(); + if ( $error_data ) { + $error_message .= ' ' . print_r( $error_data, true ); + } + + $this->add_feed_error( + sprintf( + // translators: Placeholder represents error message. + esc_html__( 'Unable to update existing contact: %s', 'gravityformsgetresponse' ), + $error_message + ), + $feed, + $entry, + $form + ); + } else { + // Log that contact was created. + $this->log_debug( __METHOD__ . '(): Contact was created.' ); + } + + return $entry; + } + + // Log the contact to be added. + $this->log_debug( __METHOD__ . '(): Contact to be added => ' . print_r( $contact, true ) ); + + // Add contact. + $created_contact = $this->api->create_contact( $contact ); + + // If contact could not be created, add feed error. + if ( is_wp_error( $created_contact ) ) { + + $error_message = $created_contact->get_error_message(); + $error_data = $created_contact->get_error_data(); + if ( $error_data ) { + $error_message .= ' ' . print_r( $error_data, true ); + } + + // Log that contact could not be created. + $this->add_feed_error( + sprintf( + // translators: Placeholder represents error message. + esc_html__( 'Unable to create contact: %s', 'gravityformsgetresponse' ), + $error_message + ), + $feed, + $entry, + $form + ); + + } else { + // Log that contact was created. + $this->log_debug( __METHOD__ . '(): Contact was created.' ); + } + + return $entry; + } + + /** + * Prepare custom field values for contact object. + * + * @since 1.3 + * + * @param array $feed Feed object. + * @param array $entry Entry object. + * @param array $form Form object. + * + * @return array + */ + private function prepare_custom_field_values( $feed, $entry, $form ) { + + // Initialize return array. + $values = array(); + + // Get custom fields map. + $custom_fields_map = self::get_dynamic_field_map_fields( $feed, 'custom_fields' ); + + // If no custom fields are mapped, return. + if ( empty( $custom_fields_map ) ) { + return $values; + } + + // Get custom fields. + $custom_fields = $this->get_custom_fields(); + + // If custom fields could not be retrieved, return values array. + if ( is_wp_error( $custom_fields ) ) { + + // Log that custom fields could not be retrieved. + $this->add_feed_error( sprintf( 'Unable to get custom fields; %s', $custom_fields->get_error_message() ), $feed, $entry, $form ); + + return $values; + + } + + // Update array keys. + foreach ( $custom_fields as $i => $custom_field ) { + $custom_fields[ $custom_field['customFieldId'] ] = $custom_field; + unset( $custom_fields[ $i ] ); + } + + // Loop through custom fields. + foreach ( $custom_fields_map as $field_name => $field_id ) { + + // If no field is paired to this key, skip it. + if ( rgblank( $field_name ) || rgblank( $field_id ) ) { + continue; + } + + // Get the field value. + $field_value = $this->get_field_value( $form, $entry, $field_id ); + + // If this field value is empty, skip it. + if ( rgblank( $field_value ) ) { + continue; + } + + // Strip "custom_" string from field name, get custom field. + $custom_field_id = substr( $field_name, 7 ); + $custom_field = rgar( $custom_fields, $custom_field_id, false ); + + // If custom field does not exist, skip. + if ( ! $custom_field ) { + continue; + } + + // Validate field value based on type. + switch ( $custom_field['type'] ) { + + case 'multi_select': + + $form_field = GFAPI::get_field( $form, $field_id ); + + if ( $form_field instanceof GF_Field_MultiSelect ) { + $field_value = $form_field->to_array( rgar( $entry, $field_id ) ); + } elseif ( ! is_array( $field_value ) ) { + $field_value = array( $field_value ); + } + + // If choices are not in list of custom field values, skip. + if ( $invalid = array_diff( $field_value, $custom_field['values'] ) ) { + $this->log_error( __METHOD__ . '(): Excluding field "' . $custom_field['name'] . '" (' . $custom_field_id . ') from contact because choices (' . implode( ', ', $invalid ) . ') are invalid.' ); + continue 2; + } + + break; + + case 'checkbox': + case 'country': + case 'currency': + case 'gender': + case 'radio': + case 'single_select': + + // If field value is not in list of custom field values, skip. + if ( ! in_array( $field_value, $custom_field['values'] ) ) { + $this->log_error( __METHOD__ . '(): Excluding value for field "' . $custom_field['name'] . '" (' . $custom_field_id . ') from contact because value "' . $field_value . '" is invalid.' ); + continue 2; + } + + break; + + case 'date': + case 'datetime': + + // Force date format. + $field_value = date( 'c', strtotime( $field_value ) ); + + break; + + case 'ip': + + // If field value is not a valid IP address, skip. + if ( ! filter_var( $field_value, FILTER_VALIDATE_IP ) ) { + $this->log_error( __METHOD__ . '(): Excluding value for field "' . $custom_field['name'] . '" (' . $custom_field_id . ') from contact because value "' . $field_value . '" is an invalid IP Address.' ); + continue 2; + } + + break; + + case 'number': + + // If field value is not numeric, skip. + if ( ! is_numeric( $field_value ) ) { + $this->log_error( __METHOD__ . '(): Excluding value for field "' . $custom_field['name'] . '" (' . $custom_field_id . ') from contact because value "' . $field_value . '" is not numeric.' ); + continue 2; + } + + break; + + case 'phone': + + // Get mapped form field. + $form_field = GFAPI::get_field( $form, $field_id ); + + // If this is not a Phone field or the phone format is not standard, skip. + if ( ! is_a( $form_field, 'GF_Field_Phone' ) || ( is_a( $form_field, 'GF_Field_Phone' ) && $form_field->phoneFormat !== 'standard' ) ) { + $this->log_error( __METHOD__ . '(): Excluding value for field "' . $custom_field['name'] . '" (' . $custom_field_id . ') from contact because it is not a Phone field whose format is standard.' ); + continue 2; + } + + // Reformat field value. + $field_value = preg_replace( '/[^0-9]/', '', $field_value ); + $field_value = '+1' . $field_value; + + break; + + case 'text': + case 'textarea': + + // If field value is too long, truncate. + if ( strlen( $field_value ) > 255 ) { + $this->log_debug( __METHOD__ . '(): Truncating value for field "' . $custom_field['name'] . '" (' . $custom_field_id . ') because length is more than 255 characters.' ); + $field_value = substr( $field_value, 0, 255 ); + } + + break; + + case 'url': + + // If field value is not a valid URL, skip. + if ( ! GFCommon::is_valid_url( $field_value ) ) { + $this->log_error( __METHOD__ . '(): Excluding value for field "' . $custom_field['name'] . '" (' . $custom_field_id . ') from contact because value "' . $field_value . '" is an invalid URL.' ); + continue 2; + } + + break; + + } + + // Add custom field to contact object. + $values[] = array( + 'customFieldId' => $custom_field_id, + 'value' => is_array( $field_value ) ? $field_value : array( $field_value ), + ); + + } + + return $values; + + } + + + + + // # HELPER METHODS ------------------------------------------------------------------------------------------------ + + /** + * Initializes GetResponse API if credentials are valid. + * + * @since 1.0 + * + * @return bool|null + */ + public function initialize_api() { + + // If API object is already setup, return true. + if ( ! is_null( $this->api ) ) { + return true; + } + + // Get the plugin settings. + $settings = $this->get_plugin_settings(); + + // If the API key is empty, return null. + if ( ! rgar( $settings, 'api_key' ) ) { + return null; + } + + // Load the GetResponse API library. + if ( ! class_exists( 'GF_GetResponse_API' ) ) { + require_once( 'includes/class-gf-getresponse-api.php' ); + } + + // Log that were testing the API credentials. + $this->log_debug( __METHOD__ . '(): Validating API credentials.' ); + + // Setup a new GetResponse API object. + $getresponse = new GF_GetResponse_API( $settings['api_key'], rgar( $settings, 'domain' ), rgar( $settings, 'max_tld' ) ); + + // Attempt to get account details. + $accounts = $getresponse->get_accounts(); + + if ( is_wp_error( $accounts ) ) { + + // Log that test failed. + $this->log_error( __METHOD__ . '(): API credentials are invalid; ' . $accounts->get_error_message() ); + + return false; + + } + + // Assign the GetResponse API object to this instance. + $this->api = $getresponse; + + // Log that test passed. + $this->log_debug( __METHOD__ . '(): API credentials are valid.' ); + + return true; + + } + + /** + * Find GetResponse contact by email address. + * + * @since 1.2 + * + * @param string $email Email address to search for. + * @param string $campaign_id Campaign ID to look for the email address in. + * + * @return array|bool + */ + public function get_contact_by_email( $email = '', $campaign_id = '' ) { + + // If API is not initialized, return. + if ( ! $this->initialize_api() ) { + return false; + } + + // Prepare search query. + $query = array( + urlencode( 'query[email]' ) => urlencode( $email ), + urlencode( 'query[campaignId]' ) => urlencode( $campaign_id ), + ); + + // Get contacts. + $contacts = $this->api->get_contacts( $query ); + + // If contacts could not be retrieved, return false. + if ( is_wp_error( $contacts ) ) { + + // Log that contacts could not be retrieved. + $this->log_error( __METHOD__ . '(): Unable to get contacts; ' . $contacts->get_error_message() ); + + return false; + + } + + // Loop through contacts. + foreach ( $contacts as $contact ) { + + // If this is the contact we are searching for, return it. + if ( $email === $contact['email'] ) { + return $contact; + } + + } + + return false; + + } + + /** + * Gets the GetResponse campaigns. + * + * @since 1.7 + * + * @return array|WP_Error + */ + public function get_campaigns() { + $limit = $this->get_campaigns_limit(); + $cache_key = $this->get_slug() . '_campaigns_' . $limit; + + $campaigns = GFCache::get( $cache_key ); + + if ( ! empty( $campaigns ) ) { + return $campaigns; + } + + $campaigns = $this->api->get_campaigns( $limit ); + + if ( is_wp_error( $campaigns ) ) { + return $campaigns; + } + + GFCache::set( $cache_key, $campaigns, true, HOUR_IN_SECONDS ); + + return $campaigns; + } + + /** + * Gets the maximum number of campaigns which should be retrieved. + * + * @since 1.7 + * + * @return int + */ + public function get_campaigns_limit() { + /** + * Allows the maximum number of campaigns which are retrieved to be overridden. + * + * @since 1.7 + * + * @param int $limit The campaigns limit. Defaults to 100. + */ + return (int) apply_filters( 'gform_getresponse_limit_pre_get_campaigns', 100 ); + } + + + + /** + * Gets the GetResponse custom fields. + * + * @since 1.5 + * + * @return array|WP_Error + */ + public function get_custom_fields() { + $limit = $this->get_custom_fields_limit(); + $cache_key = $this->get_slug() . '_custom_fields_' . $limit; + + $custom_fields = GFCache::get( $cache_key ); + + if ( ! empty( $custom_fields ) ) { + return $custom_fields; + } + + $custom_fields = $this->api->get_custom_fields( $limit ); + + if ( is_wp_error( $custom_fields ) ) { + return $custom_fields; + } + + GFCache::set( $cache_key, $custom_fields, true, HOUR_IN_SECONDS ); + + return $custom_fields; + } + + /** + * Gets the maximum number of custom fields which should be retrieved. + * + * @since 1.5 + * + * @return int + */ + public function get_custom_fields_limit() { + /** + * Allows the maximum number of custom fields which are retrieved to be overridden. + * + * @since 1.5 + * + * @param int $limit The custom fields limit. Defaults to 100. + */ + return (int) apply_filters( 'gform_getresponse_limit_pre_get_custom_fields', 100 ); + } + + + + // # UPGRADES ------------------------------------------------------------------------------------------------------ + + /** + * Run required routines when upgrading from previous versions of Add-On. + * + * @since 1.3 + * + * @param string $previous_version Previous version number. + */ + public function upgrade( $previous_version ) { + + // Determine if previous version is before API/360 upgrade. + $previous_is_pre_360 = ! empty( $previous_version ) && version_compare( $previous_version, '1.3', '<' ); + + // If previous version is not before the API/360 upgrade, exit. + if ( ! $previous_is_pre_360 ) { + return; + } + + // Get feeds. + $feeds = $this->get_feeds(); + + // If no feeds are configured, exit. + if ( empty( $feeds ) ) { + return; + } + + // If API is not initialize, exit. + if ( ! $this->initialize_api() ) { + $this->log_error( __METHOD__ . '(): Unable to upgrade feeds because API could not be initialized.' ); + return; + } + + // Get GetResponse custom fields. + $custom_fields = $this->get_custom_fields(); + + // If custom fields could not be retrieved, abort upgrade process. + if ( is_wp_error( $custom_fields ) ) { + + // Log that custom fields could not be retrieved. + $this->log_error( __METHOD__ . '(): Could not retrieve custom fields; ' . $custom_fields->get_error_message() ); + + return; + + } + + // Loop through feeds, update custom field map. + foreach ( $feeds as $feed ) { + + // If no custom fields are defined, skip feed. + if ( ! rgars( $feed, 'meta/custom_fields' ) ) { + continue; + } + + // Loop through custom field map, update keys. + foreach ( $feed['meta']['custom_fields'] as $i => $mapping ) { + + // If mapping is using a custom key, skip. + if ( 'gf_custom' === rgar( $mapping, 'key' ) ) { + continue; + } + + // Loop through custom fields, look for matching key. + foreach ( $custom_fields as $cf ) { + + // If custom field name does not match key, skip. + if ( $cf['name'] !== $mapping['key'] ) { + continue; + } + + // Update key. + $feed['meta']['custom_fields'][ $i ]['key'] = 'custom_' . $cf['customFieldId']; + + } + + } + + // Update feed. + $this->update_feed_meta( $feed['id'], $feed['meta'] ); + + } + + } + +} diff --git a/getresponse.php b/getresponse.php new file mode 100644 index 0000000..de6d083 --- /dev/null +++ b/getresponse.php @@ -0,0 +1,74 @@ +api_key = $api_key; + $this->domain = $domain; + + if ( $max_tld && $max_tld !== '.com' ) { + $this->api_url_360 = str_replace( '.com', $max_tld, $this->api_url_360 ); + } + + } + + + + + + // # ACCOUNT METHODS ----------------------------------------------------------------------------------------------- + + /** + * Get account details. + * + * @since 1.3 + * + * @return array|WP_Error + */ + public function get_accounts() { + + return $this->make_request( 'accounts' ); + + } + + + + + + // # CAMPAIGN METHODS ---------------------------------------------------------------------------------------------- + + /** + * Get individual campaign. + * + * @since 1.3 + * + * @param string $campaign_id Campaign ID. + * + * @return array|WP_Error + */ + public function get_campaign( $campaign_id = '' ) { + + return $this->make_request( 'campaigns/' . $campaign_id ); + + } + + /** + * Get campaigns for account. + * + * @since 1.3 + * @since 1.7 Added the limit param. + * + * @param int $limit The maximum number of campaigns which should be retrieved. Defaults to 100. + * + * @return array|WP_Error + */ + public function get_campaigns( $limit = 100 ) { + + $options = array(); + + if ( $limit !== 100 ) { + $options['perPage'] = $limit; + } + + return $this->make_request( 'campaigns', $options ); + + } + + + + + + // # CONTACT METHODS ----------------------------------------------------------------------------------------------- + + /** + * Create contact. + * + * @since 1.3 + * + * @param array $contact Contact object. + * + * @return array|WP_Error + */ + public function create_contact( $contact ) { + + return $this->make_request( 'contacts', $contact, 'POST', 202 ); + + } + + /** + * Get contacts. + * + * @since 1.3 + * + * @param array $query Search query. + * + * @return array|WP_Error + */ + public function get_contacts( $query ) { + + return $this->make_request( 'contacts', $query ); + + } + + /** + * Create contact. + * + * @since 1.3 + * + * @param string $contact_id Contact ID. + * @param array $contact Contact object. + * + * @return array|WP_Error + */ + public function update_contact( $contact_id, $contact ) { + + return $this->make_request( 'contacts/' . $contact_id, $contact, 'POST' ); + + } + + + + + + // # CUSTOM FIELDS METHODS ----------------------------------------------------------------------------------------- + + /** + * Create custom field. + * + * @since 1.3 + * + * @param array $custom_field Custom field object. + * + * @return array|WP_Error + */ + public function create_custom_field( $custom_field ) { + + return $this->make_request( 'custom-fields', $custom_field, 'POST', 201 ); + + } + + /** + * Get custom fields for account. + * + * @since 1.3 + * @since 1.5 Added the limit param. + * + * @param int $limit The maximum number of custom fields which should be retrieved. Defaults to 100. + * + * @return array|WP_Error + */ + public function get_custom_fields( $limit = 100 ) { + + $options = array(); + + if ( $limit !== 100 ) { + $options['perPage'] = $limit; + } + + return $this->make_request( 'custom-fields', $options ); + + } + + + + + + // # REQUEST METHODS ----------------------------------------------------------------------------------------------- + + /** + * Make API request. + * + * @since 1.3 + * @access private + * + * @param string $action Request action. + * @param array $options Request options. + * @param string $method HTTP method. Defaults to GET. + * @param int $response_code Expected HTTP response code. Defaults to 200. + * + * @return array|string|WP_Error + */ + private function make_request( $action, $options = array(), $method = 'GET', $response_code = 200 ) { + + // Prepare request URL. + $request_url = ( $this->domain ? $this->api_url_360 : $this->api_url ) . $action; + gf_getresponse()->log_debug( sprintf( '%s(): Sending request to the %s endpoint.', __METHOD__, $request_url ) ); + + // Add query parameters. + if ( 'GET' === $method ) { + $request_url = add_query_arg( $options, $request_url ); + } + + // Build request arguments. + $args = array( + 'method' => $method, + 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), + 'headers' => array( + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'X-Auth-Token' => 'api-key ' . $this->api_key, + ), + ); + + // Add account domain to request headers. + if ( $this->domain ) { + $args['headers']['X-Domain'] = $this->domain; + } + + // Add body to non-GET requests. + if ( 'GET' !== $method ) { + $args['body'] = json_encode( $options ); + } + + // Execute API request. + $result = wp_remote_request( $request_url, $args ); + + // If API request returns a WordPress error, return. + if ( is_wp_error( $result ) ) { + return $result; + } + + // Convert JSON response to array. + $response = wp_remote_retrieve_body( $result ); + $response = gf_getresponse()->maybe_decode_json( $response ); + + // If result response code is not the expected response code, return error. + if ( wp_remote_retrieve_response_code( $result ) !== $response_code && is_array( $response ) ) { + $wp_error = new WP_Error( $response['code'], $response['codeDescription'] ); + + if ( ! empty( $response['context'] ) ) { + $wp_error->add_data( $response['context'] ); + } + + return $wp_error; + } + + return $response; + + } + +} diff --git a/languages/gravityformsgetresponse.pot b/languages/gravityformsgetresponse.pot new file mode 100644 index 0000000..65ac023 --- /dev/null +++ b/languages/gravityformsgetresponse.pot @@ -0,0 +1,178 @@ +# Copyright (C) 2022 Gravity Forms +# This file is distributed under the GPL-2.0+. +msgid "" +msgstr "" +"Project-Id-Version: Gravity Forms GetResponse Add-On 1.7\n" +"Report-Msgid-Bugs-To: https://gravityforms.com/support\n" +"Last-Translator: Gravity Forms \n" +"Language-Team: Gravity Forms \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2022-08-09T17:12:57+00:00\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"X-Generator: WP-CLI 2.5.0\n" +"X-Domain: gravityformsgetresponse\n" + +#. Plugin Name of the plugin +msgid "Gravity Forms GetResponse Add-On" +msgstr "" + +#. Plugin URI of the plugin +#. Author URI of the plugin +msgid "https://gravityforms.com" +msgstr "" + +#. Description of the plugin +msgid "Integrates Gravity Forms with GetResponse, allowing form submissions to be automatically sent to your GetResponse account." +msgstr "" + +#. Author of the plugin +msgid "Gravity Forms" +msgstr "" + +#: class-gf-getresponse.php:182 +msgid "Subscribe contact to GetResponse only when payment is received." +msgstr "" + +#: class-gf-getresponse.php:218 +msgid "GetResponse Account Information" +msgstr "" + +#: class-gf-getresponse.php:223 +msgid "Account Type" +msgstr "" + +#: class-gf-getresponse.php:230 +msgid "Standard" +msgstr "" + +#: class-gf-getresponse.php:234 +msgid "MAX" +msgstr "" + +#: class-gf-getresponse.php:241 +msgid "API Key" +msgstr "" + +#: class-gf-getresponse.php:248 +msgid "Domain" +msgstr "" + +#: class-gf-getresponse.php:256 +msgid "MAX Endpoint" +msgstr "" + +#: class-gf-getresponse.php:262 +msgid "Standard (.com)" +msgstr "" + +#: class-gf-getresponse.php:266 +msgid "Europe (.pl)" +msgstr "" + +#: class-gf-getresponse.php:275 +msgid "GetResponse settings have been updated." +msgstr "" + +#: class-gf-getresponse.php:297 +msgid "GetResponse makes it easy to send email newsletters to your customers, manage your subscriber lists, and track campaign performance. Use Gravity Forms to collect customer information and automatically add it to your GetResponse subscriber list. If you don't have a GetResponse account, you can %1$s sign up for one here.%2$s" +msgstr "" + +#: class-gf-getresponse.php:308 +msgid "Gravity Forms GetResponse Add-On requires your GetResponse API key, which can be found in the %1$sGetResponse API tab%2$s under your account details." +msgstr "" + +#: class-gf-getresponse.php:339 +#: class-gf-getresponse.php:346 +#: class-gf-getresponse.php:476 +#: class-gf-getresponse.php:736 +msgid "Name" +msgstr "" + +#: class-gf-getresponse.php:347 +msgid "Enter a feed name to uniquely identify this setup." +msgstr "" + +#: class-gf-getresponse.php:352 +#: class-gf-getresponse.php:360 +#: class-gf-getresponse.php:737 +msgid "GetResponse Campaign" +msgstr "" + +#: class-gf-getresponse.php:357 +msgid "Please create a GetResponse Campaign to continue setup." +msgstr "" + +#: class-gf-getresponse.php:361 +msgid "Select which GetResponse campaign this feed will add contacts to." +msgstr "" + +#: class-gf-getresponse.php:366 +#: class-gf-getresponse.php:372 +msgid "Map Fields" +msgstr "" + +#: class-gf-getresponse.php:373 +msgid "Select which Gravity Form fields pair with their respective GetResponse field." +msgstr "" + +#: class-gf-getresponse.php:378 +#: class-gf-getresponse.php:385 +msgid "Custom Fields" +msgstr "" + +#: class-gf-getresponse.php:386 +msgid "Select or create a new custom GetResponse field to pair with Gravity Forms fields. Custom field names can only contain up to 32 lowercase alphanumeric characters and underscores." +msgstr "" + +#: class-gf-getresponse.php:391 +#: class-gf-getresponse.php:398 +msgid "Conditional Logic" +msgstr "" + +#: class-gf-getresponse.php:394 +msgid "Enable" +msgstr "" + +#: class-gf-getresponse.php:395 +msgid "Export to GetResponse if" +msgstr "" + +#: class-gf-getresponse.php:399 +msgid "When conditional logic is enabled, form submissions will only be exported to GetResponse when the condition is met. When disabled, all form submissions will be exported." +msgstr "" + +#: class-gf-getresponse.php:442 +msgid "Select a Campaign" +msgstr "" + +#: class-gf-getresponse.php:481 +msgid "Email Address" +msgstr "" + +#: class-gf-getresponse.php:523 +msgid "Select a Custom Field" +msgstr "" + +#: class-gf-getresponse.php:801 +msgid "Unable to subscribe user to campaign because API was not initialized." +msgstr "" + +#: class-gf-getresponse.php:816 +msgid "Unable to subscribe user to campaign because an invalid or empty email address was provided." +msgstr "" + +#: class-gf-getresponse.php:822 +msgid "Unable to subscribe user to campaign because no name was provided." +msgstr "" + +#. translators: Placeholder represents error message. +#: class-gf-getresponse.php:892 +msgid "Unable to update existing contact: %s" +msgstr "" + +#. translators: Placeholder represents error message. +#: class-gf-getresponse.php:926 +msgid "Unable to create contact: %s" +msgstr ""