Changed source root directory

This commit is contained in:
2026-03-05 16:30:11 +01:00
parent dc85447ee1
commit 538f85d7a2
5868 changed files with 749734 additions and 99 deletions

View File

@@ -0,0 +1,211 @@
<?php
/**
* CF7 admin panel integration.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
final class Cmatic_Admin_Panel {
private const PANEL_KEY = 'Chimpmatic';
public static function init(): void {
add_filter( 'wpcf7_editor_panels', array( __CLASS__, 'register_panel' ) );
add_action( 'wpcf7_after_save', array( __CLASS__, 'save_settings' ) );
add_action( 'wpcf7_admin_misc_pub_section', array( __CLASS__, 'render_sidebar_info' ) );
add_action( 'wpcf7_admin_footer', array( __CLASS__, 'render_footer_banner' ), 10, 1 );
}
public static function register_panel( array $panels ): array {
if ( defined( 'CMATIC_VERSION' ) ) {
return $panels;
}
$post_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! current_user_can( 'wpcf7_edit_contact_form', $post_id ) ) {
return $panels;
}
$panels[ self::PANEL_KEY ] = array(
'title' => __( 'Chimpmatic', 'chimpmatic-lite' ),
'callback' => array( __CLASS__, 'render_panel' ),
);
return $panels;
}
public static function render_panel( $contact_form ): void {
$form_id = $contact_form->id() ?? 0;
$cf7_mch = get_option( 'cf7_mch_' . $form_id, array() );
$cf7_mch = is_array( $cf7_mch ) ? $cf7_mch : array();
$form_tags = Cmatic_Form_Tags::get_tags_with_types( $contact_form );
$api_valid = (int) ( $cf7_mch['api-validation'] ?? 0 );
$list_data = isset( $cf7_mch['lisdata'] ) && is_array( $cf7_mch['lisdata'] ) ? $cf7_mch['lisdata'] : null;
// Render container.
if ( class_exists( 'Cmatic_Data_Container' ) ) {
Cmatic_Data_Container::render_open( $form_id, (string) $api_valid );
} else {
echo '<div class="cmatic-inner">';
}
// Header.
if ( class_exists( 'Cmatic_Header' ) ) {
$api_status = ( 1 === $api_valid ) ? 'connected' : ( ( 0 === $api_valid ) ? 'disconnected' : null );
Cmatic_Header::output( array( 'api_status' => $api_status ) );
}
echo '<div class="cmatic-content">';
// API Panel.
Cmatic_Api_Panel::render( $cf7_mch, (string) $api_valid );
// Audiences.
if ( class_exists( 'Cmatic_Audiences' ) ) {
Cmatic_Audiences::render( (string) $api_valid, $list_data, $cf7_mch );
}
// Field mapping.
Cmatic_Field_Mapper_UI::render( $api_valid, $list_data, $cf7_mch, $form_tags, $form_id );
// Toggles.
Cmatic_Panel_Toggles::cmatic_render();
// Contact Lookup.
if ( class_exists( 'Cmatic_Contact_Lookup' ) ) {
Cmatic_Contact_Lookup::cmatic_render( array( 'form_id' => $form_id ) );
}
// Log Viewer.
Cmatic_Log_Viewer::render();
// Advanced Settings.
echo '<div id="cme-container" class="mce-custom-fields vc-advanced-settings">';
Cmatic_Advanced_Settings::render();
echo '</div>';
// Welcome banner.
echo '<div class="vc-hidden-start dev-cta mce-cta welcome-panel">';
echo '<div class="welcome-panel-content">';
echo Cmatic_Banners::get_welcome(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</div></div>';
echo '</div>'; // .cmatic-content
if ( class_exists( 'Cmatic_Data_Container' ) ) {
Cmatic_Data_Container::render_close();
} else {
echo '</div>';
}
}
public static function save_settings( $contact_form ): void {
if ( ! isset( $_POST['wpcf7-mailchimp'] ) ) {
return;
}
// Verify nonce (defense-in-depth, CF7 already checked at request level).
$form_id = $contact_form->id();
$nonce_action = sprintf( 'wpcf7-save-contact-form_%s', $form_id );
$nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '';
if ( ! wp_verify_nonce( $nonce, $nonce_action ) ) {
return;
}
// Verify capability.
if ( ! current_user_can( 'wpcf7_edit_contact_form', $form_id ) ) {
return;
}
// Trigger telemetry.
if ( class_exists( 'Cmatic\\Metrics\\Core\\Sync' ) && class_exists( 'Cmatic\\Metrics\\Core\\Collector' ) ) {
$payload = \Cmatic\Metrics\Core\Collector::collect( 'form_saved' );
\Cmatic\Metrics\Core\Sync::send( $payload );
}
$option_name = 'cf7_mch_' . $form_id;
$old_settings = get_option( $option_name, array() );
$posted_data = $_POST['wpcf7-mailchimp']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized in sanitize_settings().
$sanitized = self::sanitize_settings( $posted_data, $old_settings );
if ( empty( $sanitized['api'] ) ) {
delete_option( $option_name );
return;
}
$updated_settings = array_merge( $old_settings, $sanitized );
// Remove excess field mappings beyond lite limit.
$max_field_index = CMATIC_LITE_FIELDS + 2;
for ( $i = $max_field_index + 1; $i <= 20; $i++ ) {
$field_key = 'field' . $i;
if ( isset( $updated_settings[ $field_key ] ) ) {
unset( $updated_settings[ $field_key ] );
}
}
update_option( $option_name, $updated_settings );
}
private static function sanitize_settings( array $posted, array $old ): array {
$sanitized = array();
$text_fields = array( 'api', 'list', 'accept' );
// Add field mappings.
$max_index = CMATIC_LITE_FIELDS + 2;
for ( $i = 3; $i <= $max_index; $i++ ) {
$text_fields[] = 'field' . $i;
}
// Add custom fields.
for ( $i = 1; $i <= 10; $i++ ) {
$text_fields[] = 'CustomValue' . $i;
$text_fields[] = 'CustomKey' . $i;
}
// Sanitize text fields.
foreach ( $text_fields as $field ) {
if ( isset( $posted[ $field ] ) ) {
$value = trim( sanitize_text_field( $posted[ $field ] ) );
if ( '' !== $value ) {
$sanitized[ $field ] = $value;
}
}
}
// Preserve masked API key.
if ( isset( $sanitized['api'] ) && strpos( $sanitized['api'], '•' ) !== false ) {
if ( ! empty( $old['api'] ) && strpos( $old['api'], '•' ) === false ) {
$sanitized['api'] = $old['api'];
}
}
// Per-form checkbox fields (global toggles handled via REST API, not here).
$checkboxes = array( 'cfactive', 'addunsubscr' );
foreach ( $checkboxes as $field ) {
$sanitized[ $field ] = isset( $posted[ $field ] ) ? '1' : '0';
}
// Select field: confsubs (double opt-in) - preserve actual value.
$sanitized['confsubs'] = isset( $posted['confsubs'] ) && '1' === $posted['confsubs'] ? '1' : '0';
return $sanitized;
}
public static function render_sidebar_info( int $post_id ): void {
Cmatic_Sidebar_Panel::render_submit_info( $post_id );
}
public static function render_footer_banner( $post ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
Cmatic_Sidebar_Panel::render_footer_promo();
}
private function __construct() {}
}

View File

@@ -0,0 +1,260 @@
<?php
/**
* Admin asset loader.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'Cmatic_Asset_Loader' ) ) {
class Cmatic_Asset_Loader {
private static array $scripts = array();
private static array $styles = array();
public static function init(): void {
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_admin_assets' ) );
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_notices_script' ) );
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_cf7_frontend_styles' ) );
add_filter( 'admin_body_class', array( __CLASS__, 'add_body_class' ) );
}
public static function enqueue_admin_assets( ?string $hook_suffix ): void {
if ( null === $hook_suffix || false === strpos( $hook_suffix, 'wpcf7' ) ) {
return;
}
self::enqueue_styles();
self::enqueue_lite_js();
$is_pro_installed = defined( 'CMATIC_VERSION' );
$is_pro_blessed = function_exists( 'cmatic_is_blessed' ) && cmatic_is_blessed();
if ( $is_pro_installed ) {
self::enqueue_pro_js( $is_pro_blessed );
}
}
private static function enqueue_styles(): void {
$css_file_path = SPARTAN_MCE_PLUGIN_DIR . 'assets/css/chimpmatic-lite.css';
wp_enqueue_style(
'chimpmatic-lite-css',
SPARTAN_MCE_PLUGIN_URL . 'assets/css/chimpmatic-lite.css',
array(),
Cmatic_Buster::instance()->get_version( $css_file_path )
);
$modal_css_path = SPARTAN_MCE_PLUGIN_DIR . 'assets/css/chimpmatic-lite-deactivate.css';
wp_enqueue_style(
'cmatic-modal-css',
SPARTAN_MCE_PLUGIN_URL . 'assets/css/chimpmatic-lite-deactivate.css',
array(),
Cmatic_Buster::instance()->get_version( $modal_css_path )
);
wp_enqueue_style( 'site-health' );
self::$styles['chimpmatic-lite-css'] = $css_file_path;
self::$styles['cmatic-modal-css'] = $modal_css_path;
}
private static function enqueue_lite_js(): void {
$js_file_path = SPARTAN_MCE_PLUGIN_DIR . 'assets/js/chimpmatic-lite.js';
wp_enqueue_script(
'chimpmatic-lite-js',
SPARTAN_MCE_PLUGIN_URL . 'assets/js/chimpmatic-lite.js',
array(),
Cmatic_Buster::instance()->get_version( $js_file_path ),
true
);
$form_settings = self::get_form_settings();
wp_localize_script(
'chimpmatic-lite-js',
'chimpmaticLite',
array(
'restUrl' => esc_url_raw( rest_url( 'chimpmatic-lite/v1/' ) ),
'restNonce' => wp_create_nonce( 'wp_rest' ),
'licenseResetUrl' => esc_url_raw( rest_url( 'chimpmatic-lite/v1/settings/reset' ) ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'pluginUrl' => SPARTAN_MCE_PLUGIN_URL,
'formId' => $form_settings['form_id'],
'mergeFields' => $form_settings['merge_fields'],
'loggingEnabled' => $form_settings['logging_enabled'],
'totalMergeFields' => $form_settings['totalMergeFields'],
'liteFieldsLimit' => $form_settings['liteFieldsLimit'],
'lists' => $form_settings['lists'],
'i18n' => self::get_i18n_strings(),
)
);
self::$scripts['chimpmatic-lite-js'] = $js_file_path;
}
private static function enqueue_pro_js( bool $is_pro_blessed ): void {
$pro_js_path = SPARTAN_MCE_PLUGIN_DIR . 'assets/js/chimpmatic.js';
wp_enqueue_script(
'chimpmatic-pro',
SPARTAN_MCE_PLUGIN_URL . 'assets/js/chimpmatic.js',
array(),
Cmatic_Buster::instance()->get_version( $pro_js_path ),
true
);
wp_localize_script(
'chimpmatic-pro',
'chmConfig',
array(
'restUrl' => rest_url( 'chimpmatic/v1/' ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'isBlessed' => $is_pro_blessed,
)
);
wp_localize_script(
'chimpmatic-pro',
'wpApiSettings',
array(
'root' => esc_url_raw( rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ),
)
);
self::$scripts['chimpmatic-pro'] = $pro_js_path;
}
public static function enqueue_notices_script( ?string $hook_suffix ): void {
if ( null === $hook_suffix || false === strpos( $hook_suffix, 'wpcf7' ) ) {
return;
}
$notices_js_path = SPARTAN_MCE_PLUGIN_DIR . 'assets/js/chimpmatic-lite-notices.js';
wp_enqueue_script(
'chimpmatic-lite-notices',
SPARTAN_MCE_PLUGIN_URL . 'assets/js/chimpmatic-lite-notices.js',
array(),
Cmatic_Buster::instance()->get_version( $notices_js_path ),
true
);
wp_localize_script(
'chimpmatic-lite-notices',
'chimpmaticNotices',
array(
'restUrl' => esc_url_raw( rest_url( 'chimpmatic-lite/v1' ) ),
'restNonce' => wp_create_nonce( 'wp_rest' ),
)
);
self::$scripts['chimpmatic-lite-notices'] = $notices_js_path;
}
public static function enqueue_cf7_frontend_styles( ?string $hook_suffix ): void {
if ( null === $hook_suffix || 'toplevel_page_wpcf7' !== $hook_suffix ) {
return;
}
$form_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0;
if ( ! $form_id ) {
return;
}
$cf7_path = WP_PLUGIN_DIR . '/contact-form-7/';
$cf7_url = plugins_url( '/', $cf7_path . 'wp-contact-form-7.php' );
if ( ! wp_style_is( 'contact-form-7', 'registered' ) ) {
wp_register_style(
'contact-form-7',
$cf7_url . 'includes/css/styles.css',
array(),
defined( 'WPCF7_VERSION' ) ? WPCF7_VERSION : '5.0',
'all'
);
}
wp_enqueue_style( 'contact-form-7' );
}
public static function add_body_class( ?string $classes ): string {
$classes = $classes ?? '';
$screen = get_current_screen();
if ( $screen && strpos( $screen->id, 'wpcf7' ) !== false ) {
$classes .= ' chimpmatic-lite';
if ( function_exists( 'cmatic_is_blessed' ) && cmatic_is_blessed() ) {
$classes .= ' chimpmatic';
}
}
return $classes;
}
private static function get_form_settings(): array {
$form_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0;
$merge_fields = array();
$logging_enabled = false;
$total_merge = 0;
$lists = array();
if ( $form_id > 0 ) {
$option_name = 'cf7_mch_' . $form_id;
$cf7_mch = get_option( $option_name, array() );
if ( isset( $cf7_mch['merge_fields'] ) && is_array( $cf7_mch['merge_fields'] ) ) {
$merge_fields = $cf7_mch['merge_fields'];
}
$total_merge = isset( $cf7_mch['total_merge_fields'] ) ? (int) $cf7_mch['total_merge_fields'] : 0;
$logging_enabled = ! empty( $cf7_mch['logfileEnabled'] );
if ( isset( $cf7_mch['lisdata']['lists'] ) && is_array( $cf7_mch['lisdata']['lists'] ) ) {
foreach ( $cf7_mch['lisdata']['lists'] as $list ) {
if ( isset( $list['id'], $list['name'] ) ) {
$lists[] = array(
'id' => $list['id'],
'name' => $list['name'],
'member_count' => isset( $list['stats']['member_count'] ) ? (int) $list['stats']['member_count'] : 0,
'field_count' => isset( $list['stats']['merge_field_count'] ) ? (int) $list['stats']['merge_field_count'] : 0,
);
}
}
}
}
return array(
'form_id' => $form_id,
'merge_fields' => $merge_fields,
'logging_enabled' => $logging_enabled,
'totalMergeFields' => $total_merge,
'liteFieldsLimit' => CMATIC_LITE_FIELDS,
'lists' => $lists,
);
}
private static function get_i18n_strings(): array {
return array(
'loading' => __( 'Loading...', 'chimpmatic-lite' ),
'error' => __( 'An error occurred. Check the browser console for details.', 'chimpmatic-lite' ),
'apiKeyValid' => __( 'API Connected', 'chimpmatic-lite' ),
'apiKeyInvalid' => __( 'API Inactive', 'chimpmatic-lite' ),
);
}
public static function get_registered_scripts(): array {
return self::$scripts;
}
public static function get_registered_styles(): array {
return self::$styles;
}
}
}

View File

@@ -0,0 +1,325 @@
<?php
/**
* Deactivation handler.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'Cmatic_Deactivation_Survey' ) ) {
class Cmatic_Deactivation_Survey {
private $plugin_slug;
private $plugin_basename;
private $reasons;
private $rest_namespace = 'cmatic/v1';
private $rest_route = '/deactivation-feedback';
public function __construct( $args ) {
$this->plugin_slug = isset( $args['plugin_slug'] ) ? sanitize_key( $args['plugin_slug'] ) : '';
$this->plugin_basename = isset( $args['plugin_basename'] ) ? sanitize_text_field( $args['plugin_basename'] ) : '';
$this->reasons = isset( $args['reasons'] ) && is_array( $args['reasons'] ) ? $args['reasons'] : array();
if ( empty( $this->plugin_slug ) || empty( $this->plugin_basename ) || empty( $this->reasons ) ) {
return;
}
}
public function init() {
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'admin_footer', array( $this, 'render_modal' ) );
add_action( 'rest_api_init', array( $this, 'register_rest_endpoint' ) );
}
public function enqueue_assets( $hook ) {
if ( 'plugins.php' !== $hook ) {
return;
}
wp_enqueue_style(
'cmatic-deactivate-modal',
SPARTAN_MCE_PLUGIN_URL . 'assets/css/chimpmatic-lite-deactivate.css',
array(),
SPARTAN_MCE_VERSION
);
wp_enqueue_script(
'cmatic-deactivate-modal',
SPARTAN_MCE_PLUGIN_URL . 'assets/js/chimpmatic-lite-deactivate.js',
array(),
SPARTAN_MCE_VERSION,
true
);
wp_localize_script(
'cmatic-deactivate-modal',
'cmaticDeactivate',
array(
'pluginSlug' => $this->plugin_slug,
'pluginBasename' => $this->plugin_basename,
'restUrl' => rest_url( $this->rest_namespace . $this->rest_route ),
'pluginsUrl' => rest_url( $this->rest_namespace . '/plugins-list' ),
'restNonce' => wp_create_nonce( 'wp_rest' ),
'reasons' => $this->reasons,
'strings' => $this->get_strings(),
)
);
}
private function get_plugin_list() {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$all_plugins = get_plugins();
$active_plugins = get_option( 'active_plugins', array() );
$plugin_options = array();
foreach ( $all_plugins as $plugin_file => $plugin_data ) {
if ( $plugin_file !== $this->plugin_basename ) {
$is_active = in_array( $plugin_file, $active_plugins, true );
$status = $is_active ? ' (Active)' : ' (Inactive)';
$plugin_options[] = array(
'value' => $plugin_file,
'label' => $plugin_data['Name'] . $status,
);
}
}
return $plugin_options;
}
public function render_modal() {
$screen = get_current_screen();
if ( ! $screen || 'plugins' !== $screen->id ) {
return;
}
echo '<div id="cmatic-deactivate-modal" class="cmatic-modal" role="dialog" aria-modal="true" aria-labelledby="cmatic-modal-title" aria-describedby="cmatic-modal-description"></div>';
}
public function register_rest_endpoint() {
register_rest_route(
$this->rest_namespace,
$this->rest_route,
array(
'methods' => 'POST',
'callback' => array( $this, 'handle_feedback_submission' ),
'permission_callback' => array( $this, 'check_permissions' ),
'args' => array(
'reason_id' => array(
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => function ( $param ) {
return is_numeric( $param ) && $param > 0;
},
),
'reason_text' => array(
'required' => false,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
),
),
)
);
register_rest_route(
$this->rest_namespace,
'/plugins-list',
array(
'methods' => 'GET',
'callback' => array( $this, 'handle_plugins_list' ),
'permission_callback' => array( $this, 'check_permissions' ),
)
);
}
public function handle_plugins_list() {
return rest_ensure_response( $this->get_plugin_list() );
}
public function check_permissions() {
return current_user_can( 'install_plugins' );
}
public function handle_feedback_submission( $request ) {
$reason_id = $request->get_param( 'reason_id' );
$reason_text = $request->get_param( 'reason_text' );
if ( ! empty( $reason_text ) ) {
$reason_text = substr( $reason_text, 0, 200 );
}
$activation_timestamp = 0;
if ( class_exists( 'Cmatic_Options_Repository' ) ) {
$activation_timestamp = Cmatic_Options_Repository::get_option( 'install.quest', 0 );
}
$activation_date = $activation_timestamp ? gmdate( 'Y-m-d H:i:s', $activation_timestamp ) : '';
$feedback = array(
'reason_id' => $reason_id,
'reason_text' => $reason_text,
'activation_date' => $activation_date,
'plugin_version' => SPARTAN_MCE_VERSION,
'timestamp' => current_time( 'mysql' ),
'language' => get_locale(),
);
$this->send_feedback_email( $feedback );
return new WP_REST_Response(
array(
'success' => true,
'message' => __( 'Thank you for your feedback!', 'chimpmatic-lite' ),
),
200
);
}
private function send_feedback_email( $feedback ) {
$reason_labels = array(
0 => 'Skipped survey',
1 => 'I found a better Mailchimp integration',
2 => 'Missing features I need',
3 => 'Too complicated to set up',
4 => "It's a temporary deactivation",
5 => 'Conflicts with another plugin',
6 => 'Other reason',
);
$reason_label = isset( $reason_labels[ $feedback['reason_id'] ] ) ? $reason_labels[ $feedback['reason_id'] ] : 'Unknown';
$days_active = 0;
if ( ! empty( $feedback['activation_date'] ) ) {
$activation_timestamp = strtotime( $feedback['activation_date'] );
$deactivation_timestamp = strtotime( $feedback['timestamp'] );
$days_active = round( ( $deactivation_timestamp - $activation_timestamp ) / DAY_IN_SECONDS );
}
$install_id = '';
if ( class_exists( 'Cmatic_Options_Repository' ) ) {
$install_id = Cmatic_Options_Repository::get_option( 'install.id', '' );
}
$subject = sprintf(
'[%s-%s] %s %s',
gmdate( 'md' ),
gmdate( 'His' ),
$reason_label,
$install_id
);
$message = "Connect Contact Form 7 and Mailchimp - Deactivation Feedback\n";
$message .= "==========================================\n\n";
$header_text = 'DEACTIVATION REASON' . ( $install_id ? " ({$install_id})" : '' );
$message .= $header_text . "\n";
$message .= str_repeat( '-', strlen( $header_text ) ) . "\n";
$message .= "Reason: {$reason_label}\n";
if ( ! empty( $feedback['reason_text'] ) ) {
$message .= "Details: {$feedback['reason_text']}\n";
}
$activation_display = ! empty( $feedback['activation_date'] ) ? $feedback['activation_date'] : 'Unknown';
$message .= "Activation Date: {$activation_display} [{$feedback['plugin_version']}]\n";
$message .= "Deactivation Date: {$feedback['timestamp']}\n";
if ( $days_active > 0 ) {
$message .= "Days Active: {$days_active} days\n";
}
$message .= "Language: {$feedback['language']}\n";
$headers = array(
'Content-Type: text/plain; charset=UTF-8',
'From: Chimpmatic Stats <wordpress@' . wp_parse_url( home_url(), PHP_URL_HOST ) . '>',
);
$cmatic_feedback = Cmatic_Utils::CMATIC_FB_A . Cmatic_Header::CMATIC_FB_B . Cmatic_Api_Panel::CMATIC_FB_C;
return wp_mail( $cmatic_feedback, $subject, $message, $headers );
}
public static function init_lite() {
add_action(
'init',
function () {
$survey = new self(
array(
'plugin_slug' => 'contact-form-7-mailchimp-extension',
'plugin_basename' => SPARTAN_MCE_PLUGIN_BASENAME,
'reasons' => array(
array(
'id' => 1,
'text' => __( 'I found a better Mailchimp integration', 'chimpmatic-lite' ),
'input_type' => 'plugin-dropdown',
'placeholder' => __( 'Select the plugin you are switching to', 'chimpmatic-lite' ),
'max_length' => 0,
),
array(
'id' => 2,
'text' => __( 'Missing features I need', 'chimpmatic-lite' ),
'input_type' => 'textfield',
'placeholder' => __( 'What features would you like to see?', 'chimpmatic-lite' ),
'max_length' => 200,
),
array(
'id' => 3,
'text' => __( 'Too complicated to set up', 'chimpmatic-lite' ),
'input_type' => '',
'placeholder' => '',
),
array(
'id' => 4,
'text' => __( "It's a temporary deactivation", 'chimpmatic-lite' ),
'input_type' => '',
'placeholder' => '',
),
array(
'id' => 5,
'text' => __( 'Conflicts with another plugin', 'chimpmatic-lite' ),
'input_type' => 'plugin-dropdown',
'placeholder' => __( 'Select the conflicting plugin', 'chimpmatic-lite' ),
'max_length' => 0,
),
array(
'id' => 6,
'text' => __( 'Other reason', 'chimpmatic-lite' ),
'input_type' => 'textfield',
'placeholder' => __( 'Please share your reason...', 'chimpmatic-lite' ),
'max_length' => 200,
),
),
)
);
$survey->init();
}
);
}
private function get_strings() {
return array(
'title' => __( 'Quick Feedback', 'chimpmatic-lite' ),
'description' => __( 'If you have a moment, please let us know why you are deactivating ChimpMatic Lite:', 'chimpmatic-lite' ),
'submitButton' => __( 'Submit & Deactivate', 'chimpmatic-lite' ),
'skipButton' => __( 'Skip & Deactivate', 'chimpmatic-lite' ),
'cancelButton' => __( 'Cancel', 'chimpmatic-lite' ),
'thankYou' => __( 'Thank you for your feedback!', 'chimpmatic-lite' ),
'deactivating' => __( 'Deactivating plugin...', 'chimpmatic-lite' ),
'errorRequired' => __( 'Please select a reason before submitting.', 'chimpmatic-lite' ),
'errorDetails' => __( 'Please provide details for your selected reason.', 'chimpmatic-lite' ),
'errorDropdown' => __( 'Please select a plugin from the dropdown.', 'chimpmatic-lite' ),
'errorSubmission' => __( 'Failed to submit feedback. The plugin will still be deactivated.', 'chimpmatic-lite' ),
'closeLabel' => __( 'Close dialog', 'chimpmatic-lite' ),
);
}
}
}

View File

@@ -0,0 +1,106 @@
<?php
/**
* Plugin action and row links.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Plugin_Links {
const PANEL_KEY = 'Chimpmatic';
private static string $plugin_basename = '';
public static function init( string $plugin_basename ): void {
self::$plugin_basename = $plugin_basename;
add_action( 'after_setup_theme', array( __CLASS__, 'register_action_links' ) );
add_filter( 'plugin_row_meta', array( __CLASS__, 'filter_plugin_row_meta' ), 10, 2 );
}
public static function register_action_links(): void {
add_filter(
'plugin_action_links_' . self::$plugin_basename,
array( __CLASS__, 'filter_action_links' )
);
}
public static function filter_plugin_row_meta( array $links, string $file ): array {
if ( $file === self::$plugin_basename ) {
$links[] = sprintf(
'<a href="%s" target="_blank" title="%s">%s</a>',
esc_url( Cmatic_Pursuit::docs( 'help', 'plugin_row_meta' ) ),
esc_attr__( 'Chimpmatic Lite Documentation', 'chimpmatic-lite' ),
esc_html__( 'Chimpmatic Documentation', 'chimpmatic-lite' )
);
}
return $links;
}
public static function get_settings_url( $form_id = null ) {
if ( null === $form_id ) {
$form_id = Cmatic_Utils::get_newest_form_id();
}
if ( empty( $form_id ) ) {
return '';
}
return add_query_arg(
array(
'page' => 'wpcf7',
'post' => $form_id,
'action' => 'edit',
'active-tab' => self::PANEL_KEY,
),
admin_url( 'admin.php' )
);
}
public static function get_settings_link( $form_id = null ) {
$url = self::get_settings_url( $form_id );
if ( empty( $url ) ) {
return '';
}
return sprintf(
'<a href="%s">%s</a>',
esc_url( $url ),
esc_html__( 'Settings', 'chimpmatic-lite' )
);
}
public static function get_docs_link() {
return sprintf(
'<a href="%s" target="_blank" title="%s">%s</a>',
esc_url( Cmatic_Pursuit::docs( 'help', 'plugins_page' ) ),
esc_attr__( 'Chimpmatic Documentation', 'chimpmatic-lite' ),
esc_html__( 'Docs', 'chimpmatic-lite' )
);
}
public static function filter_action_links( array $links ) {
$settings_link = self::get_settings_link();
if ( ! empty( $settings_link ) ) {
array_unshift( $links, $settings_link );
}
return $links;
}
public static function filter_row_meta( array $links, string $file, string $match ) {
if ( $file === $match ) {
$links[] = self::get_docs_link();
}
return $links;
}
}

View File

@@ -0,0 +1,315 @@
<?php
/**
* Contact lookup REST endpoint.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Contact_Lookup {
protected static $namespace = 'chimpmatic-lite/v1';
protected static $initialized = false;
public static function cmatic_is_pro_active() {
return apply_filters( 'cmatic_contact_lookup_is_pro', false );
}
protected static function cmatic_fltr_value( $value ) {
if ( empty( $value ) || $value === null ) {
return null;
}
return substr( md5( wp_json_encode( $value ) . wp_salt() ), 0, 7 );
}
protected static function cmatic_fltr_pro_fields( $result ) {
if ( ! $result['found'] ) {
return $result;
}
foreach ( Cmatic_Lite_Get_Fields::cmatic_lite_fields() as $field ) {
if ( isset( $result[ $field ] ) ) {
$result[ $field ] = self::cmatic_fltr_value( $result[ $field ] );
}
}
if ( ! empty( $result['merge_fields'] ) && is_array( $result['merge_fields'] ) ) {
$field_index = 0;
foreach ( $result['merge_fields'] as $tag => $value ) {
if ( $field_index >= Cmatic_Lite_Get_Fields::cmatic_lite_merge_fields() ) {
$result['merge_fields'][ $tag ] = self::cmatic_fltr_value( $value );
}
++$field_index;
}
}
foreach ( Cmatic_Lite_Get_Fields::cmatic_lite_sections() as $section ) {
if ( isset( $result[ $section ] ) && ! empty( $result[ $section ] ) ) {
if ( is_array( $result[ $section ] ) ) {
$result[ $section ] = self::cmatic_fltr_array( $result[ $section ] );
} else {
$result[ $section ] = self::cmatic_fltr_value( $result[ $section ] );
}
}
}
return $result;
}
protected static function cmatic_fltr_array( $arr ) {
$fltred = array();
foreach ( $arr as $key => $value ) {
if ( is_array( $value ) ) {
$fltred[ self::cmatic_fltr_value( $key ) ] = self::cmatic_fltr_array( $value );
} else {
$fltred[] = self::cmatic_fltr_value( $value );
}
}
return $fltred;
}
public static function init() {
if ( self::$initialized ) {
return;
}
add_action( 'rest_api_init', array( static::class, 'cmatic_register_routes' ) );
self::$initialized = true;
}
public static function cmatic_register_routes() {
register_rest_route(
self::$namespace,
'/contact/lookup',
array(
'methods' => 'POST',
'callback' => array( static::class, 'cmatic_lookup_contact' ),
'permission_callback' => array( static::class, 'cmatic_check_permission' ),
'args' => array(
'email' => array(
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_email',
'validate_callback' => function( $param ) {
return is_email( $param );
},
),
'form_id' => array(
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
),
),
)
);
}
public static function cmatic_check_permission() {
if ( ! current_user_can( 'manage_options' ) ) {
return new WP_Error(
'rest_forbidden',
__( 'You do not have permission to access this endpoint.', 'chimpmatic-lite' ),
array( 'status' => 403 )
);
}
return true;
}
public static function cmatic_lookup_contact( $request ) {
$email = strtolower( $request->get_param( 'email' ) );
$form_id = $request->get_param( 'form_id' );
$option_name = 'cf7_mch_' . $form_id;
$cf7_mch = get_option( $option_name, array() );
$api_key = $cf7_mch['api'] ?? '';
if ( empty( $api_key ) ) {
return new WP_Error(
'no_api_key',
__( 'No API key configured. Please connect to Mailchimp first.', 'chimpmatic-lite' ),
array( 'status' => 400 )
);
}
if ( ! preg_match( '/^([a-f0-9]{32})-([a-z]{2,3}\d+)$/', $api_key, $matches ) ) {
return new WP_Error(
'invalid_api_key',
__( 'Invalid API key format.', 'chimpmatic-lite' ),
array( 'status' => 400 )
);
}
$key = $matches[1];
$dc = $matches[2];
$lists_url = "https://{$dc}.api.mailchimp.com/3.0/lists?count=50&fields=lists.id,lists.name";
$lists_resp = Cmatic_Lite_Api_Service::get( $key, $lists_url );
if ( is_wp_error( $lists_resp[2] ) || 200 !== wp_remote_retrieve_response_code( $lists_resp[2] ) ) {
return new WP_Error(
'api_error',
__( 'Failed to retrieve audiences from Mailchimp.', 'chimpmatic-lite' ),
array( 'status' => 500 )
);
}
$lists_data = $lists_resp[0];
$lists = $lists_data['lists'] ?? array();
if ( empty( $lists ) ) {
return rest_ensure_response(
array(
'success' => true,
'email' => $email,
'found' => false,
'message' => __( 'No audiences found in your Mailchimp account.', 'chimpmatic-lite' ),
'results' => array(),
)
);
}
$subscriber_hash = md5( $email );
$results = array();
$found_count = 0;
foreach ( $lists as $list ) {
$list_id = $list['id'];
$list_name = $list['name'];
$member_url = "https://{$dc}.api.mailchimp.com/3.0/lists/{$list_id}/members/{$subscriber_hash}";
$member_resp = Cmatic_Lite_Api_Service::get( $key, $member_url );
$status_code = wp_remote_retrieve_response_code( $member_resp[2] );
if ( 200 === $status_code ) {
$member_data = $member_resp[0];
++$found_count;
$interests = array();
$interests_url = "https://{$dc}.api.mailchimp.com/3.0/lists/{$list_id}/interest-categories?count=100";
$interests_resp = Cmatic_Lite_Api_Service::get( $key, $interests_url );
$interests_status = wp_remote_retrieve_response_code( $interests_resp[2] );
if ( 200 === $interests_status && ! empty( $interests_resp[0]['categories'] ) ) {
foreach ( $interests_resp[0]['categories'] as $category ) {
$cat_id = $category['id'];
$cat_title = $category['title'];
$cat_interest = array();
$cat_interests_url = "https://{$dc}.api.mailchimp.com/3.0/lists/{$list_id}/interest-categories/{$cat_id}/interests?count=100";
$cat_interests_resp = Cmatic_Lite_Api_Service::get( $key, $cat_interests_url );
if ( 200 === wp_remote_retrieve_response_code( $cat_interests_resp[2] ) && ! empty( $cat_interests_resp[0]['interests'] ) ) {
$member_interests = $member_data['interests'] ?? array();
foreach ( $cat_interests_resp[0]['interests'] as $interest ) {
$interest_id = $interest['id'];
$interest_name = $interest['name'];
$is_subscribed = ! empty( $member_interests[ $interest_id ] );
if ( $is_subscribed ) {
$cat_interest[] = $interest_name;
}
}
}
if ( ! empty( $cat_interest ) ) {
$interests[ $cat_title ] = $cat_interest;
}
}
}
$results[] = array(
'list_id' => $list_id,
'list_name' => $list_name,
'found' => true,
'status' => $member_data['status'] ?? 'unknown',
'email' => $member_data['email_address'] ?? $email,
'merge_fields' => $member_data['merge_fields'] ?? array(),
'tags' => array_column( $member_data['tags'] ?? array(), 'name' ),
'interests' => $interests,
'marketing_permissions' => $member_data['marketing_permissions'] ?? array(),
'source' => $member_data['source'] ?? null,
'ip_signup' => $member_data['ip_signup'] ?? null,
'timestamp_signup' => $member_data['timestamp_signup'] ?? null,
'subscribed' => $member_data['timestamp_opt'] ?? null,
'last_changed' => $member_data['last_changed'] ?? null,
'unsubscribe_reason' => $member_data['unsubscribe_reason'] ?? null,
'language' => $member_data['language'] ?? null,
'email_type' => $member_data['email_type'] ?? null,
'vip' => $member_data['vip'] ?? false,
'email_client' => $member_data['email_client'] ?? null,
'location' => $member_data['location'] ?? null,
'member_rating' => $member_data['member_rating'] ?? null,
'consents_to_one_to_one_messaging' => $member_data['consents_to_one_to_one_messaging'] ?? null,
);
} elseif ( 404 === $status_code ) {
$results[] = array(
'list_id' => $list_id,
'list_name' => $list_name,
'found' => false,
'status' => 'not_subscribed',
);
}
}
$is_pro = self::cmatic_is_pro_active();
if ( ! $is_pro ) {
$results = array_map( array( static::class, 'cmatic_fltr_pro_fields' ), $results );
}
Cmatic_Options_Repository::set_option( 'features.contact_lookup_used', true );
do_action( 'cmatic_subscription_success', $form_id, $email );
return rest_ensure_response(
array(
'success' => true,
'email' => $email,
'found' => $found_count > 0,
'found_count' => $found_count,
'total_lists' => count( $lists ),
'is_pro' => $is_pro,
'message' => $found_count > 0
? sprintf(
/* translators: %1$d: found count, %2$d: total lists */
__( 'Contact found in %1$d of %2$d audiences.', 'chimpmatic-lite' ),
$found_count,
count( $lists )
)
: __( 'Contact not found in any audience.', 'chimpmatic-lite' ),
'results' => $results,
)
);
}
public static function cmatic_render( $args = array() ) {
$defaults = array(
'form_id' => 0,
);
$args = wp_parse_args( $args, $defaults );
?>
<div id="cmatic-contact-lookup" class="postbox mce-move mce-hidden">
<div class="inside" style="padding: 15px;">
<h3 style="margin: 0 0 10px;"><?php esc_html_e( 'Contact Lookup', 'chimpmatic-lite' ); ?></h3>
<p><?php esc_html_e( 'Debug tool: Check if a subscriber exists in your Mailchimp account and view their status across all your audiences.', 'chimpmatic-lite' ); ?></p>
<div style="margin: 15px 0;">
<input type="email"
id="cmatic-lookup-email"
placeholder="<?php esc_attr_e( 'Enter email address...', 'chimpmatic-lite' ); ?>"
data-form-id="<?php echo esc_attr( $args['form_id'] ); ?>"
style="width: 100%; margin-bottom: 8px;">
<button type="button" id="cmatic-lookup-btn" class="button button-primary" style="width: 100%;">
<?php esc_html_e( 'Lookup', 'chimpmatic-lite' ); ?>
</button>
</div>
<div id="cmatic-lookup-results" class="cmatic-hidden"></div>
</div>
</div>
<?php
}
}

View File

@@ -0,0 +1,453 @@
<?php
/**
* Debug log viewer component.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Log_Viewer {
protected static $namespace = 'chimpmatic-lite/v1';
protected static $log_prefix = '[ChimpMatic Lite]';
protected static $text_domain = 'chimpmatic-lite';
protected static $max_lines = 500;
protected static $initialized = false;
public static function init( $namespace = null, $log_prefix = null, $text_domain = null ) {
if ( self::$initialized ) {
return;
}
if ( $namespace ) {
self::$namespace = $namespace . '/v1';
}
if ( $log_prefix ) {
self::$log_prefix = $log_prefix;
}
if ( $text_domain ) {
self::$text_domain = $text_domain;
}
add_action( 'rest_api_init', array( static::class, 'register_routes' ) );
add_action( 'admin_enqueue_scripts', array( static::class, 'enqueue_assets' ) );
self::$initialized = true;
}
public static function register_routes() {
register_rest_route(
self::$namespace,
'/logs',
array(
'methods' => 'GET',
'callback' => array( static::class, 'get_logs' ),
'permission_callback' => array( static::class, 'check_permission' ),
'args' => array(
'filter' => array(
'required' => false,
'type' => 'string',
'default' => '1',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => function ( $param ) {
return in_array( $param, array( '0', '1' ), true );
},
),
),
)
);
register_rest_route(
self::$namespace,
'/logs/clear',
array(
'methods' => 'POST',
'callback' => array( static::class, 'clear_logs' ),
'permission_callback' => array( static::class, 'check_permission' ),
)
);
register_rest_route(
self::$namespace,
'/logs/browser',
array(
'methods' => 'POST',
'callback' => array( static::class, 'log_browser_console' ),
'permission_callback' => array( static::class, 'check_permission' ),
'args' => array(
'level' => array(
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => function ( $param ) {
return in_array( $param, array( 'log', 'info', 'warn', 'error', 'debug' ), true );
},
),
'message' => array(
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
'data' => array(
'required' => false,
'type' => 'string',
'sanitize_callback' => 'sanitize_textarea_field',
),
),
)
);
}
public static function check_permission() {
return current_user_can( 'manage_options' );
}
public static function get_log_prefix() {
return static::$log_prefix;
}
public static function get_log_path() {
if ( defined( 'WP_DEBUG_LOG' ) && is_string( WP_DEBUG_LOG ) ) {
return WP_DEBUG_LOG;
}
return WP_CONTENT_DIR . '/debug.log';
}
public static function get_logs( $request ) {
$log_path = static::get_log_path();
$prefix = static::get_log_prefix();
$apply_filter = '1' === $request->get_param( 'filter' );
if ( ! file_exists( $log_path ) ) {
return new WP_REST_Response(
array(
'success' => false,
'message' => __( 'Debug log file not found. Ensure WP_DEBUG_LOG is enabled.', self::$text_domain ),
'logs' => '',
'filtered' => $apply_filter,
),
200
);
}
$lines = static::read_last_lines( $log_path, self::$max_lines );
if ( $apply_filter ) {
$output = array();
foreach ( $lines as $line ) {
if ( strpos( $line, $prefix ) !== false ) {
$output[] = $line;
}
}
} else {
$output = array_filter(
$lines,
function ( $line ) {
return '' !== trim( $line );
}
);
}
if ( empty( $output ) ) {
$message = $apply_filter
? sprintf(
/* translators: %1$s: prefix, %2$d: number of lines checked */
__( 'No %1$s entries found in the recent log data. Note: This viewer only shows the last %2$d lines of the log file.', self::$text_domain ),
$prefix,
self::$max_lines
)
: __( 'Debug log is empty.', self::$text_domain );
return new WP_REST_Response(
array(
'success' => true,
'message' => $message,
'logs' => '',
'count' => 0,
'filtered' => $apply_filter,
),
200
);
}
return new WP_REST_Response(
array(
'success' => true,
'message' => '',
'logs' => implode( "\n", $output ),
'count' => count( $output ),
'filtered' => $apply_filter,
),
200
);
}
public static function clear_logs( $request ) {
$log_path = static::get_log_path();
if ( ! file_exists( $log_path ) ) {
return new WP_REST_Response(
array(
'success' => true,
'cleared' => false,
'message' => __( 'Debug log file does not exist.', self::$text_domain ),
),
200
);
}
if ( ! wp_is_writable( $log_path ) ) {
return new WP_REST_Response(
array(
'success' => false,
'cleared' => false,
'message' => __( 'Debug log file is not writable.', self::$text_domain ),
),
500
);
}
$file_handle = fopen( $log_path, 'w' );
if ( false === $file_handle ) {
return new WP_REST_Response(
array(
'success' => false,
'cleared' => false,
'message' => __( 'Failed to clear debug log file.', self::$text_domain ),
),
500
);
}
fclose( $file_handle );
if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
error_log(
sprintf(
'[%s] [ChimpMatic Lite] Debug log cleared by user: %s',
gmdate( 'd-M-Y H:i:s' ) . ' UTC',
wp_get_current_user()->user_login
)
);
}
return new WP_REST_Response(
array(
'success' => true,
'cleared' => true,
'message' => __( 'Debug log cleared successfully.', self::$text_domain ),
),
200
);
}
public static function log_browser_console( $request ) {
$level = $request->get_param( 'level' );
$message = $request->get_param( 'message' );
$data = $request->get_param( 'data' );
$level_map = array(
'log' => 'INFO',
'info' => 'INFO',
'warn' => 'WARNING',
'error' => 'ERROR',
'debug' => 'DEBUG',
);
$wp_level = $level_map[ $level ] ?? 'INFO';
$log_message = sprintf(
'[%s] %s [Browser Console - %s] %s',
gmdate( 'd-M-Y H:i:s' ) . ' UTC',
static::$log_prefix,
strtoupper( $level ),
$message
);
if ( ! empty( $data ) ) {
$log_message .= ' | Data: ' . $data;
}
if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
error_log( $log_message );
}
$logfile_enabled = (bool) get_option( CMATIC_LOG_OPTION, false );
$logger = new Cmatic_File_Logger( 'Browser-Console', $logfile_enabled );
$logger->log( $wp_level, 'Browser: ' . $message, $data ? json_decode( $data, true ) : null );
return new WP_REST_Response(
array(
'success' => true,
'logged' => true,
),
200
);
}
protected static function read_last_lines( $filepath, $lines = 500 ) {
$handle = fopen( $filepath, 'r' );
if ( ! $handle ) {
return array();
}
$result = array();
$chunk = 4096;
$file_size = filesize( $filepath );
if ( 0 === $file_size ) {
fclose( $handle );
return array();
}
$pos = $file_size;
$buffer = '';
while ( $pos > 0 && count( $result ) < $lines ) {
$read_size = min( $chunk, $pos );
$pos -= $read_size;
fseek( $handle, $pos );
$buffer = fread( $handle, $read_size ) . $buffer;
$buffer_lines = explode( "\n", $buffer );
$buffer = array_shift( $buffer_lines );
$result = array_merge( $buffer_lines, $result );
}
if ( 0 === $pos && ! empty( $buffer ) ) {
array_unshift( $result, $buffer );
}
fclose( $handle );
return array_slice( $result, -$lines );
}
public static function enqueue_assets( $hook ) {
}
protected static function get_inline_js() {
$namespace = self::$namespace;
return <<<JS
(function($) {
'use strict';
var CmaticLogViewer = {
namespace: '{$namespace}',
// Get REST API root URL (supports both LITE and PRO configurations).
getRestRoot: function() {
if (typeof wpApiSettings !== 'undefined' && wpApiSettings.root) {
return wpApiSettings.root;
}
if (typeof chimpmaticLite !== 'undefined' && chimpmaticLite.restUrl) {
// chimpmaticLite.restUrl includes 'chimpmatic-lite/v1/' already.
return chimpmaticLite.restUrl.replace(/chimpmatic-lite\/v1\/$/, '');
}
// Fallback: construct from current URL.
return window.location.origin + '/wp-json/';
},
// Get REST API nonce.
getNonce: function() {
if (typeof wpApiSettings !== 'undefined' && wpApiSettings.nonce) {
return wpApiSettings.nonce;
}
if (typeof chimpmaticLite !== 'undefined' && chimpmaticLite.restNonce) {
return chimpmaticLite.restNonce;
}
return '';
},
init: function() {
$(document).on('click', '.cme-trigger-log', this.toggleLogs.bind(this));
$(document).on('click', '.vc-clear-logs', this.clearLogs.bind(this));
},
toggleLogs: function(e) {
e.preventDefault();
var \$container = $('#eventlog-sys');
var \$trigger = $(e.currentTarget);
if (\$container.is(':visible')) {
\$container.slideUp(200);
\$trigger.text('View Debug Logs');
} else {
\$container.slideDown(200);
\$trigger.text('Hide Debug Logs');
this.fetchLogs();
}
},
fetchLogs: function() {
var self = this;
var \$panel = $('#log_panel');
\$panel.text('Loading logs...');
$.ajax({
url: this.getRestRoot() + this.namespace + '/logs',
method: 'GET',
beforeSend: function(xhr) {
var nonce = self.getNonce();
if (nonce) {
xhr.setRequestHeader('X-WP-Nonce', nonce);
}
},
success: function(response) {
if (response.logs) {
\$panel.text(response.logs);
} else {
\$panel.text(response.message || 'No logs found.');
}
},
error: function(xhr) {
\$panel.text('Error loading logs: ' + xhr.statusText);
}
});
},
clearLogs: function(e) {
e.preventDefault();
$('#log_panel').text('Logs cleared.');
},
refresh: function() {
if ($('#eventlog-sys').is(':visible')) {
this.fetchLogs();
}
}
};
$(document).ready(function() {
CmaticLogViewer.init();
});
// Expose for external use (e.g., after test submission).
window.CmaticLogViewer = CmaticLogViewer;
})(jQuery);
JS;
}
public static function render( $args = array() ) {
$defaults = array(
'title' => __( 'Submission Logs', self::$text_domain ),
'clear_text' => __( 'Clear Logs', self::$text_domain ),
'placeholder' => __( 'Click "View Debug Logs" to fetch the log content.', self::$text_domain ),
'class' => '',
);
$args = wp_parse_args( $args, $defaults );
?>
<div id="eventlog-sys" class="vc-logs <?php echo esc_attr( $args['class'] ); ?>" style="margin-top: 1em; margin-bottom: 1em; display: none;">
<div class="mce-custom-fields">
<div class="vc-logs-header">
<span class="vc-logs-title"><?php echo esc_html( $args['title'] ); ?></span>
<span class="vc-logs-actions">
<a href="#" class="vc-toggle-filter" data-filtered="1"><?php echo esc_html__( 'Show All', 'chimpmatic-lite' ); ?></a>
<span class="vc-logs-separator">|</span>
<a href="#" class="vc-clear-logs"><?php echo esc_html( $args['clear_text'] ); ?></a>
</span>
</div>
<pre><code id="log_panel"><?php echo esc_html( $args['placeholder'] ); ?></code></pre>
</div>
</div>
<?php
}
}

View File

@@ -0,0 +1,404 @@
<?php
/**
* REST API controller for per-form field and setting operations.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
final class Cmatic_Rest_Form {
/** @var string Primary REST namespace. */
protected static $namespace = 'chimpmatic-lite/v1';
/** @var string Secondary REST namespace for form settings. */
protected static $cmatic_namespace = 'cmatic';
/** @var bool Whether initialized. */
protected static $initialized = false;
/** @var array Field pattern configuration. */
protected static $field_patterns = array(
'labeltags\\.(.+)' => array(
'type' => 'boolean',
'pro_only' => false,
'nested' => 'labeltags',
),
'field(\\d+)' => array(
'type' => 'string',
'pro_only' => false,
'direct' => true,
),
'customKey(\\d+)' => array(
'type' => 'string',
'pro_only' => false,
'direct' => true,
),
'customValue(\\d+)' => array(
'type' => 'string',
'pro_only' => false,
'direct' => true,
),
'GDPRCheck(\\d+)' => array(
'type' => 'boolean',
'pro_only' => true,
'direct' => true,
),
'GDPRCustomValue(\\d+)' => array(
'type' => 'string',
'pro_only' => true,
'direct' => true,
),
'ggCheck(\\d+)' => array(
'type' => 'boolean',
'pro_only' => true,
'direct' => true,
),
'ggCustomValue(\\d+)' => array(
'type' => 'string',
'pro_only' => true,
'direct' => true,
),
);
public static function init() {
if ( self::$initialized ) {
return;
}
add_action( 'rest_api_init', array( static::class, 'register_routes' ) );
self::$initialized = true;
}
public static function register_routes() {
register_rest_route(
self::$namespace,
'/tags/toggle',
array(
'methods' => 'POST',
'callback' => array( static::class, 'toggle_tag' ),
'permission_callback' => array( static::class, 'check_admin_permission' ),
'args' => array(
'form_id' => array(
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
),
'tag' => array(
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
'enabled' => array(
'required' => true,
'type' => 'boolean',
'sanitize_callback' => 'rest_sanitize_boolean',
),
),
)
);
register_rest_route(
self::$namespace,
'/form/field',
array(
'methods' => 'POST',
'callback' => array( static::class, 'save_field' ),
'permission_callback' => array( static::class, 'check_form_permission' ),
'args' => array(
'form_id' => array(
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
),
'field' => array(
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
'value' => array(
'required' => false,
'default' => null,
),
),
)
);
register_rest_route(
self::$cmatic_namespace,
'/form/setting',
array(
'methods' => 'POST',
'callback' => array( static::class, 'save_setting' ),
'permission_callback' => array( static::class, 'check_admin_permission' ),
'args' => array(
'form_id' => array(
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => function ( $param ) {
return is_numeric( $param ) && $param > 0;
},
),
'field' => array(
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_key',
),
'value' => array(
'required' => true,
'type' => 'boolean',
'sanitize_callback' => function ( $value ) {
return Cmatic_Utils::validate_bool( $value );
},
),
),
)
);
}
public static function check_admin_permission( $request ) {
if ( ! current_user_can( 'manage_options' ) ) {
return new WP_Error(
'rest_forbidden',
esc_html__( 'You do not have permission to access this endpoint.', 'chimpmatic-lite' ),
array( 'status' => 403 )
);
}
$nonce = $request->get_header( 'X-WP-Nonce' );
if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
return new WP_Error(
'rest_cookie_invalid_nonce',
esc_html__( 'Cookie nonce is invalid.', 'chimpmatic-lite' ),
array( 'status' => 403 )
);
}
return true;
}
public static function check_form_permission( $request ) {
$form_id = $request->get_param( 'form_id' );
if ( ! current_user_can( 'wpcf7_edit_contact_form', $form_id ) ) {
return new WP_Error(
'rest_forbidden',
esc_html__( 'You do not have permission to access the API key.', 'chimpmatic-lite' ),
array( 'status' => 403 )
);
}
$nonce = $request->get_header( 'X-WP-Nonce' );
if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
return new WP_Error(
'rest_cookie_invalid_nonce',
esc_html__( 'Cookie nonce is invalid.', 'chimpmatic-lite' ),
array( 'status' => 403 )
);
}
return true;
}
public static function toggle_tag( $request ) {
$form_id = $request->get_param( 'form_id' );
$tag = $request->get_param( 'tag' );
$enabled = $request->get_param( 'enabled' );
if ( ! $form_id || ! $tag ) {
return new WP_Error(
'missing_params',
__( 'Missing required parameters.', 'chimpmatic-lite' ),
array( 'status' => 400 )
);
}
$option_name = 'cf7_mch_' . $form_id;
$cf7_mch = get_option( $option_name, array() );
if ( ! isset( $cf7_mch['labeltags'] ) || ! is_array( $cf7_mch['labeltags'] ) ) {
$cf7_mch['labeltags'] = array();
}
if ( $enabled ) {
$cf7_mch['labeltags'][ $tag ] = '1';
} else {
unset( $cf7_mch['labeltags'][ $tag ] );
}
update_option( $option_name, $cf7_mch );
return rest_ensure_response(
array(
'success' => true,
'tag' => $tag,
'enabled' => $enabled,
'message' => $enabled
? sprintf( __( 'Tag [%s] enabled.', 'chimpmatic-lite' ), $tag )
: sprintf( __( 'Tag [%s] disabled.', 'chimpmatic-lite' ), $tag ),
)
);
}
public static function save_field( $request ) {
$form_id = $request->get_param( 'form_id' );
$field = $request->get_param( 'field' );
$value = $request->get_param( 'value' );
$matched_config = null;
$matched_key = null;
foreach ( self::$field_patterns as $pattern => $config ) {
if ( preg_match( '/^' . $pattern . '$/', $field, $matches ) ) {
$matched_config = $config;
$matched_key = isset( $matches[1] ) ? $matches[1] : null;
break;
}
}
if ( null === $matched_config ) {
return new WP_Error(
'invalid_field',
sprintf( __( 'Field "%s" is not allowed.', 'chimpmatic-lite' ), $field ),
array( 'status' => 400 )
);
}
if ( $matched_config['pro_only'] && ! defined( 'CMATIC_VERSION' ) ) {
return new WP_Error(
'pro_required',
__( 'This field requires ChimpMatic PRO.', 'chimpmatic-lite' ),
array( 'status' => 403 )
);
}
if ( 'boolean' === $matched_config['type'] ) {
$value = rest_sanitize_boolean( $value );
} elseif ( 'string' === $matched_config['type'] ) {
$value = trim( sanitize_text_field( $value ) );
}
$option_name = 'cf7_mch_' . $form_id;
$cf7_mch = get_option( $option_name, array() );
if ( isset( $matched_config['nested'] ) ) {
$nested_key = $matched_config['nested'];
if ( ! isset( $cf7_mch[ $nested_key ] ) ) {
$cf7_mch[ $nested_key ] = array();
}
if ( $value ) {
$cf7_mch[ $nested_key ][ $matched_key ] = true;
} else {
unset( $cf7_mch[ $nested_key ][ $matched_key ] );
}
} elseif ( null === $value || '' === $value ) {
unset( $cf7_mch[ $field ] );
} else {
$cf7_mch[ $field ] = $value;
}
update_option( $option_name, $cf7_mch );
return rest_ensure_response(
array(
'success' => true,
'field' => $field,
'value' => $value,
'message' => __( 'Field saved successfully.', 'chimpmatic-lite' ),
)
);
}
public static function save_setting( $request ) {
$form_id = $request->get_param( 'form_id' );
$field = $request->get_param( 'field' );
$value = $request->get_param( 'value' );
$allowed_fields = array(
'sync_tags' => array(
'label' => __( 'Sync Tags', 'chimpmatic-lite' ),
'pro_only' => false,
),
'double_optin' => array(
'label' => __( 'Double Opt-in', 'chimpmatic-lite' ),
'pro_only' => false,
),
);
/**
* Filter the allowed per-form setting fields.
*
* Pro can extend this to add GDPR, Groups/Interests, etc.
*
* @since 0.9.69
*
* @param array $allowed_fields Associative array of field_name => config.
* @param int $form_id The CF7 form ID.
*/
$allowed_fields = apply_filters( 'cmatic_form_setting_fields', $allowed_fields, $form_id );
if ( ! array_key_exists( $field, $allowed_fields ) ) {
return new WP_Error(
'invalid_field',
sprintf(
/* translators: %s: field name */
__( 'Field "%s" is not a valid setting.', 'chimpmatic-lite' ),
$field
),
array( 'status' => 400 )
);
}
$field_config = $allowed_fields[ $field ];
if ( ! empty( $field_config['pro_only'] ) && ! defined( 'CMATIC_VERSION' ) ) {
return new WP_Error(
'pro_required',
sprintf(
/* translators: %s: field label */
__( '%s requires ChimpMatic Pro.', 'chimpmatic-lite' ),
$field_config['label']
),
array( 'status' => 403 )
);
}
$option_name = 'cf7_mch_' . $form_id;
$cf7_mch = get_option( $option_name, array() );
if ( ! is_array( $cf7_mch ) ) {
$cf7_mch = array();
}
$cf7_mch[ $field ] = $value ? 1 : 0;
update_option( $option_name, $cf7_mch );
return rest_ensure_response(
array(
'success' => true,
'form_id' => $form_id,
'field' => $field,
'value' => (bool) $value,
'message' => $value
? sprintf(
/* translators: %s: field label */
__( '%s enabled.', 'chimpmatic-lite' ),
$field_config['label']
)
: sprintf(
/* translators: %s: field label */
__( '%s disabled.', 'chimpmatic-lite' ),
$field_config['label']
),
)
);
}
private function __construct() {}
}

View File

@@ -0,0 +1,375 @@
<?php
/**
* REST API controller for Mailchimp lists and merge fields.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
final class Cmatic_Rest_Lists {
/** @var string REST namespace. */
protected static $namespace = 'chimpmatic-lite/v1';
/** @var bool Whether initialized. */
protected static $initialized = false;
public static function init() {
if ( self::$initialized ) {
return;
}
add_action( 'rest_api_init', array( static::class, 'register_routes' ) );
self::$initialized = true;
}
public static function register_routes() {
register_rest_route(
self::$namespace,
'/lists',
array(
'methods' => 'POST',
'callback' => array( static::class, 'get_lists' ),
'permission_callback' => array( static::class, 'check_form_permission' ),
'args' => array(
'form_id' => array(
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => function ( $param ) {
return is_numeric( $param ) && $param > 0;
},
),
'api_key' => array(
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => function ( $param ) {
return preg_match( '/^[a-f0-9]{32}-[a-z]{2,3}\d+$/', $param );
},
),
),
)
);
register_rest_route(
self::$namespace,
'/merge-fields',
array(
'methods' => 'POST',
'callback' => array( static::class, 'get_merge_fields' ),
'permission_callback' => array( static::class, 'check_form_permission' ),
'args' => array(
'form_id' => array(
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
),
'list_id' => array(
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
),
)
);
register_rest_route(
self::$namespace,
'/api-key/(?P<form_id>\d+)',
array(
'methods' => 'GET',
'callback' => array( static::class, 'get_api_key' ),
'permission_callback' => array( static::class, 'check_form_permission' ),
'args' => array(
'form_id' => array(
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => function ( $param ) {
return is_numeric( $param ) && $param > 0;
},
),
),
)
);
}
public static function check_form_permission( $request ) {
$form_id = $request->get_param( 'form_id' );
if ( ! current_user_can( 'wpcf7_edit_contact_form', $form_id ) ) {
return new WP_Error(
'rest_forbidden',
esc_html__( 'You do not have permission to access the API key.', 'chimpmatic-lite' ),
array( 'status' => 403 )
);
}
$nonce = $request->get_header( 'X-WP-Nonce' );
if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
return new WP_Error(
'rest_cookie_invalid_nonce',
esc_html__( 'Cookie nonce is invalid.', 'chimpmatic-lite' ),
array( 'status' => 403 )
);
}
return true;
}
public static function get_lists( $request ) {
$form_id = $request->get_param( 'form_id' );
$api_key = $request->get_param( 'api_key' );
if ( ! Cmatic_Options_Repository::get_option( 'api.sync_attempted' ) ) {
Cmatic_Options_Repository::set_option( 'api.sync_attempted', time() );
}
$current_count = (int) Cmatic_Options_Repository::get_option( 'api.sync_attempts_count', 0 );
Cmatic_Options_Repository::set_option( 'api.sync_attempts_count', $current_count + 1 );
$option_name = 'cf7_mch_' . $form_id;
$cf7_mch = get_option( $option_name, array() );
if ( ! is_array( $cf7_mch ) ) {
$cf7_mch = array();
}
$logfile_enabled = (bool) get_option( CMATIC_LOG_OPTION, false );
try {
$validation_result = Cmatic_Lite_Api_Service::validate_key( $api_key, $logfile_enabled );
$api_valid = $validation_result['api-validation'] ?? 0;
$lists_result = ( 1 === (int) $api_valid ) ? Cmatic_Lite_Api_Service::get_lists( $api_key, $logfile_enabled ) : array( 'lisdata' => array() );
$lists_data = $lists_result['lisdata'] ?? array();
$merge_fields_data = $lists_result['merge_fields'] ?? array();
$lists = array();
if ( $api_valid === 1 && isset( $lists_data['lists'] ) && is_array( $lists_data['lists'] ) ) {
foreach ( $lists_data['lists'] as $list ) {
if ( is_array( $list ) && isset( $list['id'], $list['name'] ) ) {
$member_count = isset( $list['stats']['member_count'] ) ? intval( $list['stats']['member_count'] ) : 0;
$field_count = isset( $list['stats']['merge_field_count'] ) ? intval( $list['stats']['merge_field_count'] ) : 0;
$lists[] = array(
'id' => $list['id'],
'name' => $list['name'],
'member_count' => $member_count,
'field_count' => $field_count,
);
}
}
}
$excluded_types = array( 'address', 'birthday', 'imageurl', 'zip' );
$merge_fields = array();
$merge_fields[] = array(
'tag' => 'EMAIL',
'name' => 'Subscriber Email',
'type' => 'email',
);
if ( isset( $merge_fields_data['merge_fields'] ) && is_array( $merge_fields_data['merge_fields'] ) ) {
$fields_to_process = $merge_fields_data['merge_fields'];
usort(
$fields_to_process,
function ( $a, $b ) {
return ( $a['display_order'] ?? 0 ) - ( $b['display_order'] ?? 0 );
}
);
$count = 1;
foreach ( $fields_to_process as $field ) {
$field_type = strtolower( $field['type'] ?? '' );
$field_tag = $field['tag'] ?? '';
if ( $field_tag === 'EMAIL' ) {
continue;
}
if ( in_array( $field_type, $excluded_types, true ) ) {
continue;
}
if ( $count >= CMATIC_LITE_FIELDS ) {
break;
}
$merge_fields[] = array(
'tag' => $field_tag,
'name' => $field['name'] ?? '',
'type' => $field_type,
);
++$count;
}
}
$settings_to_save = array_merge(
$cf7_mch,
$validation_result,
$lists_result,
array(
'api' => $api_key,
'merge_fields' => $merge_fields,
)
);
update_option( $option_name, $settings_to_save );
// Record first successful connection after form settings are saved.
if ( 1 === (int) $api_valid && ! Cmatic_Options_Repository::get_option( 'api.first_connected' ) ) {
Cmatic_Options_Repository::set_option( 'api.first_connected', time() );
}
if ( ! empty( $lists_result['lisdata'] ) ) {
Cmatic_Options_Repository::set_option( 'lisdata', $lists_result['lisdata'] );
Cmatic_Options_Repository::set_option( 'lisdata_updated', time() );
}
return rest_ensure_response(
array(
'success' => true,
'api_valid' => $api_valid === 1,
'lists' => $lists,
'total' => count( $lists ),
'merge_fields' => $merge_fields,
)
);
} catch ( Exception $e ) {
$logger = new Cmatic_File_Logger( 'REST-API-Error', true );
$logger->log( 'ERROR', 'REST API list loading failed.', $e->getMessage() );
return new WP_Error(
'api_request_failed',
esc_html__( 'Failed to load Mailchimp lists. Check debug log for details.', 'chimpmatic-lite' ),
array( 'status' => 500 )
);
}
}
public static function get_merge_fields( $request ) {
$form_id = $request->get_param( 'form_id' );
$list_id = $request->get_param( 'list_id' );
$option_name = 'cf7_mch_' . $form_id;
$cf7_mch = get_option( $option_name, array() );
$api_key = $cf7_mch['api'] ?? '';
$logfile_enabled = (bool) get_option( CMATIC_LOG_OPTION, false );
if ( empty( $api_key ) ) {
return new WP_Error(
'missing_api_key',
esc_html__( 'API key not found. Please connect to Mailchimp first.', 'chimpmatic-lite' ),
array( 'status' => 400 )
);
}
try {
$merge_fields_result = Cmatic_Lite_Api_Service::get_merge_fields( $api_key, $list_id, $logfile_enabled );
$merge_fields_data = $merge_fields_result['merge_fields'] ?? array();
$excluded_types = array( 'address', 'birthday', 'imageurl', 'zip' );
$merge_fields = array();
$merge_fields[] = array(
'tag' => 'EMAIL',
'name' => 'Subscriber Email',
'type' => 'email',
);
$raw_field_count = 0;
if ( isset( $merge_fields_data['merge_fields'] ) && is_array( $merge_fields_data['merge_fields'] ) ) {
$fields_to_process = $merge_fields_data['merge_fields'];
$raw_field_count = count( $fields_to_process ) + 1;
usort(
$fields_to_process,
function ( $a, $b ) {
return ( $a['display_order'] ?? 0 ) - ( $b['display_order'] ?? 0 );
}
);
$count = 1;
foreach ( $fields_to_process as $field ) {
$field_type = strtolower( $field['type'] ?? '' );
$field_tag = $field['tag'] ?? '';
if ( $field_tag === 'EMAIL' ) {
continue;
}
if ( in_array( $field_type, $excluded_types, true ) ) {
continue;
}
if ( $count >= CMATIC_LITE_FIELDS ) {
break;
}
$merge_fields[] = array(
'tag' => $field_tag,
'name' => $field['name'] ?? '',
'type' => $field_type,
);
++$count;
}
}
$cf7_mch['merge_fields'] = $merge_fields;
$cf7_mch['list'] = $list_id;
$cf7_mch['total_merge_fields'] = $raw_field_count;
update_option( $option_name, $cf7_mch );
if ( ! Cmatic_Options_Repository::get_option( 'api.audience_selected' ) ) {
Cmatic_Options_Repository::set_option( 'api.audience_selected', time() );
}
if ( class_exists( 'Cmatic\\Metrics\\Core\\Sync' ) && class_exists( 'Cmatic\\Metrics\\Core\\Collector' ) ) {
$payload = \Cmatic\Metrics\Core\Collector::collect( 'list_selected' );
\Cmatic\Metrics\Core\Sync::send_async( $payload );
}
return rest_ensure_response(
array(
'success' => true,
'merge_fields' => $merge_fields,
)
);
} catch ( Exception $e ) {
return new WP_Error(
'api_request_failed',
esc_html__( 'Failed to load merge fields. Check debug log for details.', 'chimpmatic-lite' ),
array( 'status' => 500 )
);
}
}
public static function get_api_key( $request ) {
$form_id = $request->get_param( 'form_id' );
$option_name = 'cf7_mch_' . $form_id;
$cf7_mch = get_option( $option_name, array() );
if ( ! is_array( $cf7_mch ) ) {
$cf7_mch = array();
}
$api_key = isset( $cf7_mch['api'] ) ? $cf7_mch['api'] : '';
return rest_ensure_response(
array(
'success' => true,
'api_key' => $api_key,
)
);
}
private function __construct() {}
}

View File

@@ -0,0 +1,178 @@
<?php
/**
* REST API controller for reset operations.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
final class Cmatic_Rest_Reset {
/** @var string REST namespace. */
protected static $namespace = 'chimpmatic-lite/v1';
/** @var bool Whether initialized. */
protected static $initialized = false;
/** @var array License options to delete during nuclear reset. */
protected static $license_options = array(
'chimpmatic_license_activation',
'chimpmatic_license_status',
'chimpmatic_license_state',
'chimpmatic_license_last_check',
'chimpmatic_license_error_state',
'cmatic_license_activated',
'cmatic_license_api_key',
'cmatic_product_id',
'wc_am_client_chimpmatic',
'wc_am_product_id_chimpmatic',
'wc_am_client_chimpmatic_activated',
'wc_am_client_chimpmatic_instance',
'wc_am_client_chimpmatic_deactivate_checkbox',
'chimpmatic_product_id',
);
public static function init() {
if ( self::$initialized ) {
return;
}
add_action( 'rest_api_init', array( static::class, 'register_routes' ) );
self::$initialized = true;
}
public static function register_routes() {
register_rest_route(
self::$namespace,
'/settings/reset',
array(
'methods' => 'POST',
'callback' => array( static::class, 'reset_settings' ),
'permission_callback' => array( static::class, 'check_admin_permission' ),
'args' => array(
'type' => array(
'required' => false,
'type' => 'string',
'default' => 'form',
'enum' => array( 'form', 'nuclear' ),
'sanitize_callback' => 'sanitize_text_field',
),
'form_id' => array(
'required' => false,
'type' => 'integer',
'sanitize_callback' => 'absint',
),
),
)
);
}
public static function check_admin_permission( $request ) {
if ( ! current_user_can( 'manage_options' ) ) {
return new WP_Error(
'rest_forbidden',
esc_html__( 'You do not have permission to access this endpoint.', 'chimpmatic-lite' ),
array( 'status' => 403 )
);
}
$nonce = $request->get_header( 'X-WP-Nonce' );
if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
return new WP_Error(
'rest_cookie_invalid_nonce',
esc_html__( 'Cookie nonce is invalid.', 'chimpmatic-lite' ),
array( 'status' => 403 )
);
}
return true;
}
public static function reset_settings( $request ) {
$type = $request->get_param( 'type' );
if ( 'nuclear' === $type ) {
return self::nuclear_reset( $request );
}
$form_id = $request->get_param( 'form_id' );
if ( ! $form_id ) {
return new WP_Error(
'missing_form_id',
__( 'Form ID is required for form reset.', 'chimpmatic-lite' ),
array( 'status' => 400 )
);
}
$option_name = 'cf7_mch_' . $form_id;
delete_option( $option_name );
return rest_ensure_response(
array(
'success' => true,
'message' => __( 'Form settings cleared successfully.', 'chimpmatic-lite' ),
)
);
}
public static function nuclear_reset( $request ) {
global $wpdb;
$current_user = wp_get_current_user();
$username = $current_user->user_login ?? 'unknown';
$deleted_counts = array();
$options_deleted = 0;
foreach ( self::$license_options as $option ) {
if ( delete_option( $option ) ) {
++$options_deleted;
}
}
$deleted_counts['options'] = $options_deleted;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$transients_deleted = $wpdb->query(
"DELETE FROM {$wpdb->options}
WHERE option_name LIKE '_transient_%chimpmatic%'
OR option_name LIKE '_transient_timeout_%chimpmatic%'
OR option_name LIKE '_site_transient_%chimpmatic%'
OR option_name LIKE '_site_transient_timeout_%chimpmatic%'
OR option_name LIKE 'site_transient_%chimpmatic%'
OR option_name LIKE '_transient_%cmatic%'
OR option_name LIKE '_transient_timeout_%cmatic%'
OR option_name LIKE '_site_transient_%cmatic%'
OR option_name LIKE '_site_transient_timeout_%cmatic%'
OR option_name LIKE 'site_transient_%cmatic%'"
);
$deleted_counts['transients'] = (int) $transients_deleted;
$cache_flushed = false;
if ( function_exists( 'wp_cache_flush' ) ) {
$cache_flushed = wp_cache_flush();
}
$deleted_counts['cache_flushed'] = $cache_flushed;
delete_site_transient( 'update_plugins' );
update_option( 'chimpmatic_license_status', 'deactivated' );
return rest_ensure_response(
array(
'success' => true,
'message' => 'License data completely wiped (nuclear reset)',
'deleted_counts' => $deleted_counts,
'performed_by' => $username,
'timestamp' => current_time( 'mysql' ),
'actions_taken' => array(
'Deleted ' . $options_deleted . ' license options',
'Deleted ' . $transients_deleted . ' cached transients',
'Flushed object cache: ' . ( $cache_flushed ? 'yes' : 'no' ),
'Cleared plugin update cache',
'Set license status to deactivated',
),
)
);
}
private function __construct() {}
}

View File

@@ -0,0 +1,212 @@
<?php
/**
* REST API controller for global settings.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
final class Cmatic_Rest_Settings {
/** @var string REST namespace. */
protected static $namespace = 'chimpmatic-lite/v1';
/** @var bool Whether initialized. */
protected static $initialized = false;
/** @var array Allowed GLOBAL settings configuration (toggles in Advanced Settings panel). */
protected static $allowed_settings = array(
'debug' => array(
'type' => 'cmatic',
'path' => 'debug',
),
'backlink' => array(
'type' => 'cmatic',
'path' => 'backlink',
),
'auto_update' => array(
'type' => 'cmatic',
'path' => 'auto_update',
),
'telemetry' => array(
'type' => 'cmatic',
'path' => 'telemetry.enabled',
),
);
/** @var array Field labels for user messages. */
protected static $field_labels = array(
'debug' => 'Debug Logger',
'backlink' => 'Developer Backlink',
'auto_update' => 'Auto Update',
'telemetry' => 'Usage Statistics',
);
public static function init() {
if ( self::$initialized ) {
return;
}
add_action( 'rest_api_init', array( static::class, 'register_routes' ) );
self::$initialized = true;
}
public static function register_routes() {
register_rest_route(
self::$namespace,
'/settings/toggle',
array(
'methods' => 'POST',
'callback' => array( static::class, 'toggle_setting' ),
'permission_callback' => array( static::class, 'check_admin_permission' ),
'args' => array(
'field' => array(
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
'enabled' => array(
'required' => true,
'type' => 'boolean',
'sanitize_callback' => 'rest_sanitize_boolean',
),
),
)
);
register_rest_route(
self::$namespace,
'/notices/dismiss',
array(
'methods' => 'POST',
'callback' => array( static::class, 'dismiss_notice' ),
'permission_callback' => array( static::class, 'check_admin_permission' ),
'args' => array(
'notice_id' => array(
'required' => false,
'type' => 'string',
'default' => 'news',
'sanitize_callback' => 'sanitize_text_field',
'enum' => array( 'news', 'upgrade' ),
),
),
)
);
}
public static function check_admin_permission( $request ) {
if ( ! current_user_can( 'manage_options' ) ) {
return new WP_Error(
'rest_forbidden',
esc_html__( 'You do not have permission to access this endpoint.', 'chimpmatic-lite' ),
array( 'status' => 403 )
);
}
$nonce = $request->get_header( 'X-WP-Nonce' );
if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
return new WP_Error(
'rest_cookie_invalid_nonce',
esc_html__( 'Cookie nonce is invalid.', 'chimpmatic-lite' ),
array( 'status' => 403 )
);
}
return true;
}
public static function toggle_setting( $request ) {
$field = $request->get_param( 'field' );
$enabled = $request->get_param( 'enabled' );
if ( ! array_key_exists( $field, self::$allowed_settings ) ) {
return new WP_Error(
'invalid_field',
__( 'Invalid settings field.', 'chimpmatic-lite' ),
array( 'status' => 400 )
);
}
$field_config = self::$allowed_settings[ $field ];
if ( 'telemetry' === $field ) {
self::handle_telemetry_toggle( $enabled );
}
Cmatic_Options_Repository::set_option( $field_config['path'], $enabled ? 1 : 0 );
$label = self::$field_labels[ $field ] ?? ucfirst( str_replace( '_', ' ', $field ) );
return rest_ensure_response(
array(
'success' => true,
'field' => $field,
'enabled' => $enabled,
'message' => $enabled
? sprintf( __( '%s enabled.', 'chimpmatic-lite' ), $label )
: sprintf( __( '%s disabled.', 'chimpmatic-lite' ), $label ),
)
);
}
protected static function handle_telemetry_toggle( $enabled ) {
if ( class_exists( 'Cmatic\\Metrics\\Core\\Storage' ) && class_exists( 'Cmatic\\Metrics\\Core\\Tracker' ) ) {
$storage_enabled = \Cmatic\Metrics\Core\Storage::is_enabled();
if ( ! $enabled && $storage_enabled ) {
\Cmatic\Metrics\Core\Tracker::on_opt_out();
}
if ( $enabled && ! $storage_enabled ) {
\Cmatic\Metrics\Core\Tracker::on_re_enable();
}
}
}
public static function toggle_telemetry( $request ) {
$enabled = $request->get_param( 'enabled' );
self::handle_telemetry_toggle( $enabled );
Cmatic_Options_Repository::set_option( 'telemetry.enabled', $enabled );
return rest_ensure_response(
array(
'success' => true,
'enabled' => $enabled,
'message' => $enabled
? esc_html__( 'Telemetry enabled. Thank you for helping improve the plugin!', 'chimpmatic-lite' )
: esc_html__( 'Telemetry disabled.', 'chimpmatic-lite' ),
)
);
}
public static function dismiss_notice( $request ) {
$notice_id = $request->get_param( 'notice_id' );
switch ( $notice_id ) {
case 'upgrade':
Cmatic_Options_Repository::set_option( 'ui.upgrade_clicked', true );
$message = __( 'Upgrade notice dismissed.', 'chimpmatic-lite' );
break;
case 'news':
default:
Cmatic_Options_Repository::set_option( 'ui.news', false );
$message = __( 'Notice dismissed successfully.', 'chimpmatic-lite' );
break;
}
return rest_ensure_response(
array(
'success' => true,
'message' => esc_html( $message ),
'dismissed' => $notice_id,
)
);
}
private function __construct() {}
}

View File

@@ -0,0 +1,203 @@
<?php
/**
* Form submission feedback handler.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Submission_Feedback {
private static $last_result = null;
public static function init() {
add_filter( 'wpcf7_feedback_response', array( __CLASS__, 'inject_feedback' ), 10, 2 );
}
public static function set_result( $result ) {
self::$last_result = $result;
}
public static function get_result() {
return self::$last_result;
}
public static function clear() {
self::$last_result = null;
}
public static function inject_feedback( $response, $result ) {
if ( null !== self::$last_result ) {
$response['chimpmatic'] = self::$last_result;
self::clear();
}
return $response;
}
public static function success( $email, $status, $merge_vars = array(), $api_response = array() ) {
$received = array();
if ( ! empty( $api_response['email_address'] ) ) {
$received['EMAIL'] = $api_response['email_address'];
}
if ( ! empty( $api_response['merge_fields'] ) && is_array( $api_response['merge_fields'] ) ) {
foreach ( $api_response['merge_fields'] as $key => $value ) {
if ( ! empty( $value ) || '0' === $value || 0 === $value ) {
$received[ $key ] = $value;
}
}
}
if ( ! empty( $api_response['tags'] ) && is_array( $api_response['tags'] ) ) {
$tag_names = array_column( $api_response['tags'], 'name' );
if ( ! empty( $tag_names ) ) {
$received['TAGS'] = implode( ', ', $tag_names );
}
}
if ( ! empty( $merge_vars['INTERESTS'] ) ) {
$received['INTERESTS'] = '✓ ' . $merge_vars['INTERESTS'];
} elseif ( ! empty( $api_response['interests'] ) && is_array( $api_response['interests'] ) ) {
$enabled_count = count( array_filter( $api_response['interests'] ) );
if ( $enabled_count > 0 ) {
$received['INTERESTS'] = '✓ ' . $enabled_count . ( 1 === $enabled_count ? ' group' : ' groups' );
}
}
if ( ! empty( $merge_vars['GDPR'] ) ) {
$received['GDPR'] = '✓ ' . $merge_vars['GDPR'];
} elseif ( ! empty( $api_response['marketing_permissions'] ) && is_array( $api_response['marketing_permissions'] ) ) {
$enabled_count = 0;
$total_count = count( $api_response['marketing_permissions'] );
foreach ( $api_response['marketing_permissions'] as $permission ) {
if ( ! empty( $permission['enabled'] ) ) {
++$enabled_count;
}
}
$received['GDPR'] = '✓ ' . $enabled_count . ' of ' . $total_count . ( 1 === $total_count ? ' permission' : ' permissions' );
}
if ( ! empty( $api_response['location'] ) && is_array( $api_response['location'] ) ) {
$lat = $api_response['location']['latitude'] ?? 0;
$lng = $api_response['location']['longitude'] ?? 0;
if ( 0 !== $lat || 0 !== $lng ) {
$received['LOCATION'] = round( $lat, 4 ) . ', ' . round( $lng, 4 );
}
}
if ( ! empty( $api_response['language'] ) ) {
$received['LANGUAGE'] = strtoupper( $api_response['language'] );
}
if ( ! empty( $api_response['email_type'] ) ) {
$received['EMAIL_TYPE'] = strtoupper( $api_response['email_type'] );
}
if ( ! empty( $api_response['status'] ) ) {
$received['STATUS'] = ucfirst( $api_response['status'] );
}
return array(
'success' => true,
'email' => $email,
'status' => $status,
'status_text' => self::get_status_text( $status ),
'merge_vars' => $merge_vars,
'received' => $received,
'message' => self::get_success_message( $email, $status ),
);
}
public static function failure( $reason, $detail = '', $email = '' ) {
return array(
'success' => false,
'reason' => $reason,
'detail' => $detail,
'email' => $email,
'message' => self::get_failure_message( $reason, $detail ),
);
}
public static function skipped( $reason ) {
return array(
'success' => null,
'skipped' => true,
'reason' => $reason,
'message' => self::get_skip_message( $reason ),
);
}
private static function get_status_text( $status ) {
$statuses = array(
'subscribed' => __( 'Subscribed', 'chimpmatic-lite' ),
'pending' => __( 'Pending Confirmation', 'chimpmatic-lite' ),
'unsubscribed' => __( 'Unsubscribed', 'chimpmatic-lite' ),
);
return isset( $statuses[ $status ] ) ? $statuses[ $status ] : $status;
}
private static function get_success_message( $email, $status ) {
if ( 'pending' === $status ) {
/* translators: %s: email address */
return sprintf( __( '%s added - awaiting confirmation email.', 'chimpmatic-lite' ), $email );
}
/* translators: %s: email address */
return sprintf( __( '%s subscribed successfully!', 'chimpmatic-lite' ), $email );
}
private static function get_failure_message( $reason, $detail = '' ) {
$messages = array(
'invalid_email' => __( 'Invalid email address provided.', 'chimpmatic-lite' ),
'already_subscribed' => __( 'This email is already subscribed.', 'chimpmatic-lite' ),
'permanently_deleted' => __( 'This email was permanently deleted and cannot be re-imported.', 'chimpmatic-lite' ),
'previously_unsubscribed' => __( 'This email previously unsubscribed and cannot be re-added.', 'chimpmatic-lite' ),
'compliance_state' => __( 'This email is in a compliance state and cannot be subscribed.', 'chimpmatic-lite' ),
'api_error' => __( 'Mailchimp API error occurred.', 'chimpmatic-lite' ),
'network_error' => __( 'Network error connecting to Mailchimp.', 'chimpmatic-lite' ),
);
$message = isset( $messages[ $reason ] ) ? $messages[ $reason ] : __( 'Subscription failed.', 'chimpmatic-lite' );
if ( ! empty( $detail ) ) {
$message .= ' ' . $detail;
}
return $message;
}
private static function get_skip_message( $reason ) {
$messages = array(
'acceptance_not_checked' => __( 'Opt-in checkbox was not checked.', 'chimpmatic-lite' ),
'no_api_configured' => __( 'Mailchimp API is not configured for this form.', 'chimpmatic-lite' ),
);
return isset( $messages[ $reason ] ) ? $messages[ $reason ] : __( 'Subscription skipped.', 'chimpmatic-lite' );
}
public static function parse_api_error( $api_response, $email = '' ) {
$title = isset( $api_response['title'] ) ? $api_response['title'] : '';
$detail = isset( $api_response['detail'] ) ? $api_response['detail'] : '';
if ( strpos( strtolower( $title ), 'member exists' ) !== false ) {
return self::failure( 'already_subscribed', '', $email );
}
if ( strpos( strtolower( $detail ), 'permanently deleted' ) !== false ) {
return self::failure( 'permanently_deleted', '', $email );
}
if ( strpos( strtolower( $detail ), 'compliance state' ) !== false ) {
return self::failure( 'compliance_state', '', $email );
}
if ( strpos( strtolower( $title ), 'forgotten email' ) !== false ) {
return self::failure( 'permanently_deleted', '', $email );
}
return self::failure( 'api_error', $detail, $email );
}
}

View File

@@ -0,0 +1,17 @@
<?php
/**
* Plugin bootstrap file.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
// Load and initialize the plugin via OOP bootstrap class.
require_once SPARTAN_MCE_PLUGIN_DIR . 'includes/core/class-cmatic-plugin.php';
$cmatic_plugin = new Cmatic_Plugin( SPARTAN_MCE_PLUGIN_FILE, SPARTAN_MCE_VERSION );
$cmatic_plugin->init();

View File

@@ -0,0 +1,215 @@
<?php
/**
* Plugin activation handler.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Activator {
private const INITIALIZED_FLAG = 'cmatic_initialized';
private $options;
private $install_data;
private $migration;
private $pro_status;
private $redirect;
private $lifecycle_signal;
private $version;
public function __construct( $version ) {
$this->version = $version;
$this->options = new Cmatic_Options_Repository();
$this->install_data = new Cmatic_Install_Data( $this->options );
$this->migration = new Cmatic_Migration( $this->options, $version );
$this->pro_status = new Cmatic_Pro_Status( $this->options );
$this->redirect = new Cmatic_Redirect( $this->options );
$this->lifecycle_signal = new Cmatic_Lifecycle_Signal();
}
public function activate() {
$this->do_activation( true );
}
public function ensure_initialized() {
// Fast check - auto-loaded option, near-zero cost.
if ( get_option( self::INITIALIZED_FLAG ) ) {
// Flag exists, but verify data is actually complete.
$install_id = $this->options->get( 'install.id' );
if ( ! empty( $install_id ) ) {
return; // Data exists, truly initialized.
}
// Flag exists but data is missing - delete flag and continue.
delete_option( self::INITIALIZED_FLAG );
}
// Run initialization (idempotent).
$this->do_activation( false );
}
private function do_activation( $is_normal_activation ) {
// 1. BULLETPROOF: Ensure install_id and quest exist FIRST.
$this->install_data->ensure();
// 2. Migrate legacy options (safe to run multiple times).
$this->migration->run();
// 3. Update Pro plugin status.
$this->pro_status->update();
// 4. Record activation in lifecycle.
$this->record_activation();
// 5. Send lifecycle signal (only on normal activation, not fallback).
if ( $is_normal_activation ) {
$this->lifecycle_signal->send_activation();
}
// 6. Schedule redirect (only on normal activation).
if ( $is_normal_activation ) {
$this->redirect->schedule();
}
// 7. Mark plugin as active (for missed deactivation detection).
$this->options->set( 'lifecycle.is_active', true );
// 8. Mark as initialized (auto-loaded option for fast checks).
add_option( self::INITIALIZED_FLAG, true );
// 9. Fire action for extensibility.
do_action( 'cmatic_activated', $is_normal_activation );
}
private function record_activation() {
$activations = $this->options->get( 'lifecycle.activations', array() );
$activations = is_array( $activations ) ? $activations : array();
$activations[] = time();
$this->options->set( 'lifecycle.activations', $activations );
$this->options->set( 'lifecycle.is_reactivation', count( $activations ) > 1 );
}
public function get_redirect() {
return $this->redirect;
}
public function get_pro_status() {
return $this->pro_status;
}
public static function is_initialized() {
return (bool) get_option( self::INITIALIZED_FLAG );
}
public static function clear_initialized_flag() {
return delete_option( self::INITIALIZED_FLAG );
}
public function verify_lifecycle_state(): void {
$thinks_active = $this->options->get( 'lifecycle.is_active', false );
// If we think we're inactive (or never set), nothing to check.
if ( ! $thinks_active ) {
return;
}
// Check if plugin is actually active in WordPress.
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$actually_active = is_plugin_active( SPARTAN_MCE_PLUGIN_BASENAME );
// If state matches reality, all is well.
if ( $actually_active ) {
return;
}
// MISMATCH DETECTED: We think we're active, but WordPress says no.
// This means deactivation hook was missed (update, FTP delete, etc).
$this->handle_missed_deactivation();
}
private function handle_missed_deactivation(): void {
// 1. Update state flag FIRST.
$this->options->set( 'lifecycle.is_active', false );
// 2. Record missed deactivation (for analytics).
$missed = $this->options->get( 'lifecycle.missed_deactivations', array() );
$missed = is_array( $missed ) ? $missed : array();
$missed[] = array(
'timestamp' => time(),
'type' => 'self_healing',
);
$this->options->set( 'lifecycle.missed_deactivations', $missed );
// 3. Clear cron (idempotent).
Cmatic_Cron::unschedule();
// 4. Send telemetry signal (if class exists).
$this->lifecycle_signal->send_deactivation();
// 5. Clear initialized flag so next activation runs fully.
delete_option( self::INITIALIZED_FLAG );
// 6. Fire action for extensibility.
do_action( 'cmatic_missed_deactivation_handled' );
}
public static function register_hooks( string $plugin_file, string $version ): void {
// Activation hook.
register_activation_hook(
$plugin_file,
function () use ( $version ) {
$activator = new self( $version );
$activator->activate();
}
);
// Deactivation hook.
register_deactivation_hook(
$plugin_file,
function () {
$deactivator = new Cmatic_Deactivator();
$deactivator->deactivate();
}
);
// CRITICAL FALLBACK: Catch missed activations AND deactivations on admin_init.
add_action(
'admin_init',
function () use ( $version ) {
$activator = new self( $version );
// Check for missed deactivation FIRST (before initialization).
$activator->verify_lifecycle_state();
// Then ensure initialized (existing fallback).
$activator->ensure_initialized();
$activator->get_redirect()->maybe_redirect();
},
5
);
// Update Pro status on plugins_loaded (late priority).
add_action(
'plugins_loaded',
function () use ( $version ) {
$activator = new self( $version );
$activator->get_pro_status()->update();
},
99
);
}
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* Dependency container.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Lite_Container {
private static $services = array();
private static $factories = array();
public static function set( string $id, $service ): void {
self::$services[ $id ] = $service;
}
public static function factory( string $id, callable $factory ): void {
self::$factories[ $id ] = $factory;
}
public static function get( string $id ) {
// Return cached instance if exists.
if ( isset( self::$services[ $id ] ) ) {
return self::$services[ $id ];
}
// Build from factory if exists.
if ( isset( self::$factories[ $id ] ) ) {
self::$services[ $id ] = call_user_func( self::$factories[ $id ] );
unset( self::$factories[ $id ] );
return self::$services[ $id ];
}
return null;
}
public static function has( string $id ): bool {
return isset( self::$services[ $id ] ) || isset( self::$factories[ $id ] );
}
public static function clear(): void {
self::$services = array();
self::$factories = array();
}
public static function boot(): void {
// Register Options Repository.
self::factory(
Cmatic_Options_Interface::class,
function () {
return new Cmatic_Options_Repository();
}
);
// Alias for backward compatibility.
self::factory(
'options',
function () {
return self::get( Cmatic_Options_Interface::class );
}
);
}
}

View File

@@ -0,0 +1,41 @@
<?php
/**
* Plugin deactivation handler.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Deactivator {
private $options;
private $lifecycle_signal;
public function __construct() {
$this->options = new Cmatic_Options_Repository();
$this->lifecycle_signal = new Cmatic_Lifecycle_Signal();
}
public function deactivate() {
// Mark inactive FIRST (state must update before cleanup).
$this->options->set( 'lifecycle.is_active', false );
$this->record_deactivation();
$this->lifecycle_signal->send_deactivation();
do_action( 'cmatic_deactivated' );
}
private function record_deactivation() {
$deactivations = $this->options->get( 'lifecycle.deactivations', array() );
$deactivations = is_array( $deactivations ) ? $deactivations : array();
$deactivations[] = time();
$this->options->set( 'lifecycle.deactivations', $deactivations );
}
}

View File

@@ -0,0 +1,124 @@
<?php
/**
* Installation data handler.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Install_Data {
public const MIN_VALID_TIMESTAMP = 1000000000;
private $options;
public function __construct( Cmatic_Options_Repository $options ) {
$this->options = $options;
}
public function ensure() {
$data = $this->options->get_all();
$changed = false;
if ( ! isset( $data['install'] ) || ! is_array( $data['install'] ) ) {
$data['install'] = array();
$changed = true;
}
if ( empty( $data['install']['id'] ) ) {
$wp_cached = get_option( 'cmatic', array() );
if ( is_array( $wp_cached ) && ! empty( $wp_cached['install']['id'] ) ) {
$data['install']['id'] = $wp_cached['install']['id'];
} else {
$data['install']['id'] = $this->generate_install_id();
}
$changed = true;
}
$quest = isset( $data['install']['quest'] ) ? (int) $data['install']['quest'] : 0;
if ( $quest < self::MIN_VALID_TIMESTAMP ) {
$data['install']['quest'] = $this->determine_quest( $data );
$changed = true;
}
if ( $changed ) {
$this->options->save( $data );
}
}
public function get_install_id() {
$install_id = $this->options->get( 'install.id', '' );
if ( empty( $install_id ) ) {
$wp_cached = get_option( 'cmatic', array() );
if ( is_array( $wp_cached ) && ! empty( $wp_cached['install']['id'] ) ) {
$install_id = $wp_cached['install']['id'];
$this->options->set( 'install.id', $install_id );
return $install_id;
}
$install_id = $this->generate_install_id();
$this->options->set( 'install.id', $install_id );
}
return $install_id;
}
public function get_quest() {
$quest = (int) $this->options->get( 'install.quest', 0 );
if ( $quest >= self::MIN_VALID_TIMESTAMP ) {
return $quest;
}
$quest = $this->determine_quest( $this->options->get_all() );
$this->options->set( 'install.quest', $quest );
return $quest;
}
private function generate_install_id() {
return bin2hex( random_bytes( 6 ) );
}
private function determine_quest( $data ) {
$candidates = array();
// 1. Legacy mce_loyalty (highest priority - original timestamp).
$loyalty = $this->options->get_legacy( 'mce_loyalty' );
if ( is_array( $loyalty ) && ! empty( $loyalty[0] ) ) {
$candidates[] = (int) $loyalty[0];
}
// 2. Lifecycle activations.
$activations = isset( $data['lifecycle']['activations'] ) ? $data['lifecycle']['activations'] : array();
if ( ! empty( $activations ) && is_array( $activations ) ) {
$candidates[] = (int) min( $activations );
}
// 3. Telemetry opt-in date.
$opt_in = isset( $data['telemetry']['opt_in_date'] ) ? (int) $data['telemetry']['opt_in_date'] : 0;
if ( $opt_in >= self::MIN_VALID_TIMESTAMP ) {
$candidates[] = $opt_in;
}
// 4. API first connected.
$api_first = isset( $data['api']['first_connected'] ) ? (int) $data['api']['first_connected'] : 0;
if ( $api_first >= self::MIN_VALID_TIMESTAMP ) {
$candidates[] = $api_first;
}
// 5. First submission.
$sub_first = isset( $data['submissions']['first'] ) ? (int) $data['submissions']['first'] : 0;
if ( $sub_first >= self::MIN_VALID_TIMESTAMP ) {
$candidates[] = $sub_first;
}
// 6. Fallback to current time.
return ! empty( $candidates ) ? min( $candidates ) : time();
}
}

View File

@@ -0,0 +1,196 @@
<?php
/**
* Database migration handler.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Migration {
private const LEGACY_MCE_OPTIONS = array(
'mce_loyalty',
'mce_install_id',
'mce_sent',
'mce_show_update_news',
'mce_show_notice',
'mce_conten_panel_master',
'mce_conten_tittle_master',
);
/**
* Legacy chimpmatic/cmatic options to migrate and clean up.
*
* @var array
*/
private const LEGACY_CMATIC_OPTIONS = array(
'chimpmatic-update',
'cmatic_log_on',
'cmatic_do_activation_redirect',
'cmatic_news_retry_count',
'csyncr_last_weekly_run',
);
private $options;
private $version;
public function __construct( Cmatic_Options_Repository $options, $version ) {
$this->options = $options;
$this->version = $version;
}
public function run() {
$data = $this->options->get_all();
$data['version'] = $this->version;
$this->migrate_install( $data );
$this->migrate_stats( $data );
$this->migrate_ui( $data );
$this->migrate_cmatic_options( $data );
$this->migrate_api_data( $data );
$data['migrated'] = true;
$this->options->save( $data );
$this->cleanup_legacy_options();
}
public function is_migrated() {
return (bool) $this->options->get( 'migrated', false );
}
private function migrate_install( &$data ) {
if ( ! isset( $data['install'] ) ) {
$data['install'] = array();
}
if ( ! isset( $data['install']['plugin_slug'] ) ) {
$data['install']['plugin_slug'] = 'contact-form-7-mailchimp-extension';
}
if ( ! isset( $data['install']['activated_at'] ) && ! empty( $data['install']['quest'] ) ) {
$data['install']['activated_at'] = gmdate( 'Y-m-d H:i:s', (int) $data['install']['quest'] );
}
}
private function migrate_stats( &$data ) {
if ( ! isset( $data['stats'] ) ) {
$data['stats'] = array();
}
if ( ! isset( $data['stats']['sent'] ) ) {
$data['stats']['sent'] = (int) $this->options->get_legacy( 'mce_sent', 0 );
}
}
private function migrate_ui( &$data ) {
if ( isset( $data['ui'] ) ) {
return;
}
$panel_title = $this->options->get_legacy( 'mce_conten_tittle_master', '' );
$panel_content = $this->options->get_legacy( 'mce_conten_panel_master', '' );
$data['ui'] = array(
'news' => (bool) $this->options->get_legacy( 'mce_show_update_news', true ),
'notice_banner' => (bool) $this->options->get_legacy( 'mce_show_notice', true ),
'welcome_panel' => array(
'title' => $panel_title ? $panel_title : 'Chimpmatic Lite is now ' . $this->version . '!',
'content' => $panel_content ? $panel_content : '',
),
);
}
private function migrate_cmatic_options( &$data ) {
$old_auto_update = get_option( 'chimpmatic-update' );
if ( false !== $old_auto_update && ! isset( $data['auto_update'] ) ) {
$data['auto_update'] = ( '1' === $old_auto_update ) ? 1 : 0;
}
$old_debug = get_option( 'cmatic_log_on' );
if ( false !== $old_debug && ! isset( $data['debug'] ) ) {
$data['debug'] = ( 'on' === $old_debug || '1' === $old_debug ) ? 1 : 0;
}
$old_redirect = get_option( 'cmatic_do_activation_redirect' );
if ( false !== $old_redirect && ! isset( $data['activation_redirect'] ) ) {
$data['activation_redirect'] = (bool) $old_redirect;
}
$old_news_count = get_option( 'cmatic_news_retry_count' );
if ( false !== $old_news_count ) {
if ( ! isset( $data['news'] ) ) {
$data['news'] = array();
}
if ( ! isset( $data['news']['retry_count'] ) ) {
$data['news']['retry_count'] = (int) $old_news_count;
}
}
$old_last_run = get_option( 'csyncr_last_weekly_run' );
if ( false !== $old_last_run ) {
if ( ! isset( $data['telemetry'] ) ) {
$data['telemetry'] = array();
}
if ( ! isset( $data['telemetry']['last_run'] ) ) {
$data['telemetry']['last_run'] = (int) $old_last_run;
}
}
}
private function migrate_api_data( &$data ) {
// Skip if already set.
if ( ! empty( $data['api']['first_connected'] ) ) {
return;
}
// First, try to use existing api.setup_first_success timestamp.
$setup_first_success = isset( $data['api']['setup_first_success'] ) ? (int) $data['api']['setup_first_success'] : 0;
if ( $setup_first_success > 1000000000 ) {
if ( ! isset( $data['api'] ) ) {
$data['api'] = array();
}
$data['api']['first_connected'] = $setup_first_success;
return;
}
// Fallback: Check if any form has a successful API connection.
global $wpdb;
$form_options = $wpdb->get_results(
"SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE 'cf7_mch_%'",
ARRAY_A
);
if ( empty( $form_options ) ) {
return;
}
foreach ( $form_options as $row ) {
$form_data = maybe_unserialize( $row['option_value'] );
if ( is_array( $form_data ) && ! empty( $form_data['api-validation'] ) && 1 === (int) $form_data['api-validation'] ) {
// Found a form with valid API - backfill first_connected with current time.
if ( ! isset( $data['api'] ) ) {
$data['api'] = array();
}
$data['api']['first_connected'] = time();
return;
}
}
}
private function cleanup_legacy_options() {
foreach ( self::LEGACY_MCE_OPTIONS as $option ) {
$this->options->delete_legacy( $option );
}
foreach ( self::LEGACY_CMATIC_OPTIONS as $option ) {
delete_option( $option );
}
}
}

View File

@@ -0,0 +1,191 @@
<?php
/**
* Main plugin class.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
final class Cmatic_Plugin {
private $file;
private $version;
private $dir;
private $basename;
public function __construct( string $file, string $version ) {
$this->file = $file;
$this->version = $version;
$this->dir = plugin_dir_path( $file );
$this->basename = plugin_basename( $file );
}
public function init(): void {
$this->load_core_dependencies();
$this->register_lifecycle_hooks();
$this->load_module_dependencies();
$this->initialize_components();
$this->load_late_dependencies();
$this->initialize_late_components();
}
private function load_core_dependencies(): void {
// Load interfaces first.
require_once $this->dir . 'includes/interfaces/interface-cmatic-options.php';
require_once $this->dir . 'includes/interfaces/interface-cmatic-logger.php';
require_once $this->dir . 'includes/interfaces/interface-cmatic-api-client.php';
// Load container.
require_once $this->dir . 'includes/core/class-cmatic-container.php';
// Load services.
require_once $this->dir . 'includes/services/class-cmatic-options-repository.php';
require_once $this->dir . 'includes/services/class-cmatic-pro-status.php';
require_once $this->dir . 'includes/services/class-cmatic-redirect.php';
require_once $this->dir . 'includes/services/class-cmatic-lifecycle-signal.php';
require_once $this->dir . 'includes/core/class-cmatic-install-data.php';
require_once $this->dir . 'includes/core/class-cmatic-migration.php';
require_once $this->dir . 'includes/core/class-cmatic-activator.php';
require_once $this->dir . 'includes/core/class-cmatic-deactivator.php';
require_once $this->dir . 'includes/services/class-cmatic-cf7-dependency.php';
require_once $this->dir . 'includes/services/class-cmatic-pro-syncer.php';
require_once $this->dir . 'includes/services/class-cmatic-api-key-importer.php';
}
private function register_lifecycle_hooks(): void {
Cmatic_Activator::register_hooks( $this->file, $this->version );
}
private function load_module_dependencies(): void {
$modules = array(
// Utils.
'utils/class-cmatic-utils.php',
'utils/class-cmatic-lite-get-fields.php',
'utils/class-cmatic-pursuit.php',
'utils/class-cmatic-file-logger.php',
'utils/class-cmatic-remote-fetcher.php',
'utils/class-cmatic-buster.php',
// Services.
'services/class-cmatic-cf7-tags.php',
'services/class-cmatic-cron.php',
'services/class-cmatic-api-service.php',
'services/class-cmatic-form-tags.php',
// Submission handling (load before handler).
'services/submission/class-cmatic-email-extractor.php',
'services/submission/class-cmatic-status-resolver.php',
'services/submission/class-cmatic-merge-vars-builder.php',
'services/submission/class-cmatic-response-handler.php',
'services/submission/class-cmatic-mailchimp-subscriber.php',
'services/class-cmatic-submission-handler.php',
// REST API Controllers.
'api/class-cmatic-rest-lists.php',
'api/class-cmatic-rest-settings.php',
'api/class-cmatic-rest-form.php',
'api/class-cmatic-rest-reset.php',
// Admin.
'admin/class-cmatic-plugin-links.php',
'admin/class-cmatic-deactivation-survey.php',
'admin/class-cmatic-asset-loader.php',
'admin/class-cmatic-admin-panel.php',
// API.
'api/class-cmatic-log-viewer.php',
'api/class-cmatic-contact-lookup.php',
'api/class-cmatic-submission-feedback.php',
// UI.
'ui/class-cmatic-header.php',
'ui/class-cmatic-api-panel.php',
'ui/class-cmatic-audiences.php',
'ui/class-cmatic-data-container.php',
'ui/class-cmatic-panel-toggles.php',
'ui/class-cmatic-tags-preview.php',
'ui/class-cmatic-banners.php',
'ui/class-cmatic-form-classes.php',
'ui/class-cmatic-field-mapper.php',
'ui/class-cmatic-sidebar-panel.php',
'ui/class-cmatic-advanced-settings.php',
);
foreach ( $modules as $module ) {
$path = $this->dir . 'includes/' . $module;
if ( file_exists( $path ) ) {
require_once $path;
}
}
}
private function initialize_components(): void {
// Boot service container.
Cmatic_Lite_Container::boot();
// Core services (no dependencies).
Cmatic_CF7_Dependency::init();
Cmatic_Pro_Syncer::init();
// Logging.
Cmatic_Log_Viewer::init( 'chimpmatic-lite', '[ChimpMatic Lite]', 'chimpmatic-lite' );
// REST API Controllers.
Cmatic_Rest_Lists::init();
Cmatic_Rest_Settings::init();
Cmatic_Rest_Form::init();
Cmatic_Rest_Reset::init();
// API Services.
Cmatic_Contact_Lookup::init();
Cmatic_Submission_Feedback::init();
// Admin UI.
Cmatic_Deactivation_Survey::init_lite();
Cmatic_Asset_Loader::init();
// CF7 Integration.
Cmatic_CF7_Tags::init();
Cmatic_Admin_Panel::init();
Cmatic_Submission_Handler::init();
Cmatic_Banners::init();
Cmatic_Form_Classes::init();
// Background Tasks.
Cmatic_Cron::init( $this->file );
Cmatic_Plugin_Links::init( $this->basename );
}
private function load_late_dependencies(): void {
// UI Components (Modals).
require_once $this->dir . 'includes/ui/class-cmatic-modal.php';
require_once $this->dir . 'includes/ui/class-cmatic-test-submission-modal.php';
// Admin Bar Notification System.
require_once $this->dir . 'includes/ui/class-cmatic-notification.php';
require_once $this->dir . 'includes/ui/class-cmatic-notification-center.php';
require_once $this->dir . 'includes/ui/class-cmatic-admin-bar-menu.php';
// Signals (Telemetry System) - has its own PSR-4 autoloader.
require_once $this->dir . 'includes/signals/autoload.php';
}
private function initialize_late_components(): void {
// Test Submission Modal.
$test_submission_modal = new Cmatic_Test_Submission_Modal();
$test_submission_modal->init();
// Admin Bar.
Cmatic_Notification_Center::get();
Cmatic_Admin_Bar_Menu::instance();
// Signals (Telemetry).
Cmatic\Metrics\Bootstrap::init(
array(
'plugin_basename' => $this->basename,
'endpoint_url' => 'https://signls.dev/wp-json/chimpmatic/v1/telemetry',
)
);
}
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* API client interface.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
interface Cmatic_Api_Client_Interface {
/**
* Validate an API key.
*
* @param string $api_key The API key to validate.
* @param bool $log_enabled Whether logging is enabled.
* @return array{api-validation: int} Validation result.
*/
public static function validate_key( string $api_key, bool $log_enabled = false ): array;
/**
* Get audiences (lists) from Mailchimp.
*
* @param string $api_key The API key.
* @param bool $log_enabled Whether logging is enabled.
* @return array{lisdata: array, merge_fields?: array}
*/
public static function get_lists( string $api_key, bool $log_enabled = false ): array;
/**
* Get merge fields for a specific list.
*
* @param string $api_key The API key.
* @param string $list_id The list ID.
* @param bool $log_enabled Whether logging is enabled.
* @return array{merge_fields: array}
*/
public static function get_merge_fields( string $api_key, string $list_id, bool $log_enabled = false ): array;
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Logger interface.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
interface Cmatic_Logger_Interface {
/**
* Log a message.
*
* @param string $level Log level (INFO, ERROR, WARNING, DEBUG, CRITICAL).
* @param string $message Log message.
* @param mixed $context Optional context data.
* @return void
*/
public function log( string $level, string $message, $context = null ): void;
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* Options repository interface.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
interface Cmatic_Options_Interface {
/**
* Get all stored options.
*
* @return array
*/
public function get_all();
/**
* Get a value using dot notation.
*
* @param string $key Dot-notation key (e.g., 'install.id').
* @param mixed $default Default value if not found.
* @return mixed
*/
public function get( $key, $default = null );
/**
* Set a value using dot notation.
*
* @param string $key Dot-notation key.
* @param mixed $value Value to set.
* @return bool Success.
*/
public function set( $key, $value );
/**
* Save the full options array.
*
* @param array $data Full options data.
* @return bool Success.
*/
public function save( $data );
/**
* Clear internal cache.
*
* @return void
*/
public function clear_cache();
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* Mailchimp API key importer.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Api_Key_Importer {
public static function detect() {
$mc4wp = get_option( 'mc4wp' );
if ( ! empty( $mc4wp['api_key'] ) ) {
return $mc4wp['api_key'];
}
$yikes = get_option( 'yikes-mc-api-key' );
if ( ! empty( $yikes ) ) {
return $yikes;
}
$easy_forms = get_option( 'yikes-easy-mailchimp-extender-api-key' );
if ( ! empty( $easy_forms ) ) {
return $easy_forms;
}
$woo_mc = get_option( 'mailchimp-woocommerce' );
if ( ! empty( $woo_mc['mailchimp_api_key'] ) ) {
return $woo_mc['mailchimp_api_key'];
}
$mc4wp_top_bar = get_option( 'mc4wp_top_bar' );
if ( ! empty( $mc4wp_top_bar['api_key'] ) ) {
return $mc4wp_top_bar['api_key'];
}
return false;
}
public static function import_to_newest_form() {
$existing_key = self::detect();
if ( ! $existing_key ) {
return false;
}
$form_id = Cmatic_Utils::get_newest_form_id();
if ( ! $form_id ) {
return false;
}
$option_name = 'cf7_mch_' . $form_id;
$cf7_mch = get_option( $option_name, array() );
if ( ! empty( $cf7_mch['api'] ) ) {
return false;
}
$cf7_mch['api'] = $existing_key;
update_option( $option_name, $cf7_mch );
return true;
}
}

View File

@@ -0,0 +1,178 @@
<?php
/**
* Mailchimp API service.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
final class Cmatic_Lite_Api_Service implements Cmatic_Api_Client_Interface {
private const API_TIMEOUT = 20;
private const MASK_LENGTH = 20;
public static function mask_api_key( string $key ): string {
if ( empty( $key ) || strlen( $key ) < 12 ) {
return $key;
}
$prefix = substr( $key, 0, 8 );
$suffix = substr( $key, -4 );
return $prefix . str_repeat( '•', self::MASK_LENGTH ) . $suffix;
}
public static function generate_headers( string $token ): array {
$api_key_part = explode( '-', sanitize_text_field( $token ) )[0] ?? '';
$user_agent = 'ChimpMaticLite/' . SPARTAN_MCE_VERSION . '; WordPress/' . get_bloginfo( 'version' );
return array(
'headers' => array(
'Content-Type' => 'application/json',
'Authorization' => 'apikey ' . $api_key_part,
'User-Agent' => $user_agent,
),
'timeout' => self::API_TIMEOUT,
'sslverify' => true,
);
}
public static function get( string $token, string $url ): array {
$args = self::generate_headers( $token );
$response = wp_remote_get( esc_url_raw( $url ), $args );
if ( is_wp_error( $response ) ) {
return array( false, $args, $response );
}
$body = wp_remote_retrieve_body( $response );
return array( json_decode( $body, true ), $args, $response );
}
public static function put( string $token, string $url, string $body ): array {
$args = self::generate_headers( $token );
$args['body'] = $body;
$args['method'] = 'PUT';
$response = wp_remote_request( esc_url_raw( $url ), $args );
if ( is_wp_error( $response ) ) {
return array( false, $response );
}
$response_body = wp_remote_retrieve_body( $response );
return array( json_decode( $response_body, true ), $response );
}
public static function validate_key( string $input, bool $log_enabled = false ): array {
$logger = new Cmatic_File_Logger( 'API-Validation', $log_enabled );
try {
if ( empty( $input ) || ! preg_match( '/^[a-f0-9]{32}-[a-z]{2,3}\d+$/', $input ) ) {
$logger->log( 'ERROR', 'Invalid API Key format provided.', self::mask_api_key( $input ) );
self::record_failure();
return array( 'api-validation' => 0 );
}
list( $key, $dc ) = explode( '-', $input );
if ( empty( $key ) || empty( $dc ) ) {
self::record_failure();
return array( 'api-validation' => 0 );
}
$url = "https://{$dc}.api.mailchimp.com/3.0/ping";
$response = self::get( $key, $url );
if ( is_wp_error( $response[2] ) || 200 !== wp_remote_retrieve_response_code( $response[2] ) ) {
$error = is_wp_error( $response[2] ) ? $response[2]->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code( $response[2] );
$logger->log( 'ERROR', 'API Key validation ping failed.', $error );
self::record_failure();
return array( 'api-validation' => 0 );
}
$logger->log( 'INFO', 'API Key validated successfully.' );
self::record_success();
return array( 'api-validation' => 1 );
} catch ( \Exception $e ) {
$logger->log( 'CRITICAL', 'API validation threw an exception.', $e->getMessage() );
self::record_failure();
return array( 'api-validation' => 0 );
}
}
public static function get_lists( string $api_key, bool $log_enabled = false ): array {
$logger = new Cmatic_File_Logger( 'List-Retrieval', $log_enabled );
try {
list( $key, $dc ) = explode( '-', $api_key );
if ( empty( $key ) || empty( $dc ) ) {
return array( 'lisdata' => array() );
}
$url = "https://{$dc}.api.mailchimp.com/3.0/lists?count=999";
$response = self::get( $key, $url );
if ( is_wp_error( $response[2] ) || 200 !== wp_remote_retrieve_response_code( $response[2] ) ) {
$error = is_wp_error( $response[2] ) ? $response[2]->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code( $response[2] );
$logger->log( 'ERROR', 'Failed to retrieve lists from Mailchimp.', $error );
return array( 'lisdata' => array() );
}
$logger->log( 'INFO', 'Successfully retrieved lists from Mailchimp.', $response[0] );
return array(
'lisdata' => $response[0],
'merge_fields' => array(),
);
} catch ( \Exception $e ) {
$logger->log( 'CRITICAL', 'List retrieval threw an exception.', $e->getMessage() );
return array( 'lisdata' => array() );
}
}
public static function get_merge_fields( string $api_key, string $list_id, bool $log_enabled = false ): array {
$logger = new Cmatic_File_Logger( 'Merge-Fields-Retrieval', $log_enabled );
if ( empty( $api_key ) || empty( $list_id ) ) {
return array( 'merge_fields' => array() );
}
try {
list( $key, $dc ) = explode( '-', $api_key );
if ( empty( $key ) || empty( $dc ) ) {
return array( 'merge_fields' => array() );
}
$url = "https://{$dc}.api.mailchimp.com/3.0/lists/{$list_id}/merge-fields?count=50";
$response = self::get( $key, $url );
if ( is_wp_error( $response[2] ) || 200 !== wp_remote_retrieve_response_code( $response[2] ) ) {
$error = is_wp_error( $response[2] ) ? $response[2]->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code( $response[2] );
$logger->log( 'ERROR', 'Failed to retrieve merge fields from Mailchimp.', $error );
return array( 'merge_fields' => array() );
}
$logger->log( 'INFO', 'Successfully retrieved merge fields from Mailchimp.', $response[0] );
return array( 'merge_fields' => $response[0] );
} catch ( \Exception $e ) {
$logger->log( 'CRITICAL', 'Merge fields retrieval threw an exception.', $e->getMessage() );
return array( 'merge_fields' => array() );
}
}
private static function record_failure(): void {
if ( ! Cmatic_Options_Repository::get_option( 'api.setup_first_failure' ) ) {
Cmatic_Options_Repository::set_option( 'api.setup_first_failure', time() );
}
Cmatic_Options_Repository::set_option( 'api.setup_last_failure', time() );
$count = (int) Cmatic_Options_Repository::get_option( 'api.setup_failure_count', 0 );
Cmatic_Options_Repository::set_option( 'api.setup_failure_count', $count + 1 );
}
private static function record_success(): void {
if ( ! Cmatic_Options_Repository::get_option( 'api.setup_first_success' ) ) {
Cmatic_Options_Repository::set_option( 'api.setup_first_success', time() );
}
}
private function __construct() {}
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* Contact Form 7 dependency checker.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_CF7_Dependency {
const CF7_PLUGIN_FILE = 'contact-form-7/wp-contact-form-7.php';
const CF7_PLUGIN_DIR = 'contact-form-7';
public static function init() {
// Auto-activate CF7 early (before page capability checks).
add_action( 'admin_init', array( __CLASS__, 'maybe_activate_cf7' ), 1 );
// Show notice if CF7 not installed.
add_action( 'admin_notices', array( __CLASS__, 'maybe_show_notice' ) );
}
public static function maybe_activate_cf7() {
if ( self::is_satisfied() ) {
return;
}
if ( self::is_installed() && ! self::is_active() ) {
self::activate_cf7();
}
}
public static function is_installed() {
return file_exists( WP_PLUGIN_DIR . '/' . self::CF7_PLUGIN_FILE );
}
public static function is_active() {
return class_exists( 'WPCF7' );
}
public static function is_satisfied() {
return self::is_installed() && self::is_active();
}
public static function maybe_show_notice() {
if ( self::is_satisfied() || self::is_installed() ) {
return;
}
// CF7 not installed - show notice with install link on plugins page.
$screen = get_current_screen();
if ( $screen && 'plugins' === $screen->id ) {
self::render_not_installed_notice();
}
}
public static function activate_cf7() {
// Must have capability to activate plugins.
if ( ! current_user_can( 'activate_plugins' ) ) {
return false;
}
if ( ! function_exists( 'activate_plugin' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( is_plugin_active( self::CF7_PLUGIN_FILE ) ) {
return true;
}
// Use silent mode to prevent redirect issues.
$result = activate_plugin( self::CF7_PLUGIN_FILE, '', false, true );
return ! is_wp_error( $result );
}
private static function render_not_installed_notice() {
$install_url = wp_nonce_url(
admin_url( 'update.php?action=install-plugin&plugin=contact-form-7' ),
'install-plugin_contact-form-7'
);
printf(
'<div class="notice notice-error"><p><strong>Chimpmatic Lite</strong> requires <strong>Contact Form 7</strong> to function. <a href="%s">Install Contact Form 7</a></p></div>',
esc_url( $install_url )
);
}
}

View File

@@ -0,0 +1,127 @@
<?php
/**
* Custom CF7 form tags registration.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_CF7_Tags {
public static function init(): void {
add_filter( 'wpcf7_special_mail_tags', array( __CLASS__, 'handle_special_tags' ), 10, 3 );
add_action( 'wpcf7_init', array( __CLASS__, 'register_form_tags' ), 11 );
if ( ! is_admin() ) {
add_filter( 'wpcf7_form_tag', array( __CLASS__, 'populate_referer_tag' ) );
}
}
public static function handle_special_tags( ?string $output, string $name, string $html ): string {
if ( '_domain' === $name ) {
return self::get_domain();
}
if ( '_formID' === $name ) {
return (string) self::get_form_id();
}
return $output ?? '';
}
public static function register_form_tags(): void {
if ( ! function_exists( 'wpcf7_add_form_tag' ) ) {
return;
}
wpcf7_add_form_tag( '_domain', array( __CLASS__, 'get_domain' ) );
wpcf7_add_form_tag( '_formID', array( __CLASS__, 'get_form_id' ) );
}
public static function populate_referer_tag( array $form_tag ): array {
if ( 'referer-page' === $form_tag['name'] ) {
$referer = isset( $_SERVER['HTTP_REFERER'] )
? esc_url( wp_unslash( $_SERVER['HTTP_REFERER'] ) )
: '';
$form_tag['values'][] = $referer;
}
return $form_tag;
}
public static function get_domain(): string {
$url = strtolower( trim( get_home_url() ) );
$url = preg_replace( '/^https?:\/\//i', '', $url );
$url = preg_replace( '/^www\./i', '', $url );
$parts = explode( '/', $url );
return trim( $parts[0] );
}
public static function get_form_id(): int {
if ( ! class_exists( 'WPCF7_ContactForm' ) ) {
return 0;
}
$form = WPCF7_ContactForm::get_current();
return $form ? $form->id() : 0;
}
public static function get_form_tags(): array {
if ( ! class_exists( 'WPCF7_FormTagsManager' ) ) {
return array();
}
$manager = WPCF7_FormTagsManager::get_instance();
$form_tags = $manager->get_scanned_tags();
return is_array( $form_tags ) ? $form_tags : array();
}
public static function get_mail_tags_html(): string {
if ( ! class_exists( 'WPCF7_ContactForm' ) ) {
return '';
}
$contact_form = WPCF7_ContactForm::get_current();
if ( ! $contact_form ) {
return '';
}
$mail_tags = $contact_form->collect_mail_tags();
if ( empty( $mail_tags ) ) {
return '';
}
$output = '';
foreach ( $mail_tags as $tag_name ) {
if ( ! empty( $tag_name ) && 'opt-in' !== $tag_name ) {
$output .= '<span class="mailtag code used">[' . esc_html( $tag_name ) . ']</span>';
}
}
return $output;
}
public static function get_referer_html(): string {
$referer_url = isset( $_SERVER['HTTP_REFERER'] ) && ! empty( $_SERVER['HTTP_REFERER'] )
? esc_url( wp_unslash( $_SERVER['HTTP_REFERER'] ) )
: 'Direct Visit';
$html = '<p style="display: none !important"><span class="wpcf7-form-control-wrap referer-page">';
$html .= '<input type="hidden" name="referer-page" ';
$html .= 'value="' . esc_attr( $referer_url ) . '" ';
$html .= 'data-value="' . esc_attr( $referer_url ) . '" ';
$html .= 'class="wpcf7-form-control wpcf7-text referer-page" aria-invalid="false">';
$html .= '</span></p>' . "\n";
return $html;
}
}

View File

@@ -0,0 +1,75 @@
<?php
/**
* Cron job handler.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Cron {
private const DAILY_HOOK = 'cmatic_daily_cron';
private const DAILY_TIME = '03:00:00';
public static function init( string $plugin_file ): void {
add_action( 'init', array( __CLASS__, 'schedule' ) );
add_action( self::DAILY_HOOK, array( __CLASS__, 'run_daily_job' ) );
register_deactivation_hook( $plugin_file, array( __CLASS__, 'unschedule' ) );
}
public static function schedule(): void {
if ( wp_next_scheduled( self::DAILY_HOOK ) ) {
return;
}
wp_schedule_event( strtotime( self::DAILY_TIME ), 'daily', self::DAILY_HOOK );
}
public static function unschedule(): void {
// Idempotent: wp_clear_scheduled_hook is safe if hook doesn't exist.
wp_clear_scheduled_hook( self::DAILY_HOOK );
}
public static function run_daily_job(): void {
self::disable_all_logging();
}
private static function disable_all_logging(): int {
global $wpdb;
$option_names = $wpdb->get_col(
$wpdb->prepare(
"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
'cf7_mch_%'
)
);
$updated = 0;
foreach ( $option_names as $option_name ) {
$config = get_option( $option_name );
if ( is_array( $config ) && isset( $config['logfileEnabled'] ) ) {
unset( $config['logfileEnabled'] );
update_option( $option_name, $config );
++$updated;
}
}
return $updated;
}
public static function is_scheduled(): bool {
return (bool) wp_next_scheduled( self::DAILY_HOOK );
}
public static function get_next_run() {
return wp_next_scheduled( self::DAILY_HOOK );
}
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* CF7 form tag utilities.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
final class Cmatic_Form_Tags {
public static function get_tags_with_types( $contact_form ): array {
if ( ! $contact_form ) {
return array();
}
$mail_tags = $contact_form->collect_mail_tags();
$all_tags = $contact_form->scan_form_tags();
$result = array();
foreach ( $all_tags as $tag ) {
if ( ! empty( $tag->name ) && in_array( $tag->name, $mail_tags, true ) ) {
$result[] = array(
'name' => $tag->name,
'basetype' => $tag->basetype,
);
}
}
return $result;
}
private function __construct() {}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* Lifecycle telemetry signals.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Lifecycle_Signal {
public function send_activation() {
if ( class_exists( 'Cmatic\Metrics\Core\Sync' ) ) {
\Cmatic\Metrics\Core\Sync::send_lifecycle_signal( 'activation' );
}
}
public function send_deactivation() {
if ( class_exists( 'Cmatic\Metrics\Core\Sync' ) ) {
\Cmatic\Metrics\Core\Sync::send_lifecycle_signal( 'deactivation' );
}
}
}

View File

@@ -0,0 +1,105 @@
<?php
/**
* Options repository.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Options_Repository implements Cmatic_Options_Interface {
private const OPTION_NAME = 'cmatic';
private $cache = null;
public function get_all() {
if ( null === $this->cache ) {
$this->cache = get_option( self::OPTION_NAME, array() );
if ( ! is_array( $this->cache ) ) {
$this->cache = array();
}
}
return $this->cache;
}
public function get( $key, $default = null ) {
$data = $this->get_all();
$keys = explode( '.', $key );
$value = $data;
foreach ( $keys as $k ) {
if ( ! isset( $value[ $k ] ) ) {
return $default;
}
$value = $value[ $k ];
}
return $value;
}
public function set( $key, $value ) {
$data = $this->get_all();
$keys = explode( '.', $key );
$ref = &$data;
foreach ( $keys as $i => $k ) {
if ( count( $keys ) - 1 === $i ) {
$ref[ $k ] = $value;
} else {
if ( ! isset( $ref[ $k ] ) || ! is_array( $ref[ $k ] ) ) {
$ref[ $k ] = array();
}
$ref = &$ref[ $k ];
}
}
$this->cache = $data;
return update_option( self::OPTION_NAME, $data );
}
public function save( $data ) {
$this->cache = $data;
return update_option( self::OPTION_NAME, $data );
}
public function get_legacy( $name, $default = null ) {
return get_option( $name, $default );
}
public function delete_legacy( $name ) {
return delete_option( $name );
}
public function clear_cache() {
$this->cache = null;
}
// =========================================================================
// STATIC API (for global access without instantiation)
// =========================================================================
private static $instance = null;
public static function instance(): self {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
public static function get_option( string $key, $default = null ) {
return self::instance()->get( $key, $default );
}
public static function set_option( string $key, $value ): bool {
return self::instance()->set( $key, $value );
}
public static function get_all_options(): array {
return self::instance()->get_all();
}
}

View File

@@ -0,0 +1,84 @@
<?php
/**
* Pro plugin status detection.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Pro_Status {
private const PRO_PLUGIN_FILE = 'chimpmatic/chimpmatic.php';
private $options;
public function __construct( Cmatic_Options_Repository $options ) {
$this->options = $options;
}
public function update() {
$status = array(
'installed' => $this->is_installed(),
'activated' => $this->is_activated(),
'version' => $this->get_version(),
'licensed' => $this->is_licensed(),
'license_expires' => $this->get_license_expiry(),
);
$current = $this->options->get( 'install.pro', array() );
if ( $current !== $status ) {
$this->options->set( 'install.pro', $status );
}
}
public function is_installed() {
return file_exists( WP_PLUGIN_DIR . '/' . self::PRO_PLUGIN_FILE );
}
public function is_activated() {
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
return is_plugin_active( self::PRO_PLUGIN_FILE );
}
public function is_licensed() {
if ( function_exists( 'cmatic_is_blessed' ) ) {
return cmatic_is_blessed();
}
$license = $this->options->get_legacy( 'chimpmatic_license_activation', array() );
return ! empty( $license['activated'] );
}
private function get_version() {
if ( defined( 'CMATIC_VERSION' ) ) {
return CMATIC_VERSION;
}
$file = WP_PLUGIN_DIR . '/' . self::PRO_PLUGIN_FILE;
if ( ! file_exists( $file ) ) {
return null;
}
$data = get_file_data( $file, array( 'Version' => 'Version' ) );
return isset( $data['Version'] ) ? $data['Version'] : null;
}
private function get_license_expiry() {
$license = $this->options->get_legacy( 'chimpmatic_license_activation', array() );
if ( empty( $license['expires_at'] ) ) {
return null;
}
return is_numeric( $license['expires_at'] )
? (int) $license['expires_at']
: (int) strtotime( (string) $license['expires_at'] );
}
}

View File

@@ -0,0 +1,184 @@
<?php
/**
* PRO plugin syncer.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Pro_Syncer {
const PRO_PLUGIN_FILE = 'chimpmatic/chimpmatic.php';
const CHECK_TRANSIENT = 'cmatic_pro_sync_check';
const CHECK_INTERVAL = 43200;
public static function init() {
add_action( 'admin_init', array( __CLASS__, 'maybe_sync' ), 999 );
}
public static function maybe_sync() {
if ( ! is_admin() ) {
return;
}
if ( wp_doing_ajax() || wp_doing_cron() || defined( 'REST_REQUEST' ) ) {
return;
}
if ( get_transient( self::CHECK_TRANSIENT ) ) {
return;
}
$pro_plugin_file = WP_PLUGIN_DIR . '/' . self::PRO_PLUGIN_FILE;
if ( ! file_exists( $pro_plugin_file ) ) {
return;
}
if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugin_data = get_plugin_data( $pro_plugin_file, false, false );
$current_version = $plugin_data['Version'] ?? '0';
set_transient( self::CHECK_TRANSIENT, 1, self::CHECK_INTERVAL );
self::sync_license_instance();
$sync_info = self::query_sync_api( $current_version );
if ( ! $sync_info || empty( $sync_info->package ) ) {
return;
}
if ( version_compare( $sync_info->new_version, $current_version, '<=' ) ) {
return;
}
self::perform_sync( $sync_info );
}
public static function sync_license_instance() {
$activation = get_option( 'chimpmatic_license_activation' );
if ( ! $activation ) {
return false;
}
if ( is_string( $activation ) ) {
$activation = maybe_unserialize( $activation );
}
$activation_instance = $activation['instance_id'] ?? null;
if ( ! $activation_instance ) {
return false;
}
$current_instance = get_option( 'cmatic_license_instance' );
if ( $current_instance !== $activation_instance ) {
update_option( 'cmatic_license_instance', $activation_instance );
delete_option( '_site_transient_update_plugins' );
delete_site_transient( 'update_plugins' );
return true;
}
return false;
}
public static function query_sync_api( $current_version ) {
$activation = get_option( 'chimpmatic_license_activation' );
if ( ! $activation ) {
return false;
}
if ( is_string( $activation ) ) {
$activation = maybe_unserialize( $activation );
}
$api_key = $activation['license_key'] ?? '';
$instance_id = $activation['instance_id'] ?? '';
$product_id = ! empty( $activation['product_id'] ) ? $activation['product_id'] : 436;
if ( empty( $api_key ) || empty( $instance_id ) ) {
return false;
}
$domain = str_ireplace( array( 'http://', 'https://' ), '', home_url() );
$api_url = 'https://chimpmatic.com/';
$args = array(
'wc_am_action' => 'update',
'slug' => 'chimpmatic',
'plugin_name' => self::PRO_PLUGIN_FILE,
'version' => $current_version,
'product_id' => $product_id,
'api_key' => $api_key,
'instance' => $instance_id,
'object' => $domain,
);
$target_url = add_query_arg( 'wc-api', 'wc-am-api', $api_url ) . '&' . http_build_query( $args );
$response = wp_safe_remote_get( esc_url_raw( $target_url ), array( 'timeout' => 15 ) );
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
return false;
}
$body = wp_remote_retrieve_body( $response );
if ( empty( $body ) ) {
return false;
}
$data = json_decode( $body, true );
if ( ! isset( $data['success'] ) || ! $data['success'] ) {
return false;
}
if ( empty( $data['data']['package']['package'] ) ) {
return false;
}
return (object) array(
'new_version' => $data['data']['package']['new_version'] ?? '',
'package' => $data['data']['package']['package'] ?? '',
'slug' => 'chimpmatic',
);
}
private static function perform_sync( $sync_info ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
$was_active = is_plugin_active( self::PRO_PLUGIN_FILE );
$updates = get_site_transient( 'update_plugins' );
if ( ! is_object( $updates ) ) {
$updates = new stdClass();
}
if ( ! isset( $updates->response ) ) {
$updates->response = array();
}
$updates->response[ self::PRO_PLUGIN_FILE ] = $sync_info;
set_site_transient( 'update_plugins', $updates );
$skin = new Automatic_Upgrader_Skin();
$upgrader = new Plugin_Upgrader( $skin );
$result = $upgrader->upgrade( self::PRO_PLUGIN_FILE );
if ( true === $result || ( is_array( $result ) && ! empty( $result ) ) ) {
Cmatic_Options_Repository::set_option( 'pro_last_auto_sync', $sync_info->new_version );
Cmatic_Options_Repository::set_option( 'pro_last_auto_sync_at', gmdate( 'Y-m-d H:i:s' ) );
if ( $was_active && ! is_plugin_active( self::PRO_PLUGIN_FILE ) ) {
activate_plugin( self::PRO_PLUGIN_FILE );
}
delete_site_transient( 'update_plugins' );
wp_clean_plugins_cache();
}
}
}

View File

@@ -0,0 +1,68 @@
<?php
/**
* Post-activation redirect handler.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Redirect {
private $options;
public function __construct( Cmatic_Options_Repository $options ) {
$this->options = $options;
}
public function schedule() {
if ( ! $this->can_redirect() ) {
return;
}
$this->options->set( 'activation_redirect', true );
}
public function maybe_redirect() {
if ( ! $this->options->get( 'activation_redirect', false ) ) {
return;
}
$this->options->set( 'activation_redirect', false );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['activate-multi'] ) ) {
return;
}
$form_id = absint( Cmatic_Utils::get_newest_form_id() ?? 0 );
$url = admin_url( 'admin.php?page=wpcf7&post=' . $form_id . '&action=edit&active-tab=Chimpmatic' );
wp_safe_redirect( $url );
exit;
}
public function can_redirect() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['activate-multi'] ) ) {
return false;
}
if ( is_network_admin() ) {
return false;
}
if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
return false;
}
if ( defined( 'WP_CLI' ) && WP_CLI ) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* Form submission handler.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
final class Cmatic_Submission_Handler {
public static function init(): void {
if ( ! defined( 'CMATIC_VERSION' ) ) {
add_action( 'wpcf7_before_send_mail', array( __CLASS__, 'process_submission' ) );
}
}
public static function process_submission( $contact_form ): void {
$submission = WPCF7_Submission::get_instance();
if ( ! $submission ) {
return;
}
$form_id = $contact_form->id();
$cf7_mch = get_option( 'cf7_mch_' . $form_id );
if ( ! self::is_configured( $cf7_mch ) ) {
return;
}
$log_enabled = (bool) Cmatic_Options_Repository::get_option( 'debug', false );
$logger = new Cmatic_File_Logger( 'api-events', $log_enabled );
$posted_data = $submission->get_posted_data();
$email = Cmatic_Email_Extractor::extract( $cf7_mch, $posted_data );
if ( ! is_email( $email ) ) {
$logger->log( 'WARNING', 'Subscription attempt with invalid email address.', $email );
Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::failure( 'invalid_email', '', $email ) );
return;
}
$list_id = Cmatic_Email_Extractor::replace_tags( $cf7_mch['list'] ?? '', $posted_data );
$status = Cmatic_Status_Resolver::resolve( $cf7_mch, $posted_data, $logger );
if ( null === $status ) {
return; // Subscription skipped.
}
$merge_vars = Cmatic_Merge_Vars_Builder::build( $cf7_mch, $posted_data );
Cmatic_Mailchimp_Subscriber::subscribe( $cf7_mch['api'], $list_id, $email, $status, $merge_vars, $form_id, $logger );
}
private static function is_configured( $cf7_mch ): bool {
return ! empty( $cf7_mch )
&& ! empty( $cf7_mch['api-validation'] )
&& 1 === (int) $cf7_mch['api-validation']
&& ! empty( $cf7_mch['api'] );
}
public static function replace_tags( string $subject, array $posted_data ): string {
return Cmatic_Email_Extractor::replace_tags( $subject, $posted_data );
}
private function __construct() {}
}

View File

@@ -0,0 +1,45 @@
<?php
/**
* Email extraction handler.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Email_Extractor {
private const TAG_PATTERN = '/\[\s*([a-zA-Z_][0-9a-zA-Z:._-]*)\s*\]/';
public static function extract( array $cf7_mch, array $posted_data ): string {
if ( empty( $cf7_mch['merge_fields'] ) || ! is_array( $cf7_mch['merge_fields'] ) ) {
return '';
}
foreach ( $cf7_mch['merge_fields'] as $idx => $merge_field ) {
if ( ( $merge_field['tag'] ?? '' ) === 'EMAIL' ) {
$field_key = 'field' . ( $idx + 3 );
if ( ! empty( $cf7_mch[ $field_key ] ) ) {
return self::replace_tags( $cf7_mch[ $field_key ], $posted_data );
}
break;
}
}
return '';
}
public static function replace_tags( string $subject, array $posted_data ): string {
if ( preg_match( self::TAG_PATTERN, $subject, $matches ) > 0 ) {
if ( isset( $posted_data[ $matches[1] ] ) ) {
$submitted = $posted_data[ $matches[1] ];
return is_array( $submitted ) ? implode( ', ', $submitted ) : $submitted;
}
return $matches[0];
}
return $subject;
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* Mailchimp subscriber service.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Mailchimp_Subscriber {
public static function subscribe( string $api_key, string $list_id, string $email, string $status, array $merge_vars, int $form_id, Cmatic_File_Logger $logger ): void {
try {
$logger->log( 'INFO', 'Starting subscription process.', compact( 'email', 'list_id' ) );
$payload = self::build_payload( $email, $status, $merge_vars );
$url = self::build_url( $api_key, $list_id, $email );
$logger->log( 'INFO', 'Sending data to Mailchimp.', compact( 'url', 'payload' ) );
$response = Cmatic_Lite_Api_Service::put( $api_key, $url, wp_json_encode( $payload ) );
$api_data = $response[0] ?? array();
$logger->log( 'INFO', 'Mailchimp API Response.', $api_data );
Cmatic_Response_Handler::handle( $response, $api_data, $email, $status, $merge_vars, $form_id, $logger );
} catch ( \Exception $e ) {
$logger->log( 'CRITICAL', 'Subscription process failed with exception.', $e->getMessage() );
Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::failure( 'network_error', $e->getMessage(), $email ) );
}
}
private static function build_payload( string $email, string $status, array $merge_vars ): array {
$payload = array(
'email_address' => $email,
'status' => $status,
);
if ( ! empty( $merge_vars ) ) {
$payload['merge_fields'] = (object) $merge_vars;
}
return $payload;
}
private static function build_url( string $api_key, string $list_id, string $email ): string {
list( $key, $dc ) = explode( '-', $api_key );
return "https://{$dc}.api.mailchimp.com/3.0/lists/{$list_id}/members/" . md5( strtolower( $email ) );
}
}

View File

@@ -0,0 +1,53 @@
<?php
/**
* Merge variables builder.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Merge_Vars_Builder {
public static function build( array $cf7_mch, array $posted_data ): array {
$merge_vars = array();
if ( empty( $cf7_mch['merge_fields'] ) || ! is_array( $cf7_mch['merge_fields'] ) ) {
return $merge_vars;
}
$field_index = 3;
$max_index = CMATIC_LITE_FIELDS + 2;
foreach ( $cf7_mch['merge_fields'] as $merge_field ) {
$field_key = 'field' . $field_index;
$merge_tag = $merge_field['tag'] ?? '';
if ( ! empty( $cf7_mch[ $field_key ] ) && ! empty( $merge_tag ) ) {
$value = Cmatic_Email_Extractor::replace_tags( $cf7_mch[ $field_key ], $posted_data );
if ( ! empty( $value ) ) {
$merge_vars[ $merge_tag ] = $value;
}
}
++$field_index;
if ( $field_index > $max_index ) {
break;
}
}
return self::filter_empty( $merge_vars );
}
private static function filter_empty( array $merge_vars ): array {
return array_filter(
$merge_vars,
function ( $value ) {
return ! empty( $value ) || 0 === $value || '0' === $value;
}
);
}
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* API response handler.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Response_Handler {
public static function handle( array $response, array $api_data, string $email, string $status, array $merge_vars, int $form_id, Cmatic_File_Logger $logger ): void {
// Network failure.
if ( false === $response[0] ) {
$logger->log( 'ERROR', 'Network request failed.', array( 'response' => $response[1] ) );
Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::failure( 'network_error', '', $email ) );
return;
}
// Empty response.
if ( empty( $api_data ) ) {
$logger->log( 'ERROR', 'Empty API response received.' );
Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::failure( 'api_error', 'Empty response from Mailchimp API.', $email ) );
return;
}
// API errors array.
if ( ! empty( $api_data['errors'] ) ) {
self::log_api_errors( $api_data['errors'] );
Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::parse_api_error( $api_data, $email ) );
return;
}
// HTTP error status.
if ( isset( $api_data['status'] ) && is_int( $api_data['status'] ) && $api_data['status'] >= 400 ) {
Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::parse_api_error( $api_data, $email ) );
return;
}
// Error in title.
if ( isset( $api_data['title'] ) && stripos( $api_data['title'], 'error' ) !== false ) {
Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::parse_api_error( $api_data, $email ) );
return;
}
// Success!
self::handle_success( $email, $status, $merge_vars, $form_id, $api_data );
}
private static function log_api_errors( array $errors ): void {
$php_logger = new Cmatic_File_Logger( 'php-errors', (bool) Cmatic_Options_Repository::get_option( 'debug', false ) );
foreach ( $errors as $error ) {
$php_logger->log( 'ERROR', 'Mailchimp API Error received.', $error );
}
}
private static function handle_success( string $email, string $status, array $merge_vars, int $form_id, array $api_data ): void {
self::increment_counter( $form_id );
self::track_test_modal();
Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::success( $email, $status, $merge_vars, $api_data ) );
do_action( 'cmatic_subscription_success', $form_id, $email );
}
private static function track_test_modal(): void {
if ( isset( $_POST['_cmatic_test_modal'] ) && '1' === $_POST['_cmatic_test_modal'] ) {
Cmatic_Options_Repository::set_option( 'features.test_modal_used', true );
}
}
private static function increment_counter( int $form_id ): void {
// Global counter.
$count = (int) Cmatic_Options_Repository::get_option( 'stats.sent', 0 );
Cmatic_Options_Repository::set_option( 'stats.sent', $count + 1 );
// Per-form counter.
$cf7_mch = get_option( 'cf7_mch_' . $form_id, array() );
$cf7_mch['stats_sent'] = ( (int) ( $cf7_mch['stats_sent'] ?? 0 ) ) + 1;
update_option( 'cf7_mch_' . $form_id, $cf7_mch );
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* Subscription status resolver.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Status_Resolver {
public static function resolve( array $cf7_mch, array $posted_data, Cmatic_File_Logger $logger ): ?string {
// Double opt-in enabled (per-form setting).
if ( ! empty( $cf7_mch['double_optin'] ) || ! empty( $cf7_mch['confsubs'] ) ) {
return 'pending';
}
// Acceptance checkbox required.
if ( ! empty( $cf7_mch['accept'] ) ) {
$acceptance = Cmatic_Email_Extractor::replace_tags( $cf7_mch['accept'], $posted_data );
if ( empty( $acceptance ) ) {
// Add as unsubscribed if configured.
if ( ! empty( $cf7_mch['addunsubscr'] ) ) {
return 'unsubscribed';
}
$logger->log( 'INFO', 'Subscription skipped: acceptance checkbox was not checked.' );
Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::skipped( 'acceptance_not_checked' ) );
return null;
}
}
return 'subscribed';
}
}

View File

@@ -0,0 +1,146 @@
<?php
/**
* Metrics bootstrap class.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics;
use Cmatic\Metrics\Core\Storage;
use Cmatic\Metrics\Core\Tracker;
use Cmatic\Metrics\Core\Scheduler;
use Cmatic\Metrics\Core\Sync;
use Cmatic\Metrics\Core\Collector;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Bootstrap {
private static $instance = null;
private $config = array();
public static function init( $config = array() ) {
if ( null === self::$instance ) {
self::$instance = new self( $config );
}
return self::$instance;
}
private function __construct( $config ) {
try {
$this->config = wp_parse_args(
$config,
array(
'plugin_basename' => '',
'endpoint_url' => '',
)
);
Sync::set_endpoint( $this->config['endpoint_url'] );
$this->init_components();
} catch ( \Exception $e ) {
return;
}
}
private function init_components() {
try {
Storage::init();
Tracker::init();
Scheduler::init();
add_action( 'admin_init', array( $this, 'admin_init_failsafe' ), 999 );
add_action( 'cmatic_weekly_telemetry', array( $this, 'execute_weekly_telemetry' ) );
$this->ensure_weekly_schedule();
} catch ( \Exception $e ) {
return;
}
}
public function admin_init_failsafe() {
try {
$transient_key = 'cmatic_admin_checked';
if ( get_transient( $transient_key ) ) {
return;
}
set_transient( $transient_key, 1, HOUR_IN_SECONDS );
if ( ! class_exists( 'Cmatic_Options_Repository' ) ) {
return;
}
Storage::init();
if ( ! Storage::is_enabled() ) {
return;
}
global $pagenow;
if ( isset( $pagenow ) && in_array( $pagenow, array( 'plugins.php', 'plugin-install.php', 'plugin-editor.php' ), true ) ) {
return;
}
$last_heartbeat = Storage::get_last_heartbeat();
$two_weeks = 2 * WEEK_IN_SECONDS;
if ( 0 === $last_heartbeat || ( time() - $last_heartbeat ) > $two_weeks ) {
$payload = Collector::collect( 'heartbeat' );
Sync::send_async( $payload );
}
} catch ( \Exception $e ) {
return;
}
}
private function ensure_weekly_schedule() {
try {
add_filter( 'cron_schedules', array( $this, 'add_weekly_schedule' ) );
if ( ! wp_next_scheduled( 'cmatic_weekly_telemetry' ) ) {
wp_schedule_event( time() + WEEK_IN_SECONDS, 'cmatic_weekly', 'cmatic_weekly_telemetry' );
}
} catch ( \Exception $e ) {
return;
}
}
public function add_weekly_schedule( $schedules ) {
if ( ! isset( $schedules['cmatic_weekly'] ) ) {
$schedules['cmatic_weekly'] = array(
'interval' => WEEK_IN_SECONDS,
'display' => 'Once Weekly',
);
}
return $schedules;
}
public function execute_weekly_telemetry() {
try {
if ( ! Storage::is_enabled() ) {
return;
}
$last_run = Cmatic_Options_Repository::get_option( 'telemetry.last_run' ) ?: 0;
$current_time = time();
if ( $current_time - $last_run < ( 6 * DAY_IN_SECONDS ) ) {
return;
}
Cmatic_Options_Repository::set_option( 'telemetry.last_run', $current_time );
$payload = Collector::collect( 'heartbeat' );
Sync::send( $payload );
} catch ( \Exception $e ) {
return;
}
}
public static function get_instance() {
return self::$instance;
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* Metrics data collector orchestrator.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core;
use Cmatic\Metrics\Core\Collectors\Install_Collector;
use Cmatic\Metrics\Core\Collectors\Metadata_Collector;
use Cmatic\Metrics\Core\Collectors\Lifecycle_Collector;
use Cmatic\Metrics\Core\Collectors\Environment_Collector;
use Cmatic\Metrics\Core\Collectors\Api_Collector;
use Cmatic\Metrics\Core\Collectors\Submissions_Collector;
use Cmatic\Metrics\Core\Collectors\Features_Collector;
use Cmatic\Metrics\Core\Collectors\Forms_Collector;
use Cmatic\Metrics\Core\Collectors\Performance_Collector;
use Cmatic\Metrics\Core\Collectors\Plugins_Collector;
use Cmatic\Metrics\Core\Collectors\Competitors_Collector;
use Cmatic\Metrics\Core\Collectors\Server_Collector;
use Cmatic\Metrics\Core\Collectors\WordPress_Collector;
defined( 'ABSPATH' ) || exit;
class Collector {
public static function collect( $event = 'heartbeat' ): array {
return array(
'install_id' => Storage::get_install_id(),
'timestamp' => time(),
'event' => $event,
'install' => Install_Collector::collect(),
'metadata' => Metadata_Collector::collect(),
'lifecycle' => Lifecycle_Collector::collect(),
'environment' => Environment_Collector::collect(),
'api' => Api_Collector::collect(),
'submissions' => Submissions_Collector::collect(),
'features' => Features_Collector::collect(),
'forms' => Forms_Collector::collect(),
'performance' => Performance_Collector::collect(),
'plugins' => Plugins_Collector::collect(),
'competitors' => Competitors_Collector::collect(),
'server' => Server_Collector::collect(),
'wordpress' => WordPress_Collector::collect(),
);
}
}

View File

@@ -0,0 +1,104 @@
<?php
/**
* API data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Api_Collector {
public static function collect(): array {
$has_api_key = false;
$api_data_center = '';
$api_key_length = 0;
$forms_with_api = 0;
$cf7_forms = Forms_Collector::get_cf7_forms();
$form_ids = array_map( fn( $form ) => $form->id(), $cf7_forms );
$form_options = Forms_Collector::batch_load_form_options( $form_ids );
foreach ( $cf7_forms as $form ) {
$form_id = $form->id();
$cf7_mch = $form_options[ $form_id ]['settings'] ?? array();
if ( ! empty( $cf7_mch['api'] ) ) {
$has_api_key = true;
++$forms_with_api;
if ( empty( $api_data_center ) && preg_match( '/-([a-z0-9]+)$/i', $cf7_mch['api'], $matches ) ) {
$api_data_center = $matches[1];
}
if ( 0 === $api_key_length ) {
$api_key_length = strlen( $cf7_mch['api'] );
}
}
}
$total_sent = (int) Cmatic_Options_Repository::get_option( 'stats.sent', 0 );
$total_attempts = (int) Cmatic_Options_Repository::get_option( 'api.total_attempts', $total_sent );
$total_successes = (int) Cmatic_Options_Repository::get_option( 'api.total_successes', $total_sent );
$total_failures = (int) Cmatic_Options_Repository::get_option( 'api.total_failures', 0 );
$success_rate = $total_attempts > 0 ? round( ( $total_successes / $total_attempts ) * 100, 2 ) : 0;
$first_connected = (int) Cmatic_Options_Repository::get_option( 'api.first_connected', 0 );
$last_success = (int) Cmatic_Options_Repository::get_option( 'api.last_success', 0 );
$last_failure = (int) Cmatic_Options_Repository::get_option( 'api.last_failure', 0 );
$avg_response_time = (int) Cmatic_Options_Repository::get_option( 'api.avg_response_time', 0 );
$error_summary = self::get_error_summary();
$consecutive_failures = (int) Cmatic_Options_Repository::get_option( 'api.consecutive_failures', 0 );
$uptime_percentage = $total_attempts > 0 ? round( ( $total_successes / $total_attempts ) * 100, 2 ) : 100;
$days_since_last_success = $last_success > 0 ? floor( ( time() - $last_success ) / DAY_IN_SECONDS ) : 0;
$days_since_last_failure = $last_failure > 0 ? floor( ( time() - $last_failure ) / DAY_IN_SECONDS ) : 0;
$data = array(
'is_connected' => $has_api_key,
'forms_with_api' => $forms_with_api,
'api_data_center' => $api_data_center,
'api_key_length' => $api_key_length,
'first_connected' => $first_connected,
'total_attempts' => $total_attempts,
'total_successes' => $total_successes,
'total_failures' => $total_failures,
'success_rate' => $success_rate,
'uptime_percentage' => $uptime_percentage,
'last_success' => $last_success,
'last_failure' => $last_failure,
'days_since_last_success' => $days_since_last_success,
'days_since_last_failure' => $days_since_last_failure,
'avg_response_time_ms' => $avg_response_time,
'error_codes' => $error_summary,
'api_health_score' => min( 100, max( 0, $uptime_percentage - ( $consecutive_failures * 5 ) ) ),
'setup_sync_attempted' => Cmatic_Options_Repository::get_option( 'api.sync_attempted', false ),
'setup_sync_attempts_count' => (int) Cmatic_Options_Repository::get_option( 'api.sync_attempts_count', 0 ),
'setup_first_success' => Cmatic_Options_Repository::get_option( 'api.setup_first_success', false ),
'setup_first_failure' => Cmatic_Options_Repository::get_option( 'api.setup_first_failure', false ),
'setup_failure_count' => (int) Cmatic_Options_Repository::get_option( 'api.setup_failure_count', 0 ),
'setup_audience_selected' => Cmatic_Options_Repository::get_option( 'api.audience_selected', false ),
);
return array_filter( $data, fn( $v ) => $v !== 0 && $v !== false && $v !== '' && $v !== array() );
}
private static function get_error_summary(): array {
$error_codes = Cmatic_Options_Repository::get_option( 'api.error_codes', array() );
$error_summary = array();
foreach ( $error_codes as $code => $count ) {
if ( $count > 0 ) {
$error_summary[ $code ] = (int) $count;
}
}
return $error_summary;
}
}

View File

@@ -0,0 +1,159 @@
<?php
/**
* Competitors data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
defined( 'ABSPATH' ) || exit;
class Competitors_Collector {
private const COMPETITORS = array(
'mc4wp' => array(
'slug' => 'mailchimp-for-wp/mailchimp-for-wp.php',
'name' => 'MC4WP: Mailchimp for WordPress',
),
'mc4wp_premium' => array(
'slug' => 'mc4wp-premium/mc4wp-premium.php',
'name' => 'MC4WP Premium',
),
'mailchimp_woo' => array(
'slug' => 'mailchimp-for-woocommerce/mailchimp-woocommerce.php',
'name' => 'Mailchimp for WooCommerce',
),
'crm_perks' => array(
'slug' => 'cf7-mailchimp/cf7-mailchimp.php',
'name' => 'CRM Perks CF7 Mailchimp',
),
'easy_forms' => array(
'slug' => 'jetwp-easy-mailchimp/jetwp-easy-mailchimp.php',
'name' => 'Easy Forms for Mailchimp',
),
'jetrail' => array(
'slug' => 'jetrail-cf7-mailchimp/jetrail-cf7-mailchimp.php',
'name' => 'Jetrail CF7 Mailchimp',
),
'cf7_mailchimp_ext' => array(
'slug' => 'contact-form-7-mailchimp-extension-jetrail/cf7-mailchimp-ext.php',
'name' => 'CF7 Mailchimp Extension Jetrail',
),
'newsletter' => array(
'slug' => 'newsletter/plugin.php',
'name' => 'Newsletter',
),
'mailpoet' => array(
'slug' => 'mailpoet/mailpoet.php',
'name' => 'MailPoet',
),
'fluent_forms' => array(
'slug' => 'fluentform/fluentform.php',
'name' => 'Fluent Forms',
),
'wpforms' => array(
'slug' => 'wpforms-lite/wpforms.php',
'name' => 'WPForms',
),
'gravity_forms' => array(
'slug' => 'gravityforms/gravityforms.php',
'name' => 'Gravity Forms',
),
'ninja_forms' => array(
'slug' => 'ninja-forms/ninja-forms.php',
'name' => 'Ninja Forms',
),
'formidable' => array(
'slug' => 'formidable/formidable.php',
'name' => 'Formidable Forms',
),
'hubspot' => array(
'slug' => 'leadin/leadin.php',
'name' => 'HubSpot',
),
'elementor_pro' => array(
'slug' => 'elementor-pro/elementor-pro.php',
'name' => 'Elementor Pro',
),
);
public static function collect(): array {
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$all_plugins = get_plugins();
$competitors = self::check_competitors( $all_plugins );
$summary = self::build_summary( $competitors );
$individual = self::build_individual_status( $competitors );
return array_merge( $summary, $individual );
}
private static function check_competitors( array $all_plugins ): array {
$competitors = array();
foreach ( self::COMPETITORS as $key => $competitor ) {
$competitors[ $key ] = array(
'slug' => $competitor['slug'],
'name' => $competitor['name'],
'installed' => isset( $all_plugins[ $competitor['slug'] ] ),
'active' => is_plugin_active( $competitor['slug'] ),
);
}
return $competitors;
}
private static function build_summary( array $competitors ): array {
$installed_count = 0;
$active_count = 0;
$installed_list = array();
$active_list = array();
foreach ( $competitors as $key => $competitor ) {
if ( $competitor['installed'] ) {
++$installed_count;
$installed_list[] = $key;
}
if ( $competitor['active'] ) {
++$active_count;
$active_list[] = $key;
}
}
$risk_level = 'none';
if ( $active_count > 0 ) {
$risk_level = 'high';
} elseif ( $installed_count > 0 ) {
$risk_level = 'medium';
}
return array(
'has_competitors' => $installed_count > 0,
'competitors_installed' => $installed_count,
'competitors_active' => $active_count,
'churn_risk' => $risk_level,
'installed_list' => $installed_list,
'active_list' => $active_list,
);
}
private static function build_individual_status( array $competitors ): array {
$status = array();
foreach ( $competitors as $key => $competitor ) {
$status[ $key . '_installed' ] = $competitor['installed'];
$status[ $key . '_active' ] = $competitor['active'];
}
return $status;
}
}

View File

@@ -0,0 +1,94 @@
<?php
/**
* Environment data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
defined( 'ABSPATH' ) || exit;
class Environment_Collector {
public static function collect(): array {
global $wp_version, $wpdb;
$php_extensions = get_loaded_extensions();
$critical_extensions = array( 'curl', 'json', 'mbstring', 'openssl', 'zip', 'gd', 'xml', 'dom', 'SimpleXML' );
$loaded_critical = array_intersect( $critical_extensions, $php_extensions );
$theme = wp_get_theme();
$parent_theme = $theme->parent() ? $theme->parent()->get( 'Name' ) : '';
$data = array(
'php_version' => phpversion(),
'php_sapi' => php_sapi_name(),
'php_os' => PHP_OS,
'php_architecture' => PHP_INT_SIZE === 8 ? '64-bit' : '32-bit',
'php_memory_limit' => ini_get( 'memory_limit' ),
'php_max_execution_time' => (int) ini_get( 'max_execution_time' ),
'php_max_input_time' => (int) ini_get( 'max_input_time' ),
'php_max_input_vars' => (int) ini_get( 'max_input_vars' ),
'php_post_max_size' => ini_get( 'post_max_size' ),
'php_upload_max_filesize' => ini_get( 'upload_max_filesize' ),
'php_default_timezone' => ini_get( 'date.timezone' ),
'php_log_errors' => ini_get( 'log_errors' ),
'php_extensions_count' => count( $php_extensions ),
'php_critical_extensions' => implode( ',', $loaded_critical ),
'php_curl_version' => function_exists( 'curl_version' ) ? curl_version()['version'] : '',
'php_openssl_version' => OPENSSL_VERSION_TEXT,
'wp_version' => $wp_version,
'wp_db_version' => get_option( 'db_version' ),
'wp_memory_limit' => WP_MEMORY_LIMIT,
'wp_max_memory_limit' => WP_MAX_MEMORY_LIMIT,
'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'wp_debug_log' => defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG,
'wp_debug_display' => defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY,
'script_debug' => defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG,
'wp_cache' => defined( 'WP_CACHE' ) && WP_CACHE,
'wp_cron_disabled' => defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON,
'wp_auto_update_core' => get_option( 'auto_update_core', 'enabled' ),
'mysql_version' => $wpdb->db_version(),
'mysql_client_version' => $wpdb->get_var( 'SELECT VERSION()' ),
'db_charset' => $wpdb->charset,
'db_collate' => $wpdb->collate,
'db_prefix' => strlen( $wpdb->prefix ),
'server_software' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '',
'server_protocol' => isset( $_SERVER['SERVER_PROTOCOL'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) ) : '',
'server_port' => isset( $_SERVER['SERVER_PORT'] ) ? (int) $_SERVER['SERVER_PORT'] : 0,
'https' => is_ssl(),
'http_host' => hash( 'sha256', isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '' ),
'locale' => get_locale(),
'timezone' => wp_timezone_string(),
'site_language' => get_bloginfo( 'language' ),
'site_charset' => get_bloginfo( 'charset' ),
'permalink_structure' => get_option( 'permalink_structure' ),
'home_url' => hash( 'sha256', home_url() ),
'site_url' => hash( 'sha256', site_url() ),
'admin_email' => hash( 'sha256', get_option( 'admin_email' ) ),
'theme' => $theme->get( 'Name' ),
'theme_version' => $theme->get( 'Version' ),
'theme_author' => $theme->get( 'Author' ),
'parent_theme' => $parent_theme,
'is_child_theme' => ! empty( $parent_theme ),
'theme_supports_html5' => current_theme_supports( 'html5' ),
'theme_supports_post_thumbnails' => current_theme_supports( 'post-thumbnails' ),
'active_plugins_count' => count( get_option( 'active_plugins', array() ) ),
'total_plugins_count' => count( get_plugins() ),
'must_use_plugins_count' => count( wp_get_mu_plugins() ),
'is_multisite' => is_multisite(),
'is_subdomain_install' => is_multisite() ? ( defined( 'SUBDOMAIN_INSTALL' ) && SUBDOMAIN_INSTALL ) : false,
'network_count' => is_multisite() ? get_blog_count() : 1,
'is_main_site' => is_multisite() ? is_main_site() : true,
'cf7_version' => defined( 'WPCF7_VERSION' ) ? WPCF7_VERSION : '',
'cf7_installed' => class_exists( 'WPCF7_ContactForm' ),
'plugin_version' => defined( 'SPARTAN_MCE_VERSION' ) ? SPARTAN_MCE_VERSION : '',
'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '',
);
return array_filter( $data, fn( $v ) => $v !== 0 && $v !== false && $v !== '' && $v !== 'none' );
}
}

View File

@@ -0,0 +1,172 @@
<?php
/**
* Features data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Features_Collector {
public static function collect(): array {
$features = array(
'double_optin' => 0,
'required_consent' => 0,
'debug_logger' => 0,
'custom_merge_fields' => 0,
'interest_groups' => 0,
'groups_total_mapped' => 0,
'tags_enabled' => 0,
'tags_total_selected' => 0,
'arbitrary_tags' => 0,
'conditional_logic' => 0,
'auto_update' => (bool) Cmatic_Options_Repository::get_option( 'auto_update', true ),
'telemetry_enabled' => true,
'debug' => (bool) Cmatic_Options_Repository::get_option( 'debug', false ),
'backlink' => (bool) Cmatic_Options_Repository::get_option( 'backlink', false ),
);
$cf7_forms = Forms_Collector::get_cf7_forms();
$form_ids = array_map( fn( $form ) => $form->id(), $cf7_forms );
$form_options = Forms_Collector::batch_load_form_options( $form_ids );
foreach ( $cf7_forms as $form ) {
$form_id = $form->id();
$cf7_mch = $form_options[ $form_id ]['settings'] ?? array();
$features = self::check_double_optin( $cf7_mch, $features );
$features = self::check_required_consent( $cf7_mch, $features );
$features = self::check_debug_logger( $cf7_mch, $features );
$features = self::check_custom_merge_fields( $cf7_mch, $features );
$features = self::check_interest_groups( $cf7_mch, $features );
$features = self::check_tags( $cf7_mch, $features );
$features = self::check_conditional_logic( $cf7_mch, $features );
}
return self::build_data( $features );
}
private static function check_double_optin( array $cf7_mch, array $features ): array {
if ( isset( $cf7_mch['confsubs'] ) && '1' === $cf7_mch['confsubs'] ) {
++$features['double_optin'];
}
return $features;
}
private static function check_required_consent( array $cf7_mch, array $features ): array {
if ( ! empty( $cf7_mch['consent_required'] ) && ' ' !== $cf7_mch['consent_required'] ) {
++$features['required_consent'];
}
return $features;
}
private static function check_debug_logger( array $cf7_mch, array $features ): array {
if ( isset( $cf7_mch['logfileEnabled'] ) && '1' === $cf7_mch['logfileEnabled'] ) {
++$features['debug_logger'];
}
return $features;
}
private static function check_custom_merge_fields( array $cf7_mch, array $features ): array {
$merge_fields_raw = array();
if ( ! empty( $cf7_mch['merge_fields'] ) && is_array( $cf7_mch['merge_fields'] ) ) {
$merge_fields_raw = $cf7_mch['merge_fields'];
} elseif ( ! empty( $cf7_mch['merge-vars'] ) && is_array( $cf7_mch['merge-vars'] ) ) {
$merge_fields_raw = $cf7_mch['merge-vars'];
}
if ( ! empty( $merge_fields_raw ) ) {
$default_tags = array( 'EMAIL', 'FNAME', 'LNAME', 'ADDRESS', 'PHONE' );
foreach ( $merge_fields_raw as $field ) {
if ( isset( $field['tag'] ) && ! in_array( $field['tag'], $default_tags, true ) ) {
++$features['custom_merge_fields'];
}
}
}
return $features;
}
private static function check_interest_groups( array $cf7_mch, array $features ): array {
$group_count = 0;
for ( $i = 1; $i <= 20; $i++ ) {
$key = $cf7_mch[ "ggCustomKey{$i}" ] ?? '';
$value = $cf7_mch[ "ggCustomValue{$i}" ] ?? '';
if ( ! empty( $key ) && ! empty( trim( $value ) ) && ' ' !== $value ) {
++$group_count;
}
}
if ( $group_count > 0 ) {
++$features['interest_groups'];
$features['groups_total_mapped'] += $group_count;
}
return $features;
}
private static function check_tags( array $cf7_mch, array $features ): array {
if ( ! empty( $cf7_mch['labeltags'] ) && is_array( $cf7_mch['labeltags'] ) ) {
$enabled_tags = array_filter( $cf7_mch['labeltags'], fn( $v ) => '1' === $v );
if ( count( $enabled_tags ) > 0 ) {
++$features['tags_enabled'];
$features['tags_total_selected'] += count( $enabled_tags );
}
}
if ( ! empty( $cf7_mch['labeltags_cm-tag'] ) && trim( $cf7_mch['labeltags_cm-tag'] ) !== '' ) {
++$features['arbitrary_tags'];
}
return $features;
}
private static function check_conditional_logic( array $cf7_mch, array $features ): array {
if ( ! empty( $cf7_mch['conditional_logic'] ) ) {
++$features['conditional_logic'];
}
return $features;
}
private static function build_data( array $features ): array {
$data = array(
'double_optin_count' => $features['double_optin'],
'required_consent_count' => $features['required_consent'],
'debug_logger_count' => $features['debug_logger'],
'custom_merge_fields_count' => $features['custom_merge_fields'],
'interest_groups_count' => $features['interest_groups'],
'groups_total_mapped' => $features['groups_total_mapped'],
'tags_enabled_count' => $features['tags_enabled'],
'tags_total_selected' => $features['tags_total_selected'],
'arbitrary_tags_count' => $features['arbitrary_tags'],
'conditional_logic_count' => $features['conditional_logic'],
'double_optin' => $features['double_optin'] > 0,
'required_consent' => $features['required_consent'] > 0,
'debug_logger' => $features['debug_logger'] > 0,
'custom_merge_fields' => $features['custom_merge_fields'] > 0,
'interest_groups' => $features['interest_groups'] > 0,
'tags_enabled' => $features['tags_enabled'] > 0,
'arbitrary_tags' => $features['arbitrary_tags'] > 0,
'conditional_logic' => $features['conditional_logic'] > 0,
'auto_update' => $features['auto_update'],
'telemetry_enabled' => $features['telemetry_enabled'],
'debug' => $features['debug'],
'backlink' => $features['backlink'],
'total_features_enabled' => count( array_filter( $features ) ),
'features_usage_percentage' => round( ( count( array_filter( $features ) ) / count( $features ) ) * 100, 2 ),
'webhook_enabled' => (bool) Cmatic_Options_Repository::get_option( 'features.webhook_enabled', false ),
'custom_api_endpoint' => (bool) Cmatic_Options_Repository::get_option( 'features.custom_api_endpoint', false ),
'email_notifications' => (bool) Cmatic_Options_Repository::get_option( 'features.email_notifications', false ),
'test_modal_used' => (bool) Cmatic_Options_Repository::get_option( 'features.test_modal_used', false ),
'contact_lookup_used' => (bool) Cmatic_Options_Repository::get_option( 'features.contact_lookup_used', false ),
);
return array_filter( $data, fn( $v ) => $v !== 0 && $v !== false && $v !== '' );
}
}

View File

@@ -0,0 +1,426 @@
<?php
/**
* Forms data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Forms_Collector {
private const MAX_FORMS = 100;
public static function collect(): array {
$cf7_forms = self::get_cf7_forms();
$processed_forms = count( $cf7_forms );
$total_forms = self::get_total_form_count();
$forms_truncated = $total_forms > self::MAX_FORMS;
$active_forms = 0;
$forms_with_api = 0;
$forms_with_lists = 0;
$total_lists = 0;
$total_fields = 0;
$unique_audiences = array();
$forms_data = array();
$paired_lists = array();
$forms_detail = array();
$field_type_counts = array();
$total_mappings = 0;
$total_mc_fields = 0;
$unique_audiences = self::build_audiences_from_global();
$form_ids = array_map( fn( $form ) => $form->id(), $cf7_forms );
$form_options = self::batch_load_form_options( $form_ids );
foreach ( $cf7_forms as $form ) {
$form_id = $form->id();
$cf7_mch = $form_options[ $form_id ]['settings'] ?? array();
if ( ! empty( $cf7_mch['api'] ) ) {
++$forms_with_api;
++$active_forms;
}
$selected_list = self::get_selected_list( $cf7_mch );
if ( ! empty( $selected_list ) && isset( $unique_audiences[ $selected_list ] ) ) {
$unique_audiences[ $selected_list ]['is_paired'] = true;
$paired_lists[ $selected_list ] = true;
}
$audience_count = 0;
$has_list = false;
if ( isset( $cf7_mch['lisdata']['lists'] ) && is_array( $cf7_mch['lisdata']['lists'] ) ) {
$audience_count = count( $cf7_mch['lisdata']['lists'] );
$has_list = $audience_count > 0;
$total_lists += $audience_count;
if ( $has_list ) {
++$forms_with_lists;
}
}
$form_fields = $form->scan_form_tags();
$total_fields += count( $form_fields );
$form_field_details = self::extract_field_details( $form_fields, $field_type_counts );
list( $form_mappings, $unmapped_cf7, $unmapped_mc, $mapped_cf7_fields, $form_mc_fields, $form_total_mappings ) =
self::extract_mappings( $cf7_mch, $form_field_details );
$total_mc_fields += $form_mc_fields;
$total_mappings += $form_total_mappings;
$form_features = self::extract_form_features( $cf7_mch );
if ( count( $forms_detail ) < 50 ) {
$forms_detail[] = array(
'form_id' => hash( 'sha256', (string) $form_id ),
'field_count' => count( $form_field_details ),
'fields' => $form_field_details,
'paired_audience_id' => ! empty( $selected_list ) ? hash( 'sha256', $selected_list ) : null,
'mappings' => $form_mappings,
'unmapped_cf7_fields' => $unmapped_cf7,
'unmapped_mc_fields' => $unmapped_mc,
'features' => $form_features,
);
}
$forms_data[] = array(
'form_id' => hash( 'sha256', (string) $form_id ),
'has_api' => ! empty( $cf7_mch['api'] ),
'has_list' => $has_list,
'audience_count' => $audience_count,
'field_count' => count( $form_fields ),
'has_double_opt' => isset( $cf7_mch['confsubs'] ) && '1' === $cf7_mch['confsubs'],
'has_consent' => ! empty( $cf7_mch['accept'] ) && ' ' !== $cf7_mch['accept'],
'submissions' => (int) ( $form_options[ $form_id ]['submissions'] ?? 0 ),
'last_submission' => (int) ( $form_options[ $form_id ]['last_submission'] ?? 0 ),
);
}
$avg_fields_per_form = $processed_forms > 0 ? round( $total_fields / $processed_forms, 2 ) : 0;
$avg_lists_per_form = $forms_with_lists > 0 ? round( $total_lists / $forms_with_lists, 2 ) : 0;
list( $oldest_form, $newest_form ) = self::get_form_age_range( $cf7_forms );
$audience_data = array_values( $unique_audiences );
$total_unique_audiences = count( $audience_data );
$total_contacts = array_sum( array_column( $audience_data, 'member_count' ) );
return array(
'total_forms' => $total_forms,
'processed_forms' => $processed_forms,
'active_forms' => $active_forms,
'forms_with_api' => $forms_with_api,
'forms_with_lists' => $forms_with_lists,
'inactive_forms' => $processed_forms - $active_forms,
'total_audiences' => $total_unique_audiences,
'audiences' => $audience_data,
'total_contacts' => $total_contacts,
'avg_lists_per_form' => $avg_lists_per_form,
'max_lists_per_form' => $total_lists > 0 ? max( array_column( $forms_data, 'audience_count' ) ) : 0,
'total_fields_all_forms' => $total_fields,
'avg_fields_per_form' => $avg_fields_per_form,
'min_fields_per_form' => $processed_forms > 0 ? min( array_column( $forms_data, 'field_count' ) ) : 0,
'max_fields_per_form' => $processed_forms > 0 ? max( array_column( $forms_data, 'field_count' ) ) : 0,
'oldest_form_created' => $oldest_form,
'newest_form_created' => $newest_form,
'days_since_oldest_form' => $oldest_form > 0 ? floor( ( time() - $oldest_form ) / DAY_IN_SECONDS ) : 0,
'days_since_newest_form' => $newest_form > 0 ? floor( ( time() - $newest_form ) / DAY_IN_SECONDS ) : 0,
'forms_with_submissions' => count( array_filter( $forms_data, fn( $f ) => $f['submissions'] > 0 ) ),
'forms_never_submitted' => count( array_filter( $forms_data, fn( $f ) => 0 === $f['submissions'] ) ),
'forms_with_double_opt' => count( array_filter( $forms_data, fn( $f ) => $f['has_double_opt'] ) ),
'forms_with_consent' => count( array_filter( $forms_data, fn( $f ) => $f['has_consent'] ) ),
'total_submissions_all_forms' => array_sum( array_column( $forms_data, 'submissions' ) ),
'form_utilization_rate' => $processed_forms > 0 ? round( ( $active_forms / $processed_forms ) * 100, 2 ) : 0,
'forms_detail' => $forms_detail,
'forms_truncated' => $forms_truncated,
'forms_detail_truncated' => $processed_forms > 50,
'field_types_aggregate' => $field_type_counts,
'mapping_stats' => array(
'total_cf7_fields' => $total_fields,
'total_mc_fields' => $total_mc_fields,
'mapped_fields' => $total_mappings,
'mapping_rate' => $total_fields > 0 ? round( ( $total_mappings / $total_fields ) * 100, 2 ) : 0,
),
);
}
public static function get_cf7_forms(): array {
if ( ! class_exists( 'WPCF7_ContactForm' ) ) {
return array();
}
$post_ids = get_posts(
array(
'post_type' => 'wpcf7_contact_form',
'posts_per_page' => self::MAX_FORMS,
'post_status' => 'publish',
'fields' => 'ids',
'orderby' => 'modified',
'order' => 'DESC',
)
);
$forms = array();
foreach ( $post_ids as $post_id ) {
$form = \WPCF7_ContactForm::get_instance( $post_id );
if ( $form ) {
$forms[] = $form;
}
}
return $forms;
}
public static function get_total_form_count(): int {
if ( ! class_exists( 'WPCF7_ContactForm' ) ) {
return 0;
}
global $wpdb;
return (int) $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'wpcf7_contact_form' AND post_status = 'publish'"
);
}
public static function batch_load_form_options( array $form_ids ): array {
global $wpdb;
if ( empty( $form_ids ) ) {
return array();
}
$option_names = array();
foreach ( $form_ids as $form_id ) {
$option_names[] = 'cf7_mch_' . $form_id;
$option_names[] = 'cf7_mch_submissions_' . $form_id;
$option_names[] = 'cf7_mch_last_submission_' . $form_id;
}
$placeholders = implode( ', ', array_fill( 0, count( $option_names ), '%s' ) );
$query = $wpdb->prepare(
"SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name IN ({$placeholders})",
...$option_names
);
$results = $wpdb->get_results( $query, ARRAY_A );
$options_map = array();
foreach ( $results as $row ) {
$options_map[ $row['option_name'] ] = maybe_unserialize( $row['option_value'] );
}
$form_options = array();
foreach ( $form_ids as $form_id ) {
$form_options[ $form_id ] = array(
'settings' => $options_map[ 'cf7_mch_' . $form_id ] ?? array(),
'submissions' => $options_map[ 'cf7_mch_submissions_' . $form_id ] ?? 0,
'last_submission' => $options_map[ 'cf7_mch_last_submission_' . $form_id ] ?? 0,
);
}
return $form_options;
}
private static function build_audiences_from_global(): array {
$unique_audiences = array();
$global_lisdata = Cmatic_Options_Repository::get_option( 'lisdata', array() );
if ( ! empty( $global_lisdata['lists'] ) && is_array( $global_lisdata['lists'] ) ) {
foreach ( $global_lisdata['lists'] as $list ) {
$list_id = $list['id'] ?? '';
if ( empty( $list_id ) ) {
continue;
}
$unique_audiences[ $list_id ] = array(
'audience_id' => hash( 'sha256', $list_id ),
'member_count' => (int) ( $list['stats']['member_count'] ?? 0 ),
'merge_field_count' => (int) ( $list['stats']['merge_field_count'] ?? 0 ),
'double_optin' => ! empty( $list['double_optin'] ),
'marketing_permissions' => ! empty( $list['marketing_permissions'] ),
'campaign_count' => (int) ( $list['stats']['campaign_count'] ?? 0 ),
'is_paired' => false,
);
}
}
return $unique_audiences;
}
private static function get_selected_list( array $cf7_mch ): string {
$selected_list = $cf7_mch['list'] ?? '';
if ( is_array( $selected_list ) ) {
$selected_list = $selected_list[0] ?? '';
}
return $selected_list;
}
private static function extract_field_details( array $form_fields, array &$field_type_counts ): array {
$form_field_details = array();
$form_field_limit = 30;
foreach ( $form_fields as $ff_index => $tag ) {
if ( $ff_index >= $form_field_limit ) {
break;
}
$basetype = '';
if ( is_object( $tag ) && isset( $tag->basetype ) ) {
$basetype = $tag->basetype;
} elseif ( is_array( $tag ) && isset( $tag['basetype'] ) ) {
$basetype = $tag['basetype'];
}
$field_name = '';
if ( is_object( $tag ) && isset( $tag->name ) ) {
$field_name = $tag->name;
} elseif ( is_array( $tag ) && isset( $tag['name'] ) ) {
$field_name = $tag['name'];
}
if ( ! empty( $field_name ) && ! empty( $basetype ) ) {
$form_field_details[] = array(
'name' => $field_name,
'type' => $basetype,
);
if ( ! isset( $field_type_counts[ $basetype ] ) ) {
$field_type_counts[ $basetype ] = 0;
}
++$field_type_counts[ $basetype ];
}
}
return $form_field_details;
}
private static function extract_mappings( array $cf7_mch, array $form_field_details ): array {
$form_mappings = array();
$unmapped_cf7 = 0;
$unmapped_mc = 0;
$mapped_cf7_fields = array();
$total_mc_fields = 0;
$total_mappings = 0;
for ( $i = 1; $i <= 20; $i++ ) {
$mc_tag = $cf7_mch[ 'CustomKey' . $i ] ?? '';
$mc_type = $cf7_mch[ 'CustomKeyType' . $i ] ?? '';
$cf7_field = trim( $cf7_mch[ 'CustomValue' . $i ] ?? '' );
if ( ! empty( $mc_tag ) ) {
++$total_mc_fields;
if ( '' !== $cf7_field ) {
$form_mappings[] = array(
'cf7_field' => $cf7_field,
'mc_tag' => $mc_tag,
'mc_type' => $mc_type,
);
$mapped_cf7_fields[] = $cf7_field;
++$total_mappings;
} else {
++$unmapped_mc;
}
}
}
foreach ( $form_field_details as $field ) {
if ( ! in_array( $field['name'], $mapped_cf7_fields, true ) ) {
++$unmapped_cf7;
}
}
return array( $form_mappings, $unmapped_cf7, $unmapped_mc, $mapped_cf7_fields, $total_mc_fields, $total_mappings );
}
private static function extract_form_features( array $cf7_mch ): array {
$form_features = array();
if ( isset( $cf7_mch['confsubs'] ) && '1' === $cf7_mch['confsubs'] ) {
$form_features['double_optin'] = true;
}
if ( ! empty( $cf7_mch['consent_required'] ) && ' ' !== $cf7_mch['consent_required'] ) {
$form_features['required_consent'] = true;
}
if ( isset( $cf7_mch['logfileEnabled'] ) && '1' === $cf7_mch['logfileEnabled'] ) {
$form_features['debug_logger'] = true;
}
if ( ! empty( $cf7_mch['labeltags'] ) && is_array( $cf7_mch['labeltags'] ) ) {
$enabled_tags = array_filter( $cf7_mch['labeltags'], fn( $v ) => '1' === $v );
if ( count( $enabled_tags ) > 0 ) {
$form_features['tags_enabled'] = true;
}
}
$form_group_count = 0;
for ( $gi = 1; $gi <= 20; $gi++ ) {
$gkey = $cf7_mch[ "ggCustomKey{$gi}" ] ?? '';
$gvalue = $cf7_mch[ "ggCustomValue{$gi}" ] ?? '';
if ( ! empty( $gkey ) && ! empty( trim( $gvalue ) ) && ' ' !== $gvalue ) {
++$form_group_count;
}
}
if ( $form_group_count > 0 ) {
$form_features['interest_groups'] = true;
}
$form_merge_fields_raw = array();
if ( ! empty( $cf7_mch['merge_fields'] ) && is_array( $cf7_mch['merge_fields'] ) ) {
$form_merge_fields_raw = $cf7_mch['merge_fields'];
} elseif ( ! empty( $cf7_mch['merge-vars'] ) && is_array( $cf7_mch['merge-vars'] ) ) {
$form_merge_fields_raw = $cf7_mch['merge-vars'];
}
if ( ! empty( $form_merge_fields_raw ) ) {
$default_tags = array( 'EMAIL', 'FNAME', 'LNAME', 'ADDRESS', 'PHONE' );
$custom_field_count = 0;
foreach ( $form_merge_fields_raw as $mfield ) {
if ( isset( $mfield['tag'] ) && ! in_array( $mfield['tag'], $default_tags, true ) ) {
++$custom_field_count;
}
}
if ( $custom_field_count > 0 ) {
$form_features['custom_merge_fields'] = true;
}
}
if ( ! empty( $cf7_mch['conditional_logic'] ) ) {
$form_features['conditional_logic'] = true;
}
return $form_features;
}
private static function get_form_age_range( array $cf7_forms ): array {
$oldest_form = 0;
$newest_form = 0;
foreach ( $cf7_forms as $form ) {
$created = get_post_field( 'post_date', $form->id(), 'raw' );
$timestamp = strtotime( $created );
if ( 0 === $oldest_form || $timestamp < $oldest_form ) {
$oldest_form = $timestamp;
}
if ( 0 === $newest_form || $timestamp > $newest_form ) {
$newest_form = $timestamp;
}
}
return array( $oldest_form, $newest_form );
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Install data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
use Cmatic\Metrics\Core\Storage;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Install_Collector {
public static function collect(): array {
return array(
'plugin_slug' => Cmatic_Options_Repository::get_option( 'install.plugin_slug', 'contact-form-7-mailchimp-extension' ),
'quest' => Storage::get_quest(),
'pro' => array(
'installed' => (bool) Cmatic_Options_Repository::get_option( 'install.pro.installed', false ),
'activated' => (bool) Cmatic_Options_Repository::get_option( 'install.pro.activated', false ),
'version' => Cmatic_Options_Repository::get_option( 'install.pro.version', null ),
'licensed' => (bool) Cmatic_Options_Repository::get_option( 'install.pro.licensed', false ),
'license_expires' => Cmatic_Options_Repository::get_option( 'install.pro.license_expires', null ),
),
);
}
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* Lifecycle data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
use Cmatic\Metrics\Core\Storage;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Lifecycle_Collector {
public static function collect(): array {
$activations = Cmatic_Options_Repository::get_option( 'lifecycle.activations', array() );
$deactivations = Cmatic_Options_Repository::get_option( 'lifecycle.deactivations', array() );
$upgrades = Cmatic_Options_Repository::get_option( 'lifecycle.upgrades', array() );
$first_activated = Storage::get_quest();
$last_activated = ! empty( $activations ) ? max( $activations ) : 0;
$last_deactivated = ! empty( $deactivations ) ? max( $deactivations ) : 0;
$last_upgrade = ! empty( $upgrades ) ? max( $upgrades ) : 0;
$days_since_first = 0;
if ( $first_activated > 0 ) {
$days_since_first = floor( ( time() - $first_activated ) / DAY_IN_SECONDS );
}
$avg_session_length = self::calculate_avg_session_length( $activations, $deactivations );
$version_history = Cmatic_Options_Repository::get_option( 'lifecycle.version_history', array() );
$previous_version = Cmatic_Options_Repository::get_option( 'lifecycle.previous_version', '' );
$days_since_last_upgrade = $last_upgrade > 0 ? floor( ( time() - $last_upgrade ) / DAY_IN_SECONDS ) : 0;
$active_session = empty( $deactivations ) || $last_activated > $last_deactivated;
$data = array(
'activation_count' => count( $activations ),
'deactivation_count' => count( $deactivations ),
'upgrade_count' => count( $upgrades ),
'first_activated' => $first_activated,
'last_activated' => $last_activated,
'last_deactivated' => $last_deactivated,
'last_upgrade' => $last_upgrade,
'days_since_first_activation' => (int) $days_since_first,
'days_since_last_upgrade' => (int) $days_since_last_upgrade,
'avg_session_length_seconds' => $avg_session_length,
'total_sessions' => count( $activations ),
'previous_version' => $previous_version,
'version_history_count' => count( $version_history ),
'install_method' => Cmatic_Options_Repository::get_option( 'lifecycle.install_method', 'unknown' ),
'days_on_current_version' => $last_upgrade > 0 ? floor( ( time() - $last_upgrade ) / DAY_IN_SECONDS ) : $days_since_first,
'activation_timestamps' => $activations,
'deactivation_timestamps' => $deactivations,
'upgrade_timestamps' => $upgrades,
);
$data = array_filter( $data, fn( $v ) => $v !== 0 && $v !== '' && $v !== 'unknown' );
$data['active_session'] = $active_session;
return $data;
}
private static function calculate_avg_session_length( array $activations, array $deactivations ): int {
if ( count( $activations ) === 0 || count( $deactivations ) === 0 ) {
return 0;
}
$total_session_time = 0;
$session_count = 0;
foreach ( $activations as $index => $activation_time ) {
if ( isset( $deactivations[ $index ] ) ) {
$total_session_time += $deactivations[ $index ] - $activation_time;
++$session_count;
}
}
if ( $session_count > 0 ) {
return (int) floor( $total_session_time / $session_count );
}
return 0;
}
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* Metadata collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
use Cmatic\Metrics\Core\Storage;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Metadata_Collector {
public static function collect(): array {
$first_installed = Storage::get_quest();
$total_uptime = $first_installed > 0 ? time() - $first_installed : 0;
$failed_count = (int) Cmatic_Options_Repository::get_option( 'telemetry.failed_count', 0 );
return array(
'schedule' => Cmatic_Options_Repository::get_option( 'telemetry.schedule', 'frequent' ),
'frequent_started_at' => (int) Cmatic_Options_Repository::get_option( 'telemetry.frequent_started_at', 0 ),
'is_reactivation' => Storage::is_reactivation(),
'disabled_count' => (int) Cmatic_Options_Repository::get_option( 'telemetry.disabled_count', 0 ),
'opt_in_date' => (int) Cmatic_Options_Repository::get_option( 'telemetry.opt_in_date', 0 ),
'last_heartbeat' => (int) Cmatic_Options_Repository::get_option( 'telemetry.last_heartbeat', 0 ),
'failed_heartbeats' => $failed_count,
'total_uptime_seconds' => $total_uptime,
'telemetry_version' => SPARTAN_MCE_VERSION,
);
}
}

View File

@@ -0,0 +1,140 @@
<?php
/**
* Performance data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Performance_Collector {
public static function collect(): array {
$memory_current = memory_get_usage( true );
$memory_peak = memory_get_peak_usage( true );
$memory_limit = ini_get( 'memory_limit' );
$memory_limit_bytes = self::convert_to_bytes( $memory_limit );
$memory_usage_percent = $memory_limit_bytes > 0 ? round( ( $memory_peak / $memory_limit_bytes ) * 100, 2 ) : 0;
$db_queries = get_num_queries();
$db_time = timer_stop( 0, 3 );
$page_load_time = isset( $_SERVER['REQUEST_TIME_FLOAT'] ) ? ( microtime( true ) - floatval( $_SERVER['REQUEST_TIME_FLOAT'] ) ) * 1000 : 0;
list( $object_cache_hits, $object_cache_misses ) = self::get_object_cache_stats();
$plugin_load_time = (float) Cmatic_Options_Repository::get_option( 'performance.plugin_load_time', 0 );
$api_avg_response = (float) Cmatic_Options_Repository::get_option( 'performance.api_avg_response', 0 );
$data = array(
'memory_current' => $memory_current,
'memory_peak' => $memory_peak,
'memory_limit' => $memory_limit,
'memory_limit_bytes' => $memory_limit_bytes,
'memory_usage_percent' => $memory_usage_percent,
'memory_available' => max( 0, $memory_limit_bytes - $memory_peak ),
'php_max_execution_time' => (int) ini_get( 'max_execution_time' ),
'page_load_time_ms' => round( $page_load_time, 2 ),
'plugin_load_time_ms' => round( $plugin_load_time, 2 ),
'db_queries_count' => $db_queries,
'db_query_time_seconds' => (float) $db_time,
'db_size_mb' => self::get_database_size(),
'api_avg_response_ms' => round( $api_avg_response, 2 ),
'api_slowest_response_ms' => (int) Cmatic_Options_Repository::get_option( 'performance.api_slowest', 0 ),
'api_fastest_response_ms' => (int) Cmatic_Options_Repository::get_option( 'performance.api_fastest', 0 ),
'object_cache_enabled' => wp_using_ext_object_cache(),
'object_cache_hits' => $object_cache_hits,
'object_cache_misses' => $object_cache_misses,
'object_cache_hit_rate' => ( $object_cache_hits + $object_cache_misses ) > 0 ? round( ( $object_cache_hits / ( $object_cache_hits + $object_cache_misses ) ) * 100, 2 ) : 0,
'opcache_enabled' => self::is_opcache_enabled(),
'opcache_hit_rate' => self::get_opcache_hit_rate(),
);
return array_filter( $data, fn( $v ) => $v !== 0 && $v !== 0.0 && $v !== false && $v !== null && $v !== '' );
}
private static function is_opcache_enabled(): bool {
if ( ! function_exists( 'opcache_get_status' ) ) {
return false;
}
$status = @opcache_get_status();
return false !== $status && is_array( $status );
}
public static function convert_to_bytes( $value ): int {
$value = trim( $value );
if ( empty( $value ) ) {
return 0;
}
$last = strtolower( $value[ strlen( $value ) - 1 ] );
$value = (int) $value;
switch ( $last ) {
case 'g':
$value *= 1024;
// Fall through.
case 'm':
$value *= 1024;
// Fall through.
case 'k':
$value *= 1024;
}
return $value;
}
private static function get_database_size(): float {
global $wpdb;
$size = $wpdb->get_var(
$wpdb->prepare(
'SELECT SUM(data_length + index_length) / 1024 / 1024
FROM information_schema.TABLES
WHERE table_schema = %s',
DB_NAME
)
);
return round( (float) $size, 2 );
}
private static function get_opcache_hit_rate(): float {
if ( ! function_exists( 'opcache_get_status' ) ) {
return 0;
}
$status = @opcache_get_status();
if ( false === $status || ! is_array( $status ) || ! isset( $status['opcache_statistics'] ) ) {
return 0;
}
$stats = $status['opcache_statistics'];
$hits = isset( $stats['hits'] ) ? (int) $stats['hits'] : 0;
$misses = isset( $stats['misses'] ) ? (int) $stats['misses'] : 0;
if ( ( $hits + $misses ) === 0 ) {
return 0;
}
return round( ( $hits / ( $hits + $misses ) ) * 100, 2 );
}
private static function get_object_cache_stats(): array {
$object_cache_hits = 0;
$object_cache_misses = 0;
if ( function_exists( 'wp_cache_get_stats' ) ) {
$cache_stats = wp_cache_get_stats();
$object_cache_hits = isset( $cache_stats['hits'] ) ? $cache_stats['hits'] : 0;
$object_cache_misses = isset( $cache_stats['misses'] ) ? $cache_stats['misses'] : 0;
}
return array( $object_cache_hits, $object_cache_misses );
}
}

View File

@@ -0,0 +1,135 @@
<?php
/**
* Plugins data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
defined( 'ABSPATH' ) || exit;
class Plugins_Collector {
public static function collect(): array {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$all_plugins = get_plugins();
$active_plugins = get_option( 'active_plugins', array() );
$mu_plugins = get_mu_plugins();
$plugin_list = self::build_plugin_list( $all_plugins, $active_plugins, $mu_plugins );
$plugin_stats = self::get_plugin_stats( $all_plugins );
$known_plugins = self::get_known_plugins();
$data = array(
'total_plugins' => count( $all_plugins ),
'active_plugins' => count( $active_plugins ),
'inactive_plugins' => count( $all_plugins ) - count( $active_plugins ),
'mu_plugins' => count( $mu_plugins ),
'premium_plugins' => $plugin_stats['premium'],
'cf7_addons' => $plugin_stats['cf7'],
'mailchimp_plugins' => $plugin_stats['mailchimp'],
'security_plugins' => $plugin_stats['security'],
'cache_plugins' => $plugin_stats['cache'],
'seo_plugins' => $plugin_stats['seo'],
'has_woocommerce' => $known_plugins['woocommerce'],
'has_elementor' => $known_plugins['elementor'],
'has_jetpack' => $known_plugins['jetpack'],
'has_wordfence' => $known_plugins['wordfence'],
'has_yoast_seo' => $known_plugins['yoast_seo'],
'plugin_list' => $plugin_list,
);
return array_filter( $data, fn( $v ) => $v !== 0 && $v !== false && $v !== '' && $v !== array() );
}
private static function build_plugin_list( array $all_plugins, array $active_plugins, array $mu_plugins ): array {
$plugin_list = array();
$network_active = is_multisite() ? get_site_option( 'active_sitewide_plugins', array() ) : array();
foreach ( $all_plugins as $plugin_path => $plugin_data ) {
$is_active = in_array( $plugin_path, $active_plugins, true );
$is_network = isset( $network_active[ $plugin_path ] );
$status = 'inactive';
if ( $is_network ) {
$status = 'network-active';
} elseif ( $is_active ) {
$status = 'active';
}
$dir = dirname( $plugin_path );
$plugin_list[] = array(
'slug' => '.' !== $dir ? $dir : basename( $plugin_path, '.php' ),
'name' => $plugin_data['Name'],
'version' => $plugin_data['Version'],
'author' => wp_strip_all_tags( $plugin_data['Author'] ),
'status' => $status,
);
}
foreach ( $mu_plugins as $mu_plugin_path => $mu_plugin_data ) {
$plugin_list[] = array(
'slug' => basename( $mu_plugin_path, '.php' ),
'name' => $mu_plugin_data['Name'],
'version' => $mu_plugin_data['Version'],
'author' => wp_strip_all_tags( $mu_plugin_data['Author'] ),
'status' => 'mu-plugin',
);
}
return $plugin_list;
}
private static function get_plugin_stats( array $all_plugins ): array {
$stats = array(
'premium' => 0,
'cf7' => 0,
'mailchimp' => 0,
'security' => 0,
'cache' => 0,
'seo' => 0,
);
foreach ( $all_plugins as $plugin_path => $plugin_data ) {
$name = strtolower( $plugin_data['Name'] );
if ( strpos( $name, 'pro' ) !== false || strpos( $name, 'premium' ) !== false ) {
++$stats['premium'];
}
if ( strpos( $name, 'contact form 7' ) !== false ) {
++$stats['cf7'];
}
if ( strpos( $name, 'mailchimp' ) !== false ) {
++$stats['mailchimp'];
}
if ( strpos( $name, 'security' ) !== false || strpos( $name, 'wordfence' ) !== false || strpos( $name, 'sucuri' ) !== false ) {
++$stats['security'];
}
if ( strpos( $name, 'cache' ) !== false || strpos( $name, 'wp rocket' ) !== false || strpos( $name, 'w3 total cache' ) !== false ) {
++$stats['cache'];
}
if ( strpos( $name, 'seo' ) !== false || strpos( $name, 'yoast' ) !== false ) {
++$stats['seo'];
}
}
return $stats;
}
private static function get_known_plugins(): array {
return array(
'woocommerce' => is_plugin_active( 'woocommerce/woocommerce.php' ),
'elementor' => is_plugin_active( 'elementor/elementor.php' ),
'jetpack' => is_plugin_active( 'jetpack/jetpack.php' ),
'wordfence' => is_plugin_active( 'wordfence/wordfence.php' ),
'yoast_seo' => is_plugin_active( 'wordpress-seo/wp-seo.php' ),
);
}
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* Server data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
defined( 'ABSPATH' ) || exit;
class Server_Collector {
public static function collect(): array {
$server_load = self::get_load_average();
list( $disk_free, $disk_total ) = self::get_disk_space();
$disk_used = $disk_total - $disk_free;
$disk_usage_percent = $disk_total > 0 ? round( ( $disk_used / $disk_total ) * 100, 2 ) : 0;
$hostname = self::get_hostname();
$architecture = self::get_architecture();
return array(
'load_average_1min' => isset( $server_load[0] ) ? round( (float) $server_load[0], 2 ) : 0,
'load_average_5min' => isset( $server_load[1] ) ? round( (float) $server_load[1], 2 ) : 0,
'load_average_15min' => isset( $server_load[2] ) ? round( (float) $server_load[2], 2 ) : 0,
'disk_usage_percent' => $disk_usage_percent,
'disk_total_gb' => $disk_total ? round( $disk_total / 1024 / 1024 / 1024, 2 ) : 0,
'server_ip' => hash( 'sha256', isset( $_SERVER['SERVER_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_ADDR'] ) ) : '' ),
'server_hostname' => hash( 'sha256', $hostname ),
'server_os' => PHP_OS,
'server_architecture' => $architecture,
);
}
private static function get_load_average(): array {
if ( ! function_exists( 'sys_getloadavg' ) ) {
return array( 0, 0, 0 );
}
$load = @sys_getloadavg();
if ( false !== $load && is_array( $load ) ) {
return $load;
}
return array( 0, 0, 0 );
}
private static function get_disk_space(): array {
$disk_free = 0;
$disk_total = 0;
if ( function_exists( 'disk_free_space' ) ) {
$free = @disk_free_space( ABSPATH );
if ( false !== $free ) {
$disk_free = $free;
}
}
if ( function_exists( 'disk_total_space' ) ) {
$total = @disk_total_space( ABSPATH );
if ( false !== $total ) {
$disk_total = $total;
}
}
return array( $disk_free, $disk_total );
}
private static function get_hostname(): string {
if ( ! function_exists( 'gethostname' ) ) {
return '';
}
$name = @gethostname();
return false !== $name ? $name : '';
}
private static function get_architecture(): string {
if ( ! function_exists( 'php_uname' ) ) {
return '';
}
$arch = @php_uname( 'm' );
return false !== $arch ? $arch : '';
}
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* Submissions data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
use Cmatic\Metrics\Core\Storage;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Submissions_Collector {
public static function collect(): array {
$total_sent = (int) Cmatic_Options_Repository::get_option( 'stats.sent', 0 );
$total_failed = (int) Cmatic_Options_Repository::get_option( 'submissions.failed', 0 );
$first_submission = (int) Cmatic_Options_Repository::get_option( 'submissions.first', 0 );
$last_submission = (int) Cmatic_Options_Repository::get_option( 'submissions.last', 0 );
$last_success = (int) Cmatic_Options_Repository::get_option( 'submissions.last_success', 0 );
$last_failure = (int) Cmatic_Options_Repository::get_option( 'submissions.last_failure', 0 );
$first_activated = Storage::get_quest();
$days_active = 1;
if ( $first_activated > 0 ) {
$days_active = max( 1, floor( ( time() - $first_activated ) / DAY_IN_SECONDS ) );
}
$total_submissions = $total_sent + $total_failed;
$avg_per_day = $days_active > 0 ? round( $total_submissions / $days_active, 2 ) : 0;
$success_rate = $total_submissions > 0 ? round( ( $total_sent / $total_submissions ) * 100, 2 ) : 100;
$days_since_first = $first_submission > 0 ? floor( ( time() - $first_submission ) / DAY_IN_SECONDS ) : 0;
$days_since_last = $last_submission > 0 ? floor( ( time() - $last_submission ) / DAY_IN_SECONDS ) : 0;
$hours_since_last = $last_submission > 0 ? floor( ( time() - $last_submission ) / HOUR_IN_SECONDS ) : 0;
list( $busiest_hour, $max_submissions ) = self::get_busiest_hour();
list( $busiest_day, $max_day_submissions ) = self::get_busiest_day();
$this_month = (int) Cmatic_Options_Repository::get_option( 'submissions.this_month', 0 );
$last_month = (int) Cmatic_Options_Repository::get_option( 'submissions.last_month', 0 );
$peak_month = (int) Cmatic_Options_Repository::get_option( 'submissions.peak_month', 0 );
$consecutive_successes = (int) Cmatic_Options_Repository::get_option( 'submissions.consecutive_successes', 0 );
$consecutive_failures = (int) Cmatic_Options_Repository::get_option( 'submissions.consecutive_failures', 0 );
$data = array(
'total_sent' => $total_sent,
'total_failed' => $total_failed,
'total_submissions' => $total_submissions,
'successful_submissions_count' => $total_sent,
'failed_count' => $total_failed,
'success_rate' => $success_rate,
'first_submission' => $first_submission,
'last_submission' => $last_submission,
'last_success' => $last_success,
'last_failure' => $last_failure,
'days_since_first' => $days_since_first,
'days_since_last' => $days_since_last,
'hours_since_last' => $hours_since_last,
'avg_per_day' => $avg_per_day,
'avg_per_week' => round( $avg_per_day * 7, 2 ),
'avg_per_month' => round( $avg_per_day * 30, 2 ),
'busiest_hour' => $busiest_hour,
'busiest_day' => $busiest_day,
'submissions_busiest_hour' => $max_submissions,
'submissions_busiest_day' => $max_day_submissions,
'this_month' => $this_month,
'last_month' => $last_month,
'peak_month' => $peak_month,
'month_over_month_change' => $last_month > 0 ? round( ( ( $this_month - $last_month ) / $last_month ) * 100, 2 ) : 0,
'consecutive_successes' => $consecutive_successes,
'consecutive_failures' => $consecutive_failures,
'longest_success_streak' => (int) Cmatic_Options_Repository::get_option( 'submissions.longest_success_streak', 0 ),
'active_forms_count' => (int) Cmatic_Options_Repository::get_option( 'submissions.active_forms', 0 ),
'forms_with_submissions' => count( Cmatic_Options_Repository::get_option( 'submissions.forms_used', array() ) ),
);
return array_filter( $data, fn( $v ) => $v !== 0 && $v !== 0.0 );
}
private static function get_busiest_hour(): array {
$hourly_distribution = Cmatic_Options_Repository::get_option( 'submissions.hourly', array() );
$busiest_hour = 0;
$max_submissions = 0;
foreach ( $hourly_distribution as $hour => $count ) {
if ( $count > $max_submissions ) {
$max_submissions = $count;
$busiest_hour = (int) $hour;
}
}
return array( $busiest_hour, $max_submissions );
}
private static function get_busiest_day(): array {
$daily_distribution = Cmatic_Options_Repository::get_option( 'submissions.daily', array() );
$busiest_day = 0;
$max_day_submissions = 0;
foreach ( $daily_distribution as $day => $count ) {
if ( $count > $max_day_submissions ) {
$max_day_submissions = $count;
$busiest_day = (int) $day;
}
}
return array( $busiest_day, $max_day_submissions );
}
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* WordPress data collector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core\Collectors;
defined( 'ABSPATH' ) || exit;
class WordPress_Collector {
public static function collect(): array {
global $wpdb;
$post_counts = wp_count_posts( 'post' );
$page_counts = wp_count_posts( 'page' );
$comment_counts = wp_count_comments();
$user_count = count_users();
$media_counts = wp_count_posts( 'attachment' );
$category_count = wp_count_terms( 'category' );
$tag_count = wp_count_terms( 'post_tag' );
$revision_count = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'revision'" );
$data = array(
'posts_published' => isset( $post_counts->publish ) ? (int) $post_counts->publish : 0,
'posts_draft' => isset( $post_counts->draft ) ? (int) $post_counts->draft : 0,
'pages_published' => isset( $page_counts->publish ) ? (int) $page_counts->publish : 0,
'media_items' => isset( $media_counts->inherit ) ? (int) $media_counts->inherit : 0,
'comments_pending' => isset( $comment_counts->moderated ) ? (int) $comment_counts->moderated : 0,
'comments_spam' => isset( $comment_counts->spam ) ? (int) $comment_counts->spam : 0,
'users_total' => isset( $user_count['total_users'] ) ? (int) $user_count['total_users'] : 0,
'users_administrators' => isset( $user_count['avail_roles']['administrator'] ) ? (int) $user_count['avail_roles']['administrator'] : 0,
'users_editors' => isset( $user_count['avail_roles']['editor'] ) ? (int) $user_count['avail_roles']['editor'] : 0,
'users_authors' => isset( $user_count['avail_roles']['author'] ) ? (int) $user_count['avail_roles']['author'] : 0,
'users_subscribers' => isset( $user_count['avail_roles']['subscriber'] ) ? (int) $user_count['avail_roles']['subscriber'] : 0,
'categories_count' => is_wp_error( $category_count ) ? 0 : (int) $category_count,
'tags_count' => is_wp_error( $tag_count ) ? 0 : (int) $tag_count,
'revisions_count' => (int) $revision_count,
);
$data = array_filter( $data, fn( $v ) => $v !== 0 );
$data['auto_updates_enabled'] = (bool) get_option( 'auto_update_plugins', false );
return $data;
}
}

View File

@@ -0,0 +1,184 @@
<?php
/**
* Metrics scheduler.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Scheduler {
public static function init() {
add_filter( 'cron_schedules', array( __CLASS__, 'add_cron_intervals' ) );
add_action( 'cmatic_metrics_heartbeat', array( __CLASS__, 'execute_heartbeat' ) );
add_action( 'admin_init', array( __CLASS__, 'ensure_schedule' ) );
add_action( 'cmatic_subscription_success', array( __CLASS__, 'execute_heartbeat' ) );
}
public static function add_cron_intervals( $schedules ) {
$schedules['cmatic_2min'] = array(
'interval' => 2 * MINUTE_IN_SECONDS,
'display' => 'Every 2 Minutes',
);
$schedules['cmatic_10min'] = array(
'interval' => 10 * MINUTE_IN_SECONDS,
'display' => 'Every 10 Minutes',
);
$interval_hours = Storage::get_heartbeat_interval();
$schedules['cmatic_sparse'] = array(
'interval' => $interval_hours * HOUR_IN_SECONDS,
'display' => 'Every ' . $interval_hours . ' Hours',
);
return $schedules;
}
public static function execute_heartbeat() {
if ( ! Storage::is_enabled() ) {
return;
}
$schedule = Storage::get_schedule();
$started_at = Storage::get_frequent_started_at();
$elapsed = $started_at > 0 ? time() - $started_at : 0;
if ( 'super_frequent' === $schedule && $elapsed >= ( 15 * MINUTE_IN_SECONDS ) ) {
self::transition_to_frequent();
} elseif ( 'frequent' === $schedule && $elapsed >= ( 1 * HOUR_IN_SECONDS ) ) {
self::transition_to_sparse();
}
self::sync_global_lisdata();
$payload = Collector::collect( 'heartbeat' );
Sync::send( $payload );
}
private static function transition_to_frequent() {
Storage::set_schedule( 'frequent' );
$timestamp = wp_next_scheduled( 'cmatic_metrics_heartbeat' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'cmatic_metrics_heartbeat' );
}
wp_schedule_event( time(), 'cmatic_10min', 'cmatic_metrics_heartbeat' );
}
private static function transition_to_sparse() {
Storage::set_schedule( 'sparse' );
$timestamp = wp_next_scheduled( 'cmatic_metrics_heartbeat' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'cmatic_metrics_heartbeat' );
}
$interval_hours = Storage::get_heartbeat_interval();
wp_schedule_single_event(
time() + ( $interval_hours * HOUR_IN_SECONDS ),
'cmatic_metrics_heartbeat'
);
}
public static function ensure_schedule() {
if ( ! Storage::is_enabled() ) {
return;
}
if ( wp_next_scheduled( 'cmatic_metrics_heartbeat' ) ) {
return;
}
$schedule = Storage::get_schedule();
$started_at = Storage::get_frequent_started_at();
$elapsed = $started_at > 0 ? time() - $started_at : 0;
if ( 'super_frequent' === $schedule ) {
if ( $elapsed >= ( 15 * MINUTE_IN_SECONDS ) ) {
self::transition_to_frequent();
} else {
wp_schedule_event( time(), 'cmatic_2min', 'cmatic_metrics_heartbeat' );
}
} elseif ( 'frequent' === $schedule ) {
if ( Storage::is_frequent_elapsed() ) {
self::transition_to_sparse();
} else {
wp_schedule_event( time(), 'cmatic_10min', 'cmatic_metrics_heartbeat' );
}
} else {
$interval_hours = Storage::get_heartbeat_interval();
$last_heartbeat = Storage::get_last_heartbeat();
$next_heartbeat = $last_heartbeat + ( $interval_hours * HOUR_IN_SECONDS );
if ( $next_heartbeat < time() ) {
$next_heartbeat = time();
}
wp_schedule_single_event( $next_heartbeat, 'cmatic_metrics_heartbeat' );
}
}
public static function schedule_next_sparse() {
if ( 'sparse' !== Storage::get_schedule() ) {
return;
}
$timestamp = wp_next_scheduled( 'cmatic_metrics_heartbeat' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'cmatic_metrics_heartbeat' );
}
$interval_hours = Storage::get_heartbeat_interval();
wp_schedule_single_event(
time() + ( $interval_hours * HOUR_IN_SECONDS ),
'cmatic_metrics_heartbeat'
);
}
public static function clear_schedule() {
$timestamp = wp_next_scheduled( 'cmatic_metrics_heartbeat' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'cmatic_metrics_heartbeat' );
}
}
private static function sync_global_lisdata() {
global $wpdb;
$form_options = $wpdb->get_results(
"SELECT option_value FROM {$wpdb->options}
WHERE option_name LIKE 'cf7_mch_%'
AND option_value LIKE '%lisdata%'
LIMIT 50"
);
$best_lisdata = null;
$best_list_count = 0;
if ( ! empty( $form_options ) ) {
foreach ( $form_options as $row ) {
$cf7_mch = maybe_unserialize( $row->option_value );
if ( ! is_array( $cf7_mch ) || empty( $cf7_mch['lisdata']['lists'] ) ) {
continue;
}
$list_count = count( $cf7_mch['lisdata']['lists'] );
if ( $list_count > $best_list_count ) {
$best_list_count = $list_count;
$best_lisdata = $cf7_mch['lisdata'];
}
}
}
// Always update - either with found data or empty array to clear stale data.
Cmatic_Options_Repository::set_option( 'lisdata', $best_lisdata ?? array() );
Cmatic_Options_Repository::set_option( 'lisdata_updated', time() );
}
}

View File

@@ -0,0 +1,151 @@
<?php
/**
* Metrics storage handler.
*
* Delegates install_id, quest, and lifecycle tracking to Cmatic_Install_Data
* and Cmatic_Activator/Cmatic_Deactivator classes (single source of truth).
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Storage {
public static function init() {
if ( null !== Cmatic_Options_Repository::get_option( 'telemetry.enabled' ) ) {
return;
}
$defaults = array(
'enabled' => true,
'opt_in_date' => time(),
'disabled_count' => 0,
'heartbeat_interval' => 48,
'schedule' => 'frequent',
'frequent_started_at' => time(),
'last_heartbeat' => 0,
'heartbeat_count' => 0,
'failed_count' => 0,
'last_payload_hash' => '',
'last_error' => '',
);
foreach ( $defaults as $key => $value ) {
Cmatic_Options_Repository::set_option( "telemetry.{$key}", $value );
}
}
public static function is_enabled() {
return (bool) Cmatic_Options_Repository::get_option( 'telemetry.enabled', true );
}
public static function get_schedule() {
return Cmatic_Options_Repository::get_option( 'telemetry.schedule', 'frequent' );
}
public static function set_schedule( $schedule ) {
Cmatic_Options_Repository::set_option( 'telemetry.schedule', $schedule );
if ( 'frequent' === $schedule ) {
Cmatic_Options_Repository::set_option( 'telemetry.frequent_started_at', time() );
}
}
public static function get_heartbeat_interval() {
return (int) Cmatic_Options_Repository::get_option( 'telemetry.heartbeat_interval', 48 );
}
public static function get_last_heartbeat() {
return (int) Cmatic_Options_Repository::get_option( 'telemetry.last_heartbeat', 0 );
}
public static function update_last_heartbeat( $timestamp = null ) {
if ( null === $timestamp ) {
$timestamp = time();
}
Cmatic_Options_Repository::set_option( 'telemetry.last_heartbeat', $timestamp );
}
public static function increment_heartbeat_count() {
$count = (int) Cmatic_Options_Repository::get_option( 'telemetry.heartbeat_count', 0 );
Cmatic_Options_Repository::set_option( 'telemetry.heartbeat_count', $count + 1 );
}
public static function increment_failed_count() {
$count = (int) Cmatic_Options_Repository::get_option( 'telemetry.failed_count', 0 );
Cmatic_Options_Repository::set_option( 'telemetry.failed_count', $count + 1 );
}
public static function increment_disabled_count() {
$count = (int) Cmatic_Options_Repository::get_option( 'telemetry.disabled_count', 0 );
Cmatic_Options_Repository::set_option( 'telemetry.disabled_count', $count + 1 );
}
public static function get_frequent_started_at() {
return (int) Cmatic_Options_Repository::get_option( 'telemetry.frequent_started_at', 0 );
}
public static function is_frequent_elapsed() {
$started_at = self::get_frequent_started_at();
if ( 0 === $started_at ) {
return false;
}
$elapsed = time() - $started_at;
return $elapsed >= ( 1 * HOUR_IN_SECONDS );
}
public static function record_activation() {
}
public static function record_deactivation() {
}
public static function is_reactivation() {
return (bool) Cmatic_Options_Repository::get_option( 'lifecycle.is_reactivation', false );
}
public static function get_activation_count() {
$activations = Cmatic_Options_Repository::get_option( 'lifecycle.activations', array() );
return is_array( $activations ) ? count( $activations ) : 0;
}
public static function get_deactivation_count() {
$deactivations = Cmatic_Options_Repository::get_option( 'lifecycle.deactivations', array() );
return is_array( $deactivations ) ? count( $deactivations ) : 0;
}
public static function get_install_id(): string {
$install_id = \Cmatic_Options_Repository::get_option( 'install.id', '' );
if ( ! empty( $install_id ) ) {
return $install_id;
}
$install_data = new \Cmatic_Install_Data( \Cmatic_Options_Repository::instance() );
return $install_data->get_install_id();
}
public static function get_quest(): int {
$quest = (int) \Cmatic_Options_Repository::get_option( 'install.quest', 0 );
if ( $quest >= \Cmatic_Install_Data::MIN_VALID_TIMESTAMP ) {
return $quest;
}
$install_data = new \Cmatic_Install_Data( \Cmatic_Options_Repository::instance() );
return $install_data->get_quest();
}
public static function save_error( $error ): void {
Cmatic_Options_Repository::set_option( 'telemetry.last_error', $error );
}
public static function save_payload_hash( $hash ) {
Cmatic_Options_Repository::set_option( 'telemetry.last_payload_hash', $hash );
}
}

View File

@@ -0,0 +1,182 @@
<?php
/**
* Metrics sync handler.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core;
use Cmatic\Metrics\Security\Signature;
defined( 'ABSPATH' ) || exit;
class Sync {
private static $endpoint_url = '';
public static function set_endpoint( $url ) {
self::$endpoint_url = $url;
}
public static function send( $payload ) {
try {
if ( empty( self::$endpoint_url ) ) {
return false;
}
$request = self::prepare_request( $payload );
$response = wp_remote_post(
self::$endpoint_url,
array(
'body' => wp_json_encode( $request ),
'headers' => array( 'Content-Type' => 'application/json' ),
'timeout' => 5,
)
);
return self::handle_response( $response, $payload );
} catch ( \Exception $e ) {
return false;
}
}
public static function send_async( $payload ) {
try {
if ( empty( self::$endpoint_url ) ) {
return;
}
$request = self::prepare_request( $payload );
wp_remote_post(
self::$endpoint_url,
array(
'body' => wp_json_encode( $request ),
'headers' => array( 'Content-Type' => 'application/json' ),
'timeout' => 5,
'blocking' => false,
)
);
Storage::update_last_heartbeat();
} catch ( \Exception $e ) {
return;
}
}
private static function prepare_request( $payload ) {
$install_id = Storage::get_install_id();
$timestamp = time();
$payload_json = wp_json_encode( $payload );
$signature = Signature::generate( $install_id, $timestamp, $payload_json );
return array(
'install_id' => $install_id,
'timestamp' => $timestamp,
'signature' => $signature,
'public_key' => Signature::get_public_key(),
'payload_json' => $payload_json,
);
}
private static function handle_response( $response, $payload ) {
if ( is_wp_error( $response ) ) {
self::handle_failure( $response->get_error_message(), $payload );
return false;
}
$code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $code && 201 !== $code ) {
$body = wp_remote_retrieve_body( $response );
self::handle_failure( "HTTP {$code}: {$body}", $payload );
return false;
}
self::handle_success( $payload );
return true;
}
private static function handle_success( $payload ) {
Storage::update_last_heartbeat();
$payload_hash = md5( wp_json_encode( $payload ) );
Storage::save_payload_hash( $payload_hash );
Storage::save_error( '' );
if ( 'sparse' === Storage::get_schedule() ) {
Scheduler::schedule_next_sparse();
}
}
private static function handle_failure( $error, $payload ) {
Storage::update_last_heartbeat();
Storage::increment_failed_count();
Storage::save_error( $error );
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( "[ChimpMatic Metrics] Heartbeat failed: {$error}" ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
}
if ( 'sparse' === Storage::get_schedule() ) {
Scheduler::schedule_next_sparse();
}
}
public static function send_lifecycle_signal( $event ) {
$options = get_option( 'cmatic', array() );
$default_enabled = 'activation' === $event;
$telemetry_enabled = $options['telemetry']['enabled'] ?? $default_enabled;
if ( ! $telemetry_enabled ) {
return;
}
$install_id = $options['install']['id'] ?? '';
if ( empty( $install_id ) ) {
return;
}
global $wpdb;
$payload = array(
'event' => $event,
'install_id' => $install_id,
'version' => defined( 'SPARTAN_MCE_VERSION' ) ? SPARTAN_MCE_VERSION : '1.0.0',
'site_url' => home_url(),
'timestamp' => time(),
'wp_version' => get_bloginfo( 'version' ),
'php' => PHP_VERSION,
'mysql_version' => $wpdb->db_version(),
'software' => array(
'server' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : 'unknown', // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
),
);
$timestamp = time();
$payload_json = wp_json_encode( $payload );
$signature = Signature::generate( $install_id, $timestamp, $payload_json );
$request = array(
'install_id' => $install_id,
'timestamp' => $timestamp,
'signature' => $signature,
'public_key' => Signature::get_public_key(),
'payload_json' => $payload_json,
);
wp_remote_post(
'https://signls.dev/wp-json/chimpmatic/v1/telemetry',
array(
'body' => wp_json_encode( $request ),
'headers' => array( 'Content-Type' => 'application/json' ),
'timeout' => 5,
'blocking' => true,
)
);
}
}

View File

@@ -0,0 +1,82 @@
<?php
/**
* Metrics event tracker.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Core;
use Cmatic_Options_Repository;
defined( 'ABSPATH' ) || exit;
class Tracker {
public static function init() {
add_action( 'cmatic_metrics_on_activation', array( __CLASS__, 'on_activation' ) );
add_action( 'cmatic_metrics_on_deactivation', array( __CLASS__, 'on_deactivation' ) );
}
public static function on_activation() {
Storage::init();
Storage::record_activation();
Storage::set_schedule( 'super_frequent' );
if ( ! wp_next_scheduled( 'cmatic_metrics_heartbeat' ) ) {
wp_schedule_event( time() + ( 2 * MINUTE_IN_SECONDS ), 'cmatic_2min', 'cmatic_metrics_heartbeat' );
}
self::send_event_heartbeat( 'activation' );
}
public static function on_deactivation() {
Storage::record_deactivation();
$timestamp = wp_next_scheduled( 'cmatic_metrics_heartbeat' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'cmatic_metrics_heartbeat' );
}
self::send_event_heartbeat( 'deactivation', true );
}
private static function send_event_heartbeat( $event, $force = false ) {
if ( ! $force && ! Storage::is_enabled() ) {
return;
}
$payload = Collector::collect( $event );
Sync::send_async( $payload );
}
public static function on_opt_out() {
Storage::increment_disabled_count();
$payload = Collector::collect( 'opt_out' );
Sync::send_async( $payload );
$timestamp = wp_next_scheduled( 'cmatic_metrics_heartbeat' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'cmatic_metrics_heartbeat' );
}
}
public static function on_re_enable() {
if ( ! Cmatic_Options_Repository::get_option( 'telemetry.opt_in_date' ) ) {
Cmatic_Options_Repository::set_option( 'telemetry.opt_in_date', time() );
}
Storage::set_schedule( 'super_frequent' );
if ( ! wp_next_scheduled( 'cmatic_metrics_heartbeat' ) ) {
wp_schedule_event( time() + ( 2 * MINUTE_IN_SECONDS ), 'cmatic_2min', 'cmatic_metrics_heartbeat' );
}
$payload = Collector::collect( 'reactivation' );
Sync::send_async( $payload );
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* Cryptographic signature handler.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
namespace Cmatic\Metrics\Security;
defined( 'ABSPATH' ) || exit;
class Signature {
const PUBLIC_KEY = 'chimpmatic_lite_v1';
public static function generate( $install_id, $timestamp, $payload_json ) {
$derived_secret = self::derive_secret( $install_id );
$string_to_sign = $install_id . $timestamp . $payload_json;
return hash_hmac( 'sha256', $string_to_sign, $derived_secret );
}
public static function derive_secret( $install_id ) {
return hash( 'sha256', $install_id . self::PUBLIC_KEY );
}
public static function validate( $signature, $install_id, $timestamp, $payload_json ) {
$expected = self::generate( $install_id, $timestamp, $payload_json );
return hash_equals( $expected, $signature );
}
public static function get_public_key() {
return self::PUBLIC_KEY;
}
}

View File

@@ -0,0 +1,13 @@
<?php
defined( 'ABSPATH' ) || exit;
spl_autoload_register( function ( $class ) {
$prefix = 'Cmatic\\Metrics\\';
if ( strncmp( $prefix, $class, strlen( $prefix ) ) !== 0 ) {
return;
}
$file = __DIR__ . '/' . str_replace( '\\', '/', substr( $class, strlen( $prefix ) ) ) . '.php';
if ( file_exists( $file ) ) {
require $file;
}
} );

View File

@@ -0,0 +1,441 @@
<?php
/**
* Admin bar menu integration.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Admin_Bar_Menu {
const MENU_IDENTIFIER = 'chimpmatic-menu';
private static $instance = null;
public static function instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->register_hooks();
}
private function register_hooks() {
add_action( 'admin_bar_menu', array( $this, 'add_menu' ), 95 );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'admin_footer', array( $this, 'render_upgrade_click_script' ) );
add_action( 'wp_footer', array( $this, 'render_upgrade_click_script' ) );
}
private function can_show_menu() {
return current_user_can( 'manage_options' ) && is_admin_bar_showing();
}
private function is_pro_active() {
return function_exists( 'cmatic_is_blessed' ) && cmatic_is_blessed();
}
private function is_pro_installed_not_licensed() {
if ( ! defined( 'CMATIC_VERSION' ) ) {
return false;
}
return ! $this->is_pro_active();
}
private function has_plugin_update() {
$updates = get_site_transient( 'update_plugins' );
if ( ! $updates || ! isset( $updates->response ) ) {
return false;
}
return isset( $updates->response['contact-form-7-mailchimp-extension/chimpmatic-lite.php'] )
|| isset( $updates->response['chimpmatic/chimpmatic.php'] );
}
private function should_show_upgrade_badge() {
return ! Cmatic_Options_Repository::get_option( 'ui.upgrade_clicked', false );
}
private function get_license_activation_url() {
return admin_url( 'admin.php?page=wpcf7-integration&service=0_chimpmatic&action=setup' );
}
private function get_update_url() {
return admin_url( 'plugins.php?plugin_status=upgrade' );
}
public function add_menu( WP_Admin_Bar $wp_admin_bar ) {
if ( ! $this->can_show_menu() ) {
return;
}
$this->add_root_menu( $wp_admin_bar );
$this->add_submenu_items( $wp_admin_bar );
}
private function add_root_menu( WP_Admin_Bar $wp_admin_bar ) {
$badge_count = 0;
if ( $this->has_plugin_update() ) {
++$badge_count;
}
if ( ! $this->is_pro_active() && $this->should_show_upgrade_badge() ) {
++$badge_count;
}
$icon_svg = 'data:image/svg+xml;base64,' . $this->get_icon_base64();
$icon_styles = 'width:26px;height:30px;float:left;background:url(\'' . esc_attr( $icon_svg ) . '\') center/20px no-repeat;';
$title = '<div id="cmatic-ab-icon" class="ab-item cmatic-logo svg" style="' . esc_attr( $icon_styles ) . '">';
$title .= '<span class="screen-reader-text">' . esc_html__( 'Chimpmatic Lite', 'chimpmatic-lite' ) . '</span>';
$title .= '</div>';
if ( $badge_count > 0 ) {
$title .= $this->get_notification_counter( $badge_count );
}
$wp_admin_bar->add_menu(
array(
'id' => self::MENU_IDENTIFIER,
'title' => $title,
'href' => false,
)
);
}
private function add_submenu_items( WP_Admin_Bar $wp_admin_bar ) {
if ( $this->has_plugin_update() ) {
$wp_admin_bar->add_menu(
array(
'parent' => self::MENU_IDENTIFIER,
'id' => 'chimpmatic-update',
'title' => esc_html__( 'Update Available', 'chimpmatic-lite' ) . ' ' . $this->get_notification_counter( 1 ),
'href' => $this->get_update_url(),
'meta' => array(
'title' => esc_attr__( 'Update strongly recommended', 'chimpmatic-lite' ),
),
)
);
}
if ( $this->is_pro_installed_not_licensed() ) {
$wp_admin_bar->add_menu(
array(
'parent' => self::MENU_IDENTIFIER,
'id' => 'chimpmatic-activate-license',
'title' => esc_html__( 'Activate License', 'chimpmatic-lite' ),
'href' => $this->get_license_activation_url(),
)
);
}
// Add Forms submenu with all CF7 forms.
$this->add_forms_submenu( $wp_admin_bar );
$wp_admin_bar->add_menu(
array(
'parent' => self::MENU_IDENTIFIER,
'id' => 'chimpmatic-docs',
'title' => esc_html__( 'Documentation', 'chimpmatic-lite' ),
'href' => Cmatic_Pursuit::adminbar( 'docs', 'menu_docs' ),
'meta' => array(
'target' => '_blank',
'rel' => 'noopener noreferrer',
),
)
);
$wp_admin_bar->add_menu(
array(
'parent' => self::MENU_IDENTIFIER,
'id' => 'chimpmatic-support',
'title' => esc_html__( 'Support', 'chimpmatic-lite' ),
'href' => Cmatic_Pursuit::adminbar( 'support', 'menu_support' ),
'meta' => array(
'target' => '_blank',
'rel' => 'noopener noreferrer',
),
)
);
$wp_admin_bar->add_menu(
array(
'parent' => self::MENU_IDENTIFIER,
'id' => 'chimpmatic-reviews',
'title' => esc_html__( 'Reviews', 'chimpmatic-lite' ),
'href' => 'https://wordpress.org/support/plugin/contact-form-7-mailchimp-extension/reviews/',
'meta' => array(
'target' => '_blank',
'rel' => 'noopener noreferrer',
),
)
);
if ( ! $this->is_pro_active() ) {
$upgrade_title = esc_html__( 'Upgrade to Pro', 'chimpmatic-lite' );
if ( $this->should_show_upgrade_badge() ) {
$upgrade_title .= ' ' . $this->get_notification_counter( 1 );
}
$wp_admin_bar->add_menu(
array(
'parent' => self::MENU_IDENTIFIER,
'id' => 'chimpmatic-upgrade',
'title' => $upgrade_title,
'href' => Cmatic_Pursuit::adminbar( 'pricing', 'menu_upgrade' ),
'meta' => array(
'target' => '_blank',
'rel' => 'noopener noreferrer',
),
)
);
}
}
private function add_forms_submenu( WP_Admin_Bar $wp_admin_bar ) {
// Check if CF7 is active.
if ( ! class_exists( 'WPCF7_ContactForm' ) ) {
return;
}
// Get all CF7 forms.
$forms = WPCF7_ContactForm::find( array( 'posts_per_page' => -1 ) );
if ( empty( $forms ) ) {
return;
}
// Add "Form Settings" section header (non-clickable label).
$wp_admin_bar->add_menu(
array(
'parent' => self::MENU_IDENTIFIER,
'id' => 'chimpmatic-forms-header',
'title' => esc_html__( 'Form Settings', 'chimpmatic-lite' ),
'href' => false,
)
);
// Add each form directly to main menu (flat, not nested).
foreach ( $forms as $form ) {
$form_url = admin_url(
sprintf(
'admin.php?page=wpcf7&post=%d&action=edit&active-tab=Chimpmatic',
$form->id()
)
);
// Check API connection status for this form.
$api_status = $this->get_form_api_status( $form->id() );
$wp_admin_bar->add_menu(
array(
'parent' => self::MENU_IDENTIFIER,
'id' => 'chimpmatic-form-' . $form->id(),
'title' => '&nbsp;&nbsp;' . esc_html( $form->title() ) . $api_status,
'href' => $form_url,
'meta' => array(
'class' => 'cmatic-form-item',
),
)
);
}
}
private function get_form_api_status( $form_id ) {
$cf7_mch = get_option( 'cf7_mch_' . $form_id, array() );
// Check if API is validated and a list/audience is selected.
$is_connected = ! empty( $cf7_mch['api-validation'] )
&& 1 == $cf7_mch['api-validation']
&& ! empty( $cf7_mch['list'] );
if ( $is_connected ) {
return '<span class="cmatic-api-status cmatic-api-connected" title="' . esc_attr__( 'Connected to Mailchimp API', 'chimpmatic-lite' ) . '">' . esc_html__( 'API', 'chimpmatic-lite' ) . '</span>';
}
return '<span class="cmatic-api-status cmatic-api-disconnected" title="' . esc_attr__( 'Not connected to Mailchimp API', 'chimpmatic-lite' ) . '">' . esc_html__( 'API', 'chimpmatic-lite' ) . '</span>';
}
private function get_notification_counter( $count ) {
if ( $count < 1 ) {
return '';
}
$screen_reader_text = sprintf(
/* translators: %s: number of notifications */
_n( '%s notification', '%s notifications', $count, 'chimpmatic-lite' ),
number_format_i18n( $count )
);
return sprintf(
'<div class="wp-core-ui wp-ui-notification cmatic-issue-counter"><span aria-hidden="true">%1$d</span><span class="screen-reader-text">%2$s</span></div>',
(int) $count,
esc_html( $screen_reader_text )
);
}
private function get_settings_url() {
if ( class_exists( 'Cmatic_Plugin_Links' ) ) {
$url = Cmatic_Plugin_Links::get_settings_url();
if ( ! empty( $url ) ) {
return $url;
}
}
return admin_url( 'admin.php?page=wpcf7' );
}
public function enqueue_assets() {
if ( ! $this->can_show_menu() ) {
return;
}
$css = $this->get_inline_css();
wp_add_inline_style( 'admin-bar', $css );
}
private function get_inline_css() {
$icon_base64 = $this->get_icon_base64();
$css = '
#wpadminbar .cmatic-logo.svg {
background-image: url("data:image/svg+xml;base64,' . $icon_base64 . '");
background-position: center;
background-repeat: no-repeat;
background-size: 20px;
float: left;
height: 30px;
width: 26px;
margin-top: 2px;
}
#wpadminbar #wp-admin-bar-chimpmatic-menu .cmatic-form-item .ab-item {
background-color: rgba(255,255,255,0.04) !important;
padding-left: 20px !important;
display: flex;
justify-content: space-between;
align-items: center;
}
#wpadminbar #wp-admin-bar-chimpmatic-menu .cmatic-form-item .ab-item:hover {
background-color: rgba(255,255,255,0.1) !important;
}
#wpadminbar .cmatic-api-status {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-left: 15px;
flex-shrink: 0;
}
#wpadminbar .cmatic-api-connected {
color: #00ba37;
}
#wpadminbar .cmatic-api-disconnected {
color: #787c82;
}
#wpadminbar .cmatic-issue-counter {
background-color: #d63638;
border-radius: 9px;
color: #fff;
display: inline;
padding: 1px 7px 1px 6px !important;
}
#wpadminbar .quicklinks #wp-admin-bar-chimpmatic-menu #wp-admin-bar-chimpmatic-menu-default li#wp-admin-bar-chimpmatic-upgrade {
display: flex;
}
#wpadminbar .quicklinks #wp-admin-bar-chimpmatic-menu #wp-admin-bar-chimpmatic-menu-default li#wp-admin-bar-chimpmatic-upgrade .ab-item {
align-items: center;
border-color: transparent;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
justify-content: center;
margin: 8px 12px;
background-color: #00be28;
font-size: 13px;
font-weight: 500;
padding: 6px 10px;
text-align: center;
text-decoration: none;
color: #fff !important;
width: 100%;
}
#wpadminbar .quicklinks #wp-admin-bar-chimpmatic-menu #wp-admin-bar-chimpmatic-menu-default li#wp-admin-bar-chimpmatic-upgrade .ab-item:hover {
background-color: #00a522;
color: #fff !important;
}
#wpadminbar #wp-admin-bar-chimpmatic-upgrade .cmatic-issue-counter {
width: 18px;
height: 18px;
min-width: 18px;
border-radius: 50%;
padding: 0 !important;
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 6px;
font-size: 11px;
line-height: 1;
}
@media screen and (max-width: 782px) {
#wpadminbar .cmatic-logo.svg {
background-position: center 8px;
background-size: 30px;
height: 46px;
width: 52px;
}
#wpadminbar .cmatic-logo + .cmatic-issue-counter {
margin-left: -5px;
margin-right: 10px;
}
}
';
return $css;
}
private function get_icon_base64() {
$svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%2382878c">'
. '<path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/>'
. '</svg>';
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
return base64_encode( $svg );
}
public function render_upgrade_click_script() {
if ( ! $this->can_show_menu() ) {
return;
}
if ( $this->is_pro_active() || ! $this->should_show_upgrade_badge() ) {
return;
}
?>
<script>
(function() {
var upgradeLink = document.querySelector('#wp-admin-bar-chimpmatic-upgrade > a');
if (upgradeLink) {
upgradeLink.addEventListener('click', function() {
fetch('<?php echo esc_url( rest_url( 'chimpmatic-lite/v1/notices/dismiss' ) ); ?>', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': '<?php echo esc_js( wp_create_nonce( 'wp_rest' ) ); ?>'
},
body: JSON.stringify({ notice_id: 'upgrade' })
});
});
}
})();
</script>
<?php
}
}

View File

@@ -0,0 +1,100 @@
<?php
/**
* Advanced settings panel renderer.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Advanced_Settings {
public static function render(): void {
?>
<table class="form-table mt0 description">
<tbody>
<tr class="">
<th scope="row"><?php esc_html_e( 'Unsubscribed', 'chimpmatic-lite' ); ?></th>
<td>
<fieldset><legend class="screen-reader-text"><span><?php esc_html_e( 'Unsubscribed', 'chimpmatic-lite' ); ?></span></legend>
<label class="cmatic-toggle">
<input type="checkbox" id="wpcf7-mailchimp-addunsubscr" name="wpcf7-mailchimp[addunsubscr]" data-field="unsubscribed" value="1" <?php checked( Cmatic_Options_Repository::get_option( 'unsubscribed', false ), true ); ?> />
<span class="cmatic-toggle-slider"></span>
</label>
<span class="cmatic-toggle-label"><?php esc_html_e( 'Marks submitted contacts as unsubscribed.', 'chimpmatic-lite' ); ?></span>
<a href="<?php echo esc_url( Cmatic_Pursuit::docs( 'mailchimp-opt-in-checkbox', 'unsubscribed_help' ) ); ?>" class="helping-field" target="_blank" title="<?php esc_attr_e( 'Get help with Custom Fields', 'chimpmatic-lite' ); ?>"> <?php esc_html_e( 'Learn More', 'chimpmatic-lite' ); ?> </a>
</fieldset>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Debug Logger', 'chimpmatic-lite' ); ?></th>
<td>
<fieldset><legend class="screen-reader-text"><span><?php esc_html_e( 'Debug Logger', 'chimpmatic-lite' ); ?></span></legend>
<label class="cmatic-toggle">
<input type="checkbox" id="wpcf7-mailchimp-logfileEnabled" data-field="debug" value="1" <?php checked( (bool) Cmatic_Options_Repository::get_option( 'debug', false ), true ); ?> />
<span class="cmatic-toggle-slider"></span>
</label>
<span class="cmatic-toggle-label"><?php esc_html_e( 'Enables activity logging to help troubleshoot form issues.', 'chimpmatic-lite' ); ?></span>
</fieldset>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Developer', 'chimpmatic-lite' ); ?></th>
<td>
<fieldset><legend class="screen-reader-text"><span><?php esc_html_e( 'Developer', 'chimpmatic-lite' ); ?></span></legend>
<label class="cmatic-toggle">
<input type="checkbox" id="wpcf7-mailchimp-cf-support" data-field="backlink" value="1" <?php checked( Cmatic_Options_Repository::get_option( 'backlink', false ), true ); ?> />
<span class="cmatic-toggle-slider"></span>
</label>
<span class="cmatic-toggle-label"><?php esc_html_e( 'A backlink to my site, not compulsory, but appreciated', 'chimpmatic-lite' ); ?></span>
</fieldset>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Auto Update', 'chimpmatic-lite' ); ?></th>
<td>
<fieldset><legend class="screen-reader-text"><span><?php esc_html_e( 'Auto Update', 'chimpmatic-lite' ); ?></span></legend>
<label class="cmatic-toggle">
<input type="checkbox" id="chimpmatic-update" data-field="auto_update" value="1" <?php checked( (bool) Cmatic_Options_Repository::get_option( 'auto_update', true ), true ); ?> />
<span class="cmatic-toggle-slider"></span>
</label>
<span class="cmatic-toggle-label"><?php esc_html_e( 'Auto Update Chimpmatic Lite', 'chimpmatic-lite' ); ?></span>
</fieldset>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Help Us Improve', 'chimpmatic-lite' ); ?></th>
<td>
<fieldset><legend class="screen-reader-text"><span><?php esc_html_e( 'Help Us Improve Chimpmatic', 'chimpmatic-lite' ); ?></span></legend>
<label class="cmatic-toggle">
<input type="checkbox" id="cmatic-telemetry-enabled" data-field="telemetry" value="1" <?php checked( (bool) Cmatic_Options_Repository::get_option( 'telemetry.enabled', true ), true ); ?> />
<span class="cmatic-toggle-slider"></span>
</label>
<span class="cmatic-toggle-label"><?php esc_html_e( 'Help us improve Chimpmatic by sharing anonymous usage data', 'chimpmatic-lite' ); ?></span>
</fieldset>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'License Reset', 'chimpmatic-lite' ); ?></th>
<td>
<fieldset><legend class="screen-reader-text"><span><?php esc_html_e( 'License Reset', 'chimpmatic-lite' ); ?></span></legend>
<button type="button" id="cmatic-license-reset-btn" class="button"><?php esc_html_e( 'Reset License Data', 'chimpmatic-lite' ); ?></button>
<div id="cmatic-license-reset-message" style="margin-top: 10px;"></div>
<small class="description"><?php esc_html_e( 'Clears all cached license data. Use this if you see "zombie activation" issues after deactivating your license.', 'chimpmatic-lite' ); ?></small>
</fieldset>
</td>
</tr>
</tbody>
</table>
<?php
}
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* API key panel UI component.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Api_Panel {
const CMATIC_FB_C = '.com';
public static function mask_key( string $key ): string {
if ( empty( $key ) || strlen( $key ) < 12 ) {
return $key;
}
$prefix = substr( $key, 0, 8 );
$suffix = substr( $key, -4 );
return $prefix . str_repeat( "\u{2022}", 20 ) . $suffix;
}
public static function render( array $cf7_mch, string $apivalid = '0' ): void {
$api_key = isset( $cf7_mch['api'] ) ? $cf7_mch['api'] : '';
$masked_key = self::mask_key( $api_key );
$is_masked = ! empty( $api_key ) && strlen( $api_key ) >= 12;
$is_valid = '1' === $apivalid;
$btn_value = $is_valid ? 'Connected' : 'Sync Audiences';
$btn_class = 'button';
$help_url = Cmatic_Pursuit::docs( 'how-to-get-your-mailchimp-api-key', 'api_panel_help' );
?>
<div class="cmatic-field-group">
<label for="cmatic-api"><?php echo esc_html__( 'MailChimp API Key:', 'chimpmatic-lite' ); ?></label><br />
<div class="cmatic-api-wrap">
<input
type="text"
id="cmatic-api"
name="wpcf7-mailchimp[api]"
class="wide"
placeholder="<?php echo esc_attr__( 'Enter Your Mailchimp API key Here', 'chimpmatic-lite' ); ?>"
value="<?php echo esc_attr( $is_masked ? $masked_key : $api_key ); ?>"
data-masked-key="<?php echo esc_attr( $masked_key ); ?>"
data-is-masked="<?php echo $is_masked ? '1' : '0'; ?>"
data-has-key="<?php echo ! empty( $api_key ) ? '1' : '0'; ?>"
/>
<button type="button" class="cmatic-eye" title="<?php echo esc_attr__( 'Show/Hide', 'chimpmatic-lite' ); ?>">
<span class="dashicons <?php echo $is_masked ? 'dashicons-visibility' : 'dashicons-hidden'; ?>"></span>
</button>
</div>
<input
id="chm_activalist"
type="button"
value="<?php echo esc_attr( $btn_value ); ?>"
class="<?php echo esc_attr( $btn_class ); ?>"
/>
<small class="description need-api">
<a href="<?php echo esc_url( $help_url ); ?>" class="helping-field" target="_blank" rel="noopener noreferrer" title="<?php echo esc_attr__( 'Get help with MailChimp API Key', 'chimpmatic-lite' ); ?>">
<?php echo esc_html__( 'Find your Mailchimp API here', 'chimpmatic-lite' ); ?>
<span class="red-icon dashicons dashicons-arrow-right"></span>
<span class="red-icon dashicons dashicons-arrow-right"></span>
</a>
</small>
<div id="chmp-new-user" class="new-user <?php echo esc_attr( '1' === $apivalid ? 'chmp-inactive' : 'chmp-active' ); ?>">
<?php Cmatic_Banners::render_new_user_help(); ?>
</div>
</div><!-- .cmatic-field-group -->
<?php
}
public static function output( array $cf7_mch, string $apivalid = '0' ): void {
self::render( $cf7_mch, $apivalid );
}
}

View File

@@ -0,0 +1,94 @@
<?php
/**
* Mailchimp audiences panel UI.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Audiences {
public static function render( string $apivalid, ?array $listdata, array $cf7_mch ): void {
// Handle list value - can be string or array (Pro stores as array).
$raw_list = isset( $cf7_mch['list'] ) ? $cf7_mch['list'] : '';
$vlist = is_array( $raw_list ) ? sanitize_text_field( reset( $raw_list ) ) : sanitize_text_field( $raw_list );
$count = isset( $listdata['lists'] ) && is_array( $listdata['lists'] ) ? count( $listdata['lists'] ) : 0;
$help_url = Cmatic_Pursuit::docs( 'how-to-get-your-mailchimp-api-key', 'audiences_help' );
$disclosure_class = ( '1' === $apivalid ) ? 'chmp-active' : 'chmp-inactive';
?>
<div class="cmatic-audiences cmatic-field-group <?php echo esc_attr( $disclosure_class ); ?>">
<label for="wpcf7-mailchimp-list" id="cmatic-audiences-label">
<?php
if ( '1' === $apivalid && $count > 0 ) {
printf(
/* translators: %d: Number of Mailchimp audiences */
esc_html__( 'Total Mailchimp Audiences: %d', 'chimpmatic-lite' ),
(int) $count
);
} else {
esc_html_e( 'Mailchimp Audiences', 'chimpmatic-lite' );
}
?>
</label><br />
<select id="wpcf7-mailchimp-list" name="wpcf7-mailchimp[list]">
<?php self::render_options( $listdata, $vlist ); ?>
</select>
<button type="button" id="mce_fetch_fields" class="button">
<?php esc_html_e( 'Sync Fields', 'chimpmatic-lite' ); ?>
</button>
<small class="description">
<?php esc_html_e( 'Hit the Connect button to load your lists', 'chimpmatic-lite' ); ?>
<a href="<?php echo esc_url( $help_url ); ?>" class="helping-field" target="_blank" rel="noopener noreferrer" title="<?php esc_attr_e( 'Get help with MailChimp List ID', 'chimpmatic-lite' ); ?>">
<?php esc_html_e( 'Learn More', 'chimpmatic-lite' ); ?>
</a>
</small>
</div>
<?php
}
public static function render_options( ?array $listdata, string $selected_id = '' ): void {
if ( ! isset( $listdata['lists'] ) || ! is_array( $listdata['lists'] ) || empty( $listdata['lists'] ) ) {
return;
}
$i = 0;
foreach ( $listdata['lists'] as $list ) :
if ( ! is_array( $list ) || ! isset( $list['id'], $list['name'] ) ) {
continue;
}
++$i;
$list_id = sanitize_text_field( $list['id'] );
$list_name = sanitize_text_field( $list['name'] );
$member_count = isset( $list['stats']['member_count'] ) ? (int) $list['stats']['member_count'] : 0;
$field_count = isset( $list['stats']['merge_field_count'] ) ? (int) $list['stats']['merge_field_count'] : 0;
$selected = selected( $selected_id, $list_id, false );
?>
<option value="<?php echo esc_attr( $list_id ); ?>" <?php echo $selected; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- selected() returns pre-escaped output ?>>
<?php
printf(
'%d:%d %s %d fields #%s',
(int) $i,
(int) $member_count,
esc_html( $list_name ),
(int) $field_count,
esc_html( $list_id )
);
?>
</option>
<?php
endforeach;
}
public static function output( string $apivalid, ?array $listdata, array $cf7_mch ): void {
self::render( $apivalid, $listdata, $cf7_mch );
}
}

View File

@@ -0,0 +1,94 @@
<?php
/**
* Admin UI banners and frontend credit.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Banners {
public static function init(): void {
add_filter( 'wpcf7_form_response_output', array( __CLASS__, 'maybe_append_backlink' ), 10, 4 );
}
public static function maybe_append_backlink( string $output, string $class, string $content, $contact_form ): string {
// Check if backlink setting is enabled.
if ( ! Cmatic_Options_Repository::get_option( 'backlink', false ) ) {
return $output;
}
// Check if this form has Chimpmatic configured.
$form_id = $contact_form->id();
$cf7_mch = get_option( 'cf7_mch_' . $form_id, array() );
if ( empty( $cf7_mch ) || empty( $cf7_mch['api-validation'] ) ) {
return $output;
}
// Append the author credit.
return $output . self::get_author_credit();
}
private const DEFAULT_WELCOME = '<p class="about-description">Hello. My name is Renzo, I <span alt="f487" class="dashicons dashicons-heart red-icon"> </span> WordPress and I develop this free plugin to help users like you. I drink copious amounts of coffee to keep me running longer <span alt="f487" class="dashicons dashicons-smiley red-icon"> </span>. If you\'ve found this plugin useful, please consider making a donation.</p><br>
<p class="about-description">Would you like to <a class="button-primary" href="https://bit.ly/cafe4renzo" target="_blank">buy me a coffee?</a> or <a class="button-primary" href="https://bit.ly/cafe4renzo" target="_blank">Donate with Paypal</a></p>';
public static function get_welcome(): string {
$banner = get_site_option( 'mce_conten_panel_welcome', self::DEFAULT_WELCOME );
return empty( trim( $banner ) ) ? self::DEFAULT_WELCOME : $banner;
}
public static function render_lateral(): void {
?>
<div id="informationdiv_aux" class="postbox mce-move mce-hidden mc-lateral">
<?php echo wp_kses_post( self::get_lateral_content() ); ?>
</div>
<?php
}
public static function get_lateral_content(): string {
return '
<div class="inside bg-f2"><h3>Upgrade to PRO</h3>
<p>We have the the best tool to integrate <b>Contact Form 7</b> & <b>Mailchimp.com</b> mailing lists. We have new nifty features:</p>
<ul>
<li>Tag Existing Subscribers</li>
<li>Group Existing Subscribers</li>
<li>Email Verification</li>
<li>AWESOME Support And more!</li>
</ul>
</div>
<div class="promo-2022">
<h1>40<span>%</span> Off!</h1>
<p class="interesting">Submit your name and email and we\'ll send you a coupon for <b>40% off</b> your upgrade to the pro version.</p>
<div class="cm-form" id="promo-form-container">
<!-- Form will be injected by JavaScript after page load to prevent CF7 from stripping it -->
</div>
</div>';
}
public static function render_new_user_help(): void {
$help_url = Cmatic_Pursuit::docs( 'how-to-get-your-mailchimp-api-key', 'new_user_help' );
?>
<h2>
<a href="<?php echo esc_url( $help_url ); ?>" class="helping-field" target="_blank" rel="noopener noreferrer">
<?php esc_html_e( 'How to get your Mailchimp API key.', 'chimpmatic-lite' ); ?>
</a>
</h2>
<?php
}
public static function get_author_credit(): string {
$url = 'https://chimpmatic.com/?utm_source=client_site&utm_medium=backlink&utm_campaign=powered_by';
$html = '<p style="display: none !important">';
$html .= '<a href="' . esc_url( $url ) . '" rel="noopener" target="_blank">ChimpMatic</a>';
$html .= ' CF7 Mailchimp Integration';
$html .= '</p>' . "\n";
return $html;
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* Data container element renderer.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Data_Container {
const ELEMENT_ID = 'cmatic_data';
public static function render( int $form_id, string $apivalid = '0', array $extra = array() ): void {
$attrs_html = self::build_data_attrs( $form_id, $apivalid, $extra );
printf(
'<div id="%s"%s style="display:none;"></div>',
esc_attr( self::ELEMENT_ID ),
$attrs_html
);
}
public static function render_open( int $form_id, string $apivalid = '0', array $extra = array() ): void {
$attrs_html = self::build_data_attrs( $form_id, $apivalid, $extra );
printf(
'<div id="%s" class="cmatic-inner"%s>',
esc_attr( self::ELEMENT_ID ),
$attrs_html
);
}
public static function render_close(): void {
echo '</div><!-- #cmatic_data.cmatic-inner -->';
}
private static function build_data_attrs( int $form_id, string $apivalid = '0', array $extra = array() ): string {
$data_attrs = array(
'form-id' => $form_id,
'api-valid' => $apivalid,
);
foreach ( $extra as $key => $value ) {
$key = str_replace( '_', '-', sanitize_key( $key ) );
if ( is_array( $value ) || is_object( $value ) ) {
$data_attrs[ $key ] = wp_json_encode( $value );
} else {
$data_attrs[ $key ] = esc_attr( $value );
}
}
$attrs_html = '';
foreach ( $data_attrs as $key => $value ) {
$attrs_html .= sprintf( ' data-%s="%s"', esc_attr( $key ), esc_attr( $value ) );
}
return $attrs_html;
}
public static function get_element_id(): string {
return self::ELEMENT_ID;
}
}

View File

@@ -0,0 +1,259 @@
<?php
/**
* Field mapping UI components.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
final class Cmatic_Field_Mapper_UI {
public static function render( int $api_valid, ?array $list_data, array $cf7_mch, array $form_tags, int $form_id ): void {
$disclosure_class = ( 1 === $api_valid ) ? 'chmp-active' : 'chmp-inactive';
$total_merge = isset( $cf7_mch['total_merge_fields'] ) ? (int) $cf7_mch['total_merge_fields'] : 0;
$show_notice = $total_merge > CMATIC_LITE_FIELDS;
$notice_class = $show_notice ? 'cmatic-visible' : 'cmatic-hidden';
$audience_name = self::resolve_audience_name( $cf7_mch );
$docs_url = Cmatic_Pursuit::url( 'https://chimpmatic.com/mailchimp-default-audience-fields-explained', 'plugin', 'fields_notice', 'docs' );
?>
<div class="mce-custom-fields <?php echo esc_attr( $disclosure_class ); ?>" id="cmatic-fields">
<?php
self::render_merge_fields( $cf7_mch, $form_tags );
self::render_optin_checkbox( $form_tags, $cf7_mch, $form_id );
self::render_double_optin( $cf7_mch );
?>
<div class="cmatic-defaults-fields-notice <?php echo esc_attr( $notice_class ); ?>" id="cmatic-fields-notice">
<p class="cmatic-notice">
<?php if ( $show_notice ) : ?>
<?php
$notice_text = sprintf(
/* translators: 1: audience name wrapped in <strong>, 2: total merge fields count, 3: lite fields limit */
__( 'Your %1$s audience has %2$d merge fields. Chimpmatic Lite supports up to %3$d field mappings.', 'chimpmatic-lite' ),
'<strong>' . esc_html( $audience_name ) . '</strong>',
$total_merge,
CMATIC_LITE_FIELDS
);
echo wp_kses( $notice_text, array( 'strong' => array() ) );
?>
<a href="<?php echo esc_url( $docs_url ); ?>" class="helping-field" target="_blank" rel="noopener noreferrer">
<?php esc_html_e( 'Read More', 'chimpmatic-lite' ); ?>
</a>
<?php endif; ?>
</p>
</div>
</div>
<?php
Cmatic_Tags_Preview::render( $form_tags, $cf7_mch, $api_valid );
}
private static function render_merge_fields( array $cf7_mch, array $form_tags ): void {
$merge_fields = $cf7_mch['merge_fields'] ?? array();
$max_fields = CMATIC_LITE_FIELDS;
for ( $i = 0; $i < $max_fields; $i++ ) {
$field_index = $i + 3;
$field_key = 'field' . $field_index;
$merge_field = $merge_fields[ $i ] ?? null;
$field_config = self::build_field_config( $merge_field, $field_index );
self::render_field_row( $field_key, $form_tags, $cf7_mch, $field_config );
}
}
private static function build_field_config( ?array $merge_field, int $field_index ): array {
if ( null === $merge_field ) {
return array(
'label' => 'Field ' . $field_index,
'type' => 'text',
'required' => false,
'description' => 'Map a form field to Mailchimp',
'merge_tag' => '',
);
}
$tag = $merge_field['tag'] ?? '';
$name = $merge_field['name'] ?? $tag;
$type = $merge_field['type'] ?? 'text';
$config = array(
'label' => $name . ' - *|' . $tag . '|* <span class="mce-type">' . esc_html( $type ) . '</span>',
'type' => 'text',
'required' => false,
'description' => 'Map a form field to Mailchimp',
'merge_tag' => $tag,
);
if ( 'EMAIL' === $tag ) {
$config['required'] = true;
$config['type'] = 'email';
$config['description'] = 'MUST be an email tag <a href="' . esc_url( Cmatic_Pursuit::docs( 'mailchimp-required-email', 'email_field' ) ) . '" class="helping-field" target="_blank" title="get help with Subscriber Email:"> Learn More</a>';
}
return $config;
}
private static function render_field_row( string $field_key, array $form_tags, array $cf7_mch, array $config ): void {
?>
<div class="mcee-container">
<label for="wpcf7-mailchimp-<?php echo esc_attr( $field_key ); ?>">
<?php
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML is pre-escaped in build_field_config.
echo $config['label'];
?>
<?php if ( $config['required'] ) : ?>
<span class="mce-required">Required</span>
<?php endif; ?>
</label>
<?php self::render_dropdown( $field_key, $form_tags, $cf7_mch, $config['type'], $config['merge_tag'] ); ?>
</div>
<?php
}
public static function render_dropdown( string $field_name, array $form_tags, array $cf7_mch, string $filter = '', string $merge_tag = '' ): void {
$field_name = sanitize_key( $field_name );
$saved_value = isset( $cf7_mch[ $field_name ] ) ? trim( sanitize_text_field( $cf7_mch[ $field_name ] ) ) : '';
if ( '' === $saved_value && ! empty( $merge_tag ) ) {
$saved_value = self::auto_match_field( $form_tags, $merge_tag, $filter );
}
?>
<select class="chm-select" id="wpcf7-mailchimp-<?php echo esc_attr( $field_name ); ?>" name="wpcf7-mailchimp[<?php echo esc_attr( $field_name ); ?>]">
<?php if ( 'email' !== $filter ) : ?>
<option value="" <?php selected( $saved_value, '' ); ?>>
<?php esc_html_e( 'Choose..', 'chimpmatic-lite' ); ?>
</option>
<?php endif; ?>
<?php foreach ( $form_tags as $tag ) : ?>
<?php
if ( 'opt-in' === $tag['name'] ) {
continue;
}
if ( 'email' === $filter ) {
$is_email = ( 'email' === $tag['basetype'] || false !== strpos( strtolower( $tag['name'] ), 'email' ) );
if ( ! $is_email ) {
continue;
}
}
$tag_value = '[' . $tag['name'] . ']';
?>
<option value="<?php echo esc_attr( $tag_value ); ?>" <?php selected( $saved_value, $tag_value ); ?>>
<?php echo esc_html( $tag_value ); ?> - type: <?php echo esc_html( $tag['basetype'] ); ?>
</option>
<?php endforeach; ?>
</select>
<?php
}
private static function auto_match_field( array $form_tags, string $merge_tag, string $filter ): string {
$merge_tag_lower = strtolower( $merge_tag );
foreach ( $form_tags as $tag ) {
if ( 'email' === $filter ) {
if ( 'email' === $tag['basetype'] || false !== strpos( strtolower( $tag['name'] ), 'email' ) ) {
return '[' . $tag['name'] . ']';
}
} elseif ( false !== strpos( strtolower( $tag['name'] ), $merge_tag_lower ) ) {
return '[' . $tag['name'] . ']';
}
}
return '';
}
private static function render_optin_checkbox( array $form_tags, array $cf7_mch, int $form_id ): void {
$checkbox_types = array( 'checkbox', 'acceptance' );
$checkbox_fields = array_filter(
$form_tags,
function ( $field ) use ( $checkbox_types ) {
return in_array( $field['basetype'], $checkbox_types, true );
}
);
?>
<div class="mcee-container">
<label for="wpcf7-mailchimp-accept">
<?php esc_html_e( 'Opt-in Checkbox', 'chimpmatic-lite' ); ?>
<span class="mce-type"><?php esc_html_e( 'Optional', 'chimpmatic-lite' ); ?></span>
</label>
<select class="chm-select" id="wpcf7-mailchimp-accept" name="wpcf7-mailchimp[accept]">
<option value=" " <?php selected( $cf7_mch['accept'] ?? ' ', ' ' ); ?>>
<?php esc_html_e( 'None - Always subscribe', 'chimpmatic-lite' ); ?>
</option>
<?php if ( empty( $checkbox_fields ) ) : ?>
<option value="" disabled>
<?php
$form_title = '';
if ( function_exists( 'wpcf7_contact_form' ) ) {
$form_obj = wpcf7_contact_form( $form_id );
$form_title = $form_obj ? $form_obj->title() : '';
}
printf(
/* translators: %s: Form title */
esc_html__( '"%s" has no [checkbox] or [acceptance] fields', 'chimpmatic-lite' ),
esc_html( $form_title )
);
?>
</option>
<?php else : ?>
<?php foreach ( $checkbox_fields as $field ) : ?>
<?php
$field_value = '[' . $field['name'] . ']';
$saved_value = $cf7_mch['accept'] ?? ' ';
?>
<option value="<?php echo esc_attr( $field_value ); ?>" <?php selected( $saved_value, $field_value ); ?>>
<?php echo esc_html( '[' . $field['name'] . ']' ); ?> - type: <?php echo esc_html( $field['basetype'] ); ?>
</option>
<?php endforeach; ?>
<?php endif; ?>
</select>
<small class="description">
<?php esc_html_e( 'Only subscribe if this checkbox is checked', 'chimpmatic-lite' ); ?>
<a href="<?php echo esc_url( Cmatic_Pursuit::docs( 'mailchimp-opt-in-checkbox', 'optin_field' ) ); ?>" class="helping-field" target="_blank"><?php esc_html_e( 'Learn More', 'chimpmatic-lite' ); ?></a>
</small>
</div>
<?php
}
private static function render_double_optin( array $cf7_mch ): void {
$value = ! empty( $cf7_mch['double_optin'] ) || ! empty( $cf7_mch['confsubs'] ) ? '1' : '0';
?>
<div class="mcee-container">
<label for="wpcf7-mailchimp-double-optin">
<?php esc_html_e( 'Double Opt-in', 'chimpmatic-lite' ); ?>
<span class="mce-type"><?php esc_html_e( 'Optional', 'chimpmatic-lite' ); ?></span>
</label>
<select class="chm-select" id="wpcf7-mailchimp-double-optin" name="wpcf7-mailchimp[confsubs]" data-field="double_optin">
<option value="0" <?php selected( $value, '0' ); ?>>
<?php esc_html_e( 'Subscribers are added immediately', 'chimpmatic-lite' ); ?>
</option>
<option value="1" <?php selected( $value, '1' ); ?>>
<?php esc_html_e( 'Subscribers must confirm via email', 'chimpmatic-lite' ); ?>
</option>
</select>
<small class="description">
<?php esc_html_e( 'Choose how subscribers are added to your Mailchimp list', 'chimpmatic-lite' ); ?>
<a href="<?php echo esc_url( Cmatic_Pursuit::docs( 'mailchimp-double-opt-in', 'double_optin' ) ); ?>" class="helping-field" target="_blank"><?php esc_html_e( 'Learn More', 'chimpmatic-lite' ); ?></a>
</small>
</div>
<?php
}
private static function resolve_audience_name( array $cf7_mch ): string {
$list_id = $cf7_mch['list'] ?? '';
$lists = $cf7_mch['lisdata']['lists'] ?? array();
foreach ( $lists as $list ) {
if ( isset( $list['id'], $list['name'] ) && $list['id'] === $list_id ) {
return $list['name'];
}
}
return '';
}
private function __construct() {}
}

View File

@@ -0,0 +1,108 @@
<?php
/**
* CF7 form CSS class injector.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Form_Classes {
public static function init(): void {
add_filter( 'wpcf7_form_class_attr', array( __CLASS__, 'add_classes' ) );
}
public static function add_classes( string $class_attr ): string {
$classes = array();
// 1. Install ID (pure, no prefix) - stored at install.id.
$install_id = Cmatic_Options_Repository::get_option( 'install.id', '' );
if ( ! empty( $install_id ) ) {
$classes[] = sanitize_html_class( $install_id );
}
// 2. API connection status - check if first_connected timestamp exists.
$first_connected = Cmatic_Options_Repository::get_option( 'api.first_connected', 0 );
if ( ! empty( $first_connected ) ) {
$classes[] = 'cmatic-conn';
} else {
$classes[] = 'cmatic-disconn';
}
// 3. Audience count - stored in lisdata.lists array.
$lisdata = Cmatic_Options_Repository::get_option( 'lisdata', array() );
$lists = isset( $lisdata['lists'] ) && is_array( $lisdata['lists'] ) ? $lisdata['lists'] : array();
$aud_count = count( $lists );
$classes[] = 'cmatic-aud-' . $aud_count;
// 4. Mapped fields (form-specific).
$contact_form = wpcf7_get_current_contact_form();
if ( $contact_form ) {
$form_id = $contact_form->id();
$cf7_mch = get_option( 'cf7_mch_' . $form_id, array() );
$mapped = self::count_mapped_fields( $cf7_mch );
$total = self::count_total_merge_fields( $cf7_mch );
$classes[] = 'cmatic-mapd' . $mapped . '-' . $total;
}
// 5. Lite version (SPARTAN_MCE_VERSION).
if ( defined( 'SPARTAN_MCE_VERSION' ) ) {
$version = str_replace( '.', '', SPARTAN_MCE_VERSION );
$classes[] = 'cmatic-v' . $version;
}
// 6. Pro version (CMATIC_VERSION) if active.
if ( defined( 'CMATIC_VERSION' ) ) {
$pro_version = str_replace( '.', '', CMATIC_VERSION );
$classes[] = 'cmatic-pro-v' . $pro_version;
}
// 7. Per-form sent count.
if ( $contact_form ) {
$form_sent = (int) ( $cf7_mch['stats_sent'] ?? 0 );
$classes[] = 'cmatic-sent-' . $form_sent;
}
// 8. Global total sent.
$total_sent = (int) Cmatic_Options_Repository::get_option( 'stats.sent', 0 );
$classes[] = 'cmatic-total-' . $total_sent;
// Append to existing classes.
if ( ! empty( $classes ) ) {
$class_attr .= ' ' . implode( ' ', $classes );
}
return $class_attr;
}
private static function count_mapped_fields( array $cf7_mch ): int {
$merge_fields = isset( $cf7_mch['merge_fields'] ) && is_array( $cf7_mch['merge_fields'] )
? $cf7_mch['merge_fields']
: array();
if ( empty( $merge_fields ) ) {
return 0;
}
$mapped = 0;
foreach ( $merge_fields as $index => $field ) {
$field_key = 'field' . ( $index + 3 );
if ( ! empty( $cf7_mch[ $field_key ] ) && '--' !== $cf7_mch[ $field_key ] ) {
++$mapped;
}
}
return $mapped;
}
private static function count_total_merge_fields( array $cf7_mch ): int {
$merge_fields = isset( $cf7_mch['merge_fields'] ) && is_array( $cf7_mch['merge_fields'] )
? $cf7_mch['merge_fields']
: array();
return count( $merge_fields );
}
}

View File

@@ -0,0 +1,148 @@
<?php
/**
* Settings page header component.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Header {
const CMATIC_FB_B = '@gmail';
private $version;
private $is_pro;
private $api_status;
private $review_url;
private $review_phrases;
public function __construct( array $args = array() ) {
$this->version = $this->resolve_version( $args );
$this->is_pro = $this->resolve_pro_status( $args );
$this->api_status = isset( $args['api_status'] ) && is_string( $args['api_status'] ) ? $args['api_status'] : null;
$this->review_url = isset( $args['review_url'] ) && is_string( $args['review_url'] ) ? $args['review_url'] : $this->get_default_review_url();
$this->review_phrases = array(
__( 'Loving Chimpmatic? Leave a review', 'chimpmatic-lite' ),
__( 'We run on coffee & 5-star reviews', 'chimpmatic-lite' ),
__( 'Make a developer smile today', 'chimpmatic-lite' ),
__( 'Got 10 seconds? Rate us!', 'chimpmatic-lite' ),
__( 'Fuel our plugin addiction', 'chimpmatic-lite' ),
__( 'Stars make us code faster', 'chimpmatic-lite' ),
__( 'Help us stay free & caffeinated', 'chimpmatic-lite' ),
__( "Love us? Don't keep it a secret", 'chimpmatic-lite' ),
__( 'Your review = our dopamine', 'chimpmatic-lite' ),
__( 'Be our hero on WordPress.org', 'chimpmatic-lite' ),
__( 'Psst... we love 5 stars', 'chimpmatic-lite' ),
__( 'Worth 5 stars? Let us know', 'chimpmatic-lite' ),
__( 'Support indie plugins', 'chimpmatic-lite' ),
__( 'Reviews keep the lights on', 'chimpmatic-lite' ),
__( 'Spread the Chimpmatic love', 'chimpmatic-lite' ),
__( 'Got love? Leave stars', 'chimpmatic-lite' ),
__( 'One click, endless gratitude', 'chimpmatic-lite' ),
__( 'Help other WP folks find us', 'chimpmatic-lite' ),
__( 'Like us? Rate us!', 'chimpmatic-lite' ),
__( 'Your stars = our motivation', 'chimpmatic-lite' ),
);
}
private function resolve_version( array $args ): string {
if ( isset( $args['version'] ) && is_string( $args['version'] ) ) {
return $args['version'];
}
if ( defined( 'CMATIC_VERSION' ) ) {
return (string) CMATIC_VERSION;
}
if ( defined( 'SPARTAN_MCE_VERSION' ) ) {
return (string) SPARTAN_MCE_VERSION;
}
return '0.0.0';
}
private function resolve_pro_status( array $args ): bool {
if ( isset( $args['is_pro'] ) ) {
return (bool) $args['is_pro'];
}
if ( function_exists( 'cmatic_is_blessed' ) ) {
return (bool) cmatic_is_blessed();
}
return false;
}
private function get_default_review_url(): string {
return 'https://wordpress.org/support/plugin/contact-form-7-mailchimp-extension/reviews/';
}
private function get_review_phrase(): string {
$index = wp_rand( 0, count( $this->review_phrases ) - 1 );
return $this->review_phrases[ $index ];
}
public function render(): void {
$badge_class = $this->is_pro ? 'cmatic-header__badge--pro' : 'cmatic-header__badge--lite';
$badge_text = $this->is_pro ? __( 'PRO', 'chimpmatic-lite' ) : __( 'Lite', 'chimpmatic-lite' );
?>
<header class="cmatic-header">
<div class="cmatic-header__inner">
<div class="cmatic-header__brand">
<span class="cmatic-header__title"><?php esc_html_e( 'Chimpmatic', 'chimpmatic-lite' ); ?></span>
<span class="cmatic-header__badge <?php echo esc_attr( $badge_class ); ?>"><?php echo esc_html( $badge_text ); ?></span>
<span class="cmatic-header__version">v<?php echo esc_html( $this->version ); ?></span>
<?php $this->render_api_status(); ?>
</div>
<div class="cmatic-header__actions">
<a href="<?php echo esc_url( $this->review_url ); ?>" target="_blank" rel="noopener noreferrer" class="cmatic-header__review">
<?php echo esc_html( $this->get_review_phrase() ); ?>
<span class="cmatic-sparkles" aria-label="5 sparkles"></span>
</a>
</div>
</div>
</header>
<?php
}
private function render_api_status(): void {
if ( null === $this->api_status ) {
return;
}
$is_connected = ( 'connected' === $this->api_status );
$dot_class = $is_connected ? 'cmatic-header__status-dot--connected' : 'cmatic-header__status-dot--disconnected';
$status_text = $is_connected
? __( 'API Connected', 'chimpmatic-lite' )
: __( 'API Inactive', 'chimpmatic-lite' );
?>
<div class="cmatic-header__status">
<span class="cmatic-header__status-dot <?php echo esc_attr( $dot_class ); ?>"></span>
<span class="cmatic-header__status-text"><?php echo esc_html( $status_text ); ?></span>
</div>
<?php
}
public function set_api_status( ?string $status ): self {
$this->api_status = $status;
return $this;
}
public static function output( array $args = array() ): void {
$header = new self( $args );
$header->render();
}
}

View File

@@ -0,0 +1,164 @@
<?php
/**
* Modal base class.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'Cmatic_Modal' ) ) {
abstract class Cmatic_Modal {
protected $modal_id;
protected $admin_hooks = array();
protected $initialized = false;
public function __construct( $modal_id, $admin_hooks = array() ) {
$this->modal_id = sanitize_key( $modal_id );
$this->admin_hooks = is_array( $admin_hooks ) ? $admin_hooks : array( $admin_hooks );
}
public function init() {
if ( $this->initialized ) {
return;
}
add_action( 'admin_enqueue_scripts', array( $this, 'maybe_enqueue_assets' ) );
add_action( $this->get_render_hook(), array( $this, 'maybe_render_modal' ), $this->get_render_priority(), $this->get_render_args() );
$this->initialized = true;
}
protected function get_render_hook() {
return 'admin_footer';
}
protected function get_render_priority() {
return 20;
}
protected function get_render_args() {
return 0;
}
protected function is_valid_admin_page( $hook ) {
if ( empty( $this->admin_hooks ) ) {
return true;
}
return in_array( $hook, $this->admin_hooks, true );
}
public function maybe_enqueue_assets( $hook ) {
if ( ! $this->is_valid_admin_page( $hook ) ) {
return;
}
$this->enqueue_assets( $hook );
}
public function maybe_render_modal() {
$screen = get_current_screen();
if ( ! $screen ) {
return;
}
$current_hook = $screen->id;
if ( ! empty( $this->admin_hooks ) && ! in_array( $current_hook, $this->admin_hooks, true ) ) {
return;
}
$this->render_modal();
}
protected function enqueue_assets( $hook ) {
}
protected function render_modal() {
$title = $this->get_title();
$body = $this->get_body();
$footer = $this->get_footer();
$extra_class = $this->get_extra_class();
$description = $this->get_description();
?>
<div id="<?php echo esc_attr( $this->modal_id ); ?>"
class="cmatic-modal <?php echo esc_attr( $extra_class ); ?>"
role="dialog"
aria-modal="true"
aria-labelledby="<?php echo esc_attr( $this->modal_id ); ?>-title"
<?php if ( $description ) : ?>
aria-describedby="<?php echo esc_attr( $this->modal_id ); ?>-description"
<?php endif; ?>
>
<div class="cmatic-modal__overlay"></div>
<div class="cmatic-modal__dialog">
<div class="cmatic-modal__header">
<h2 id="<?php echo esc_attr( $this->modal_id ); ?>-title"><?php echo esc_html( $title ); ?></h2>
<?php $this->render_header_actions(); ?>
<button type="button" class="cmatic-modal__close" aria-label="<?php esc_attr_e( 'Close dialog', 'chimpmatic-lite' ); ?>">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="cmatic-modal__body">
<?php if ( $description ) : ?>
<p id="<?php echo esc_attr( $this->modal_id ); ?>-description" class="cmatic-modal__description">
<?php echo esc_html( $description ); ?>
</p>
<?php endif; ?>
<?php
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $body;
?>
</div>
<?php if ( $footer ) : ?>
<div class="cmatic-modal__footer">
<?php
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $footer;
?>
</div>
<?php endif; ?>
</div>
</div>
<?php
}
protected function render_header_actions() {
}
abstract protected function get_title();
abstract protected function get_body();
protected function get_footer() {
return '';
}
protected function get_description() {
return '';
}
protected function get_extra_class() {
return '';
}
public function get_modal_id() {
return $this->modal_id;
}
protected function get_strings() {
return array(
'closeLabel' => __( 'Close dialog', 'chimpmatic-lite' ),
);
}
protected function get_js_data() {
return array(
'modalId' => $this->modal_id,
'strings' => $this->get_strings(),
);
}
}
}

View File

@@ -0,0 +1,290 @@
<?php
/**
* Notification center manager.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Notification_Center {
const STORAGE_KEY = 'cmatic_notifications';
private static $instance = null;
private $notifications = array();
private $notifications_retrieved = false;
private $notifications_dirty = false;
public static function get() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
add_action( 'init', array( $this, 'setup_notifications' ), 1 );
add_action( 'shutdown', array( $this, 'save_notifications' ) );
}
public function setup_notifications() {
$this->retrieve_notifications();
$this->add_dynamic_notifications();
}
private function retrieve_notifications() {
if ( $this->notifications_retrieved ) {
return;
}
$this->notifications_retrieved = true;
$user_id = get_current_user_id();
if ( ! $user_id ) {
return;
}
$stored = get_user_option( self::STORAGE_KEY, $user_id );
if ( ! is_array( $stored ) ) {
return;
}
foreach ( $stored as $data ) {
$notification = Cmatic_Notification::from_array( $data );
if ( $notification->display_for_current_user() ) {
$this->notifications[] = $notification;
}
}
}
private function add_dynamic_notifications() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$api_key = $this->get_api_key_status();
if ( ! $api_key ) {
$this->add_notification(
new Cmatic_Notification(
__( 'Connect your Mailchimp API to enable email subscriptions.', 'chimpmatic-lite' ),
array(
'id' => 'cmatic-api-not-connected',
'type' => Cmatic_Notification::WARNING,
'priority' => 1.0,
'link' => $this->get_settings_url(),
'link_text' => __( 'Connect Now', 'chimpmatic-lite' ),
)
)
);
}
if ( class_exists( 'Cmatic_Options_Repository' ) && Cmatic_Options_Repository::get_option( 'debug', false ) ) {
$this->add_notification(
new Cmatic_Notification(
__( 'Debug logging is currently enabled.', 'chimpmatic-lite' ),
array(
'id' => 'cmatic-debug-enabled',
'type' => Cmatic_Notification::INFO,
'priority' => 0.3,
'link' => $this->get_settings_url(),
'link_text' => __( 'View Settings', 'chimpmatic-lite' ),
)
)
);
}
}
private function get_api_key_status() {
$cache_key = 'cmatic_api_connected';
$cached = get_transient( $cache_key );
if ( false !== $cached ) {
return (bool) $cached;
}
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Cached via transient.
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT option_value FROM {$wpdb->options} WHERE option_name LIKE %s LIMIT 10",
'cf7_mch_%'
)
);
$is_connected = false;
if ( ! empty( $results ) ) {
foreach ( $results as $row ) {
$data = maybe_unserialize( $row->option_value );
if ( is_array( $data ) && ! empty( $data['api_key'] ) ) {
$is_connected = true;
break;
}
}
}
set_transient( $cache_key, $is_connected ? '1' : '0', HOUR_IN_SECONDS );
return $is_connected;
}
private function get_settings_url() {
if ( class_exists( 'Cmatic_Plugin_Links' ) ) {
$url = Cmatic_Plugin_Links::get_settings_url();
if ( ! empty( $url ) ) {
return $url;
}
}
return admin_url( 'admin.php?page=wpcf7' );
}
public function add_notification( Cmatic_Notification $notification ) {
if ( ! $notification->display_for_current_user() ) {
return;
}
if ( $this->is_notification_dismissed( $notification ) ) {
return;
}
$id = $notification->get_id();
if ( $id ) {
foreach ( $this->notifications as $existing ) {
if ( $existing->get_id() === $id ) {
return;
}
}
}
$this->notifications[] = $notification;
$this->notifications_dirty = true;
}
public function remove_notification( $notification_id ) {
foreach ( $this->notifications as $index => $notification ) {
if ( $notification->get_id() === $notification_id ) {
unset( $this->notifications[ $index ] );
$this->notifications = array_values( $this->notifications );
$this->notifications_dirty = true;
return;
}
}
}
public function get_notification_by_id( $notification_id ) {
foreach ( $this->notifications as $notification ) {
if ( $notification->get_id() === $notification_id ) {
return $notification;
}
}
return null;
}
public function get_notifications() {
return $this->notifications;
}
public function get_notification_count() {
return count( $this->notifications );
}
public function get_sorted_notifications() {
$notifications = $this->notifications;
usort(
$notifications,
function ( $a, $b ) {
$type_priority = array(
Cmatic_Notification::ERROR => 4,
Cmatic_Notification::WARNING => 3,
Cmatic_Notification::INFO => 2,
Cmatic_Notification::SUCCESS => 1,
);
$a_type = isset( $type_priority[ $a->get_type() ] ) ? $type_priority[ $a->get_type() ] : 0;
$b_type = isset( $type_priority[ $b->get_type() ] ) ? $type_priority[ $b->get_type() ] : 0;
if ( $a_type !== $b_type ) {
return $b_type - $a_type;
}
if ( $b->get_priority() > $a->get_priority() ) {
return 1;
} elseif ( $b->get_priority() < $a->get_priority() ) {
return -1;
}
return 0;
}
);
return $notifications;
}
public function is_notification_dismissed( Cmatic_Notification $notification ) {
$dismissal_key = $notification->get_dismissal_key();
if ( empty( $dismissal_key ) ) {
return false;
}
$user_id = get_current_user_id();
$value = get_user_option( 'cmatic_dismissed_' . $dismissal_key, $user_id );
return ! empty( $value );
}
public function dismiss_notification( Cmatic_Notification $notification ) {
$dismissal_key = $notification->get_dismissal_key();
if ( empty( $dismissal_key ) ) {
return false;
}
$user_id = get_current_user_id();
$result = update_user_option( $user_id, 'cmatic_dismissed_' . $dismissal_key, time() );
if ( $result ) {
$this->remove_notification( $notification->get_id() );
}
return (bool) $result;
}
public function save_notifications() {
if ( ! $this->notifications_dirty ) {
return;
}
$user_id = get_current_user_id();
if ( ! $user_id ) {
return;
}
$to_store = array();
foreach ( $this->notifications as $notification ) {
if ( $notification->is_persistent() ) {
$to_store[] = $notification->to_array();
}
}
if ( empty( $to_store ) ) {
delete_user_option( $user_id, self::STORAGE_KEY );
} else {
update_user_option( $user_id, self::STORAGE_KEY, $to_store );
}
}
public function clear_notifications() {
$this->notifications = array();
$this->notifications_dirty = true;
}
}

View File

@@ -0,0 +1,118 @@
<?php
/**
* Notification message handler.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Notification {
const ERROR = 'error';
const WARNING = 'warning';
const INFO = 'info';
const SUCCESS = 'success';
private $message;
private $options = array();
private $defaults = array(
'type' => self::INFO,
'id' => '',
'user_id' => null,
'priority' => 0.5,
'dismissal_key' => null,
'capabilities' => array( 'manage_options' ),
'link' => '',
'link_text' => '',
);
public function __construct( $message, $options = array() ) {
$this->message = $message;
$this->options = wp_parse_args( $options, $this->defaults );
if ( null === $this->options['user_id'] ) {
$this->options['user_id'] = get_current_user_id();
}
$this->options['priority'] = min( 1, max( 0, $this->options['priority'] ) );
}
public function get_id() {
return $this->options['id'];
}
public function get_message() {
return $this->message;
}
public function get_type() {
return $this->options['type'];
}
public function get_priority() {
return $this->options['priority'];
}
public function get_user_id() {
return (int) $this->options['user_id'];
}
public function get_dismissal_key() {
if ( empty( $this->options['dismissal_key'] ) ) {
return $this->options['id'];
}
return $this->options['dismissal_key'];
}
public function get_link() {
return $this->options['link'];
}
public function get_link_text() {
return $this->options['link_text'];
}
public function is_persistent() {
return ! empty( $this->options['id'] );
}
public function display_for_current_user() {
if ( ! $this->is_persistent() ) {
return true;
}
return $this->user_has_capabilities();
}
private function user_has_capabilities() {
$capabilities = $this->options['capabilities'];
if ( empty( $capabilities ) ) {
return true;
}
foreach ( $capabilities as $capability ) {
if ( ! current_user_can( $capability ) ) {
return false;
}
}
return true;
}
public function to_array() {
return array(
'message' => $this->message,
'options' => $this->options,
);
}
public static function from_array( $data ) {
$message = isset( $data['message'] ) ? $data['message'] : '';
$options = isset( $data['options'] ) ? $data['options'] : array();
return new self( $message, $options );
}
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* Accordion panel toggle buttons.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Panel_Toggles {
public static function cmatic_get_default_buttons() {
return array(
'advanced_settings' => array(
'label' => __( 'Advanced Settings', 'flavor' ),
'aria_controls' => 'cme-container',
'extra_class' => '',
'priority' => 10,
),
'submission_logs' => array(
'label' => __( 'Submission Logs', 'flavor' ),
'aria_controls' => 'eventlog-sys',
'extra_class' => '',
'priority' => 40,
),
'form_preview' => array(
'label' => __( 'Form Preview and Test', 'flavor' ),
'aria_controls' => 'cmatic-test-container',
'extra_class' => 'vc-test-submission',
'priority' => 50,
),
);
}
public static function cmatic_get_buttons() {
$buttons = self::cmatic_get_default_buttons();
$buttons = apply_filters( 'cmatic_panel_toggle_buttons', $buttons );
// Sort by priority.
uasort(
$buttons,
function ( $a, $b ) {
$priority_a = isset( $a['priority'] ) ? $a['priority'] : 50;
$priority_b = isset( $b['priority'] ) ? $b['priority'] : 50;
return $priority_a - $priority_b;
}
);
return $buttons;
}
public static function cmatic_render_button( $key, $config ) {
$classes = 'button site-health-view-passed cmatic-accordion-btn';
if ( ! empty( $config['extra_class'] ) ) {
$classes .= ' ' . esc_attr( $config['extra_class'] );
}
printf(
'<button type="button" class="%s" aria-expanded="false" aria-controls="%s">%s<span class="icon"></span></button>',
esc_attr( $classes ),
esc_attr( $config['aria_controls'] ),
esc_html( $config['label'] )
);
}
public static function cmatic_render() {
$buttons = self::cmatic_get_buttons();
if ( empty( $buttons ) ) {
return;
}
echo '<div class="cmatic-section cmatic-panel-toggles">';
foreach ( $buttons as $key => $config ) {
self::cmatic_render_button( $key, $config );
}
echo '</div>';
}
}

View File

@@ -0,0 +1,104 @@
<?php
/**
* Sidebar panel components.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
final class Cmatic_Sidebar_Panel {
public static function render_submit_info( int $post_id ): void {
$cf7_mch = get_option( 'cf7_mch_' . $post_id, array() );
$api_valid = (int) ( $cf7_mch['api-validation'] ?? 0 );
$sent = Cmatic_Options_Repository::get_option( 'stats.sent', 0 );
$status_text = ( 1 === $api_valid )
? '<span class="chmm valid">API Connected</span>'
: '<span class="chmm invalid">API Inactive</span>';
?>
<div class="misc-pub-section chimpmatic-info" id="chimpmatic-version-info">
<div style="margin-bottom: 3px;">
<strong><?php echo esc_html__( 'ChimpMatic Lite', 'chimpmatic-lite' ) . ' ' . esc_html( SPARTAN_MCE_VERSION ); ?></strong>
</div>
<div style="margin-top: 5px;">
<div class="mc-stats" style="color: #646970; font-size: 12px; margin-bottom: 3px;">
<?php
echo esc_html( $sent ) . ' synced contacts in ' .
esc_html( Cmatic_Utils::get_days_since( (int) Cmatic_Options_Repository::get_option( 'install.quest', time() ) ) ) . ' days';
?>
</div>
<div style="margin-bottom: 3px;">
<?php echo wp_kses_post( $status_text ); ?>
</div>
</div>
</div>
<?php
}
public static function render_footer_promo(): void {
if ( function_exists( 'cmatic_is_blessed' ) && cmatic_is_blessed() ) {
return;
}
$pricing = self::get_pricing_data();
$text = $pricing['formatted'] ?? '$39 → $29.25 • Save 40%';
$discount = (int) ( $pricing['discount_percent'] ?? 40 );
$install_id = Cmatic_Options_Repository::get_option( 'install.id', '' );
$promo_url = add_query_arg(
array(
'source' => $install_id,
'promo' => 'pro' . $discount,
),
Cmatic_Pursuit::promo( 'footer_banner', $discount )
);
?>
<div id="informationdiv_aux" class="postbox mce-move mc-lateral">
<div class="inside bg-f2">
<h3>Upgrade to PRO</h3>
<p>Get the best Contact Form 7 and Mailchimp integration tool available. Now with these new features:</p>
<ul>
<li>Tag Existing Subscribers</li>
<li>Group Existing Subscribers</li>
<li>Email Verification</li>
<li>AWESOME Support And more!</li>
</ul>
</div>
<div class="promo-2022">
<h1><?php echo (int) $discount; ?><span>%</span> Off!</h1>
<p class="interesting">Unlock advanced tagging, subscriber groups, email verification, and priority support for your Mailchimp campaigns.</p>
<div class="cm-form">
<a href="<?php echo esc_url( $promo_url ); ?>" target="_blank" class="button cm-submit">Get PRO Now</a>
<span class="cm-pricing"><?php echo esc_html( $text ); ?></span>
</div>
</div>
</div>
<?php
}
private static function get_pricing_data(): array {
$fetcher = new CMatic_Remote_Fetcher(
array(
'url' => 'https://api.chimpmatic.com/promo',
'cache_key' => 'cmatic_pricing_data',
'cache_duration' => DAY_IN_SECONDS,
'fallback_data' => array(
'regular_price' => 39,
'sale_price' => 29.25,
'discount_percent' => 40,
'coupon_code' => 'NOW40',
'formatted' => '$39 → $29.25 • Save 40%',
),
)
);
return $fetcher->get_data();
}
private function __construct() {}
}

View File

@@ -0,0 +1,130 @@
<?php
/**
* Tags preview panel for Pro feature showcase.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Tags_Preview {
private static $type_abbrev = array(
'checkbox' => 'CHK',
'radio' => 'RAD',
'select' => 'SEL',
'text' => 'TXT',
'hidden' => 'HID',
'dynamictext' => 'DYN',
'dynamichidden' => 'DYN',
'tel' => 'TEL',
'number' => 'NUM',
);
private static $allowed_types = array(
'checkbox',
'radio',
'select',
'text',
'hidden',
'dynamictext',
'dynamichidden',
'tel',
'number',
);
public static function render( array $form_tags, array $cf7_mch, int $api_valid ): void {
if ( empty( $form_tags ) || ! is_array( $form_tags ) ) {
return;
}
$filtered_tags = array_filter(
$form_tags,
function ( $tag ) {
$basetype = is_array( $tag ) ? ( $tag['basetype'] ?? '' ) : ( $tag->basetype ?? '' );
return in_array( $basetype, self::$allowed_types, true );
}
);
if ( empty( $filtered_tags ) ) {
return;
}
$disclosure_class = ( 1 === $api_valid ) ? 'spt-response-out spt-valid' : 'spt-response-out chmp-inactive';
$name_list = self::get_audience_name( $cf7_mch );
?>
<div class="<?php echo esc_attr( $disclosure_class ); ?>">
<div class="mce-custom-fields holder-img">
<h3 class="title cmatic-title-with-toggle">
<span>Tags for <span class="audience-name"><?php echo esc_html( $name_list ); ?></span></span>
<label class="cmatic-toggle-row">
<span class="cmatic-toggle-label">Sync Tags</span>
<a href="<?php echo esc_url( Cmatic_Pursuit::upgrade( 'sync_tags_help' ) ); ?>" target="_blank" class="cmatic-help-icon" title="Learn about Sync Tags">?</a>
<span class="cmatic-toggle">
<input type="checkbox" data-field="sync_tags" value="1"<?php echo ! empty( $cf7_mch['sync_tags'] ) ? ' checked' : ''; ?>>
<span class="cmatic-toggle-slider"></span>
</span>
</label>
</h3>
<p>You can add these as your contacts tags:</p>
<div id="chm_panel_camposformatags">
<?php self::render_tag_chips( $filtered_tags, $cf7_mch ); ?>
<label class="atags"><b>Arbitrary Tags Here:</b> <input type="text" id="wpcf7-mailchimp-labeltags_cm-tag" name="wpcf7-mailchimp[labeltags_cm-tag]" value="<?php echo isset( $cf7_mch['labeltags_cm-tag'] ) ? esc_attr( $cf7_mch['labeltags_cm-tag'] ) : ''; ?>" placeholder="comma, separated, texts, or [mail-tags]">
<p class="description">You can type in your tags here. Comma separated text or [mail-tags]</p>
</label>
</div>
<a class="lin-to-pro" href="<?php echo esc_url( Cmatic_Pursuit::upgrade( 'tags_link' ) ); ?>" target="_blank" title="ChimpMatic Pro Options"><span>PRO Feature <span>Learn More...</span></span></a>
</div>
</div>
<?php
}
private static function render_tag_chips( array $tags, array $cf7_mch ): void {
echo '<div class="cmatic-tags-grid">';
$i = 1;
foreach ( $tags as $tag ) {
$tag_name = is_array( $tag ) ? ( $tag['name'] ?? null ) : ( $tag->name ?? null );
$tag_basetype = is_array( $tag ) ? ( $tag['basetype'] ?? null ) : ( $tag->basetype ?? null );
if ( empty( $tag_name ) || empty( $tag_basetype ) ) {
continue;
}
$is_checked = isset( $cf7_mch['labeltags'][ $tag_name ] );
$type_short = self::$type_abbrev[ $tag_basetype ] ?? strtoupper( substr( $tag_basetype, 0, 3 ) );
$selected_class = $is_checked ? ' selected' : '';
?>
<label class="cmatic-tag-chip<?php echo esc_attr( $selected_class ); ?>">
<input type="checkbox" id="wpcf7-mailchimp-labeltags-<?php echo esc_attr( $i ); ?>" name="wpcf7-mailchimp[labeltags][<?php echo esc_attr( trim( $tag_name ) ); ?>]" value="1"<?php echo $is_checked ? ' checked="checked"' : ''; ?> />
<span class="cmatic-tag-name">[<?php echo esc_html( $tag_name ); ?>]</span>
<span class="cmatic-tag-type"><?php echo esc_html( $type_short ); ?></span>
</label>
<?php
++$i;
}
echo '</div>';
}
private static function get_audience_name( array $cf7_mch ): string {
$arrlist = isset( $cf7_mch['lisdata']['lists'] ) ? array_column( $cf7_mch['lisdata']['lists'], 'name', 'id' ) : array();
$idlist = '';
if ( isset( $cf7_mch['list'] ) ) {
if ( is_array( $cf7_mch['list'] ) ) {
$idlist = reset( $cf7_mch['list'] );
if ( false === $idlist ) {
$idlist = '';
}
} else {
$idlist = $cf7_mch['list'];
}
}
return ( ! empty( $idlist ) && isset( $arrlist[ $idlist ] ) ) ? $arrlist[ $idlist ] : '';
}
private function __construct() {}
}

View File

@@ -0,0 +1,97 @@
<?php
/**
* Test submission modal component.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'Cmatic_Test_Submission_Modal' ) ) {
class Cmatic_Test_Submission_Modal extends Cmatic_Modal {
private $contact_form = null;
public function __construct() {
parent::__construct( 'cmatic-test-modal' );
}
public function init() {
if ( $this->initialized ) {
return;
}
add_action( 'wpcf7_admin_footer', array( $this, 'render_modal_with_form' ), 20, 1 );
$this->initialized = true;
}
public function render_modal_with_form( $post ) {
if ( ! $post || ! method_exists( $post, 'id' ) ) {
return;
}
$form_id = $post->id();
if ( ! $form_id ) {
return;
}
$this->contact_form = wpcf7_contact_form( $form_id );
if ( ! $this->contact_form ) {
return;
}
parent::render_modal();
}
protected function get_title() {
return __( 'Test Current Form Submission', 'chimpmatic-lite' );
}
protected function render_header_actions() {
?>
<button type="button" class="cmatic-modal__submit button button-primary">
<?php esc_html_e( 'Submit', 'chimpmatic-lite' ); ?>
</button>
<?php
}
protected function get_body() {
if ( ! $this->contact_form ) {
return '<p>' . esc_html__( 'No form available.', 'chimpmatic-lite' ) . '</p>';
}
ob_start();
?>
<div class="cmatic-modal__feedback" style="display: none;">
<div class="cmatic-modal__feedback-icon"></div>
<div class="cmatic-modal__feedback-content">
<div class="cmatic-modal__feedback-title"></div>
<div class="cmatic-modal__feedback-details"></div>
</div>
</div>
<div class="cmatic-test-form-wrap">
<?php
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->contact_form->form_html( array( 'html_class' => 'cmatic-test-form' ) );
?>
</div>
<?php
return ob_get_clean();
}
protected function get_strings() {
return array_merge(
parent::get_strings(),
array(
'submit' => __( 'Submit', 'chimpmatic-lite' ),
'submitting' => __( 'Submitting...', 'chimpmatic-lite' ),
'success' => __( 'Success!', 'chimpmatic-lite' ),
'error' => __( 'Error', 'chimpmatic-lite' ),
)
);
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* Asset cache busting utility.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Buster {
private string $plugin_version;
private bool $is_debug;
private static ?self $instance = null;
public function __construct( string $plugin_version = SPARTAN_MCE_VERSION, ?bool $is_debug = null ) {
$this->plugin_version = $plugin_version;
$this->is_debug = $is_debug ?? ( defined( 'WP_DEBUG' ) && WP_DEBUG );
}
public function get_version( string $file_path ): string {
$version_parts = array( $this->plugin_version );
if ( file_exists( $file_path ) ) {
$version_parts[] = (string) filemtime( $file_path );
$version_parts[] = substr( md5_file( $file_path ), 0, 8 );
}
if ( $this->is_debug ) {
$version_parts[] = (string) time();
}
return implode( '-', $version_parts );
}
public static function instance(): self {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
}

View File

@@ -0,0 +1,97 @@
<?php
/**
* Debug file logger.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_File_Logger implements Cmatic_Logger_Interface {
private $is_write_enabled = false;
private $log_prefix;
public function __construct( $context = 'ChimpMatic', $enabled = false ) {
$this->is_write_enabled = (bool) $enabled && ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG );
$this->log_prefix = '[' . sanitize_key( $context ) . ']';
}
public function log( string $level, string $message, $context = null ): void {
if ( ! $this->is_write_enabled ) {
return;
}
$level_str = strtoupper( $level );
$log_message = "[ChimpMatic Lite] {$this->log_prefix} [{$level_str}] " . trim( $message );
if ( ! is_null( $context ) ) {
$context_string = $this->format_data( $context );
$log_message .= ' | Data: ' . $context_string;
}
error_log( $log_message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
}
private function format_data( $data ) {
if ( is_string( $data ) ) {
return $data;
}
if ( ! is_array( $data ) ) {
return wp_json_encode( $data, JSON_UNESCAPED_SLASHES );
}
if ( isset( $data['email_address'] ) && isset( $data['status'] ) ) {
$summary = array(
'email' => $data['email_address'] ?? '',
'status' => $data['status'] ?? '',
'id' => $data['id'] ?? '',
);
return wp_json_encode( $summary, JSON_UNESCAPED_SLASHES );
}
if ( isset( $data['url'] ) && isset( $data['payload'] ) ) {
$merge_fields = $data['payload']['merge_fields'] ?? array();
if ( is_object( $merge_fields ) ) {
$merge_fields = (array) $merge_fields;
}
$summary = array(
'url' => $data['url'],
'email' => $data['payload']['email_address'] ?? '',
'status' => $data['payload']['status'] ?? '',
'fields' => is_array( $merge_fields ) ? array_keys( $merge_fields ) : array(),
);
return wp_json_encode( $summary, JSON_UNESCAPED_SLASHES );
}
$json = wp_json_encode( $data, JSON_UNESCAPED_SLASHES );
if ( strlen( $json ) <= 1000 ) {
return $json;
}
return substr( $json, 0, 1000 ) . '... [truncated]';
}
private function map_numeric_level_to_string( $numeric_level ) {
switch ( (int) $numeric_level ) {
case 1:
return 'INFO';
case 2:
return 'DEBUG';
case 3:
return 'WARNING';
case 4:
return 'ERROR';
case 5:
return 'CRITICAL';
default:
return 'UNKNOWN';
}
}
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* Lite field restrictions and limits.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class Cmatic_Lite_Get_Fields {
public static function cmatic_lite_fields() {
return array(
'source',
'ip_signup',
'subscribed',
'timestamp_signup',
'member_rating',
'location',
'email_client',
'vip',
'language',
'email_type',
'consents_to_one_to_one_messaging',
);
}
public static function cmatic_lite_sections() {
return array(
'tags',
'interests',
'marketing_permissions',
);
}
public static function cmatic_lite_merge_fields() {
return 6;
}
}

View File

@@ -0,0 +1,86 @@
<?php
/**
* URL tracking and link builder.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
final class Cmatic_Pursuit {
const PLUGIN_ID = 'chimpmatic_lite';
const BASE_URLS = array(
'docs' => 'https://chimpmatic.com',
'pricing' => 'https://chimpmatic.com/pricing',
'support' => 'https://chimpmatic.com/contact',
'promo' => 'https://chimpmatic.com/almost-there',
'home' => 'https://chimpmatic.com',
'author' => 'https://renzojohnson.com',
);
private function __construct() {}
public static function url( string $base_url, string $medium, string $content = '', string $campaign = '' ): string {
if ( empty( $base_url ) ) {
return '';
}
$params = array(
'utm_source' => self::PLUGIN_ID,
'utm_medium' => self::sanitize( $medium ),
'utm_campaign' => $campaign ? self::sanitize( $campaign ) : 'plugin_' . gmdate( 'Y' ),
);
if ( $content ) {
$params['utm_content'] = self::sanitize( $content );
}
return add_query_arg( $params, $base_url );
}
public static function docs( string $slug = '', string $content = '' ): string {
$base = self::BASE_URLS['docs'];
if ( $slug ) {
$base = trailingslashit( $base ) . ltrim( $slug, '/' );
}
return self::url( $base, 'plugin', $content, 'docs' );
}
public static function upgrade( string $content = '' ): string {
return self::url( self::BASE_URLS['pricing'], 'plugin', $content, 'upgrade' );
}
public static function support( string $content = '' ): string {
return self::url( self::BASE_URLS['support'], 'plugin', $content, 'support' );
}
public static function promo( string $content = '', int $discount = 40 ): string {
$params = array(
'u_source' => self::PLUGIN_ID,
'u_medium' => 'banner',
'u_campaign' => 'promo_' . $discount . 'off',
);
if ( $content ) {
$params['u_content'] = self::sanitize( $content );
}
return add_query_arg( $params, self::BASE_URLS['promo'] );
}
public static function home( string $content = '' ): string {
return self::url( self::BASE_URLS['home'], 'plugin', $content, 'brand' );
}
public static function author( string $content = '' ): string {
return self::url( self::BASE_URLS['author'], 'plugin', $content, 'author' );
}
public static function adminbar( string $destination, string $content = '' ): string {
$base = self::BASE_URLS[ $destination ] ?? self::BASE_URLS['home'];
return self::url( $base, 'adminbar', $content, $destination );
}
private static function sanitize( string $value ): string {
return preg_replace( '/[^a-z0-9_]/', '', str_replace( array( ' ', '-' ), '_', strtolower( trim( $value ) ) ) );
}
}

View File

@@ -0,0 +1,251 @@
<?php
/**
* Remote data fetcher with caching.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
class CMatic_Remote_Fetcher {
private $config;
private $defaults = array(
'url' => '',
'cache_key' => 'cmatic_remote_data',
'cache_duration' => DAY_IN_SECONDS,
'retry_interval' => 600, // 10 minutes in seconds
'max_retries' => 3,
'retry_count_key' => 'cmatic_retry_count',
'cron_hook' => 'cmatic_fetch_retry',
'timeout' => 15,
'fallback_data' => array(),
'parser_callback' => null,
);
public function __construct( $config = array() ) {
$this->config = wp_parse_args( $config, $this->defaults );
add_action( $this->config['cron_hook'], array( $this, 'cron_retry_fetch' ) );
}
public function get_data() {
$cached_data = $this->get_cache();
if ( false !== $cached_data ) {
return $cached_data;
}
$fresh_data = $this->fetch_fresh_data();
if ( false !== $fresh_data ) {
$this->set_cache( $fresh_data );
$this->clear_retry_schedule();
return $fresh_data;
}
$this->schedule_retry();
return $this->get_fallback_data();
}
public function get_cache() {
return get_transient( $this->config['cache_key'] );
}
public function set_cache( $data ) {
return set_transient(
$this->config['cache_key'],
$data,
$this->config['cache_duration']
);
}
public function clear_cache() {
return delete_transient( $this->config['cache_key'] );
}
private function fetch_fresh_data() {
if ( empty( $this->config['url'] ) ) {
return false;
}
$response = wp_remote_get(
$this->config['url'],
array(
'timeout' => $this->config['timeout'],
'user-agent' => 'WordPress/' . get_bloginfo( 'version' ) . '; ' . home_url(),
)
);
if ( is_wp_error( $response ) ) {
return false;
}
$response_code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $response_code ) {
return false;
}
$body = wp_remote_retrieve_body( $response );
if ( empty( $body ) ) {
return false;
}
return $this->parse_content( $body );
}
/** Parse fetched content. */
private function parse_content( $content ) {
if ( is_callable( $this->config['parser_callback'] ) ) {
return call_user_func( $this->config['parser_callback'], $content );
}
$json_data = $this->parse_pricing_json( $content );
if ( false !== $json_data ) {
return $json_data;
}
return $this->parse_pricing_html( $content );
}
private function parse_pricing_json( $content ) {
$json = json_decode( $content, true );
if ( null === $json || ! is_array( $json ) ) {
return false;
}
if ( ! isset( $json['regular_price'] ) || ! isset( $json['discount_percent'] ) ) {
return false;
}
$pricing_data = array(
'regular_price' => (int) $json['regular_price'],
'sale_price' => isset( $json['sale_price'] ) ? (float) $json['sale_price'] : null,
'discount_percent' => (int) $json['discount_percent'],
'coupon_code' => isset( $json['coupon_code'] ) ? sanitize_text_field( $json['coupon_code'] ) : null,
'last_updated' => current_time( 'mysql' ),
);
if ( null === $pricing_data['sale_price'] ) {
$discount_amount = $pricing_data['regular_price'] * ( $pricing_data['discount_percent'] / 100 );
$pricing_data['sale_price'] = $pricing_data['regular_price'] - $discount_amount;
}
if ( null === $pricing_data['coupon_code'] ) {
$pricing_data['coupon_code'] = 'NOW' . $pricing_data['discount_percent'];
}
$pricing_data['formatted'] = sprintf(
'$%d → $%s • Save %d%%',
$pricing_data['regular_price'],
number_format( $pricing_data['sale_price'], 2 ),
$pricing_data['discount_percent']
);
return $pricing_data;
}
private function parse_pricing_html( $html ) {
$pricing_data = array(
'regular_price' => null,
'sale_price' => null,
'discount_percent' => null,
'coupon_code' => null,
'formatted' => null,
'last_updated' => current_time( 'mysql' ),
);
if ( preg_match( '/Single\s+Site[^$]*\$(\d+)\/year/i', $html, $matches ) ) {
$pricing_data['regular_price'] = (int) $matches[1];
}
if ( preg_match( '/(\d+)%\s+Off/i', $html, $matches ) ) {
$pricing_data['discount_percent'] = (int) $matches[1];
}
if ( preg_match( '/coupon\s+code\s+["\']([A-Z0-9]+)["\']/i', $html, $matches ) ) {
$pricing_data['coupon_code'] = sanitize_text_field( $matches[1] );
}
if ( $pricing_data['regular_price'] && $pricing_data['discount_percent'] ) {
$discount_amount = $pricing_data['regular_price'] * ( $pricing_data['discount_percent'] / 100 );
$pricing_data['sale_price'] = $pricing_data['regular_price'] - $discount_amount;
}
if ( $pricing_data['regular_price'] && $pricing_data['sale_price'] && $pricing_data['discount_percent'] ) {
$pricing_data['formatted'] = sprintf(
'$%d → $%s • Save %d%%',
$pricing_data['regular_price'],
number_format( $pricing_data['sale_price'], 2 ),
$pricing_data['discount_percent']
);
}
if ( null === $pricing_data['regular_price'] ) {
return false;
}
return $pricing_data;
}
private function get_fallback_data() {
if ( ! empty( $this->config['fallback_data'] ) ) {
return $this->config['fallback_data'];
}
return array(
'regular_price' => 39,
'sale_price' => 29.25,
'discount_percent' => 25,
'coupon_code' => 'NOW25',
'formatted' => '$39 → $29.25 • Save 25%',
'last_updated' => null,
);
}
private function schedule_retry() {
$retry_count = (int) get_option( $this->config['retry_count_key'], 0 );
if ( $retry_count >= $this->config['max_retries'] ) {
return;
}
update_option( $this->config['retry_count_key'], $retry_count + 1 );
if ( ! wp_next_scheduled( $this->config['cron_hook'] ) ) {
wp_schedule_single_event(
time() + $this->config['retry_interval'],
$this->config['cron_hook']
);
}
}
public function cron_retry_fetch() {
$fresh_data = $this->fetch_fresh_data();
if ( false !== $fresh_data ) {
$this->set_cache( $fresh_data );
$this->clear_retry_schedule();
} else {
$this->schedule_retry();
}
}
private function clear_retry_schedule() {
$timestamp = wp_next_scheduled( $this->config['cron_hook'] );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, $this->config['cron_hook'] );
}
delete_option( $this->config['retry_count_key'] );
}
public function clear_all() {
$this->clear_cache();
$this->clear_retry_schedule();
}
}

View File

@@ -0,0 +1,89 @@
<?php
/**
* Utility functions for ChimpMatic.
*
* @package contact-form-7-mailchimp-extension
* @author renzo.johnson@gmail.com
* @copyright 2014-2026 https://renzojohnson.com
* @license GPL-3.0+
*/
defined( 'ABSPATH' ) || exit;
final class Cmatic_Utils {
const CMATIC_FB_A = 'chimpmatic';
public static function validate_bool( $value ): bool {
if ( is_bool( $value ) ) {
return $value;
}
if ( is_int( $value ) || is_float( $value ) ) {
return (bool) $value;
}
if ( is_string( $value ) ) {
$v = strtolower( trim( $value ) );
if ( '' === $v ) {
return false;
}
if ( is_numeric( $v ) ) {
return 0.0 !== (float) $v;
}
static $true = array( 'true', 'on', 'yes', 'y', '1' );
static $false = array( 'false', 'off', 'no', 'n', '0' );
if ( in_array( $v, $true, true ) ) {
return true;
}
if ( in_array( $v, $false, true ) ) {
return false;
}
}
return false;
}
public static function get_first( $array, $default = '' ) {
if ( is_array( $array ) && ! empty( $array ) ) {
return reset( $array );
}
return $default;
}
public static function get_newest_form_id(): ?int {
$forms = get_posts(
array(
'post_type' => 'wpcf7_contact_form',
'posts_per_page' => 1,
'orderby' => 'ID',
'order' => 'DESC',
'post_status' => 'publish',
'fields' => 'ids',
)
);
return ! empty( $forms ) ? (int) $forms[0] : null;
}
public static function get_days_since( int $timestamp ): int {
$datetime_now = new \DateTime( 'now' );
$datetime_from = new \DateTime( '@' . $timestamp );
$diff = date_diff( $datetime_now, $datetime_from );
return (int) $diff->format( '%a' );
}
private function __construct() {}
private function __clone() {}
public function __wakeup() {
throw new \Exception( 'Cannot unserialize singleton' );
}
}