Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions assets/js/mailchimp.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,18 @@

for (let i = 0; i < forms.length; i++) {
const listId = forms[i].getAttribute('data-list-id');
if (listId && !tracked[listId]) {
tracked[listId] = true;
const formId = forms[i].getAttribute('data-form-id') || '';
const key = `${listId}|${formId}`;

if (listId && !tracked[key]) {
tracked[key] = true;

const formData = new FormData();
formData.append('action', 'mailchimp_sf_track_form_view');
formData.append('list_id', listId);
if (formId) {
formData.append('form_id', formId);
}
formData.append('mailchimp_sf_nonce', window.mailchimpSF.analytics_nonce);

fetch(window.mailchimpSF.analytics_ajax_url, {
Expand Down
8 changes: 8 additions & 0 deletions includes/blocks/mailchimp/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
"category": "widgets",
"description": "Mailchimp List Subscribe Form",
"attributes": {
"formId": {
"type": "string",
"default": ""
},
"formTitle": {
"type": "string",
"default": ""
},
"list_id": {
"type": "string"
},
Expand Down
24 changes: 24 additions & 0 deletions includes/blocks/mailchimp/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
PanelBody,
ToggleControl,
SelectControl,
TextControl,
Spinner,
Placeholder,
} from '@wordpress/components';
Expand Down Expand Up @@ -61,8 +62,17 @@ export const BlockEdit = (props) => {
show_required_indicator = true,
required_indicator_text,
template = 'default',
formTitle = '',
} = attributes;

// Give every block instance a stable analytics ID. Generated once when
// empty and persisted with the post, so it survives edits and reordering.
useEffect(() => {
if (!attributes.formId && typeof window.crypto?.randomUUID === 'function') {
setAttributes({ formId: window.crypto.randomUUID() });
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps -- Only run on mount.

const [listData, setListData] = useState({});
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState('');
Expand Down Expand Up @@ -417,6 +427,20 @@ export const BlockEdit = (props) => {
__nextHasNoMarginBottom
/>
</PanelBody>
<PanelBody title={__('Analytics', 'mailchimp')} initialOpen={false}>
<TextControl
label={__('Form name', 'mailchimp')}
value={formTitle}
maxLength={50}
className="mailchimp-form-title"
onChange={(value) => setAttributes({ formTitle: value })}
help={__(
'Used to identify this form in the Analytics dashboard. Defaults to the form header if left blank.',
'mailchimp',
)}
__nextHasNoMarginBottom
/>
</PanelBody>
<PanelBody title={__('Form Settings', 'mailchimp')} initialOpen={false}>
<ToggleControl
label={__('Double opt-in', 'mailchimp')}
Expand Down
17 changes: 12 additions & 5 deletions includes/blocks/mailchimp/markup.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@
}

// Make sure we have a list ID and it's valid.
$list_id = $attributes['list_id'] ?? '';
$form_id = wp_unique_id( $list_id . '_' );
$lists = ( new Mailchimp_List_Subscribe_Form_Blocks() )->get_lists();
$list_ids = array_map(
$list_id = $attributes['list_id'] ?? '';
$form_id = wp_unique_id( $list_id . '_' );

// Stable per-form analytics id from the block attribute. Empty for legacy
// blocks not yet re-saved
$mc_analytics_form_id = Mailchimp_Forms_Registry::sanitize_form_id( $attributes['formId'] ?? '' );
$lists = ( new Mailchimp_List_Subscribe_Form_Blocks() )->get_lists();
$list_ids = array_map(
function ( $single_list ) {
return $single_list['id'];
},
Expand Down Expand Up @@ -105,8 +109,11 @@ function ( $single_list ) {
}
?>
<div id="mc_signup_<?php echo esc_attr( $form_id ); ?>">
<form method="post" action="#mc_signup_<?php echo esc_attr( $form_id ); ?>" id="mc_signup_form_<?php echo esc_attr( $form_id ); ?>" class="mc_signup_form" data-list-id="<?php echo esc_attr( $list_id ); ?>">
<form method="post" action="#mc_signup_<?php echo esc_attr( $form_id ); ?>" id="mc_signup_form_<?php echo esc_attr( $form_id ); ?>" class="mc_signup_form" data-list-id="<?php echo esc_attr( $list_id ); ?>"<?php echo $mc_analytics_form_id ? ' data-form-id="' . esc_attr( $mc_analytics_form_id ) . '"' : ''; ?>>
<input type="hidden" class="mc_submit_type" name="mc_submit_type" value="html" />
<?php if ( $mc_analytics_form_id ) : ?>
<input type="hidden" name="mailchimp_sf_form_id" value="<?php echo esc_attr( $mc_analytics_form_id ); ?>" />
<?php endif; ?>
<input type="hidden" name="mcsf_action" value="mc_submit_signup_form" />
<input type="hidden" name="mailchimp_sf_list_id" value="<?php echo esc_attr( $list_id ); ?>" />
<input type="hidden" name="mailchimp_sf_update_existing_subscribers" value="<?php echo esc_attr( $update_existing_subscribers ); ?>" />
Expand Down
14 changes: 10 additions & 4 deletions includes/class-mailchimp-analytics-data.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public function init() {
add_action( 'wp_ajax_mailchimp_sf_track_form_view', array( $this, 'handle_form_view' ) );
add_action( 'wp_ajax_nopriv_mailchimp_sf_track_form_view', array( $this, 'handle_form_view' ) );
add_action( 'wp_ajax_mailchimp_sf_get_analytics', array( $this, 'handle_get_analytics' ) );
add_action( 'mailchimp_sf_form_submission_success', array( $this, 'track_submission' ) );
add_action( 'mailchimp_sf_form_submission_success', array( $this, 'track_submission' ), 10, 2 );
}

/**
Expand Down Expand Up @@ -218,7 +218,12 @@ public function handle_form_view() {
wp_send_json_error( 'Invalid list_id.', 400 );
}

$this->increment_views( $list_id );
// Optional per-form ID. Invalid/missing values fall back to the empty string
$form_id = isset( $_POST['form_id'] )
? Mailchimp_Forms_Registry::sanitize_form_id( sanitize_text_field( wp_unslash( $_POST['form_id'] ) ) )
: '';

$this->increment_views( $list_id, $form_id );
wp_send_json_success();
}

Expand Down Expand Up @@ -305,10 +310,11 @@ public function handle_get_analytics() {
* Track a successful form submission.
*
* @param string $list_id The list ID.
* @param string $form_id The form ID (optional).
*/
public function track_submission( $list_id ) {
public function track_submission( $list_id, $form_id = '' ) {
if ( ! empty( $list_id ) ) {
$this->increment_submissions( $list_id );
$this->increment_submissions( $list_id, Mailchimp_Forms_Registry::sanitize_form_id( $form_id ) );
}
}
}
6 changes: 5 additions & 1 deletion includes/class-mailchimp-form-submission.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,16 @@ public function handle_form_submission() {
$message = __( 'Success, you\'ve been signed up! Please look for our confirmation email.', 'mailchimp' );
}

// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce check is already done in the request_handler() function.
$form_id = isset( $_POST['mailchimp_sf_form_id'] ) ? Mailchimp_Forms_Registry::sanitize_form_id( sanitize_text_field( wp_unslash( $_POST['mailchimp_sf_form_id'] ) ) ) : '';

/**
* Fires after a successful form submission.
*
* @param string $list_id The list ID the user subscribed to.
* @param string $form_id The form ID, or '' for legacy/untracked forms.
*/
do_action( 'mailchimp_sf_form_submission_success', $list_id );
do_action( 'mailchimp_sf_form_submission_success', $list_id, $form_id );

// Return success message.
return $message;
Expand Down
171 changes: 171 additions & 0 deletions includes/class-mailchimp-forms-registry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<?php
/**
* Registry of block-based signup forms.
*
* Stores a stable `form_id` to human-readable title mapping so the analytics
* dashboard can label per-form data. Rows are upserted when a host post is
* saved and are never deleted, so historical labels keep resolving.
*
* @package Mailchimp
*/

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

/**
* Class Mailchimp_Forms_Registry
*/
class Mailchimp_Forms_Registry {

/**
* Database version for the forms registry table.
*
* @var string
*/
const DB_VERSION = '1.0.0';

/**
* Initialize the class.
*/
public function init() {
add_action( 'save_post', array( $this, 'sync_post_forms' ), 10, 2 );
}

/**
* Get the forms registry table name.
*
* @return string
*/
public static function get_table_name() {
global $wpdb;
return $wpdb->prefix . 'mailchimp_sf_forms';
}

/**
* Create the forms registry table.
*/
public static function create_table() {
global $wpdb;

$table_name = self::get_table_name();
$charset_collate = $wpdb->get_charset_collate();

$sql = "CREATE TABLE {$table_name} (
form_id varchar(50) NOT NULL,
title varchar(255) NOT NULL DEFAULT '',
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
Comment on lines +58 to +59
PRIMARY KEY (form_id)
) {$charset_collate};";

require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );

update_option( 'mailchimp_sf_forms_db_version', self::DB_VERSION );
}

/**
* Validate and normalize a form ID.
*
* Accepts a UUID (the value minted by the block editor) and returns it
* lower-cased. Anything else, including an empty value, returns ''
*
* @param mixed $form_id The candidate form ID.
* @return string The valid UUID, or '' if not valid.
*/
public static function sanitize_form_id( $form_id ) {
if ( ! is_string( $form_id ) ) {
return '';
}

$form_id = strtolower( trim( $form_id ) );

if ( '' === $form_id ) {
return '';
}

return preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $form_id ) ? $form_id : '';
}

/**
* Sync the registry with the Mailchimp forms found in a saved post.
*
* @param int $post_id The post ID.
* @param WP_Post $post The post object.
*/
public function sync_post_forms( $post_id, $post ) {
// Skip autosaves and revisions.
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
return;
}

// Only parse posts that actually contain the block.
if ( ! $post instanceof WP_Post || ! has_block( 'mailchimp/mailchimp', $post ) ) {
return;
}

$forms = $this->extract_forms( parse_blocks( $post->post_content ) );

foreach ( $forms as $form_id => $title ) {
$this->upsert( $form_id, $title );
}
Comment thread
iamdharmesh marked this conversation as resolved.
}

/**
* Recursively extract Mailchimp form IDs and titles from parsed blocks.
*
* @param array $blocks Parsed blocks.
* @param array $forms Accumulator of `form_id => title`.
* @return array
*/
private function extract_forms( $blocks, $forms = array() ) {
foreach ( $blocks as $block ) {
if ( 'mailchimp/mailchimp' === ( $block['blockName'] ?? '' ) ) {
$attrs = $block['attrs'] ?? array();
$form_id = self::sanitize_form_id( $attrs['formId'] ?? '' );

if ( '' !== $form_id ) {
// Fall back to the form header when no title is set.
$title = $attrs['formTitle'] ?? '';
$title = '' !== $title ? $title : ( $attrs['header'] ?? '' );
$forms[ $form_id ] = sanitize_text_field( $title );
}
Comment on lines +130 to +134
}

if ( ! empty( $block['innerBlocks'] ) ) {
$forms = $this->extract_forms( $block['innerBlocks'], $forms );
}
}

return $forms;
}

/**
* Insert or update a registry row.
*
* @param string $form_id The form ID (already validated).
* @param string $title The form title.
*/
public function upsert( $form_id, $title ) {
global $wpdb;

$table_name = self::get_table_name();
$now = current_time( 'mysql' );

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wpdb->query(
$wpdb->prepare(
"INSERT INTO {$table_name} (form_id, title, created_at, updated_at)
VALUES (%s, %s, %s, %s)
ON DUPLICATE KEY UPDATE title = VALUES(title), updated_at = VALUES(updated_at)",
$form_id,
$title,
$now,
$now
)
);
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
}
5 changes: 5 additions & 0 deletions mailchimp.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ function () {
$analytics_data = new Mailchimp_Analytics_Data();
$analytics_data->init();

// Forms registry (form_id -> title) for per-form analytics labels.
require_once plugin_dir_path( __FILE__ ) . 'includes/class-mailchimp-forms-registry.php';
$forms_registry = new Mailchimp_Forms_Registry();
$forms_registry->init();

// Subscriber activity (Mailchimp Activity API) data class.
require_once plugin_dir_path( __FILE__ ) . 'includes/class-mailchimp-subscriber-activity.php';
$subscriber_activity = new Mailchimp_Subscriber_Activity();
Expand Down
19 changes: 19 additions & 0 deletions mailchimp_upgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,25 @@ function mailchimp_sf_maybe_create_analytics_table() {

add_action( 'plugins_loaded', 'mailchimp_sf_maybe_create_analytics_table', 20 );

/**
* Ensure the forms registry table exists and is at the current schema version.
*
* Runs independently of mailchimp_version_check().
*
* @return void
*/
function mailchimp_sf_maybe_create_forms_table() {
if ( ! class_exists( 'Mailchimp_Forms_Registry' ) ) {
return;
}
if ( Mailchimp_Forms_Registry::DB_VERSION === get_option( 'mailchimp_sf_forms_db_version' ) ) {
return;
}
Mailchimp_Forms_Registry::create_table();
}

add_action( 'plugins_loaded', 'mailchimp_sf_maybe_create_forms_table', 20 );

/**
* Version 1.6.0 update routine
* - Remove MonkeyRewards checkbox option
Expand Down
Loading
Loading