diff --git a/assets/js/mailchimp.js b/assets/js/mailchimp.js
index 67e27a29..78e61513 100644
--- a/assets/js/mailchimp.js
+++ b/assets/js/mailchimp.js
@@ -38,6 +38,9 @@
// Change our submit type from HTML (default) to JS
$('.mc_submit_type').val('js');
+ // Remove the no JS field.
+ $('.mailchimp_sf_no_js').remove();
+
// Attach our form submitter action
$('.mc_signup_form').ajaxForm({
url: window.mailchimpSF.ajax_url,
diff --git a/includes/blocks/mailchimp/markup.php b/includes/blocks/mailchimp/markup.php
index 71e235a6..a7085dbd 100644
--- a/includes/blocks/mailchimp/markup.php
+++ b/includes/blocks/mailchimp/markup.php
@@ -225,6 +225,9 @@ function ( $single_list ) {
diff --git a/includes/class-mailchimp-block-form-submission.php b/includes/class-mailchimp-block-form-submission.php
deleted file mode 100644
index 3a203e6c..00000000
--- a/includes/class-mailchimp-block-form-submission.php
+++ /dev/null
@@ -1,310 +0,0 @@
- $list_id,
- 'update_existing' => $update_existing,
- 'double_opt_in' => $double_opt_in,
- )
- )
- );
-
- // Bail if the hash is invalid.
- if ( ! hash_equals( $expected, $hash ) ) {
- $msg = '
' . esc_html__( 'Invalid form submission.', 'mailchimp' ) . ' ';
- mailchimp_sf_global_msg( $msg );
- return false;
- }
-
- // Prepare request body
- $merge_fields = get_option( 'mailchimp_sf_merge_fields_' . $list_id, array() );
- $interest_groups = get_option( 'mailchimp_sf_interest_groups_' . $list_id, array() );
- $email = isset( $_POST['mc_mv_EMAIL'] ) ? wp_strip_all_tags( wp_unslash( $_POST['mc_mv_EMAIL'] ) ) : '';
- $merge_fields_body = $this->prepare_merge_fields_body( $merge_fields );
-
- // Catch errors and fail early.
- if ( is_wp_error( $merge_fields_body ) ) {
- $msg = '
' . $merge_fields_body->get_error_message() . ' ';
- mailchimp_sf_global_msg( $msg );
-
- return false;
- }
-
- $interest_groups = ! is_array( $interest_groups ) ? array() : $interest_groups;
- $groups = $this->prepare_groups_body( $interest_groups );
-
- // Clear out empty merge vars
- $merge_fields_body = mailchimp_sf_merge_remove_empty( $merge_fields_body );
- if ( isset( $_POST['email_type'] ) && in_array( $_POST['email_type'], array( 'text', 'html', 'mobile' ), true ) ) {
- $email_type = sanitize_text_field( wp_unslash( $_POST['email_type'] ) );
- } else {
- $email_type = 'html';
- }
-
- $api = mailchimp_sf_get_api();
- // If we don't have an API, then show an error message.
- if ( ! $api ) {
- $url = $this->get_signup_form_url( $list_id );
- $error = sprintf(
- '
%s ',
- wp_kses(
- sprintf(
- /* translators: 1: email address 2: url */
- __(
- 'We encountered a problem adding %1$s to the list. Please
sign up here. ',
- 'mailchimp'
- ),
- esc_html( $email ),
- esc_url( $url )
- ),
- [
- 'a' => [
- 'href' => [],
- ],
- ]
- )
- );
- mailchimp_sf_global_msg( $error );
- return false;
- }
-
- $url = 'lists/' . $list_id . '/members/' . md5( strtolower( $email ) );
- $status = mailchimp_sf_check_status( $url );
-
- // If update existing is turned off and the subscriber is not new, error out.
- $is_new_subscriber = false === $status;
- if ( 'yes' !== $update_existing && ! $is_new_subscriber ) {
- $msg = esc_html__( 'This email address has already been subscribed to this list.', 'mailchimp' );
- $error = new WP_Error( 'mailchimp-update-existing', $msg );
- mailchimp_sf_global_msg( '
' . $msg . ' ' );
- return false;
- }
-
- $request_body = mailchimp_sf_subscribe_body( $merge_fields_body, $groups, $email_type, $email, $status, 'yes' === $double_opt_in );
- $response = $api->post( $url, $request_body, 'PUT', $list_id );
-
- // If we have errors, then show them
- if ( is_wp_error( $response ) ) {
- $msg = '
' . $response->get_error_message() . ' ';
- mailchimp_sf_global_msg( $msg );
- return false;
- }
-
- if ( 'subscribed' === $response['status'] ) {
- $esc = esc_html__( 'Success, you\'ve been signed up.', 'mailchimp' );
- $msg = "
{$esc} ";
- } else {
- $esc = esc_html__( 'Success, you\'ve been signed up! Please look for our confirmation email.', 'mailchimp' );
- $msg = "
{$esc} ";
- }
-
- // Set our global message
- mailchimp_sf_global_msg( $msg );
- return true;
- }
-
- /**
- * Prepare the merge fields body for the API request.
- *
- * @param array $merge_fields Merge fields.
- * @return stdClass|WP_Error
- */
- protected function prepare_merge_fields_body( $merge_fields ) {
- // Loop through our Merge Vars, and if they're empty, but required, then print an error, and mark as failed
- $merge = new stdClass();
- foreach ( $merge_fields as $merge_field ) {
- $tag = $merge_field['tag'];
- $opt = 'mc_mv_' . $tag;
-
- // Skip if the field is not required and not submitted.
- if ( 'Y' !== $merge_field['required'] && ! isset( $_POST[ $opt ] ) ) {
- continue;
- }
-
- $opt_val = isset( $_POST[ $opt ] ) ? map_deep( stripslashes_deep( $_POST[ $opt ] ), 'sanitize_text_field' ) : '';
-
- switch ( $merge_field['type'] ) {
- /**
- * US Phone validation
- *
- * - Merge field is phone
- * - Phone format is set in Mailchimp account
- * - Phone format is US in Mailchimp account
- */
- case 'phone':
- if (
- isset( $merge_field['options']['phone_format'] )
- && 'US' === $merge_field['options']['phone_format']
- ) {
- $opt_val = mailchimp_sf_merge_validate_phone( $opt_val, $merge_field );
- if ( is_wp_error( $opt_val ) ) {
- return $opt_val;
- }
- }
- break;
-
- /**
- * Address validation
- *
- * - Merge field is address
- * - Merge field is an array (address contains multiple
elements)
- */
- case 'address':
- if ( is_array( $opt_val ) ) {
- $validate = mailchimp_sf_merge_validate_address( $opt_val, $merge_field );
- if ( is_wp_error( $validate ) ) {
- return $validate;
- }
-
- if ( $validate ) {
- $merge->$tag = $validate;
- }
- }
- break;
-
- /**
- * Handle generic array values
- *
- * Not sure what this does or is for
- *
- * - Merge field is an array, not specifically phone or address
- */
- default:
- if ( is_array( $opt_val ) ) {
- $keys = array_keys( $opt_val );
- $val = new stdClass();
- foreach ( $keys as $key ) {
- $val->$key = $opt_val[ $key ];
- }
- $opt_val = $val;
- }
- break;
- }
-
- /**
- * Required fields
- *
- * If the field is required and empty, return an error
- */
- if ( 'Y' === $merge_field['required'] && trim( $opt_val ) === '' ) {
- /* translators: %s: field name */
- $message = sprintf( esc_html__( 'You must fill in %s.', 'mailchimp' ), esc_html( $merge_field['name'] ) );
- $error = new WP_Error( 'missing_required_field', $message );
- return $error;
- } elseif ( 'EMAIL' !== $tag ) {
- $merge->$tag = $opt_val;
- }
- }
- return $merge;
- }
-
- /**
- * Prepare the interest groups body for the API request.
- *
- * @param array $interest_groups Interest groups.
- * @return stdClass
- */
- protected function prepare_groups_body( $interest_groups ) {
- // Bail if we don't have any interest groups
- if ( empty( $interest_groups ) ) {
- return new StdClass();
- }
-
- $groups = $this->set_all_groups_to_false( $interest_groups );
-
- foreach ( $interest_groups as $interest_group ) {
- $ig_id = $interest_group['id'];
- if ( isset( $_POST['group'][ $ig_id ] ) && 'hidden' !== $interest_group['type'] ) {
- switch ( $interest_group['type'] ) {
- case 'dropdown':
- case 'radio':
- // there can only be one value submitted for radio/dropdowns, so use that at the group id.
- if ( isset( $_POST['group'][ $ig_id ] ) && ! empty( $_POST['group'][ $ig_id ] ) ) {
- $value = sanitize_text_field( wp_unslash( $_POST['group'][ $ig_id ] ) );
- $groups->$value = true;
- }
- break;
- case 'checkboxes':
- if ( isset( $_POST['group'][ $ig_id ] ) ) {
- $ig_ids = array_map(
- 'sanitize_text_field',
- array_keys(
- stripslashes_deep( $_POST['group'][ $ig_id ] ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- ignoring becuase this is sanitized through array_map above
- )
- );
- foreach ( $ig_ids as $id ) {
- $groups->$id = true;
- }
- }
- break;
- default:
- // Nothing
- break;
- }
- }
- }
- return $groups;
- }
-
- /**
- * Set all interest groups to false.
- *
- * @param array $interest_groups Interest groups.
- * @return stdClass
- */
- protected function set_all_groups_to_false( $interest_groups ) {
- $groups = new StdClass();
-
- foreach ( $interest_groups as $interest_group ) {
- if ( 'hidden' !== $interest_group['type'] ) {
- foreach ( $interest_group['groups'] as $group ) {
- $id = $group['id'];
- $groups->$id = false;
- }
- }
- }
-
- return $groups;
- }
-
- /**
- * Get signup form URL for the Mailchimp list.
- *
- * @param string $list_id The list ID.
- * @return string
- */
- protected function get_signup_form_url( $list_id ) {
- $dc = get_option( 'mc_datacenter' );
- $user = get_option( 'mc_user' );
- $url = 'https://' . $dc . '.list-manage.com/subscribe?u=' . $user['account_id'] . '&id=' . $list_id;
- return $url;
- }
-}
diff --git a/includes/class-mailchimp-form-submission.php b/includes/class-mailchimp-form-submission.php
new file mode 100644
index 00000000..337eca2f
--- /dev/null
+++ b/includes/class-mailchimp-form-submission.php
@@ -0,0 +1,513 @@
+handle_form_submission();
+ $submit_type = isset( $_POST['mc_submit_type'] ) ? sanitize_text_field( wp_unslash( $_POST['mc_submit_type'] ) ) : '';
+
+ // If we have an error, then show it.
+ if ( is_wp_error( $response ) ) {
+ $error = $response->get_error_message();
+ mailchimp_sf_global_msg( '
' . $error . ' ' );
+ } else {
+ mailchimp_sf_global_msg( '
' . esc_html( $response ) . ' ' );
+ }
+
+ // Do a different action for html vs. js
+ switch ( $submit_type ) {
+ case 'html':
+ /* This gets set elsewhere! */
+ break;
+ case 'js':
+ if ( ! headers_sent() ) { // just in case...
+ header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT', true, 200 );
+ }
+ // TODO: Refactor this to use JSON response instead of setting a global message.
+ echo wp_kses_post( mailchimp_sf_global_msg() );
+ exit;
+ }
+ }
+
+ /**
+ * Handles the form submission for the Mailchimp form.
+ *
+ * @return string|WP_Error Success message or error.
+ */
+ public function handle_form_submission() {
+ $is_valid = $this->validate_form_submission();
+ if ( is_wp_error( $is_valid ) || ! $is_valid ) {
+ if ( is_wp_error( $is_valid ) ) {
+ return $is_valid;
+ }
+
+ // If the form submission is invalid, return an error.
+ return new WP_Error( 'mailchimp-invalid-form', esc_html__( 'Invalid form submission.', 'mailchimp' ) );
+ }
+
+ $list_id = get_option( 'mc_list_id' );
+ $update_existing = get_option( 'mc_update_existing' );
+ $double_opt_in = get_option( 'mc_double_optin' );
+ $merge_fields = get_option( 'mc_merge_vars', array() );
+ $interest_groups = get_option( 'mc_interest_groups', array() );
+
+ // Check if request from latest block.
+ if ( isset( $_POST['mailchimp_sf_list_id'] ) ) {
+ $list_id = isset( $_POST['mailchimp_sf_list_id'] ) ? sanitize_text_field( wp_unslash( $_POST['mailchimp_sf_list_id'] ) ) : '';
+ $update_existing = isset( $_POST['mailchimp_sf_update_existing_subscribers'] ) ? sanitize_text_field( wp_unslash( $_POST['mailchimp_sf_update_existing_subscribers'] ) ) : '';
+ $double_opt_in = isset( $_POST['mailchimp_sf_double_opt_in'] ) ? sanitize_text_field( wp_unslash( $_POST['mailchimp_sf_double_opt_in'] ) ) : '';
+ $hash = isset( $_POST['mailchimp_sf_hash'] ) ? sanitize_text_field( wp_unslash( $_POST['mailchimp_sf_hash'] ) ) : '';
+ $expected = wp_hash(
+ serialize( // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
+ array(
+ 'list_id' => $list_id,
+ 'update_existing' => $update_existing,
+ 'double_opt_in' => $double_opt_in,
+ )
+ )
+ );
+
+ // Bail if the hash is invalid.
+ if ( ! hash_equals( $expected, $hash ) ) {
+ return new WP_Error( 'mailchimp-invalid-hash', esc_html__( 'Invalid form submission.', 'mailchimp' ) );
+ }
+
+ $update_existing = 'yes' === $update_existing;
+ $double_opt_in = 'yes' === $double_opt_in;
+ $merge_fields = get_option( 'mailchimp_sf_merge_fields_' . $list_id, array() );
+ $interest_groups = get_option( 'mailchimp_sf_interest_groups_' . $list_id, array() );
+ }
+
+ // Prepare request body
+ $email = isset( $_POST['mc_mv_EMAIL'] ) ? wp_strip_all_tags( wp_unslash( $_POST['mc_mv_EMAIL'] ) ) : '';
+ $merge_fields_body = $this->prepare_merge_fields_body( $merge_fields );
+
+ // Catch errors and fail early.
+ if ( is_wp_error( $merge_fields_body ) ) {
+ return $merge_fields_body;
+ }
+
+ $interest_groups = ! is_array( $interest_groups ) ? array() : $interest_groups;
+ $groups = $this->prepare_groups_body( $interest_groups );
+
+ // Clear out empty merge fields.
+ $merge_fields_body = $this->remove_empty_merge_fields( $merge_fields_body );
+ if ( isset( $_POST['email_type'] ) && in_array( $_POST['email_type'], array( 'text', 'html', 'mobile' ), true ) ) {
+ $email_type = sanitize_text_field( wp_unslash( $_POST['email_type'] ) );
+ } else {
+ $email_type = 'html';
+ }
+
+ $response = $this->subscribe_to_list(
+ $list_id,
+ $email,
+ array(
+ 'email_type' => $email_type,
+ 'merge_fields' => $merge_fields_body,
+ 'interests' => $groups,
+ 'update_existing' => $update_existing,
+ 'double_opt_in' => $double_opt_in,
+ )
+ );
+
+ // If we have errors, then show them
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ $message = '';
+ if ( 'subscribed' === $response['status'] ) {
+ $message = __( 'Success, you\'ve been signed up.', 'mailchimp' );
+ } else {
+ $message = __( 'Success, you\'ve been signed up! Please look for our confirmation email.', 'mailchimp' );
+ }
+
+ // Return success message.
+ return $message;
+ }
+
+ /**
+ * Prepare the merge fields body for the API request.
+ *
+ * @param array $merge_fields Merge fields.
+ * @return stdClass|WP_Error
+ */
+ public function prepare_merge_fields_body( $merge_fields ) {
+ // Loop through our merge fields, and if they're empty, but required, then print an error, and mark as failed
+ $merge = new stdClass();
+ foreach ( $merge_fields as $merge_field ) {
+ $tag = $merge_field['tag'];
+ $opt = 'mc_mv_' . $tag;
+
+ // Skip if the field is not required and not submitted.
+ if ( 'Y' !== $merge_field['required'] && ! isset( $_POST[ $opt ] ) ) {
+ continue;
+ }
+
+ $opt_val = isset( $_POST[ $opt ] ) ? map_deep( stripslashes_deep( $_POST[ $opt ] ), 'sanitize_text_field' ) : '';
+
+ switch ( $merge_field['type'] ) {
+ /**
+ * US Phone validation
+ *
+ * - Merge field is phone
+ * - Phone format is set in Mailchimp account
+ * - Phone format is US in Mailchimp account
+ */
+ case 'phone':
+ if (
+ isset( $merge_field['options']['phone_format'] )
+ && 'US' === $merge_field['options']['phone_format']
+ ) {
+ $opt_val = mailchimp_sf_merge_validate_phone( $opt_val, $merge_field );
+ if ( is_wp_error( $opt_val ) ) {
+ return $opt_val;
+ }
+ }
+ break;
+
+ /**
+ * Address validation
+ *
+ * - Merge field is address
+ * - Merge field is an array (address contains multiple
elements)
+ */
+ case 'address':
+ if ( is_array( $opt_val ) ) {
+ $validate = mailchimp_sf_merge_validate_address( $opt_val, $merge_field );
+ if ( is_wp_error( $validate ) ) {
+ return $validate;
+ }
+
+ if ( $validate ) {
+ $merge->$tag = $validate;
+ }
+ }
+ break;
+
+ /**
+ * Handle generic array values
+ *
+ * Not sure what this does or is for
+ *
+ * - Merge field is an array, not specifically phone or address
+ */
+ default:
+ if ( is_array( $opt_val ) ) {
+ $keys = array_keys( $opt_val );
+ $val = new stdClass();
+ foreach ( $keys as $key ) {
+ $val->$key = $opt_val[ $key ];
+ }
+ $opt_val = $val;
+ }
+ break;
+ }
+
+ /**
+ * Required fields
+ *
+ * If the field is required and empty, return an error
+ */
+ if ( 'Y' === $merge_field['required'] && trim( $opt_val ) === '' ) {
+ /* translators: %s: field name */
+ $message = sprintf( esc_html__( 'You must fill in %s.', 'mailchimp' ), esc_html( $merge_field['name'] ) );
+ $error = new WP_Error( 'missing_required_field', $message );
+ return $error;
+ } elseif ( 'EMAIL' !== $tag ) {
+ $merge->$tag = $opt_val;
+ }
+ }
+ return $merge;
+ }
+
+ /**
+ * Prepare the interest groups body for the API request.
+ *
+ * @param array $interest_groups Interest groups.
+ * @return stdClass
+ */
+ public function prepare_groups_body( $interest_groups ) {
+ // Bail if we don't have any interest groups
+ if ( empty( $interest_groups ) ) {
+ return new stdClass();
+ }
+
+ $groups = $this->set_all_groups_to_false( $interest_groups );
+
+ foreach ( $interest_groups as $interest_group ) {
+ $ig_id = $interest_group['id'];
+ if ( isset( $_POST['group'][ $ig_id ] ) && 'hidden' !== $interest_group['type'] ) {
+ switch ( $interest_group['type'] ) {
+ case 'dropdown':
+ case 'radio':
+ // there can only be one value submitted for radio/dropdowns, so use that at the group id.
+ if ( isset( $_POST['group'][ $ig_id ] ) && ! empty( $_POST['group'][ $ig_id ] ) ) {
+ $value = sanitize_text_field( wp_unslash( $_POST['group'][ $ig_id ] ) );
+ $groups->$value = true;
+ }
+ break;
+ case 'checkboxes':
+ if ( isset( $_POST['group'][ $ig_id ] ) ) {
+ $ig_ids = array_map(
+ 'sanitize_text_field',
+ array_keys(
+ stripslashes_deep( $_POST['group'][ $ig_id ] ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- ignoring because this is sanitized through array_map above
+ )
+ );
+ foreach ( $ig_ids as $id ) {
+ $groups->$id = true;
+ }
+ }
+ break;
+ default:
+ // Nothing
+ break;
+ }
+ }
+ }
+ return $groups;
+ }
+
+ /**
+ * Set all interest groups to false.
+ *
+ * @param array $interest_groups Interest groups.
+ * @return stdClass
+ */
+ public function set_all_groups_to_false( $interest_groups ) {
+ $groups = new stdClass();
+
+ foreach ( $interest_groups as $interest_group ) {
+ if ( 'hidden' !== $interest_group['type'] ) {
+ foreach ( $interest_group['groups'] as $group ) {
+ $id = $group['id'];
+ $groups->$id = false;
+ }
+ }
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Get signup form URL for the Mailchimp list.
+ *
+ * @param string $list_id The list ID.
+ * @return string
+ */
+ public function get_signup_form_url( $list_id ) {
+ $dc = get_option( 'mc_datacenter' );
+ $user = get_option( 'mc_user' );
+ $url = 'https://' . $dc . '.list-manage.com/subscribe?u=' . $user['account_id'] . '&id=' . $list_id;
+ return $url;
+ }
+
+ /**
+ * Check the status of a subscriber.
+ *
+ * @param string $list_id The list ID.
+ * @param string $email The email address of the subscriber.
+ * @return string|bool The status of the subscriber or false on error.
+ */
+ public function get_subscriber_status( $list_id, $email ) {
+ $api = mailchimp_sf_get_api();
+ if ( ! $api ) {
+ return false;
+ }
+
+ $endpoint = 'lists/' . $list_id . '/members/' . md5( strtolower( $email ) ) . '?fields=status';
+ $subscriber = $api->get( $endpoint, null );
+ if ( is_wp_error( $subscriber ) ) {
+ return false;
+ }
+ return $subscriber['status'];
+ }
+
+ /**
+ * Subscribe to a list.
+ *
+ * @param string $list_id The list ID.
+ * @param string $email The email address of the subscriber.
+ * @param array $args Additional arguments for the subscription.
+ *
+ * @return WP_Error|array The response from the Mailchimp API or an error.
+ */
+ protected function subscribe_to_list( $list_id, $email, $args ) {
+ $api = mailchimp_sf_get_api();
+ // If we don't have an API, then show an error message.
+ if ( ! $api ) {
+ $url = $this->get_signup_form_url( $list_id );
+ $error = wp_kses(
+ sprintf(
+ /* translators: 1: email address 2: url */
+ __(
+ 'We encountered a problem adding %1$s to the list. Please
sign up here. ',
+ 'mailchimp'
+ ),
+ esc_html( $email ),
+ esc_url( $url )
+ ),
+ [
+ 'a' => [
+ 'href' => [],
+ ],
+ ]
+ );
+ return new WP_Error( 'mailchimp-auth-error', $error );
+ }
+
+ $url = 'lists/' . $list_id . '/members/' . md5( strtolower( $email ) );
+ $status = $this->get_subscriber_status( $list_id, $email );
+
+ // If update existing is turned off and the subscriber is not new, error out.
+ $is_new_subscriber = false === $status;
+ if ( ! $args['update_existing'] && ! $is_new_subscriber ) {
+ $msg = esc_html__( 'This email address has already been subscribed to this list.', 'mailchimp' );
+ return new WP_Error( 'mailchimp-update-existing', $msg );
+ }
+
+ // Prepare request body
+ $request_body = $this->prepare_subscribe_request_body( $email, $status, $args );
+ $response = $api->post( $url, $request_body, 'PUT', $list_id );
+
+ return $response;
+ }
+
+ /**
+ * Prepare the request body for the Mailchimp API.
+ *
+ * @param string $email The email address of the subscriber.
+ * @param string $status The status of the subscriber (e.g., subscribed, pending).
+ * @param array $args Additional arguments for the subscription, including:
+ * - merge_fields (array): Merge fields data.
+ * - interests (array): Interest groups data.
+ * - email_type (string): The type of email (e.g., html, text).
+ * - double_opt_in (bool): Whether to use double opt-in.
+ * - update_existing (bool): Whether to update existing subscribers.
+ *
+ * @return stdClass The prepared request body.
+ */
+ protected function prepare_subscribe_request_body( $email, $status, $args ) {
+ // Prepare the request body for the Mailchimp API.
+ $request_body = new stdClass();
+ $request_body->email_address = $email;
+ $request_body->email_type = $args['email_type'];
+ $request_body->merge_fields = $args['merge_fields'];
+
+ if ( ! empty( $args['interests'] ) ) {
+ $request_body->interests = $args['interests'];
+ }
+
+ // Early return for already subscribed users
+ if ( 'subscribed' === $status ) {
+ return $request_body;
+ }
+
+ // Subscribe the email immediately unless double opt-in is enabled
+ // "unsubscribed" and "subscribed" existing emails have been excluded at this stage
+ // "pending" emails should follow double opt-in rules
+ $request_body->status = $args['double_opt_in'] ? 'pending' : 'subscribed';
+
+ return $request_body;
+ }
+
+ /**
+ * Remove empty merge fields from the request body.
+ *
+ * @param object $merge Merge fields request body.
+ * @return object The modified merge fields request body.
+ */
+ public function remove_empty_merge_fields( $merge ) {
+ foreach ( $merge as $k => $v ) {
+ if ( is_object( $v ) && empty( $v ) ) {
+ unset( $merge->$k );
+ } elseif ( ( is_string( $v ) && trim( $v ) === '' ) || is_null( $v ) ) {
+ unset( $merge->$k );
+ }
+ }
+
+ return $merge;
+ }
+
+ /**
+ * Validate the form submission.
+ * Basic checks for the prevention of spam.
+ *
+ * @return bool|WP_Error True if valid, WP_Error if invalid.
+ */
+ protected function validate_form_submission() {
+ $spam_message = esc_html__( "We couldn't process your submission as it was flagged as potential spam. Please try again.", 'mailchimp' );
+ // Make sure the honeypot field is set, but not filled (if it is, then it's a spam).
+ if ( ! isset( $_POST['mailchimp_sf_alt_email'] ) || ! empty( $_POST['mailchimp_sf_alt_email'] ) ) {
+ return new WP_Error( 'spam', $spam_message );
+ }
+
+ // Make sure that no-js field is not present (if it is, then it's a spam).
+ if ( isset( $_POST['mailchimp_sf_no_js'] ) ) {
+ return new WP_Error( 'spam', $spam_message );
+ }
+
+ // Make sure that user-agent is set and it has reasonable length.
+ $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
+ if ( strlen( $user_agent ) < 2 ) {
+ return new WP_Error( 'spam', $spam_message );
+ }
+
+ /**
+ * Filter to allow for custom validation of the form submission.
+ *
+ * @since x.x.x
+ * @param bool $is_valid True if valid, false if invalid, return WP_Error to provide error message.
+ * @param array $post_data The $_POST data.
+ */
+ return apply_filters( 'mailchimp_sf_form_submission_validation', true, $_POST );
+ }
+}
diff --git a/includes/mailchimp-deprecated-functions.php b/includes/mailchimp-deprecated-functions.php
new file mode 100644
index 00000000..cfc74b75
--- /dev/null
+++ b/includes/mailchimp-deprecated-functions.php
@@ -0,0 +1,113 @@
+prepare_merge_fields_body( $merge_fields );
+}
+
+/**
+ * Prepare the interest groups body for the API request.
+ *
+ * @deprecated x.x.x
+ * @param array $interest_groups Interest groups.
+ * @return stdClass
+ */
+function mailchimp_sf_groups_submit( $interest_groups ) {
+ _deprecated_function( __FUNCTION__, 'x.x.x', 'Mailchimp_Form_Submission::prepare_groups_body()' );
+
+ $form_submission = new Mailchimp_Form_Submission();
+ return $form_submission->prepare_groups_body( $interest_groups );
+}
+
+/**
+ * Set all groups to false
+ *
+ * @deprecated x.x.x
+ * @return StdClass
+ */
+function mailchimp_sf_set_all_groups_to_false() {
+ _deprecated_function( __FUNCTION__, 'x.x.x', 'Mailchimp_Form_Submission::set_all_groups_to_false()' );
+
+ $interest_groups = get_option( 'mc_interest_groups' );
+ $form_submission = new Mailchimp_Form_Submission();
+ return $form_submission->set_all_groups_to_false( $interest_groups );
+}
+
+/**
+ * Get signup form URL.
+ *
+ * @deprecated x.x.x
+ * @return string
+ */
+function mailchimp_sf_signup_form_url() {
+ _deprecated_function( __FUNCTION__, 'x.x.x', 'Mailchimp_Form_Submission::get_signup_form_url()' );
+
+ $list_id = get_option( 'mc_list_id' );
+ $form_submission = new Mailchimp_Form_Submission();
+ return $form_submission->get_signup_form_url( $list_id );
+}
+
+
+/**
+ * Attempts to signup a user, per the $_POST args.
+ *
+ * This sets a global message, that is then used in the widget
+ * output to retrieve and display that message.
+ *
+ * @deprecated x.x.x
+ *
+ * @return bool
+ */
+function mailchimp_sf_signup_submit() {
+ _deprecated_function( __FUNCTION__, 'x.x.x', 'Mailchimp_Form_Submission::handle_form_submission()' );
+
+ $form_submission = new Mailchimp_Form_Submission();
+ $response = $form_submission->handle_form_submission();
+
+ // If we have an error, then show it.
+ if ( is_wp_error( $response ) ) {
+ $error = $response->get_error_message();
+ mailchimp_sf_global_msg( '
' . $error . ' ' );
+ return false;
+ }
+
+ mailchimp_sf_global_msg( '
' . esc_html( $response ) . ' ' );
+ return true;
+}
+
+/**
+ * Remove empty merge fields from the request body.
+ *
+ * @param object $merge Merge fields request body.
+ * @return object The modified merge fields request body.
+ */
+function mailchimp_sf_merge_remove_empty( $merge ) {
+ _deprecated_function( __FUNCTION__, 'x.x.x', 'Mailchimp_Form_Submission::remove_empty_merge_fields()' );
+
+ $form_submission = new Mailchimp_Form_Submission();
+ return $form_submission->remove_empty_merge_fields( $merge );
+}
diff --git a/lib/mailchimp/mailchimp.php b/lib/mailchimp/mailchimp.php
index 43214163..60106d0d 100644
--- a/lib/mailchimp/mailchimp.php
+++ b/lib/mailchimp/mailchimp.php
@@ -188,6 +188,11 @@ public function post( $endpoint, $body, $method = 'POST', $list_id = '' ) {
// Get merge fields for the list if we have a list id.
if ( ! empty( $list_id ) ) {
$merges = get_option( 'mailchimp_sf_merge_fields_' . $list_id );
+
+ // If we don't have merge fields for the list, get the default merge fields.
+ if ( empty( $merges ) ) {
+ $merges = get_option( 'mc_merge_vars' );
+ }
}
// Check if the email address is in compliance state.
diff --git a/mailchimp.php b/mailchimp.php
index babb9572..c62c17e3 100644
--- a/mailchimp.php
+++ b/mailchimp.php
@@ -103,8 +103,13 @@ function () {
$block = new Mailchimp_List_Subscribe_Form_Blocks();
$block->init();
-// Block form submission handler class.
-require_once plugin_dir_path( __FILE__ ) . 'includes/class-mailchimp-block-form-submission.php';
+// Form submission handler class.
+require_once plugin_dir_path( __FILE__ ) . 'includes/class-mailchimp-form-submission.php';
+$form_submission = new Mailchimp_Form_Submission();
+$form_submission->init();
+
+// Deprecated functions.
+require_once plugin_dir_path( __FILE__ ) . 'includes/mailchimp-deprecated-functions.php';
/**
* Do the following plugin setup steps here
@@ -266,36 +271,6 @@ function mailchimp_sf_request_handler() {
// Update the form settings
mailchimp_sf_save_general_form_settings();
break;
- case 'mc_submit_signup_form':
- // Validate nonce
- if (
- ! isset( $_POST['_mc_submit_signup_form_nonce'] ) ||
- ! wp_verify_nonce( sanitize_key( $_POST['_mc_submit_signup_form_nonce'] ), 'mc_submit_signup_form' )
- ) {
- wp_die( 'Cheatin’ huh?' );
- }
-
- // Check if request from latest block.
- if ( isset( $_POST['mailchimp_sf_list_id'] ) ) {
- $block_form_submission = new Mailchimp_Block_Form_Submission();
- $block_form_submission->handle_form_submission();
- } else {
- // Attempt the signup
- mailchimp_sf_signup_submit();
- }
-
- // Do a different action for html vs. js
- switch ( isset( $_POST['mc_submit_type'] ) ? $_POST['mc_submit_type'] : '' ) {
- case 'html':
- /* This gets set elsewhere! */
- break;
- case 'js':
- if ( ! headers_sent() ) { // just in case...
- header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT', true, 200 );
- }
- echo wp_kses_post( mailchimp_sf_frontend_msg() );
- exit;
- }
}
}
}
@@ -804,109 +779,6 @@ function mailchimp_sf_shortcode() {
}
add_shortcode( 'mailchimpsf_form', 'mailchimp_sf_shortcode' );
-/**
- * Attempts to signup a user, per the $_POST args.
- *
- * This sets a global message, that is then used in the widget
- * output to retrieve and display that message.
- *
- * @return bool
- */
-function mailchimp_sf_signup_submit() {
- $mv = get_option( 'mc_merge_vars', array() );
- $mv_tag_keys = array();
-
- $igs = get_option( 'mc_interest_groups', array() );
-
- $list_id = get_option( 'mc_list_id' );
- $email = isset( $_POST['mc_mv_EMAIL'] ) ? wp_strip_all_tags( wp_unslash( $_POST['mc_mv_EMAIL'] ) ) : '';
-
- $merge = mailchimp_sf_merge_submit( $mv );
-
- // Catch errors and fail early.
- if ( is_wp_error( $merge ) ) {
- $msg = '
' . $merge->get_error_message() . ' ';
- mailchimp_sf_frontend_msg( $msg );
-
- return false;
- }
-
- // Head back to the beginning of the merge vars array
- reset( $mv );
- // Ensure we have an array
- $igs = ! is_array( $igs ) ? array() : $igs;
- $igs = mailchimp_sf_groups_submit( $igs );
-
- // Clear out empty merge vars
- $merge = mailchimp_sf_merge_remove_empty( $merge );
- if ( isset( $_POST['email_type'] ) && in_array( $_POST['email_type'], array( 'text', 'html', 'mobile' ), true ) ) {
- $email_type = sanitize_text_field( wp_unslash( $_POST['email_type'] ) );
- } else {
- $email_type = 'html';
- }
-
- $api = mailchimp_sf_get_api();
- if ( ! $api ) {
- $url = mailchimp_sf_signup_form_url();
- $error = sprintf(
- '
%s ',
- wp_kses(
- sprintf(
- /* translators: 1: email address 2: url */
- __(
- 'We encountered a problem adding %1$s to the list. Please
sign up here. ',
- 'mailchimp'
- ),
- esc_html( $email ),
- esc_url( $url )
- ),
- [
- 'a' => [
- 'href' => [],
- ],
- ]
- )
- );
- mailchimp_sf_frontend_msg( $error );
- return false;
- }
-
- $url = 'lists/' . $list_id . '/members/' . md5( strtolower( $email ) );
- $status = mailchimp_sf_check_status( $url );
-
- // If update existing is turned off and the subscriber is not new, error out.
- $is_new_subscriber = false === $status;
- if ( ! get_option( 'mc_update_existing' ) && ! $is_new_subscriber ) {
- $msg = esc_html__( 'This email address has already been subscribed to this list.', 'mailchimp' );
- $error = new WP_Error( 'mailchimp-update-existing', $msg );
- mailchimp_sf_frontend_msg( '
' . $msg . ' ' );
- return false;
- }
-
- $body = mailchimp_sf_subscribe_body( $merge, $igs, $email_type, $email, $status, get_option( 'mc_double_optin' ) );
- $retval = $api->post( $url, $body, 'PUT' );
-
- // If we have errors, then show them
- if ( is_wp_error( $retval ) ) {
- $msg = '
' . $retval->get_error_message() . ' ';
- mailchimp_sf_frontend_msg( $msg );
- return false;
- }
-
- if ( 'subscribed' === $retval['status'] ) {
- $esc = esc_html__( 'Success, you\'ve been signed up.', 'mailchimp' );
- $msg = "
{$esc} ";
- } else {
- $esc = esc_html__( 'Success, you\'ve been signed up! Please look for our confirmation email.', 'mailchimp' );
- $msg = "
{$esc} ";
- }
-
- // Set our front end success message
- mailchimp_sf_frontend_msg( $msg );
-
- return true;
-}
-
/**
* Cleans up merge fields and interests to make them
* API 3.0-friendly.
@@ -958,102 +830,6 @@ function mailchimp_sf_check_status( $endpoint ) {
return $subscriber['status'];
}
-/**
- * Merge submit
- *
- * @param array $mv Merge Vars
- * @return mixed
- */
-function mailchimp_sf_merge_submit( $mv ) {
- // Loop through our Merge Vars, and if they're empty, but required, then print an error, and mark as failed
- $merge = new stdClass();
- foreach ( $mv as $mv_var ) {
- // We also want to create an array where the keys are the tags for easier validation later
- $tag = $mv_var['tag'];
- $mv_tag_keys[ $tag ] = $mv_var;
-
- $opt = 'mc_mv_' . $tag;
-
- $opt_val = isset( $_POST[ $opt ] ) ? map_deep( stripslashes_deep( $_POST[ $opt ] ), 'sanitize_text_field' ) : '';
-
- switch ( $mv_var['type'] ) {
- /**
- * US Phone validation
- *
- * - Merge field is phone
- * - Merge field is "included" in the Mailchimp admin options
- * - Phone format is set in Mailchimp account
- * - Phone format is US in Mailchimp account
- */
- case 'phone':
- if (
- ( 'on' === get_option( $opt ) || $mv_var['required'] )
- && isset( $mv_var['options']['phone_format'] )
- && 'US' === $mv_var['options']['phone_format']
- ) {
- $opt_val = mailchimp_sf_merge_validate_phone( $opt_val, $mv_var );
- if ( is_wp_error( $opt_val ) ) {
- return $opt_val;
- }
- }
- break;
-
- /**
- * Address validation
- *
- * - Merge field is address
- * - Merge field is "included" in the Mailchimp admin options
- * - Merge field is an array (address contains multiple
elements)
- */
- case 'address':
- if ( ( 'on' === get_option( $opt ) || $mv_var['required'] ) && is_array( $opt_val ) ) {
- $validate = mailchimp_sf_merge_validate_address( $opt_val, $mv_var );
- if ( is_wp_error( $validate ) ) {
- return $validate;
- }
-
- if ( $validate ) {
- $merge->$tag = $validate;
- }
- }
- break;
-
- /**
- * Handle generic array values
- *
- * Not sure what this does or is for
- *
- * - Merge field is an array, not specifically phone or address
- */
- default:
- if ( is_array( $opt_val ) ) {
- $keys = array_keys( $opt_val );
- $val = new stdClass();
- foreach ( $keys as $key ) {
- $val->$key = $opt_val[ $key ];
- }
- $opt_val = $val;
- }
- break;
- }
-
- /**
- * Required fields
- *
- * If the field is required and empty, return an error
- */
- if ( 'Y' === $mv_var['required'] && trim( $opt_val ) === '' ) {
- /* translators: %s: field name */
- $message = sprintf( esc_html__( 'You must fill in %s.', 'mailchimp' ), esc_html( $mv_var['name'] ) );
- $error = new WP_Error( 'missing_required_field', $message );
- return $error;
- } elseif ( 'EMAIL' !== $tag ) {
- $merge->$tag = $opt_val;
- }
- }
- return $merge;
-}
-
/**
* Validate phone
*
@@ -1113,94 +889,6 @@ function mailchimp_sf_merge_validate_address( $opt_val, $data ) {
return $merge;
}
-/**
- * Merge remove empty
- *
- * @param stdObj $merge Merge
- * @return stdObj
- */
-function mailchimp_sf_merge_remove_empty( $merge ) {
- foreach ( $merge as $k => $v ) {
- if ( is_object( $v ) && empty( $v ) ) {
- unset( $merge->$k );
- } elseif ( ( is_string( $v ) && trim( $v ) === '' ) || is_null( $v ) ) {
- unset( $merge->$k );
- }
- }
-
- return $merge;
-}
-
-/**
- * Groups submit
- *
- * @param array $igs Interest groups
- * @return stdClass
- */
-function mailchimp_sf_groups_submit( $igs ) {
- $groups = mailchimp_sf_set_all_groups_to_false();
-
- if ( empty( $igs ) ) {
- return new StdClass();
- }
-
- // get groups and ids
- // set all to false
-
- foreach ( $igs as $ig ) {
- $ig_id = $ig['id'];
- if ( get_option( 'mc_show_interest_groups_' . $ig_id ) === 'on' && 'hidden' !== $ig['type'] ) {
- switch ( $ig['type'] ) {
- case 'dropdown':
- case 'radio':
- // there can only be one value submitted for radio/dropdowns, so use that at the group id.
- if ( isset( $_POST['group'][ $ig_id ] ) && ! empty( $_POST['group'][ $ig_id ] ) ) {
- $value = sanitize_text_field( wp_unslash( $_POST['group'][ $ig_id ] ) );
- $groups->$value = true;
- }
- break;
- case 'checkboxes':
- if ( isset( $_POST['group'][ $ig_id ] ) ) {
- $ig_ids = array_map(
- 'sanitize_text_field',
- array_keys(
- stripslashes_deep( $_POST['group'][ $ig_id ] ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- ignoring becuase this is sanitized through array_map above
- )
- );
- foreach ( $ig_ids as $id ) {
- $groups->$id = true;
- }
- }
- break;
- default:
- // Nothing
- break;
- }
- }
- }
- return $groups;
-}
-
-/**
- * Set all groups to false
- *
- * @return StdClass
- */
-function mailchimp_sf_set_all_groups_to_false() {
- $toreturn = new StdClass();
-
- foreach ( get_option( 'mc_interest_groups' ) as $grouping ) {
- if ( 'hidden' !== $grouping['type'] ) {
- foreach ( $grouping['groups'] as $group ) {
- $id = $group['id'];
- $toreturn->$id = false;
- }
- }
- }
-
- return $toreturn;
-}
-
/**
* Verify key
*
@@ -1242,22 +930,6 @@ function mailchimp_sf_update_profile_url( $email ) {
return $url;
}
-/**
- * Get signup form URL.
- *
- * @param string $list_id List ID
- * @return string
- */
-function mailchimp_sf_signup_form_url( $list_id = '' ) {
- $dc = get_option( 'mc_datacenter' );
- $user = get_option( 'mc_user' );
- if ( empty( $list_id ) ) {
- $list_id = get_option( 'mc_list_id' );
- }
- $url = 'http://' . $dc . '.list-manage.com/subscribe?u=' . $user['account_id'] . '&id=' . $list_id;
- return $url;
-}
-
/**
* Delete options
*
diff --git a/mailchimp_widget.php b/mailchimp_widget.php
index 1a57912e..7186877c 100644
--- a/mailchimp_widget.php
+++ b/mailchimp_widget.php
@@ -262,8 +262,10 @@ function mailchimp_sf_signup_form( $args = array() ) {
@@ -296,6 +298,21 @@ function mailchimp_sf_signup_form( $args = array() ) {
}
}
+/**
+ * Add a hidden honeypot field
+ *
+ * @return void
+ */
+function mailchimp_sf_honeypot_field() {
+ ?>
+
+
+
+
+
+ {
cy.get('#mc_mv_LNAME').should('have.value', lastName);
});
+ it('Spam protection should work as expected', () => {
+ // Show error message to spam bots.
+ cy.visit(`/?p=${postId}`);
+ cy.get('#mc_signup').should('exist');
+ cy.get('input[name="mailchimp_sf_alt_email"]').then((el) => {
+ el.val('123');
+ });
+ cy.get('#mc_signup_submit').should('exist');
+ cy.get('#mc_signup_submit').click();
+ cy.get('.mc_error_msg').should('exist');
+ cy.get('.mc_error_msg').contains(
+ "We couldn't process your submission as it was flagged as potential spam",
+ );
+
+ // Normal user should not see the error message.
+ cy.visit(`/?p=${postId}`);
+ cy.get('#mc_signup').should('exist');
+ cy.get('#mc_signup_submit').should('exist');
+ cy.get('#mc_signup_submit').click();
+ cy.get('.mc_error_msg').should('exist');
+ cy.get('.mc_error_msg').contains('Email Address: This value should not be blank.');
+ });
+
// TODO: Add tests for the Double Opt-in and Update existing subscribers settings.
// TODO: Add tests for the block styles settings.
// TODO: Add tests for the form submission.
diff --git a/tests/cypress/e2e/settings/settings.test.js b/tests/cypress/e2e/settings/settings.test.js
index acfcd152..adb63701 100644
--- a/tests/cypress/e2e/settings/settings.test.js
+++ b/tests/cypress/e2e/settings/settings.test.js
@@ -207,6 +207,31 @@ describe('Admin can update plugin settings', () => {
cy.get('#mc_header_content').should('have.value', customHeader);
});
+ it('Spam protection should work as expected', () => {
+ // Show error message to spam bots.
+ [shortcodePostURL, blockPostPostURL].forEach((url) => {
+ cy.visit(url);
+ cy.get('#mc_signup').should('exist');
+ cy.get('input[name="mailchimp_sf_alt_email"]').then((el) => {
+ el.val('123');
+ });
+ cy.get('#mc_signup_submit').should('exist');
+ cy.get('#mc_signup_submit').click();
+ cy.get('.mc_error_msg').should('exist');
+ cy.get('.mc_error_msg').contains(
+ "We couldn't process your submission as it was flagged as potential spam",
+ );
+
+ // Normal user should not see the error message.
+ cy.visit(url);
+ cy.get('#mc_signup').should('exist');
+ cy.get('#mc_signup_submit').should('exist');
+ cy.get('#mc_signup_submit').click();
+ cy.get('.mc_error_msg').should('exist');
+ cy.get('.mc_error_msg').contains('Email Address: This value should not be blank.');
+ });
+ });
+
it('The default settings populate as expected', () => {
const options = [
'mc_header_content',