<?php
/**
* Mailchimp Marketing API v3 integration.
*
* @package TonItaliaRegistration
*/
defined( 'ABSPATH' ) || exit;
/**
* Class TON_Reg_Mailchimp
*/
class TON_Reg_Mailchimp {
const CRON_HOOK = 'ton_reg_mailchimp_sync';
/**
* @return bool
*/
public static function is_configured() {
return '1' === get_option( 'ton_reg_mailchimp_enabled', '0' )
&& '' !== self::get_api_key()
&& '' !== self::get_list_id();
}
/**
* @return string
*/
public static function get_api_key() {
return (string) get_option( 'ton_reg_mailchimp_api_key', '' );
}
/**
* @return string
*/
public static function get_list_id() {
$list_id = (string) get_option( 'ton_reg_mailchimp_list_id', '' );
if ( ! self::is_valid_list_id( $list_id ) ) {
return '';
}
return $list_id;
}
/**
* @param string $list_id List ID.
* @return bool
*/
public static function is_valid_list_id( $list_id ) {
return (bool) preg_match( '/^[a-f0-9]{8,12}$/i', (string) $list_id );
}
/**
* @param int $registration_id Registration ID.
*/
public static function schedule_sync( $registration_id ) {
$registration_id = (int) $registration_id;
if ( $registration_id <= 0 ) {
return;
}
TON_Reg_Database::update(
$registration_id,
array(
'mailchimp_status' => 'pending',
'mailchimp_error' => null,
)
);
wp_schedule_single_event( time(), self::CRON_HOOK, array( $registration_id ) );
}
/**
* @param int $registration_id Registration ID.
* @return bool True on success.
*/
public static function sync_registration( $registration_id ) {
$registration_id = (int) $registration_id;
$row = TON_Reg_Database::get( $registration_id );
if ( ! $row ) {
return false;
}
if ( empty( $row->consenso_newsletter ) ) {
self::record_status( $registration_id, 'skipped', '' );
return true;
}
if ( ! self::is_configured() ) {
self::record_status( $registration_id, 'skipped', '' );
return true;
}
$email = sanitize_email( $row->email );
if ( ! is_email( $email ) ) {
self::record_status( $registration_id, 'error', __( 'Email non valida per Mailchimp.', 'ton-italia-registration' ) );
return false;
}
$subscriber_hash = self::subscriber_hash( $email );
$list_id = self::get_list_id();
$merge_fields = array(
'FNAME' => self::truncate_merge_field( $row->nome ?? '' ),
'LNAME' => self::truncate_merge_field( $row->cognome ?? '' ),
);
$body = array(
'email_address' => $email,
'status_if_new' => 'subscribed',
'merge_fields' => $merge_fields,
);
$permissions = self::get_marketing_permissions_payload();
if ( ! empty( $permissions ) ) {
$body['marketing_permissions'] = $permissions;
}
$put = self::api_request(
'PUT',
'/lists/' . $list_id . '/members/' . $subscriber_hash,
$body
);
if ( is_wp_error( $put ) ) {
self::record_status( $registration_id, 'error', $put->get_error_message() );
self::debug_log( 'Mailchimp PUT failed for registration ' . $registration_id );
return false;
}
$member_status = isset( $put['status'] ) ? (string) $put['status'] : '';
if ( 'cleaned' === $member_status ) {
self::record_status(
$registration_id,
'error',
__( 'Contatto in stato cleaned in Mailchimp — gestione manuale richiesta.', 'ton-italia-registration' )
);
return false;
}
if ( 'unsubscribed' === $member_status ) {
$patch_body = array( 'status' => 'subscribed' );
if ( ! empty( $permissions ) ) {
$patch_body['marketing_permissions'] = $permissions;
}
$patch = self::api_request(
'PATCH',
'/lists/' . $list_id . '/members/' . $subscriber_hash,
$patch_body
);
if ( is_wp_error( $patch ) ) {
self::record_status( $registration_id, 'error', $patch->get_error_message() );
self::debug_log( 'Mailchimp PATCH failed for registration ' . $registration_id );
return false;
}
$member_status = isset( $patch['status'] ) ? (string) $patch['status'] : $member_status;
}
$tags = self::parse_tags();
if ( ! empty( $tags ) ) {
$tag_result = self::api_request(
'POST',
'/lists/' . $list_id . '/members/' . $subscriber_hash . '/tags',
array(
'tags' => array_map(
static function ( $name ) {
return array(
'name' => $name,
'status' => 'active',
);
},
$tags
),
)
);
if ( is_wp_error( $tag_result ) ) {
self::record_status( $registration_id, 'error', $tag_result->get_error_message() );
self::debug_log( 'Mailchimp tags failed for registration ' . $registration_id );
return false;
}
}
if ( 'subscribed' !== $member_status && 'pending' !== $member_status ) {
self::record_status(
$registration_id,
'error',
sprintf(
/* translators: %s: Mailchimp member status */
__( 'Stato Mailchimp inatteso: %s', 'ton-italia-registration' ),
$member_status
)
);
return false;
}
self::record_status( $registration_id, 'synced', '' );
return true;
}
/**
* Test API credentials and list access; cache marketing permission IDs.
*
* @param string $api_key_override Optional API key from unsaved form.
* @param string $list_id_override Optional list ID from unsaved form.
* @return true|WP_Error
*/
public static function test_connection( $api_key_override = '', $list_id_override = '' ) {
$api_key = '' !== (string) $api_key_override ? (string) $api_key_override : self::get_api_key();
$list_id = '' !== (string) $list_id_override ? (string) $list_id_override : (string) get_option( 'ton_reg_mailchimp_list_id', '' );
if ( '' !== (string) $api_key_override && ! preg_match( '/^.+-[a-z]{2}\d+$/i', (string) $api_key_override ) ) {
return new WP_Error( 'mailchimp', __( 'Formato API key Mailchimp non valido (atteso: chiave-us12).', 'ton-italia-registration' ) );
}
if ( '' !== (string) $api_key && ! preg_match( '/^.+-[a-z]{2}\d+$/i', (string) $api_key ) ) {
return new WP_Error( 'mailchimp', __( 'Formato API key Mailchimp non valido (atteso: chiave-us12).', 'ton-italia-registration' ) );
}
if ( '' === $api_key ) {
return new WP_Error( 'mailchimp', __( 'API key Mailchimp non configurata.', 'ton-italia-registration' ) );
}
if ( ! self::is_valid_list_id( $list_id ) ) {
return new WP_Error( 'mailchimp', __( 'List ID Mailchimp non valido.', 'ton-italia-registration' ) );
}
$ping = self::api_request_with_key( 'GET', '/ping', null, $api_key );
if ( is_wp_error( $ping ) ) {
return $ping;
}
$list = self::api_request_with_key( 'GET', '/lists/' . $list_id, null, $api_key );
if ( is_wp_error( $list ) ) {
return $list;
}
$permission_ids = self::extract_permission_ids_from_list( $list );
update_option( 'ton_reg_mailchimp_permission_ids', wp_json_encode( $permission_ids ), false );
return true;
}
/**
* Keep API key out of autoloaded options snapshot.
*/
public static function ensure_api_key_no_autoload() {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->update(
$wpdb->options,
array( 'autoload' => 'no' ),
array( 'option_name' => 'ton_reg_mailchimp_api_key' ),
array( '%s' ),
array( '%s' )
);
}
/**
* @param string $email Email.
* @return string
*/
public static function subscriber_hash( $email ) {
return md5( strtolower( trim( (string) $email ) ) );
}
/**
* @param string $value Merge field value.
* @return string
*/
private static function truncate_merge_field( $value ) {
$value = sanitize_text_field( (string) $value );
if ( function_exists( 'mb_substr' ) ) {
return mb_substr( $value, 0, 100 );
}
return substr( $value, 0, 100 );
}
/**
* @return array<int,array<string,mixed>>
*/
private static function get_marketing_permissions_payload() {
$raw = (string) get_option( 'ton_reg_mailchimp_permission_ids', '[]' );
$ids = json_decode( $raw, true );
if ( ! is_array( $ids ) || empty( $ids ) ) {
return array();
}
$payload = array();
foreach ( $ids as $id ) {
$id = sanitize_text_field( (string) $id );
if ( '' !== $id ) {
$payload[] = array(
'marketing_permission_id' => $id,
'enabled' => true,
);
}
}
return $payload;
}
/**
* @return array<int,string>
*/
private static function parse_tags() {
$raw = (string) get_option( 'ton_reg_mailchimp_tags', 'Socio TON ITALIA' );
$tags = array_map( 'trim', explode( ',', $raw ) );
$tags = array_filter(
array_map( 'sanitize_text_field', $tags ),
static function ( $tag ) {
return '' !== $tag;
}
);
return array_slice( array_values( $tags ), 0, 5 );
}
/**
* @param array<string,mixed> $list List API response.
* @return array<int,string>
*/
private static function extract_permission_ids_from_list( $list ) {
$ids = array();
if ( ! empty( $list['marketing_permissions'] ) && is_array( $list['marketing_permissions'] ) ) {
foreach ( $list['marketing_permissions'] as $permission ) {
if ( ! empty( $permission['marketing_permission_id'] ) ) {
$ids[] = sanitize_text_field( (string) $permission['marketing_permission_id'] );
}
}
}
return array_values( array_unique( $ids ) );
}
/**
* @param string $method HTTP method.
* @param string $path API path starting with /.
* @param array<string,mixed>|null $body Request body.
* @return array<string,mixed>|WP_Error
*/
private static function api_request( $method, $path, $body = null ) {
return self::api_request_with_key( $method, $path, $body, self::get_api_key() );
}
/**
* @param string $method HTTP method.
* @param string $path API path starting with /.
* @param array<string,mixed>|null $body Request body.
* @param string $api_key API key.
* @return array<string,mixed>|WP_Error
*/
private static function api_request_with_key( $method, $path, $body, $api_key ) {
$dc = self::extract_datacenter( $api_key );
if ( ! $dc ) {
return new WP_Error( 'mailchimp', __( 'API key Mailchimp non valida.', 'ton-italia-registration' ) );
}
$url = 'https://' . $dc . '.api.mailchimp.com/3.0' . $path;
$args = array(
'method' => strtoupper( $method ),
'timeout' => 10,
'headers' => array(
'Authorization' => 'Basic ' . base64_encode( 'ton_reg:' . $api_key ),
'Content-Type' => 'application/json',
),
);
if ( null !== $body ) {
$args['body'] = wp_json_encode( $body );
}
$response = wp_remote_request( $url, $args );
if ( is_wp_error( $response ) ) {
return new WP_Error( 'mailchimp', __( 'Impossibile contattare Mailchimp.', 'ton-italia-registration' ) );
}
$code = (int) wp_remote_retrieve_response_code( $response );
$raw = wp_remote_retrieve_body( $response );
$data = json_decode( $raw, true );
if ( $code < 200 || $code >= 300 ) {
return new WP_Error( 'mailchimp', self::parse_api_error( $data, $code ) );
}
return is_array( $data ) ? $data : array();
}
/**
* @param mixed $data Response JSON.
* @param int $code HTTP code.
* @return string
*/
private static function parse_api_error( $data, $code ) {
$message = sprintf(
/* translators: %d: HTTP status code */
__( 'Errore Mailchimp (HTTP %d).', 'ton-italia-registration' ),
$code
);
if ( is_array( $data ) ) {
if ( ! empty( $data['detail'] ) && is_string( $data['detail'] ) ) {
$message = sanitize_text_field( $data['detail'] );
} elseif ( ! empty( $data['title'] ) && is_string( $data['title'] ) ) {
$message = sanitize_text_field( $data['title'] );
}
}
if ( function_exists( 'mb_substr' ) ) {
return mb_substr( $message, 0, 255 );
}
return substr( $message, 0, 255 );
}
/**
* @param string $api_key API key.
* @return string Empty if invalid.
*/
private static function extract_datacenter( $api_key ) {
if ( ! preg_match( '/-([a-z]{2}\d+)$/', (string) $api_key, $matches ) ) {
return '';
}
return $matches[1];
}
/**
* @param int $registration_id Registration ID.
* @param string $status Status slug.
* @param string $error Error message.
*/
private static function record_status( $registration_id, $status, $error ) {
$data = array(
'mailchimp_status' => sanitize_text_field( $status ),
'mailchimp_error' => '' !== $error ? sanitize_text_field( $error ) : null,
);
if ( 'synced' === $status ) {
$data['mailchimp_synced_at'] = current_time( 'mysql', true );
}
TON_Reg_Database::update( $registration_id, $data );
}
/**
* @param string $message Log message without PII.
*/
private static function debug_log( $message ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log( '[ton-italia-registration] ' . $message );
}
}
/**
* @param mixed $value Submitted API key.
* @return string
*/
public static function sanitize_api_key( $value ) {
$value = sanitize_text_field( wp_unslash( (string) $value ) );
$old = (string) get_option( 'ton_reg_mailchimp_api_key', '' );
if ( '' === $value ) {
return $old;
}
if ( ! preg_match( '/^.+-[a-z]{2}\d+$/i', $value ) ) {
add_settings_error(
'ton_reg_mailchimp_api_key',
'invalid_mailchimp_api_key',
__( 'Formato API key Mailchimp non valido.', 'ton-italia-registration' ),
'error'
);
return $old;
}
return $value;
}
/**
* @param mixed $value Submitted list ID.
* @return string
*/
public static function sanitize_list_id( $value ) {
$value = sanitize_text_field( wp_unslash( (string) $value ) );
if ( '' === $value || self::is_valid_list_id( $value ) ) {
return $value;
}
add_settings_error(
'ton_reg_mailchimp_list_id',
'invalid_mailchimp_list_id',
__( 'List ID Mailchimp non valido.', 'ton-italia-registration' ),
'error'
);
return (string) get_option( 'ton_reg_mailchimp_list_id', '' );
}
}