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',