ï»¿Ã¿Ã˜Ã¿Ã  JFIF      Ã¿Ã› â€ž 	 ( %!1"%)+...383-7(-.+



-%%--------+------------/----------------------------Ã¿Ã€  Â¶" Ã¿Ã„               Ã¿Ã„ @       !1AQaqÂ�â€˜Â¡"Â±Ã�Ã‘Ã°2â€™BRrÃ¡Sâ€šÃ±3bÂ¢CTÂ²Ã’ #cÃ¿Ã„             Ã¿Ã„ '        1!AQ2a"â€˜Â±#Ã¿Ãš   ? Ã²(/pâ€˜â€”â€“Â¢Â·,M|Â¯GÂ»RÂ£Ã¼eÅ¡T. Ã´Å¸EoCFÃ±â€¡ 4Ã”kÃ¥Ã³xR|Â�Â�ÂµÃ�Â¨Ã“DÃ !Ã‡Â´ÂµËœâ€¡p Ã�Qâ€¹Â§!Ã¾Å¾^ÃµWsÅ’y Â Ã¥Ã�Â§$Ã¡Â«%ÂºqËœâ„¢JyyÃ‹0yÂ�Â«^Ã�Â�
-3ÃƒÃ�Ã‹ÃˆÂ£Ã†\Pâ€™Ã¤â€žs&â€¢â€¹Ãœs Ã¼Ã´'Ã WtÂ¹pÃ±Cp$bÃšF`Â·7.^)Â¾!)Ã’Ã™Ëœâ€“Ã…Ã�Å¡^hÂ¾Ã¸Â³Â·Ã¬eÂ±Å’aeÃ–â€”Ã�,e=B}JÃ©ÃŸÂ¦$$Â½â€”Å¡Ã™ÃœCCÂ¯Â¶Ã¨8ÃŽÂ²â€“Ã…kÃ¨wx
LÃ‰ngTÂ±Ã¦Â¹Â£Ã“cÅ“Â¿Ã�/Â©Ã‰Ã¿ â€°ÃšCÃ’Â­mq;TtÂ¶â€“}0"zÃ–Pcd1Å¡Â­Â¤tË†â€žÃ�Ã“Ã¢qÃº[â„¢Ã¶UCÅ 2ÃžÂ¿Ã˜Â¯Â­Ã‰(Â¸ÃŸÃºÃ§Å¡mW,Ã’ Âºfâ€¹Ã…nâ„¢Ã„LÃªÃƒÂ�8Ã¼Ã�kÃ�â‚¬LÂ¥Ã�uJ1Å“Ã´rÃ‚SÅ’Ã“Ã¶tÂ�!7EUÃ­Ã¯b)Â�AÃ‹iÂ¬Ã¢-Ã™Hk8Ã²Ã�iXYwÃ…)OÂ�^"Ã‡Ã˜riÃ›Ã´{Â»wRUKÃ™Ã�ÃšlÃ“Â¬â‚¬â€šâ€™Ã—Â©T]
Âªbâ€”ÃŠÃ®KC^Ã—Mâ€œxÃ¹Ã¤Ã±zÂ¬zJÃ—{4[Â¹M'IÃ†eH1LCWÃ•]Å“Ã»ÂºÂ WDÃ¦Ã Â°OÃ…â€šÂ¹cÂ²ÃžuAâ€“rL`HÅ¡pCezâ€¡Vâ€™â€”Â Å½â€šÃ‹â€žâ€“Ã±Â­'ÂµR0Ã•Ã–=Ã€:Â±GÂ»C\nÃ†Ã‰5*Â¢Â â€ Â�3Å¡hQ4Ã½Ã’i$Ã•1"Ãœ]Â¢p-q3zcQ$ÂªÃ¶Â¨Â¥Ã¦fâ€ºâ€˜Â»Â´Ã�ÃšHÃ¡â€žeÂ²JÃ‡â€“iÃŠ:Â·Ã Â¬Ã˜c5Ã…kÂ»LaÂª*wiÂ�
[0Ã“wk Â¦aÂ¦0Ã•Â²Ã…Ã…â€šTÂ¸â€™Â³q:Ã†<Â¹Ãº	Ã’ËœÃ†}2VEâ€¢Ã�â‚¬pÂ¡Ã”3ÃšÂº â€¦hÂ³^Ã‡â€¡ÂºÃ§Ã¬Â¥Ã�nÃ³|ËœÃ–Ë†ÃŽË†+Â«Â¦dÃ¸â€˜MÂºâ€“d\ÂªDÃ°Ã›â€žÃŒÃ—LÃ�â€”â€”Ã�9Ã‡|â€¢FÃ¨Ã§Vmâ„¢Â¨â‚¬â„¢3ÃµSâ€“9Ã„Ã¥Â¢<ÃŒâ€°ÃŒmÂ¢Â±`Â·ÂºËœ&ZÂ²Å¾ Ã‹Â®DÂ«Â¶Â»6â„¢â€°Â©Â­HÃˆÃ€Ã¡Â«hÂ­iÂ³04Ã¦â€™hxÅ“i3Ã¯Â¨(Ã“LÂ·â€ â€¹Â¶Ã‹pÅ’
Æ’ÃœÃ’?	:ÃµdsÃ€â€“3Ã¦Ã“Â·ÃœjÃ¢Â¬9â€”]8nÂ¼&]I FÂ£Â¼â€¦Ã¬â€“â€”Å’0vÂ°LÃ‡Â�Ã…fÃ›2TÃ–â€™aÂµËœN@Ã–BSÃ�^Ã Â´4-Â±Â°bÃ—nÃˆÃˆÅ“Ã†D=KdÅ’Ã¸â€¦Ã„ÃˆK,â€ºÃ–RÂ§fjÃ¼$^Ã”ÃŽ!â€ž6^ s:Ãœun\Ã´kCÃžÃ²Ã·<â€”LÃºl
Ã�â€ºGÂ¹Ã€PÃ¡Â¼NcÂ¡Å¡Ã’â€¦Ã™WÃ�Â³Å“Â±vÃŠÃ¡Ã�â€žÃ—9â€¹Pâ€°cEÃ˜â€˜â„¢â€ºÂµÅ¸P<Ã—g1
ÂºÂ©Ã˜Â¬wiÂ�Ã�^l5Ã—I/';Ã‹+Â´Zâ€¡Ã“Ã„FÃ•p[ÃˆÃ°â€™
HÃ„	Ã³
9Â¬Dl4â€œÃ©Â±Ã�â€�V=^HÃ°;Å¾LÃ¤dÃ‰aâ€šÂ°Ã‹,#/Å½~Â²BÃ”Ã›
QÃ£Ã±QtIeÃ³rVB,Ã�Ã‚fâ€¹4#ÃŽÅ“câ€¹Ã‰3ZIÃ–Â Å’Â¢Â¥Â·Ã¼xaâ€¡ÃƒÃ§Ã¨â€žjÅ [5 Ã„!U?,3Å¾ÃŽÃ—â€¦Ã°dÃ“Ã·hÃ¡Å Aâ€°Ã¬Â�Ã„4Ã·ÃƒÃœZÃƒE{â€°Â®+W\ZÃ�E[â€°Â»ÂµkÂ»MÃ�Â­`Â¢Â©â€ ËœÃƒVâ€¹-fÂ¢Â¡â€ Â¢aÂ«eÅ %Â«YÂ¨Â©q%jÃ¢d,Ã”q`) Â¤Â¦â€¦Å¡Ë† Â¦E>0Ã™bÂ²tâ€“Å¸Â�
Ã·jsÃ�â€šÃ’â€™KÃˆcÃ¸(Ã©â€º;k(â€™#Ã´Å’Q
â„¢Â�C>
ÃµÂ³M9Ã³ÂºÃ†ÂµÂ¤Ã§Ã¢;uÃ€Â�)5>RÃ”Â¸fÃ“~ÃˆÂ¦â€”â€™-$feÂ«"FÃ¢UË†ÃŠwÂµPÃ�u
w+"Ã¥Ã‰oÃŒÂ�Â¨6sâ„¢Ã›9Â�Â�(Ã€Â¨Ã¹Â©hEÂ°Â½â€ n`m~â€œ@Â©Â�â€ºp!VkhÂµÂ Ã�1Å¡bEqqÃŠ@ÂºÃ¨lâ€º2 â€“YÃ£%â€˜â€šhÃ›{aÂ¶â€¡Ã…ÃžMÂ²Â¬Ã§L34Â®Ã‡l]Â®â€¹Å½Ã˜Å’m'RÃœe35Ã¦Ã¶Ë†Â­Ã�Â­ cP9Å¡eÃ�kÃ¶oÂ´LÂ³ÃŒ=Â�p8]â€�â€¡BÂ¶,Å¡Âº|Ã‰Â�dzXË†Ã˜k*Ã‡Ãš{#Ã±â€°pÃ£(â‚¬Â¶â€¡9Ã½'Å¡ÃœÂ³Â½Â¯kÆ’â€ Ã‚â€™Ã«SOÆ’â€¢Ã…Â®H6+aÂ¢ÂµË†Â�â€¹YÂ¨bËœb+XË†Ã–!Â°u!Â¢"ÂµË†Â�Ë†n2Ë† Ã…0Ã„`Ã… Ã”Â®c(Â�Ã“ÃœGÂºâ€¢Ã„Â»Å’Â Ã¢WÃ®%q.Ã¡Ã�Ã„Ã·Ã®%umÃ�Â£+ÃœQ,VnÂ¦-GphUÂ¸Â¢XÂ­&,Gt
Â·KÂ¢Ã…Ã…Â·Â¥RÃ„ÃŠÃ‰bKn
NÂ­R2g$VÂµÃ•Ã³
Qr#â€“Ã“ZUÃ�Ã°0ÃŽtÃ™Ã€Å“z
Ã«Ã¶oâ€œÃŽkÂ¯nâ€žTÂ°"SÃž+Â³/â€¦â‚¬Â½"iÂ�Å¾Ã£O%Ã�%&ÃŽâ€�Ã¢â€˜Ã‹Ã˜Â´IË†Ã“Â°Ã‹â€°ÃžM<Jâ€¢â€ºC=Ã¬u*ÃœÂ§â€žÃƒÂ¤GÃ‹Å Ã´;5Â�Å’Ã�Â£Ã±Ã¹Ã¤Â¬6ÃŒ2Â¦â€˜Å¾hÂ¸RÂ�Âº<Ã•Å¡%Ã·Â â€¦Ã‡	â€šgâ€¡.
>3.â„¢yÃ§Ã�zitâ€º`4C`kÂ¢Â¼Ã¡!Ã¡lÃ„Ã¯ sÃ‚[Ã—!Â¤lÂ¾Ã¦o"Æ’Â�IÃ”'@Âªu)3â€ºÃ´Â®â€œ@Z
Ã Â¾]
Ã¼Å“'â€¡4Â¸PÃžkÃ�-qÃ„Hâ€°Ã“Ã�BdÂ´Ã¿ â€”Â¹Â°Ã„A&ÃŠUÂ�Câ€žÃ¶zâ€šâ€šRÃ¦Å tfÂ¬>ËœÃ‘idÅ¡EÃ§Ã‘LÃ·Ã¡Ã½Â¡VÂ±hÂ§Â­cÂ¿Â¥Â¨Æ’@gÂ¼â€¦Ã�Ã¶^ÃžÃ‹c.Ã…Â¾â€žÃ¦â€œIÂ�[Âª+Â·zÃ�Ã’Ãš,Â½Ã�Â»Â¶â‚¬Câ€º7jk$Ã Â¹â€“â‚¬Å½Â¾Ã…Ã™ppÂ¶Ã�Ã¾Ã©ÃŽÂ¤Â Â½Ã˜ÃŽÅ’q Ã±pÃ¦Å¾Ã…Â¥#Ã˜Ã¢4Â�9ÃƒÃ‚K^MYVâ€œ,Â¶Ã‰z_Ã²Ã¦:â€¦;Ã•Â«Â¦I3Ã¢JÂ£Â¤Ã»5$7ÃˆxÃ¼n\Ã¢]ÃŠeÂ¯(Å¸&Å½â€¹Â¶2<6Ã„â€ fÃ— Ã¦Â¢Ã†Â±qWÃ„Â±[(Ã™Ã‚Å½Ã§â€šT7Â¨@ÃˆÃ‰Ã�ÃŸâ€šÃ¯Ã™"&3TYlGÅ½Â�â€ )â€ #5Å AÂ©^T2Ã†Ã�â€ )Â©â€ Â©â€ Â¤yGXÃ�â€ Â©"â€ )Â©Â¼Ã…V WSâ€ #Â©]Râ€“râ€¹q5Ã•bÃ¢WÃ¾BhÂ¯u+ÂªÃ…Ã„Ã—Ã¼â€žnÃ‘^ÃªkÂªÃ�jâ€°jeÅ“WË†Ã•Ã•`ÂµDÂµ2ÃŒ+Ã„ ÂµDÂµÂµ5Ã”{Â¤Ãž0RFâ€™HÃ·AÃ›<ÃŠÃ�Â§ Ã†Ë†Ã¦0Ã‘Â£Ãª4 Ã¶ÃšÂµ[Â¬/1Â²â‚¬Ã¢<-Â¬Å¡Ã¶Â²rÃ—0oÃ—wÂ Â´Â�xÃ�Ã’Ã‹â€ž
g01Â¬â€žÃ¥EMÃ¨â€ºâ€¡Ãƒ\6ÂµEâ€ºâ€˜fÃ™O	kÃ·CÂºnÃ“1`iÃ®Ã’Â²Ã¡Ã‚!Ã‘LÃ„Ã¿ 
Â³Â¬Â�\Ãµ*:M=Ã‡Â»câ€¹2[â€°Ã˜sÃ•LÃ²\Å’KÂ L^â„¢Ã¼Ã„N\Â¶#Â»
Ã‡\â€¦â€¦ÃŸÃ¯IÅ“Ã‰$Â¸ÃŽÃ±Ã�Â®Ã”HÃƒâ€¡	Ã›Ã­/Â´>Liâ€�â€žâ€ g^$ 7NÃ�Ã‘Ã‘2M2ÂªNg=kÂ½Ã�}Å¸l6â€¡	â€”JWË†â€�ÃžÃ©TÂ·+ÂµÂ¦Ã¼Ã‚FÃ¨Â¢Vs6Ã½Ã†4Â»2+2IÃ�9Ã´9Â®Ã‡GÃ™Ã‡1+Ã„8;*Ã•Â§Å¾Â½D*Ã®Ã‘WÃšÃ§Ã¡3MaÂ¤Â·Ë†Â¥t}Å¡Â²Ãœâ€¦Q+Ã†Ã¼Âµ8HÃ­Sâ€�Ã¼xGÃ©Ã�Ë†Â¦Ã‹lâ€žÃ©Ãˆ	5 ÂµÃ„Æ’6Ã¤Ãœ(kIÅ¡Â¯Sâ€ fÃ–&Â¸Â®Ã—v|Ã„yË†*Ã§Â¦Å’Ã‰Ã¹@7-Â®Ã„[]ÃŽÃ–Â¿Ãª`Âºâ€¦=:â€¦Å¾JVnÃ�Â³vÃ°Ã–KEÂ¼â€š Â»mÃ  Ã·fÂ®:Ã‹â„¢â€“4Â�rÃ°Â¡4` ÃœÂ¹Ã§Å¾~â€¹C=Å“â€¦Â¾Ã�j|VDtK.Æ’"9	Ã�
Â­â€“Ã©XÃ¡Â³6WÃ’Ã¯IL-Ã�â€¡
Ã‡
uninstall.php                                                                                       0000644                 00000006526 15266052520 0007302 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Plugin uninstall handler.
 *
 * @package   contact-form-7-mailchimp-extension
 * @author    renzo.johnson@gmail.com
 * @copyright 2014-2026 https://renzojohnson.com
 * @license   GPL-3.0+
 */

defined( 'WP_UNINSTALL_PLUGIN' ) || exit;

delete_option( 'mce_loyalty' );
delete_option( 'chimpmatic-update' );
delete_option( 'cmatic_log_on' );
delete_option( 'cmatic_do_activation_redirect' );
delete_option( 'cmatic_news_retry_count' );
delete_option( 'csyncr_last_weekly_run' );

delete_option( 'cmatic' );

wp_clear_scheduled_hook( 'cmatic_daily_cron' );
wp_clear_scheduled_hook( 'csyncr_weekly_telemetry' );
wp_clear_scheduled_hook( 'csyncr_metrics_heartbeat' );

$cmatic_signls_product_hash = substr( hash( 'sha256', 'contact-form-7-mailchimp-extension' ), 0, 12 );
wp_clear_scheduled_hook( 'signls_sdk_v1_' . $cmatic_signls_product_hash . '_routine' );
wp_clear_scheduled_hook( 'signls_sdk_v1_' . $cmatic_signls_product_hash . '_refresh' );
delete_option( 'signls_sdk_v1_' . $cmatic_signls_product_hash );
delete_metadata( 'user', 0, 'cmatic_signls_consent_notice_dismissed', '', true );

global $wpdb;
foreach ( array( 'signls_signal_reason_counters', 'signls_signal_daily_counters', 'signls_signal_counters' ) as $cmatic_signls_table_suffix ) {
	$cmatic_signls_table = $wpdb->prefix . $cmatic_signls_table_suffix;
	if ( $cmatic_signls_table === $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $cmatic_signls_table ) ) ) ) {
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$cmatic_signls_table} WHERE product_hash=%s", $cmatic_signls_product_hash ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Closed SDK-owned table list and product-scoped prepared deletion.
	}
}
$cmatic_signls_other_products = (int) $wpdb->get_var(
	$wpdb->prepare(
		"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s",
		$wpdb->esc_like( 'signls_sdk_v1_' ) . '%'
	)
);
if ( 0 === $cmatic_signls_other_products ) {
	delete_option( 'signls_sdk_site_id' );
	delete_option( 'signls_sdk_site_origin' );
}
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", 'cf7_mch_%' ) );
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", 'cmatic_auth_%' ) );
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", 'cmatic_provider_auth_%' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required uninstall cleanup.
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", '_transient_cmatic_provider_backoff_%' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required uninstall cleanup.
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", '_transient_timeout_cmatic_provider_backoff_%' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required uninstall cleanup.
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", '_transient_cmatic_oauth_secret_%' ) );
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", '_transient_timeout_cmatic_oauth_secret_%' ) );
                                                                                                                                                                          assets/images/postal-stamp.png                                                                      0000644                 00000052546 15266052526 0012472 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       ‰PNG

   IHDR   ¢   C   êÞ<   tEXtSoftware Adobe ImageReadyqÉe<  xiTXtXML:com.adobe.xmp     <?xpacket begin="ï»¿" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.6-c145 79.163499, 2018/08/13-16:40:22        "> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <rdf:Description rdf:about="" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" xmlns:stRef="http://ns.adobe.com/xap/1.0/sType/ResourceRef#" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmpMM:OriginalDocumentID="1A9CB1296AB33DB2051E9B7666023352" xmpMM:DocumentID="xmp.did:E44B697ACBD911E9BF5497DE57795DBD" xmpMM:InstanceID="xmp.iid:E44B6979CBD911E9BF5497DE57795DBD" xmp:CreatorTool="Adobe Photoshop CC 2019 Macintosh"> <xmpMM:DerivedFrom stRef:instanceID="xmp.iid:55f40468-2d30-4a45-8a1c-15fa3c8b65ba" stRef:documentID="adobe:docid:photoshop:56df479b-2854-604f-af4b-70c1338cc609"/> </rdf:Description> </rdf:RDF> </x:xmpmeta> <?xpacket end="r"?>
«%é  Q„IDATxÚ”} $Wue½ªêêœ~þ“g4Ê( J( 	$–Á€aÉi½˜õ®	ÛØ+¢— k‚“Í‚A„0DP 	åÉ£I?wN•ÞžûÞ­êú=FÚ/õtwu…î»÷Üø„¼ñÓ)Cÿ…x	þ,¤”6¾Iaˆ@Ò›¡>É?·ñ���®ÂÇ»©ŽKu¾�ca|?œ]�ÏtÜäï’_&Ž[ÔA�W‡cô™#!ÎKSð¬Nôü‘?ºA _2y<yn¨ÛÍA}P/ú,Äª{	Ý^!£QXý¨ÑcÇü3}_£Éñ(c8NÉv‡|ÐjÈí´ôÜÞ7âû#x÷yNfñ½…ŸÛ|½ƒkÏÅµ|~¯.Ïû$WÃ�+êöÂ¨ãØÙ…ø¼À�£¹]ÐØ
Õ.5ßÙü»Áó)ôoêúèó„j³­Ç_”ƒÃ_òp§ðî'FG®.áãZó(`zS÷å[5CÒHãœ>šE÷Â‹ÅŠ:ÀD{<bÔD&Ô}Åñç]c¦5Ñª‚�z¾Ãì}½PÄÚÃr\ª:VƒxRÐ¹%Ã	¼gLSD{hUq¬†r#Ž=€÷ãû†TÊÞ�ß�–Œãü"îÑÄ+�ûL„a0ƒ÷ã}‹eY)îÉ–mÝm[Öã`göýgá>A*•RãˆsŽàõÜó.4eQ†ò<ocTöªq�FÁ †#«¡ù
™v
RˆÞ1†2Kt„ßz<vŠÉàßŒzr€Ï¡f< h¡Ç�¨V2!¥xÖ}ÅáI!ÝÎ5ÊeD¼D�"žÕ@-¾·^BM¬ä	žbÎØ¤§âYAbåØ|ù³Lídns˜¨­b„Dâ¸HöB(–‚çIybøÏÅD4èLÊ—™Ò‚òåêq G‘%Aãf}üVká±ØäšßÑ†ež<pÝçâk!Â™n·WÌå23™t§Ýîo£šÉdúív+ƒhü­t&múžoôû=#“ÉAa¼;“ÍžÛ7<ß7,Ó4Ð?L¨I}3@”]?ðr Æ=¹\î‹x6q¼%ÜÓäVÓœÔˆCâ·“05ô¼MDìX¾ÿ‰ãKôl¼ª Ú)õóˆžtñÛ@®âé²†qì¢ƒž'0MRÍ¡ØÌbýç’LXâ	¹Œž)„Xc„"ÒHÜê—Œä-^	¯ÎëãTj`¿»ºcŠ30yU¥žˆãÈX°ªÕGé¯ÁIÍ5&_jNhžç]4ÞT(^‰ã�ë~©öS›|¡å1µ…Ú™ÃUÓÌÉêø§6ÂW‰ÐãgL�˜ì,žù};eDòQ½0ÂómÛzz:í|œí^—ãœÇ@HÝPzïüðé`+à÷GÑÜ“¤tNO¥¬#Ë9Ôï»—·›­±tÆ©—Š¹ÿ Nû¾¨àZ}�*òßµìÜÍ¹½8ÖÐm+,.q[±SIilÃk÷çb>dï[ÐðÎÙƒ×^ô¨ÀŒóh#3¤‡ófqüü~ ×ÏKM¸ XÃÒTA_„7Þ’ÒòI� ™¼ÂA‡Wá¸01È""¾N0˜sÑÞ:%Ñª*ÀëPÌ¡5nqâ1"L“¯‰'�Û2!ÚLˆrˆe°—L¤{Ó˜”EÍãu™áKk‹Ôµ¸¶<64F¸÷1 ¦H|PK-ƒvaKŸñ+ÆFÐÄ×ø<³q³#xMxžû.Ë²oGŸ~Hœ‹Ç<ä…¿(µÄ³ÐïA(DP)^\�æd™ïW§ÅÇ—˜
I¸P1ƒÎQÌÃäçÏób¥“1²ÄR¥Í´e%ÆÌÈmîºÅs#bO¹VŸƒçÉœ&ÝÃŽ°ßÈ¬Ð³¶x~ðF¬Ê§º®ßqÝÁ4p‹q’Gÿv¦³™ßäsù–©V$èî!w‘©g¨ÅœJŠ‡hQ2‡—O G:á,Ì¶e©û&O›Å?Ogr Â6aòï÷ƒ`Œ0&w=~[ƒ0�rR¥lk…Ù�Ö&¼*ÂÕmP;Šæ$î�LjÌŠè-<	ý\ NŒïo�ó£ÃxƒI¥œ¿`†@Ð«Í
(¹46ñ“@Ù3qˆIav’ZqŒ¤úkŒíÚ·>ƒõ€¡.î9.Lóøõ±x	ÕÎ	‚0¸åÓp€ö¯D¼(¥i34eÇòÄT¸z|C^-ÄÁ Re
³Bç²çù§câNõ\ï�N÷)¡Â/@¬x'ìB—Û–)	·ôƒíV«óüºÕ�®ë*Ê*”Š¿*—K_M;©ß‘2�ûÒ„×dÔQƒÕt#É|cNE¢ÄÔ"U0î‘áˆ˜#me¼okwº/¨­Ô¯Ã$�l•ö}�Ä–‘²møMa,	æ¤RèƒGí”Ž“†B0-Ë<’ÍfCÛ¶â¤�eÒ“4‡ýšŸ›NH‡UÒDˆUJQJCõ¥bÊ¢pd
ž–¬âŠ¶âV¤Ix»¨ç@i½¤€ÊfBð¥Vî,ž/“Ú®GM¦Yaô¸}'2„Z&æÑëõ·û¾Y·Ó½Ôõ¼IŒáÀœi™!Ž+íÝ´lÃ÷‘ËeÐƒÒÄu‹¥âWR¶ãc<v¢ƒSxÞ/´ÅH±öyã§ícp‰ð¸jT4ódªÕ”Šò±óù^¯÷ªf£ó>LN�Î�œßî÷ &1ï8ÎÏ õ}Xè$½ �ƒ¸~\kïÂòÜÁ�n÷JtzÒu½­~ -L´‘vÒµR9ÿ¡l6ó�„		ƒ%úB(Ñ)X“·X«·c‘­ß}67¬¡Í
Z¯9|øÈç;í®":´qÁÉ8¿/•ŠË È:q¬øÐ2­Ã Ò�¤ à2ˆ@ïLpÍýîÀ�‚vú£1x'w{ý±¹l.æò¹oåó¹Û@¼²É¤9Ô¾WIym†ÑÒÄ"n§0—TÜŠÊ%Ì�ÚlFjheP.Åu1¥µ(	Z¸"`·¥6„CT<Ï!Ã#"ŽõíNç]õZã¥è)4Ú&c[=,†qPÌÇ<”¦úsŸ0Å$4fQ‘ž
,[ôû[1÷'`:7–B9²�\ÖÙ™Ëg/A¿çÑU¢“.ÁÁvÄ£p:k±M-8!,Ö3ÌU©3µZí·£Ï7-Kàÿ•R©ô	4ønp…;xFý�C^ˆÉî‚0ïãCc ¼-ýþ`#´Ä’ã¤oìÂŠ{a¿7¸taañY½~§R.�õ'§&>	.ù1ZÁ Šª q-ÆÌ1[tCnÅç&ú1¯Ä“ÖCã‚ƒ~j¯×ù'ú)–•z›³ÊÔ”rØ'ý—W¸òÙîÀ{Ö ?8Ÿ‰Ét:³wzzêÕ Ø_2áÊô!I×Z¹Âk˜,â„hó!eò’Š›±d0G§�b�¾‘¢²ÿ8>£¹°²I¶$ÛTñ¥hhIHðLÀ¥ç7›í+Ú­öy˜ÚÜ Ú6-ü\>{ÚqùBþØ7pî¢^Oê�Ï)AœŒ9<wÞN¥¾L\›LwB�gH„x,1œbŒdÃôZæ‹;A¿ŸÉuÅ�‡þ¡Ýn““»'&ÇßGl@xªßë9ÐïÁepË¾P›ç�,= nQ —+³¢2±´´²„kÀ�#�N[Õj¹ÊJ�2¥@Ä¿õÈ‘¹›Zí¦311½zzâ³D0˜ÛË,–Hª“Ù!Ïí°¶æ1!Šµ¬QÚÄ¤€,=SŽXƒÄpL±JÜË!.&Nãñ½Æ…¾×ã´Àh�¾÷†F£þîF£CøK�¯Z­~\âx‘æ¶Òß8ÛYé¡K<iø¶£_Ó¤èã}<jŒL,ø¼C†ò|’�èüCR:†6q‘T"1;�6eYä^D&p®³æ—ÿ`‘‘HN§SA©XøP6Ÿû
¸Ø.õ‚ûgðbTœ5úœP@¡¡®Ã˜ÞÕxS1)¶Š¬R@ƒˆ�e_U*¼âŒìaIjš,þˆÛì †Ôëí_>tøü|!ã®[7ûð¿ñÀžßë~ÛívÕ¥Õje¢™0Þv=pÇÀPÄÖIà;È°¼\;R©”f�)êó½R¥òÌLÚ¹Øã„f»S,òƒt·ÓyËüÜòÕý�›_¿qævÞŸãKø›Ì©;±á•5m±Z$%¼/1vG”XÁ4(�r.9ãA¶ì²AÊŠìõºµ[�7eœÌ6Á”ë¹ËÅrñÃù|þ{à»"Í“ÿr’_Ê6Þ¡dH¶8Œû7§1×^Æø�Ffè*{ØCO¥s€ïs*`Ä…˜“«1þ—b^*4þß]ùBöß³¹<¯ÿ“Úw¨àqdíWÑÛ‡�²ã…ÑâÔ6c¦™ \Âð!ÊÕ¦Å „5
¤W7Š4S…©^º°°ò/ÔÌØXy©\.¿?Þ!µZži·ºO®øe&›&[ÅÀlƒ¸¾×žÞnwR`ßóþEœqineye· £+µØ´råje
ŸíZ­±Ó ß€ÙÎ‚è&úòÅ…Å¿^X\˜­VÇ�¬[7ónõ|I:�²q%ÙÇSÄd‚×4¤Ëµ†úhÛà_DÞ'w-%‰8qùF£ñW€%Y’X¬'�¹/—Ëí sàË,Ôß±iäPBÙ‰úE>C®=~ÎÃŠSKùl(}7bŒ;À¬3À¶‘8ô0¿yý€Nîýßo3�n:‰[÷„Ò¶EdØ7WÙšõ(eÙžÜIXlÛe'i°†ù*f k¢¦n+`¿+»�ä �ûo»wïû@6“16oÙðR Dë¯ 5uð2€•%?_ÈY½n/„¡ü5¬±´°°´`[)¥��MMO¤€¡¶Oí 7<Õüõ ¸Ýd0mµ:føv¥RvqÏ,ˆx0;;¬$IÙ9ˆÁ½ôÀþƒ_#ýyË¶M7€ø?Ï&žÛ‰fŠ8º‘àŽOHˆO`TkØ#B´Ö…QB…æH}eö¢±�–Ú
nu¾ï…×Ùvêàe`WÏÃB­AêI=•Z°,û×§<Æ/BjÊ œg½x¬Š1o¦§Ðé´­¡¥Tt3ç»…bþV\÷+Í©I©‘3Ê¿L^e9 X¦ðé€å€Å6Ãµ›†!a—�0ë™ïœÈÐ�Ÿ";fJ/På:J4‹8FASr€Cö+³çú_Ý³gÿËÒ™Ôâ–-›^ƒ"×vÒÎÃý~ßl5Û!”	‹4f àæää¸z@³Ùª mÅmæZÍÎ‘R¹@Ú]\ma|bì$[ßAÄ¹Ç¢Þ``×·ÛÝC…B¾ ®ÐÆ=œ~¿·}jjêaœ›¯Õëüv:&'»ïþÛú¯²ýÄm„soG{g„vÏ-Z<kÑ"yekÌ¨a�Œ�ù‘MÏ"&Jù$	ñ‰Î;f�ÃìšøtL´Ò>!}NÃ{Ùô/Â“û¯ é .QNÊ� žC
ÁŒ�á£N:õk«C^|$‚(ø#³D<áM±/ö€	Æ}ºA~Â�*�Ñ'oµtïØª!y‘E^2û-¦½$!¥iÙ#þÛ ZÑÐt?uøàü›ËåbmjfâRz`nnáJˆá[ÁÕŒ|>c€È”]®:V‘õzÓ°-¥÷�9"5s?îåhúÝX™48%œ{ªânÂ¸ŸL®¦]d¸a?bùvp@%&U[©»•jé©àª¬,×À@|ƒì|“§A:<wd~Ñó{ÃÆÙëÑYpÉ°ªLäZ”±x	Ø÷m}é
Ç„	Áb±m/|"÷Ü1L^r„¾Ö2�YÊ^¨I?ÔÂNHÏ„ŽFá´Ð–�9þ¼-KrÁ™‚µ»ÙƒB¾#ò“óåäè3žl¸•ÛM’¯X�µÍ;3�cÁŽdßƒ!á$]Ã
’D‘\®ÂŠRngQNf›Ún©Úöš>d‘4+ÕÿDÜóáÀÿçáCGÞP}/ˆðBlT�ÕÊSSû]wÎð=Ïè’8îöz™Å#Õíöž‚�é´CXd+8�¾Åõ¼ópø:†dÃ¶ì�¦%îÀJ'VNÆÙeîá´Ó;mqaéa`H7ŸÏ=Dxo­Vït»Ë7®w×[Y©ï˜™™›™�v°0Û»÷ñ¯‚[Cj™_eeÅVO"®A‘ Ûv$†¦«˜–,St(&äµÂÓF­aOÀ!]oèe"ÔL¾¾�PD„²“ö$‰W!Ò¸ð÷
×iwÚ"‹Üçâ·}l3ìé—œÔºÞ _ZùŒIÓ”‘Úf™p•*]ÀKHÈµ¸»H8Â8âF¹UÉjò9—Y`BŒ8¾©ã"nü´³–ý)òÓ²ÌWrœçº¹Ã‹ß„â±<19v>5xp�†¢Wt;]»Ûígñ[âÔèvúDÀÏ+–
ßS-C€d÷u8ýÞ&`Ì:¹+ì”Ýõ Éx®{B6—[1!"ÅOm;e}"�N—Vˆù¿�˜†ÚüýA?ÌÙHÙŽƒûØ¡» ¸JµZŽ¼'<¾ÿà.\kLÏL>ßïC;Ó	{g’@Ü)‚!qt¢L¨)Ç»	L�¥%c�È“	“kš“Ž¶bÈ„’£�ëR®g—Ì!¶rØËEŠÊI8þ0›K”Õ@’ñ_Ph—AÖ…Í×&®�ï)ŒÌNâRìÚ÷¬ä{µÒ§ÂÎœ„ïßåï-võF´ø4RÁ$]:‰®2”�HÆ£â„å;E„�5}èÐÜ~¯ôúÓ—âØL´™Úr½LM¶`5ê­YÐÁJµ7ô1 Oô½v=÷h#}—m›· ÃÝ­x™'±Dž<¸MxþUÉoñ��J¥Bÿ—³Ùì_à¹Ë¸ÇI�NgG¡PÈ„AhÍÏ/vÒéÔÕxæ.pÚÇ€�ÆÑáYf{~~i7ðã7óùìËXËËðêØ.ØcAÜ²ËfªÄJ—ÑžS±gð^ÀlM§A³¥õcçÁ™ÇMË¼WìÂ�Šešwáºß=9Ïè±ƒtÄ�mÇD£5‡Y2 ñZfñÚàþ-žfT0{âP9iL¥¢ÈiN,‰�Wpß	\ü¸ÐJ-A2zÓx\Ã˜Ÿf{¤•ˆSè²mÙ`{.ÛAe¤]�kSavÂ#¦ÖöÍÇÑÏr-(ŸX^¬ýñÆÍëÞ%÷—ËËõ•±ñª‡�'lŽ@áx¹RZ†æ{b>Ÿ'[ár§ÝùŽï‡/ñý‡“¶?
âx€ž¬6ºÕó¼K¡ ¼D»Zô»AÀÿ›í„F¯×Ç-MP·bìL¿ÝîéQ¨�~ß}ðê'&+ŒÕ<:ÜS›Å+M>‰'(õÁ3€/¯ƒ¦@Ôs]×;)í8F·ÛQD’Nge»Ý2?0¡P:ÔI:Aè{ J Ïîmf²™�–)îÅ9ßBûçöÖ’\à	\«‰·8˜8Š˜·b‡‚ö´DÊe
mYÇ„Ø`££|ÅQÔ‹4r ®\B�³´(§Ù€¾çÌa'Á°À
‹h…ñ¸0l$Ò62øã�\™9(Dyâ—¤46	ú}Œ×N0¥¥œT*þál6s'ÆÿçR=KR°í‘!G!Ä/±yƒV\·ÐˆWxüÐA¿Ÿ¨¾f~náÐ«:˜ôWÛ¶õ‡LÆ¹k~~^BlÎærù#hÈóëõÆG(Ò¸R)ÿÎùbâ)iíìqH�Ô¡Ò¶Öb•,Œ8“Å–yhêÞÿ‚öüœóãjµô<ºßOE[¦ÑRŠÚíÎé6Ì–ñì{°ÜõÖM-/Õ	n\œ��:C@HŠ\‰5hÒJüxÝ^÷¦Úrã/m;eqƒß�rìo€øëŠ`6žš¶Ô9ëñÏ&'ôOGßi²Òßè¤�æîómŒ×Ë‡"\Ç`“rM¶`Äv]›�±ß<‹�1¬ª³�Îñ„
÷WKÜo…9a˜YŽ²H�ß3…xÇOÇN£Ñz=ús`Tàº®•Ëæu /+äÃFw»`0DHªgà^X %,Hbã Ü³R*Êú f˜e‹ùoA§xiÂý§ÑNtÞbL cù­rŒòòÒÊ�är~xl>dkz¬J¦·Ùh}3LˆâééiêTZõ{pÍ›Æ'Ænƒbq�¶;Ê
/ò»ªãËˆÃÎuh9ÅÐÄâØ2&¿­…œÁ½¯:VþÌÒÒò·<$g¦§þ¤PÈ}�^£Ù"¸9Žúp§ÛÛâÏ� \ˆ÷Í¸æ¦;v}¶XÈ}¦P,ü9îGâ	÷gÖœ#òºˆ´“þÆôôä�´Gðu\"jF¦cƒ@áš¡²½›–L¥œÿ“ #›L.¦)þ^�&¬GcMÉÚºXíÄPöNmFrL±êVõ.ŒÆö4žMàæsU>‘abeW…~b£Ô}hC�xTù·µ��LjgzP{ïi5Z×––Î(–Š»²ùüƒ¶]éfs™{Øü³Ÿ¯19N±Î
R–5ö�‰l£r-J¹x~ó*3Žè�"§XIo¼…åDšdB‹$³�öû×ûö>þwÐŠ¿Z«ü	QÉòRDØ0ò…Â&mœÈ%ÊÄÄ˜.tK½ÑzÝÄDõX-_Â}¾�P€¬Õ
�ZÝ&sAû.¥ 0ü39shŽÍãÕC“²§Ùl¾ÿ·333àû4ùý“ã%à4q~©‹Ï”«±Ÿò?@!› Eß
|;5»nê)xÈ¼Ô Ÿˆ%c(ìHce-1‰ß¬îIŠM3Á‘Ò|Ì×Jé*ABêFÁ¾Ñø÷ñˆ´Ár”²QÓÏSv¸(¡Œ#ˆTŽG" 9cŒŠ'©I9«ÇN™âÒL¬:Ž·á¬R·7¸RãAÅA7RÀcËAJÅ]êhëSpgjûN&@GÙ"&4Ÿ[†¦>¹ûkŽôŽÝ‚ÌÑÝ¡¡_6sÁ8�Ã�lCgå�zó
i‚"ð…(Ãm|¢RÈ2›2éÌpHÄGD¸"éUõfó�SÓ“WÈï”J³RLÖ‰,sb˜éE2�Ìv|L›@p²¦í˜XÅRVÙyï¨NKc¾–J¥¿ËfsæÒòÊ�“c¯çÝ0¿°xçÆë©-[™b€˜tÝ.,¤·:¸ð5`Ê€WÞ€v×�f‡>&CxnƒNÃHAØkÛÓ$ÛZÅ`hø5"b!bÔ•ß2=ŒÙTJœÉÉQô"{ìÓqÝÇ~ÜÑœº½¯¹˜"\Á‹†&³ÄII”ÿ£à•
Í×†ûS)Òšc9—ÈVˆN¦ˆtm¦2I¹—.\²õA£¹;I˜Ví%mW{ÚÓlŒþ‹ôÓðì]¼xZ¸qMò¬;¬â(¥œ �
+‡š£“¢¥éÎ38)�CiáhI)ÞÕA(Ÿ63;ó{Üt·â^šÛ ‚=èˆÛ¨7ŒññêVÊšZ\\þû©ÉÉ·�ï€X(B$ìUù„Á´Ž€6)AczŒC7§Ä¥4(•q t &uŒ:ó˜6xÛµ]Q¶(¬	ô,<¿xñÀ‘¹…ÏÌÎL½÷x
ÄñƒÐ®Éù_Ã9/!n‡ãûÀç8ö^pï×ƒ‰ËÞÁYŠ±Ó‹2
GÝX¬ikØ¼ÚÃ “�Œ‚½N‰øLs¥S<1¶€©ÎgB¤ˆ”	Ó²Õs &è0pØA×PœfÂ7;�Åó~ÜçŽL&ó5|ß>xÏÃÜlÇœ2ôÀ½6CAÚþ	[<çR0ˆ7 2}É´Ä¯(‰²ÿðÛYÀÔ_|)rùÌ×R)ëVÜçÚN§óQŸ<€CÀu•r¹ø`ýÏá:2:Ïë¥h¥óLjlEYOù+h×V>±è•ë×ÕÖY`VPÆk^¨ ‹-”ªJÖ|]ˆLtØ’Î°_�Œ¼y\xªëúÿ›ÜF¹\öí®Oé„}M�
¦_H«ã?Ð™�äóÙïaïÄä6[.	äÐØ21JŽa¤	É “Ž 3%(-F!SRkµ„åtòaÞÅ†NXJã™¹n·÷,ÝAÒ¡Ý^\©Tþ€I9H"e�2Ç4–„OVü¿GŽÌÿWÀ�3¡ˆ�ùàû†
3Î¦çâü8šÌ{	Áa¬hKáû±BÉ’A!‘Ÿ9ö>€ ÞêûáÕDµ :¯R)=ÄB1š'¶[Ý¯·Zí§’ÏcúþB!O¾òW–ë×£Ÿ/ÿÍ€F—Ca¼©Ûé¾Ø÷©Dƒ DZ)¹øþç!pþÅKõk6mY÷S�œfÄ¸lo5:ßïõEjS¿×?{jfì^ìD½ÖØX,åâøƒèà¹çMl‹Í+¸$n¦:æ“Â¦>šáÇä§VÁ·
ógt<¨ŠJéèyQÂóÏÂùûØ]Ò1„šçAdEP*?gÛ	&×4Ì9hÊ7ê­-étªŽÕô¦Ën“� ªŠPž˜Ëå>É\³ÄŠN��šVÂÇ˜Q¨ª*}“2¿Nfk‹ùy	Òh)‹’ì^:±Ûå|Œ-8Ö‘ß…ävu¡ÄpÊíe*~Ñ£·‰ð ^›1a¿n½¡×ío!Î3^�s~@�ÌËÆ1…"Æ„‚EJä›î%Rd“ED2ƒ‘aHGÝNïd;•rÇÆÊÞju®j6Û·AšœÙnuþ®×ïŸ	üJÁ¿g-.-ým.—©�XfÚ�Îó7mZÿÊn·s	™ó=À¤W
QÉ€€~ºDôGèŸÏ8¼î!æÌð=Ú²"¢&ð=ÀÔÅ™uS†ßîÂÜþŠæ'1¿Ñ“T{Žv	V	Q¸¯=-€?†�n2Â÷=�NLC¸œ\SP1”’ � nIxUj›Ù(-¡]­‹´@T.¶öõ¶¢Î88Chuw¬ØgaõåJú-óeM™9¤RÆ¤¡ˆˆÒ	)È˜Eîf#L3³f2 ƒÛ›tN0°iÍÂ÷)\&Ù‚/(ÎÎ ”�‰Ðg/ÁiÊi/¶›Ã“%L$´3•9Ò¥LcUŸ¥ïIÙ‚bŒÁõ#8~O½Q?¯R-…ìÿ<S‹J	•œ¡¶ÊŒ"âhtM”æˆ²bÓe‰µ’DîÅ3€-äeú|±˜oÕjáÅãW\÷|0¶÷‡ÎdÓÕ´“	û}·šrÒ÷çs9Ç‡s¹üW ‡òœrú¸b±`3–mÞ­1œ~6Ž•Â@ž66QmuºÝ×cÁÝEæ.,¾§ >Q¸…w*WŠWàô}ýžûvˆg""Ê¤gmR¦ÂéÂ\Àoe„öÙ	 Õœ
YçT„qíF‡ýåÊXÝg‘L�‡Dñ!e%¯¥®“ð)ÏÉæT;Äir)ñiÆ¥:¢Ø´
S¸ÑÓˆ‡œÜÃÜBÐ mÐ ^j›PÚ•Å“Ö0†PáE6ç¾rR�Ô‰ýºÌçi}mÍ”×A¡^ L1‰y¡Œ\iˆUžíz¥8�rÃbŸCáv* B)
w–Ë¥/¯,× ¾CŠóû-®›ÐÚ¢,J%R”™ÃŠ¬þBG_;	C³?Íù�¡ö
‹a‚9£¦Ö èP’‘(
ÿtÐŒ—Š¹%pêG¸dË9´è1îXüâTÀœ›ðºîÈÂï –ßŠ¹øTHnJ�:Ð³L³	b‰ìÀ$‰an§00ÚÛWVï‘_‚ëþç8vã1Þ›zÝÞf@©/hb¥þàë&~ÂøXåExþcŒç-%eˆ¸´Vm3,[ÏÐ‡D?1’d}=¯J²ˆ„·Š’ºÖI-@ææ//¦XkA³XÅ�fÐÙ-ƒ¾»‰™Â”HÔ†‚géùhª÷žiÊp¹A)Êµ#SÃ]p)]=Áf1G««¥)¥òAR®¹÷&´I@­x©W•²ùa74‘às¤õ¼õ-
T�ÂùVŸçbÅÔßnè²„w¢€Œv»s‡ðÓ*ÞGõb”xr4“µPR:È¨>)´�,Ð¨¼§ãçóU§Vr †*Ÿ‚çºn¦1¸àá‹—–js¾2�±~jÙÖ'5ÄGÎ|§×ëž1ô‰°:¥rñ²b©xëÜ‘ùOB´¾ËT)ŸêYSä[×ÉjªÁö4½	„HúÇ ðZà¤äùøü ùÿ•¾È™ÚJóÍËËµ›&Ò‡€žJ®‚ä%É±‡Ç Ç
…§a›
n×ûÚÆ¼BÜ’5ñ#\W§Ç°§ìŠRi�!O/Q‚$ÏqŠl´VuK”^¾Â±´]°ö3!š×£ñ+è¬ä®«‹Ñ0M‚ÜAç`òNF#èÚ‡„ŠÔ0ÊZTSÇ++Òú¯U»'âPGG“(ÖOÂåQ�OTþ0pŸ*$Ôp+Èç€‹UI¶*ˆ˜^²æÖa¬Øá MO
­\pqƒªŠ{¼çžB>”04.`-¼Í&Vz„I„Üå¼ŒS(RO~íqZJ2�ç��?<�™ü1Æ*‡ÎÍóâÕùCÑ¦œTÚí×ü|l¼Ú‡xþ+å2±ì§€ £œáÆ©’_çºÊ2'ÿ¹Vkü­ãPL¬ý^r‚³Õ‚ÀóÃÈjåz#!Œÿ±ng@\ÕÉåU uŠ�vSfœ ­özƒ«ÐœÚ›¬>\—Ì03R1|QUÆ~]Ñ¡Ú#,×ˆûà*´-±Ì>ä€í£Û,—ñ ›ì¢Jí8â�©”ìO=Ð:4e…£ÀyÄKT,S€û›d
QYþB([q�&G‹‘ý‹²É¶ãž×b~{3(ÛÑ2ÛœÚŒó¤1Ì{¡4ö$'»1§†Ÿ¢Ê[€CáÙ�á}Œ\Wø�ü—×bòï§¦a¡\ ‚9„�~÷X¡¸Ue»IenØ«L\Õ×Ÿ‰ß(Òd…ÚÎÔFÇÎ¢46ÊDU PpFÿ‚ �çC;½˜k3�d©‹‰$±ì¨ðuˆÏ²)ª¹îyî¾¼‡8aæL&}å6ƒ¸~ºG6¾CÜÜJ ½{«ÕÊõÉÔ°¨y,Ç	#�"‘£ãú’N^—$9*™læ{™îàµÀz§¥ÊâºÙt&[gÛ$¹')Í³J•*ÈîÛj6O ‚Â³ÇT&\Êî’,Šõ„¶üÒ†S¶E�JP ˆm›;Ø NÜ§hêº7„×O¢È%×óK þªeZUÏ÷fÁE}ÜÓm4š—»®w
QYJ`&“Y†Ôã(4m—“NiÇ¾_qZ=Þ�«Ú8Ú†¬Æ€2ü	�øJù wP!¹ž?I„˜vÒD€{8ûkV9Ð…ÊÅ5µ	F1óÅ!ck­ÞxU¿;0*Õâ× ß…AÙ ´ÆíêÜ\J�4Šš0å‹ò.$åï10—×ëÍòù}¦%v“i@ý“®ç�‚,bf~ÞôßœÍfn@çM“\‹. ×¯À­êù|îvˆÛ×c>dY¢K¶5Â°B¯Êƒg"8à1caÐlƒ¶•È ~�c`ïðeðÞz­õ7$”ÑŽAš¶�²ƒ6ÔA ‹hkCùNµ)ë ¬™«PR>±ƒ~\hó|Ï6�åÄ7éûÞÎ‰‰±�@—9«°ÀbJ¹êpÝ£­Nû�à\¯ÆõC£~^µR~-dj4Z7ç¹÷C›~y¯€ýîaÛc]†aÒŒ#žÅ!òúýS¡8�IŽÌbóóK·b,?„ùø8à»ëµæhÇ½xzu¬üÌÅ~,>«Ýê}9å8÷uÚ�í…böVœó`0¸®Óé½üè,üLjagÒ™ È¸»ø2ŽJ¾þ»HwB;¶ÐóÖ]‚ÍÈ†"«0“qL,Â×�&>7jó²µ1’ì†ŠªÔ …aP¡'E…|¤Ð@Åfƒ¬ïÏ ÄY¶0y9U`Ç0 ±,ú¾;™²S·³•�VÛS)–Ð÷‚×` ÏÁd®x®wà×ƒù|á‡RW!H¾ŸM¥¬¡?6ç`EÝßíõ®Ëf³ï¸Òît.&nÒïÞ)óà¿…�šl5;[|Ÿ¶ÖIµZó¼ÙÙÌ6*“�kÈC3¯¡e±)Tç¤9ò6.´D¹“ÌÙ§À‹>_+=ŽE÷ˆi™�Ä¦ ¸fE­(Eh?-¯Ì”m0¤¸:B}>JÀK0IÏÄx”À96rðA”lB�ú'úéÍfû3ý~ŸÂéþîCvÄÓ©TÜÂüâg0éF±Pø Úô#ŒEÆ¬„¡¿“úÛ(<ÿàIjÇÝä•¢²t ¨} îkñóß”Ê…óAäÿN…ÐÇ@ä¿ƒ¢ur¹ôßƒ·ø®·RLÒºDtéÒâòÓéÌáTÆþv!�5¡ôüÌú'ÆÐíJÊãL©œˆ¿G®Ì¬¶«…v6âvèä±;m_ÖeWÛ`#ÏŠ¯]8¢¤½æVå³‰B¹‘æiBq£mËLE©4ÇGÛíVµX,>vW§Ý¾"á$mORNp+è,¼ÎÇº�*#€Î·âøè£d: a}?”á6<÷”> ü�*N�B†”!5Ól´¶æùÝvÊÚÕhô^Øív=ˆ¾GÝÁàÅ­f{«P]’²DŠË/ÐíRg·^#‚C;6\…í‰c6Éô ¥R>žŽgÀb!Ûâ'X‰D�QJpßÂíw&‘Ö£”Oªj‡ƒÊ`ÄØü‚”.pð]˜äl”`ÄQãd‚x¤X,<œñ2Î®¹—Ü£8¾DùÌ… ÈÝÊN«4f“\Ÿ'@‘¹W˜íyE,¢wÞ„g¾î’žU(æÏ#ÓÙw)˜¡\.>“Nã}Ú±÷™áØÁÃåj�à¸¹¿afvúJÞË8ÐåÄø4»[-å„`wp"h—”›†2Ïâ'¶�ú¾]Hu8"žâ?³‰e)‚r@"—^R› rèô^¶ºÃxî‰!UhIkTQ45ûðú9Î}&:y7ÄýJ	mµ[Ž§Ç?&„
¤ø­õU¶eS£ëà5à4*`Î+·m<.¶ZíwbÕÿÏ€ÏÅo$ÜB6“™ÓÆr9¯q•ÜP©”_†{åÜ�ûbp‚»z½Ô?vƒ�üšcFf kÈdÇ¤çeÙâGx$g¥"«sg”A{!áÎKÅ©“"vÞ+ä&²u©Å8´�YÉÃ$–À©~Ææ rifø\TxDFô¶ä2¢8þ£„±¼ÇÈ~û°Z0l¦ÛÍÁ¤Fæ§EŽ´4…ó
NÊ�|„qj¿-ósÉPm“û�`›TYvj$‰œ��V\Õ3´«�Q| "Æ•%‚\³rÅPz€”±¤þ*¹öÚœ%ai3ÅŒ‡©T–u„-Ý5ò�PP£)”VU7],ò¯Ã‹&øÙËËµÏ�;m)WÊ�ƒµ¿
ÿyÜæ2 ášàBbùb\—ƒ‚'6áÆW[Í:)ç\ÇIwêõÆ	Õjùûk'€ƒ	pŒA'¸ �ÎÞâ�2 Q}°ËEÙ\æ÷7t:ÝÏ wqõ#ª§3&´Éå¼à�™tZE
CÉÙ®êÙ +IíÿÞÍæ{4Š‰×[eè–Ì5E¬ˆéxN}<ÅdÕ×âŠì¶¢epPÂHà„ÃºK0šÊ&†µ{H¼ÈXO)¶¸Ë'8,ßÃ„¨T"MÖz�¸è©TÜ««*¾r‡ˆ@1›-Ð$i§+7·µX:Q¬ ;	l®ÖÒÅŸ´í—ËÔ„qM"HR(¸mV¦¢Ú’F"Œm˜3 ìzB—‘�J¤�8úŒAT–ã¨lÀ";ê],÷\"ÁˆÌ”w"÷T*¥A4—¤RNÄRÒ¥iÑl6n„ŽI¢ûæf³õW¥bñ_–—W.v¸v||Œ4J„u÷ÔôÄËÐùˆÊŸ°«yˆñ:éÔFò/ƒè�˜úÄÛ"8öžJ¥ò3t¼îw'^¿'±	±óV`&ª~E¡b)ã,Ä>~¯rÅ³%h¹vYuØž×FpªG]žÍŽ 9<y0ÈA²–d‚3FÉg§"¤âH¡*ëŽ&Ç‡Ç(û§Ì,â<‡¤	e&áèk{ÄË“ü‹òŒÍØ6ª›'•JwÉçXƒ¶v8ÄÅTS\È*HÔž”\rÚ”qÔ‘*¶*9êÇO¸?é°Ã¹î‚ûŒÔÐ\…¹‡ZYŠ1Ñs”t„	žå€Ù¼²÷éÒeŒ@@gßÎ^†%òMå"=à¦&4ÿ¬\.¿œEþ#ÐšÈÅE”îÀï%¶É-ƒ€ö@1ÙûÇ¬,§Ÿ:��K\zÚDõmhb©$ô<»»úäò¢ÉÆ3÷I.¯âãÚ:k\DkÎdR"Û¤“NW(V‘IõÛ4#Åa<Q0Y\@¬J—kˆ”£D›ª¤ÎÕRÄ.	1�—”*Ãºjbèü¯yÞì5R�%°Ãiži.ÝSHq#)Ù†q/r¿j÷=O…Þ%·GØ¯Ì­Çö@CFájš;zÜa¬î§<^e9›Ù©dÃ¬º“ü×íµzpílvêµ¸ågeT‘TE4ËE•„,Ì†M©ŠiÜcpÖŸ
Q'ZÂý~ÇÏ"‹SÇáûÁ¶Z:‡„\…*ã?ª¯lZ1X§¢sÔ}DEÕqÊÖ$*™’¸š–z±(üEm
`›I1ÃnËe)ß¹Ð¼góùœ
%OŒº^íã:D�b �–ŠË4†Iæºú­É$|V:âlqôŠ\ÆÙg§ÿ“­?)—RH?V¥\RÐBËýr¤`rþùèu’sYrd‡Å9óDŒÄÍ”VÓ‘G‹]u+œáçp0oW×5WQ7]î&.ÍÁFŽ1xD;2~Ž H$]$G¾[#•}×ª4,¢t _ý‹îE¨±K½~"vŽØO"“ŸÄ×/¨+g¡ƒÒå²T‚Es}mˆsíÒñUôešéz}5¶’)Îãå*
`KC}¶6]v-ªbEX mœ¼"ÄåØºW ]Ž 9‰ŠðÁ“‘�šzHYâNy,$¯tSDœL—±ó9šªâ«XÍ¹t†ÃÒ!4†m«¡JÉq.ÉÕ&�Ë>sxOK×žáâ™Räu…ª²uK·SîÓ€Ó Úœ§2n¨±‘sZ¡ (ÆµsÄvöÁO*{¨r*¨Üç1•÷,Å2›×šÊ Måf¨=†ª™mq¹¼�ÿ›Ìõ�ÅUa#QÒx­JÃÑéÒÅR×å#M§ÇaX„É–Â0MaöKL¤©„ÿÑå|–¨ˆ¤d‡v2D*Ô±{\EÁŠRnTJ—sÊ£²ºÓˆ3™ªÔU¸ÒÏ-j-WqODî#-‚—„Î¿�a×ØYp{Èá¾‘Â‘Ègì{6¥¾‡~—D3@ú©±ÌFù¨´Ü€Ó’{¾¦ž@µxê‰�M1WŽ¬)æ í‘Á·ù¼'Ã	9@WÊ@hÎ)à>á,cDWûä•Ë‘¦@¡è’Æ*2žÌcd#»)m—qX›Ý‹ª£ùlol±òá²dRc¥ÂþÁ „¶›’Ÿ
�š
OÆEÕ"Ë*ý!.^ BÒNg—àÁã‰d6.È806*¬Íbgw6›ùê`à¿ÏóýKR¶ýëDé
?á?ô"Â"'äm.¢�õ¸²â:š×S¹†Œ6
�xó—£&ÊæõC‰ˆƒ¸·Ë>l®Í ëœï0Ø¢
ëØÙãÓ&ÕÆ|?¸Èq
$ÝÉZò$Wa¥y
>˜ªxµˆƒÈ}ùÚ „î„îAJ©Ä¸” 8ÕIZÛvêat¼¤ö(Ñ¥>B]D¾ïÿÆÇÌ5+k¬]ªÆµøÏê´{ï\*õ\«Ü.þ�äŸÇ—Ð�5­›‹¥Â—À4ÞGØ¶Ûí}}:=ŸÏ~je¹þ×õ¶7'í�"÷"œócc¸+I��“�µÕ‚Ÿ0ôØµUU!*NTkå'r¼¦7T˜�nç>ñ¦?Êò‚ëâÖ¤¥!Ú­Ââñ•æÙŒjšLqB7´ÕG(sÏí»¤
Ôf¶›QD‰æ`’3¹Ò\67£WŒÒLOÔæ3åóã²œÐÎÄŸf.=`L±jg+™ˆÄOl‘ásä4‰Îí4ˆTEE�Hù©óeÿÀñz!ßˆ|¥÷SÐµk0ð®Íe³-r'ðçÉÊHOIA�$(-Rlv]ÿ G>“MaøSî ßòƒ€¼*Yª|F÷Æ¤Ó“;´'•j*K�”»-ÛÚ"%Ÿv„°Jßí‰2Ë’ÀÉ2yÖHR=ýÆF­yi6—9‚5°<?·øîfÓ¼…"oðzÁüüÒnwöøx•LVòuw:ýgg2é;@„'f3éŸ¦ûÞf«MîÂ­TJ×âüß$ŠmVÈL©¶§šzÚ¸u¦�+˜ì(È¸„ò¢À÷©ÔÙžç—16TÄ�žKeôz´ÃAø%Ó´\Û²Ðÿ=¶cÝeAñ¥:‚ä–µ)5b3ÂDÐ°âˆfBã#â!nE8pWÊ±¨ôKòƒ”„Qt
‹M¥LP€+¸Â>LÜpî±:ïAÃ¶§ç»)UVXÙ“’ƒ,˜ðüÑêëÇ¨#ãP*Êgb¦Óé~‹äRÛ�`N…Ü
qû.)Ã¢¡	INJ;Úžç½ÙsÝuÅBîÕœ
ºSÙÔ¤×àÛÜáùÞë—þ8ôC#“Íö2çÞl¦ð´ìäñ¡@œç«ˆ$©üå¤|µAŒc˜¨×ó7�w>ÇÎìv:eŒ‹™ÍfŽ¶=”Éf^ÂµÂW	G�É6Ðqÿ‰ÉêexÞ£Kíz£ù�jµü¯
xõe°(xw(UV#ŸËwÀEJ¡¥ßŠå<4 }[Œf³ó—a ·Ë¸‹¥ƒ«+ËŠÃ”»®”5iPÔQ‘BÂú®ûÂAoðaÖ†V³1ãPrÕ£ôaŒ÷.ŒÉÏtá¦ö*1”L?¡OîvÜgbL<Ì�)$h³¡ÉÉÉ[Ðž7ó£„ŠÚ´G„ã?Åvwæsé�4í¿€€=KžùZªÐ…×¬ATÉG´¼üàäF½q6ÜÀZÚð}Ô(Kío09\+LØÀÖÚ^ÍâJRþÐŽ'mHÃý¬OR0D­^ÿSt”~¾®(7”2†ûÅ¸ì’ª,--_NÑ™´Ç‚=YÕ4Œ=J»7T@mÖæO&''^Œu/×“‰Úµ‘Í;÷E6W2bæä&þSIù™C^àSðÃÕà–—£/›Ðæƒ‘[+r‰U•µtälF›üÌCÚ’aµÂ@i°Q;[íNºPÈ6©ÜÄ÷úï‚0<�<b˜ô¢;èRÔ­T$ ÇnÃCþÄaÚÅõ©j³‡pïÃ<Æiþ%§¹Øaú5›­‹ó¹¬Q,äâŒUw [Ïc!ÞÃyÍ;YÄ›•5êjç9UcÒuÝSz½Þ[…f@å¸ =Ï¿`9�w|R²�É¦?U¯µþ¢^o¾¥:Vº�5 }«ÒÂuîmP(ä^G¡n˜èW*åsðùj+¼+““0wHÿ¨œãG×8ö†Ž±1Yêí2–ÁaÞ�06Vý&ƒö¤Û£LÚ±œPèýõˆÈ.îvû'�£|E×.R},
!›l¨R@/ùt1q{G�Ììë]"iãªZáhAS®Ü/�Eâu3ŽÝL%²wòd¨(µD:¯Læ·@¬ï7T o÷C$ö:­îÌôì$aÁ—âT,3D_nnwºW‚hn €,®­ü+ä��z{4"D‰sŒAzpº]È¥xžM•æ©‹&æÎ³‹óâò¥bá=åRñµ¬Uïæ å¢©£¶RMz+4½=šJ!U[Mí>@>ò]iÀ¼>CZ=kÛ�HaRþD^�^¤ÑêúËæî|!w+ÄÂ•åJá˜¤¯°4qµó"`ß)
¯h·:÷-.®üb|¼ò<Š¯BÈD+	Ò#Å†ÓD”¼5RÒŽìoÃD¥[HJW¤Ýn‹ëôUŒ(-†9&ÑL{XL¯ÃdÕA¸ä	*p€e!ÎQ´�6Êoˆþú¬üØ#+%²r‡‰š€–šaü¸ÖŸ6�¬VÊbs¼{—©³
‚Y†ÆòÒÒ¨öÎôÌÄ�H,/c±SžÉfˆ¸¤�ôd£Ñ|m©\¤h—ƒBY’Œ}T%ãñg²÷R>K/QP]r5JuyLZ¬@X%O‘OXR–¸†ŽG6B¡RqÕ¼rÔ�Jnó÷¨¶v"ï:š\¥Õýä“g¶¬Ë€W¸•ÐˆTæ]©œÿp§Û»ãÏ*•âwT);©Úb&¸™2cŠù“€³Z\XþÍøÄØ›mÛº%¡9%œ¤]wÙ2o±÷ HàÈhb‚˜ùÆÅ ¢Ì:²U©<‘. TJ•®˜tf®Ã¹&…|Íñ€ì
sª$ô´m*°V7ê)±ž ¬—©S+c¢Jú‘�U&Q&ÓOÁÊ.ˆð2ßnÜ¸áÕ¦eþO®ªÑëN÷½Ð˜;2ÿo JR(@ƒ Æ&‹„pt:�q€i\p‚l§¶ŽÐ&OTW™Á„
’°µ¥CÅd’Wc"ÉPL'}gqÝYµ�� ôF Z´A+‰aI#6nO Pi¼ß�‚&¸¾Gé‡¬´…‘ï‘ÂöIdœI ]ªdðH±PÜì§X‰ß¾º¿¿IsOésá¢dŠÊÌ¢÷jµr!0Û‡k+õ›¡X|Ž1AY´õÆ�Ï€ÍåÖ:‘g†a»‚.="ŸBÉYÐØÞÜju¾ÄI[+zu©h›¹¥'x×:M±Š³õ&™=p�dŠu¶™"ÊÌªP.©<)ÿ_âØõ�UÒX¬~ÅûSÆß’ÚaTGÇ']ˆ¶ç 6Õ †ïßGù.!}'mQ¢+»îÀƒžàè
¿�Ëð2 <ÖH$1)c¶Ž
Ñâ”_Îðì&4þ	íá26S pl�j‡ª8¨ÁîLÉQEBoÐ©¹¬´˜ý¾«êò1`µ†v?Â9:ð³Z-})›I¹„uNªzR¬Qã�Ì@hˆï¯¾­×í_-ôP·Û£ …-Q™`®33ê¥ÒgÑˆÍ`!‡aUbÅóƒë––k�-,,Jg³rX•ÂfOˆ««	½yŽTÛyAëüùüüÂ•c·àû¿KrO
•>ÚS	[!e*§É5�Sì£e,^ždaweªÿ?„¸1ÿÜ�KJË®Œ‘Ur<NiµZë‹ÅÜÇg×­»|vÝì9™t*h·Û[i×�0 Wx7aÝf£sK³Ù=R«5¯ŸœšÑÚ·CØ1
<�KDÛáªÈù�J¢#uöæ�)‰±çòÃ"ÞR.®0›´ŠÐ „ò÷Žá½-„ÆíqZ®öØYï½àŽJª&mRÝAÛ™†ˆÝ}øÐÜ5ù|~=VÖ7xµ(ä(dC3?@ØÄ‚É9ŸËg>‹÷4»ÓÀÿ
ZäyýþàÙÐ¾Î²L«­³áÙÿ(÷õd¼SA ì`Ð¿ªÝîüi½VÿT«Ù¾‚ŒÇããÕkr¹ì§1Ð›•¶ËƒÉšx‹3®ûBn¸K�YøR.›ù]¥Z¹bâé*gšÊ]è�Ò*_FÛµì¨À€ZÝ‚m–šJÃc“\ãe$6Á�C—Ÿ8Öù2á.ÃT]™ÃX½1›Ë~�j…sÉåÀ÷¼«ÑÑË �~AãJA«Xôd»Ü‚×÷€'C¼ïvô@x‡\Ï=¹TÌÝV*—®£‘p÷h§U‘Ø‡[r]E2c-qTR�ð¹dþ¢�ªTÂ½.ˆ°‡W>?íx0t�SJ"iÕÃ–öPWÐc+ÂnIqÜÒšÞ§¨’è�z½ñÕ¥Åå?ny= Èw9¨v™>¤fµ}MR&4ÉÓÙ¯L×WC´¾©Ûíd3Ù,ÄÉÀ!ËªZÔ|JRõª½Þ\Ï#»VËqRwË%Šš&¥„"—¹Â©Â9N¢)<ìÓ%ÍÞÝë{þ¦uë§)*™r*N„Î»Wëþ$Ü’VÍÄ¹ØÃª¨kÍÝ¡LåòpÑdzÊ&°°ÇÐÁ<^-í¨ÔK"˜"Z`Tw±ÃÊ^Àõ#‰YPUÞSe^»…î;%™‘Ii‰•�·ù…l»¼;±[„i7nŠQdˆ´4¬Ÿ­Å-Ws¹ŠåêPF~I´å<ó?9~€G)6—eˆkƒ‹U
=wJÉˆ&Äp$*ÂHT„ï3ûÍ o}§V«]13;}S&“ùÖ$Ó®•‘Ñ
 )\™Ãä¨œŒª¡ïú¯TA³A°Ú…iÑ–Bü	8à2GU‘&v§„Æð"òµ}L¹[ËKµ¯×ëõ—mÛ¶ùï„i¾DølØ”ûÌ+=`ÜÕgV÷UÉf•b@�LºçÌ8;Ñ8ª˜ûFCW»w¦%Â'³QñŠžšìØ¯&Sí¬ÊZšÉ‹nXQÙˆ¤2¸È‘)Wkï¡1ÜOfÔƒ#Gæ;*ãœæ â(ÌÔÂ”÷,.³ÂúUûX�1½�“í£m}ÉeJ›;µôÂQDèFé¤‰¤(˜Æ³Ùµ&Gc·#‰n¤³íTnÂ‘r¥t%švÛáÃóïÜ¸qý<ØÿG8¬‹¬äÖÀT† öÐÐpªªÃ åë[ÍÖ{ Ž§ÓÎ­™LöÏØd�åNnùezžÿˆŸ»�}>nÄUD£å# ­vJ·˜ƒ�Ñ^YY¹{yyùœÍ[6ß"ü Þc[Üc¨H …ª+‘Âák)^pA\çF‡·=
ÃÓ€»Î!{x­G¨$å|xøí!Ë4},œzŠ¢!Mó¹¸ž\h÷pücàA#Á…©´yåi$a ‘~3t·ÉÔP‚¶©}÷‚Š,Š6Ù!›`&a‹´Å°”LŽÝS}Ž·"—*sáä¦*Æ¥kØ„¼/ý<‹û½§|�£}h@—UM´Q�NjC�ò€„6íŒÚb#fd¹²Ú„\×üco‡­bÖ†Q°‚à*‚#=¼R©ôRp°o<pøÃ3³S³�²Ÿâz6TÍa'VG»>™‘6	-îË6ÊårïjµÚ·ƒ¿;“vÞÉÕ#R‹(í:”—õzÝÓ�I)çeC¨4üp†¶GÐUp®!cî¥8B¶ÕìÜéi[·mþëTÊù~Å (°ÜJøSWÖŒAEAƒ f»�Þ_†�q&�§ÛVÉ@Üe�‘ÄF	[.~Ë“��–?û�Guüôí:Œç»ÀÑoÇy{€ßv¥lkPìLÔ»˜CŒ«­2tu’ÑÍ{‡[FˆHÄ©°;‹±¤Éƒü×c2#˜bFalq|c´ ´™K&
€f;$›¿ÃÌEAU1n ¶Ÿêöz�Ñn`2�§ª½%ýÀ¶œ4å˜bH†œG¿ª6™Æo�´ó˜m§:Ð1(Íd7ó<:$tå�ÔêíäQF&‡hu-qZ&‰Iò/Åbá9¶ehqqù˜ˆWLOO\!ô	”`å±êð.ëÄq¶»®—Ïf³ïÁdÞƒë_L»bðã,©EU´jÒýÁàß1¹¯üà!tèó¼ê»rX2Ujh¼àTo�;2ÿ	šË™Ù™ëÁŸ¾¡6)8‰]”ä02Gš‰B¥dû:¥ÙhþWk4�uæòùÒ:XxYÓKÜ½¦iíR¤U¾¢ØCß$,*èt†ïEß<ÐïOÖkME¬˜¼F&›¾ÍNYß´-ó÷\UŸÂÜ,ŒÙ¬®C¨˜ÏƒHìyyGEb”Õ½ÉAG¢™HÜ+&•¦öb×>—vudºªÓé¼±ßsIÁ£+}Ïuß�N§šùlîgµzí…P6KÙö!™Ÿ2Ì=¸ø$0›9„(Âjàº×¸omkÒë÷©ö¶155ñàþ7e:oTöf–#£rÝwDq1c][k“ª|Ý�ý‡¾Ig®[?ûAÇ±?A�˜:‘\±nU1ÕuÝk»ÝîÛ*•Ê+8�Áà:{U.äÓgüASm¶ZÆÓ–3™Ü$äÙÕjùlcÕ^Ï±øô^¯ÓâÂò•xö}À®×¨¤(ÊkV†Xíâ3¶)Å1„Úx­åÂÒ»(¨’¿6‘¡{‰½óÌ=2Ì=ü5‹1$õk¬óm®;¸p´b=ÃóF£Ù0Ê•Ê¾ñ‰±´Uà€
“«â 4YIbã(h]aMq3Ã”«Ëá­Jˆˆ•wRˆBîüÕFôØ¤¸èóóÂ9•ê»äÕÉóŸÙ’FÞ¤ü 06aá¯ÃçÛFúžŠRU�x£(c#îC3]ZyÐmP<¿Ìc9ÜÞ"¸áæ¸‚Ã¼UÁ’qõ+ŸW¥ÍiŒ‘2òf†S\xç²…ù¥wC9xêÄäÄ#Õ±ÊÑµ_pÞÄz§~àŸÚ¨·î(
žú!mèq÷]¹ŒÊtÓÞF‘¥f³õ9pBr˜/áó%›6o —á�%m.Ì‰8Èa<?·ôÕ~¯ÒÔôøÃù|îƒ8þ¹P¨¯¸ƒæö�cx¦Ê¨n<P:´]ï óž¹ö5Çþ	C²¦\‰sÁ�ØîèFÖ‡z„þxª¶ÿ‰¶ŸN:‰þ\Þl¶_ ü:fZmQûïccÕÿÅå€»¬˜eÙHß�$´<ÚNÌ)—9–j¾TÔÓ€³ëB½�›"ì*'ÿÓ5Sl§<¥Óé½~nžŠ(ÕJù÷`ömq¦_%CÔØZá&HÙäÚßé”’Ó
Lœóí¹³‘³ ÷FJ£ª¨¼ýæç©(\U iU4N~ìj®ÔIÛqEi–ç×[\Xy'ÑV¥RúðÒO!Î>Í6�Að��Fã¿g2éÝ¹|þÓXQ_dfŠkT¢|ä>Xüõ Äs)˜ƒu^.—'7í#<ë{þ»kõú+ÛíN1›É6¦¦'n‘~Ä}‚ñ*8µ¥M
Ê8½‰÷4¦R{¹ô®âŽ\ jŠJà©2Ê‘6ÎLE:…qtpCr�=+Áªù¥kìÏK'Ð¯—×kõ×QaZ¤Á×ÁˆT*å_@|}œ«lŒšzˆ@OeŽrXj…C»<Õ8ª”€/±Fb£7û^Òl´þ¬Þ¨m*äóÇÆÆ¿Êà Ó÷ØZá²×ÉÃr{f‚øMÅ¥êLÈ,1ˆŠÔ‡FüÊºöXM‘èçœw;Þg%¼á+2µŒhu�V”ºí¯•Q3¼çÝ¯œ™½þ¿4Ík hCð§3Î�i'Eð5ž¤+(ý!¢`CU	Vâ-·²¼¶ø!)Ý³:íö½noÄ\»!�É|×½�öúÀØ|]êÊãªò°cD@Ù8WæF€|LWVuV´†Ak'ü$	#¡Ôi—£¤Ð{NîcÃÕ”÷(`iBU1ÎntÚ�×µ[íY¶MÅÐ+ŸËµ±ÈjPòî Æ:lŠØ·ÿV€ºÆÐ¹à²8œà¤)?€Ô‚³=×;±ÝéœÝëv·•KeÓ¡œßvÓÅü|³X*ý-Ú»ƒ[9¡¶Á�*Z?ÃÁÈÑ6¹¾Ô9Î&_Tñ,Ò¼ÍD@³š¡T@K´£@´ÀåP²!ÞxKšUj™°x?×”Lì-cŽ0Ü(q=8×Ð¬þ¾ÛíMC±1¬TêwÅRžÊo<œÐÃ`Ô¹.à~ÆaÄaÏõüàtßó~éj÷§kûÝÞº½ŽEÕ­Êåò÷KåÒgœ”óV8"{”-W›KDBkwåp/Z“kJ®¤ÕRÚ«‰§­.a¬
ÍJn†4EžŒ>WQ@'HÑ>šR)†¹)±È2Ä°–ÌsÐ÷yp¬W³]àÜqŒÁ„ë»žÚGYê0�l.¿Ë´ÌHœ9�UgÐÖpÆ5ÉtÆj#°3iù¾N’)h³óN:õ£B1ÿuÐá8ãßH�£ãªÀIEFUÁ¬�Åª•HÿˆC÷"»cìAÑê¡bµZbÈ8‡~uª‰ˆE³¼ñÓvb°å«>ÎMY#èdXº7RìÄèõT\žÙjµ¯ÆÀü@p«¿BUÝ@¨T´ˆ"ŠU^ˆr}Z­vÛH�xÁùêÐ˜ïÊæÒ}hç?Å#ÿi¤mÑ¦ƒW$0†UeÅªímUx—JÏT[=,rÈ’É«>§¬Hlèp/¨pÖže¬Þ½=ÚBM0Ç%øbèJòù8ú  �’Êz:ÎQ´K
oC`±õ@ys"åä‚À÷Ç …^Œ±ZG›gâ’“Áå,P•ŠYVÃÂX§JÅ oÞA:âëPQ&*ñl½/‘,½UÅjnZàÂJn”ÇlŒ’†„ŽÐB´g£PÅÝ9ñ^D›±EÃ/Å=ÝÕvÄcïÅg¬Yí±Qr‡£jM›¦(xõÐªÚ(4±ìg ÜRŽóP^Q´AêØU„˜µ,s:—Ig~)LAñt;¹ PÀbº+uå.²£-‰¡é!ÁeÖä\’“¿Ý¨�‰‰ò‡£"õ¨ª¥ro…�òÊìãö8I£ÿÐÛ¡ëoÓ€‡2|%¹>9üÉîÅ²*É<\£÷ªdû�öFÑó¾ˆ
á'¶½`¼÷ cðüHèœ;:�r8ù&
¼DÙå¤ùf4o()f£M×}C•*¦#+×Øøo0H ˆ¶@;ÖŸ?²
ÖŠÁeÁaÒ`¬9iÖña×Zä÷‚Ðþ¹P*+%yšàC
÷[â(ÞdÇ×Â¦HäÄ =¸±YJÄ ŒÛ©�¹jår„h‡uß÷Ÿ"¼ƒ	ÑOÄSšÆê­céÏ4Ì¯òjðŒµEÕšñŒÃÜr6)éÂôºfµö7¼¨Óªº†‚3Òd±×IÜ�•'El�FTqÅV•NáGÐB›îâŸ‚‘vYI»$‹b9Üm=®.“¢›îÏ‘ÿfLŒZq´Ö—Ñýr�‰Ã¸^
“Ã0�sdÓèˆxDÌ¥"Ÿì½ìÆ‰í\b•Œq0�º›»Q¹#“qÛ£	†7V“`&Vt˜0Þ‹ðSq-mÂ"-Ï4økož-ýÈ›àÆa7³æh¬ÞhÍtÙcIÆêªÌûÕ£� Õ-z‰Íšb%)Î/×_òF›ic=ÙÉe²dI¤D%b%{i2l³ôGBÆd¬|¬~Ÿµg+Úã�÷©I¤{Hw­Ei¯’õº³>ë˜LkÅã	ÞèÅH˜4ÖÂ¼Ù¤àÓrBDiÿéP–
3á0‡fˆ£fP'@Um‚Íõxìærx‡æÐ8m¸¨±$oI¦?
Ù�Ýgl3K`6É{â�¶-âDÒ8ª¾Íqxa<Ž«ÂÇ"GC8ŒºUuÞ**‡D6¥!ˆU«6\«ÊÂha©ññ?™O” :“µßˆ€E<×C§5bh7#^´ÖfIöˆ Wÿ9ZsbØ^ó	VûZ5†ä1PûU@�#ØZXVŽŒ·?Œ‘"æ1çc�-ªk£Ý‡fl˜f“Å–�[,i Â(Zs¨¯ÈD€·X«ïCn¥kÐ˜1ÞMÔ‚9ÆøHã7>N’Õä�H�äa\ggÄû6:§1ÖL¸,Åê<Zùÿ Òø¿™\n4    IEND®B`‚                                                                                                                                                          assets/images/postal-marks.png                                                                      0000644                 00000000523 15266052533 0012445 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       ‰PNG

   IHDR  ±      O„ß   tEXtSoftware Adobe ImageReadyqÉe<   õIDATxÚìÐ1NAÅñ?;»@I¥DCã,=…vF¯`"à-µ’ÂxKB¡R
•�¿Khz}“¼Lòeòæ7Sá%¾ñÛŠ–Ì2·Ô,•õìlúG9‡IÄ³4ÁÍÞáéžoáãuy~ÛJÊ¾Õ,Îòm9¹€ó+üAg=yˆLuîL:Yý'ëþM÷È/¿üòË/¿üòËÿgýéFä
–•6‡N®ÛÐÊR¾XX�}ÀtÃ{xìa£åcâŽO(º½¥ZžÍËùqN/	û-Bpxç™û”›1ôF;.¿üòË/¿üòË/ÿ¿ðÿ0 ?ÜGÅ±µ¢&    IEND®B`‚                                                                                                                                                                             assets/images/newsletter-stripe.svg                                                                 0000644                 00000000413 15266052540 0013537 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <svg xmlns="http://www.w3.org/2000/svg" viewBox="7 9.5 8 1" enable-background="new 7 9.5 8 1" xml:space="preserve" preserveAspectRatio="none slice"><path fill="#CE1D00" d="M10.9 9.5l-1 1H12l1-1z"/><path fill="#2284C0" d="M14.9 9.5l-1 1H15v-1zM7 9.5v1h1l1-1z"/></svg>
                                                                                                                                                                                                                                                     assets/images/providers/mailchimp.svg                                                               0000644                 00000016767 15266052545 0014047 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
 "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
 width="996.931960pt" height="265.263093pt" viewBox="0 0 996.931960 265.263093"
 preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.16, written by Peter Selinger 2001-2019
</metadata>
<g transform="translate(-0.642132,265.913993) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1066 2645 c-78 -22 -223 -97 -320 -166 -50 -35 -146 -118 -214 -184
-318 -307 -525 -652 -526 -880 0 -95 24 -146 101 -213 l60 -52 -16 -71 c-12
-57 -13 -83 -4 -136 15 -88 55 -165 118 -228 62 -61 138 -102 224 -118 53 -10
62 -15 73 -41 27 -65 115 -186 192 -261 129 -128 276 -209 475 -263 87 -23
113 -25 286 -25 173 0 199 2 286 25 208 56 344 133 475 270 45 47 100 117 124
157 60 102 101 227 107 331 5 74 3 92 -15 126 -13 25 -33 46 -50 53 -24 10
-33 23 -46 69 -16 52 -16 60 -2 94 37 88 7 195 -68 247 -23 16 -80 42 -126 59
l-85 30 -6 128 c-8 150 -31 236 -84 313 l-35 51 34 37 c50 56 113 158 136 222
13 32 22 82 22 117 4 240 -280 313 -638 166 l-72 -30 -68 69 c-88 89 -147 119
-228 118 -34 0 -83 -7 -110 -14z m182 -99 c15 -8 43 -32 63 -53 68 -74 73 -62
-58 -148 -406 -267 -627 -515 -781 -875 l-41 -95 -60 -23 c-34 -14 -82 -43
-111 -69 l-50 -45 -35 26 c-101 77 -95 203 20 431 141 281 375 546 635 720
184 124 340 172 418 131z m282 -371 c-13 -14 -21 -27 -18 -30 3 -3 33 -10 67
-16 62 -10 176 -47 168 -55 -2 -3 -38 1 -78 7 -101 16 -284 6 -379 -21 -133
-37 -218 -73 -310 -131 -29 -18 -25 -12 25 40 70 73 160 147 227 185 l48 28
-16 -37 c-8 -21 -13 -39 -11 -42 3 -2 33 10 67 27 56 28 182 68 219 69 10 1 7
-7 -9 -24z m415 -357 c43 -34 56 -78 66 -241 6 -95 14 -159 23 -172 8 -13 46
-30 102 -48 107 -34 149 -59 170 -100 59 -112 -151 -267 -438 -322 -60 -12
-128 -15 -249 -13 -144 2 -169 0 -192 -15 -61 -39 -81 -116 -52 -193 34 -91
155 -144 324 -144 159 0 282 30 422 102 69 35 178 123 166 134 -2 2 -43 -16
-90 -41 -161 -86 -337 -135 -481 -135 -79 0 -189 24 -230 49 -29 18 -66 71
-49 71 3 0 42 -7 86 -16 56 -12 108 -15 171 -11 86 5 156 21 156 36 0 3 -57 7
-127 8 -133 2 -273 27 -280 50 -2 6 6 19 20 27 20 14 36 14 108 5 223 -28 518
41 696 162 l41 29 16 -47 c9 -26 16 -55 16 -65 0 -11 13 -23 34 -32 41 -17 50
-42 41 -104 -29 -191 -169 -361 -377 -456 -106 -49 -187 -66 -312 -66 -119 0
-199 16 -280 57 -177 88 -266 229 -266 420 0 133 32 228 107 321 42 51 40 58
-32 141 -25 30 -58 77 -73 105 -23 44 -27 63 -27 141 0 81 3 95 29 143 17 30
47 67 69 84 66 52 101 59 261 52 129 -6 146 -5 187 13 25 11 67 37 93 57 57
43 108 48 151 14z m-1347 -567 c76 -39 124 -116 142 -227 13 -81 6 -150 -22
-217 -47 -109 -156 -143 -285 -87 -59 26 -132 97 -159 155 -27 57 -32 160 -12
227 15 51 77 120 130 147 57 28 154 29 206 2z"/>
<path d="M1864 1596 c-21 -16 -23 -74 -3 -134 16 -51 27 -62 59 -62 21 0 29 6
34 24 7 27 -8 116 -24 146 -16 31 -44 41 -66 26z"/>
<path d="M1385 1407 c-22 -12 -41 -31 -43 -41 -3 -16 0 -18 25 -12 77 18 151
18 196 1 25 -9 51 -15 57 -13 16 5 -25 55 -60 73 -44 23 -128 19 -175 -8z"/>
<path d="M1904 1326 c-3 -8 -4 -27 -2 -43 2 -22 8 -28 28 -28 31 0 51 45 30
69 -16 20 -49 21 -56 2z"/>
<path d="M1491 1317 c-35 -12 -61 -41 -49 -53 4 -3 21 -1 39 5 18 6 54 11 80
11 58 0 63 15 14 36 -40 16 -40 16 -84 1z"/>
<path d="M1755 1270 c-10 -16 4 -49 26 -61 24 -12 55 -4 63 17 13 33 -70 74
-89 44z"/>
<path d="M445 1166 c-44 -19 -82 -68 -90 -117 -8 -49 9 -77 29 -45 47 78 51
81 90 81 31 0 43 -6 59 -27 18 -24 19 -32 9 -66 -7 -22 -12 -56 -12 -76 0 -66
67 -108 115 -73 24 18 11 37 -25 37 -40 0 -45 26 -19 91 36 91 11 162 -68 195
-39 17 -49 17 -88 0z"/>
<path d="M3633 2332 c-71 -25 -129 -67 -165 -120 -53 -77 -58 -111 -58 -384
l0 -249 93 3 92 3 5 242 c5 224 6 244 26 269 38 51 71 69 129 69 58 0 91 -18
129 -69 20 -25 21 -45 26 -269 l5 -242 93 -3 93 -3 -3 268 -3 268 -27 50 c-34
65 -101 128 -163 156 -70 31 -200 37 -272 11z"/>
<path d="M3030 1960 l0 -380 95 0 95 0 0 380 0 380 -95 0 -95 0 0 -380z"/>
<path d="M4200 2250 l0 -90 110 0 110 0 0 -290 0 -290 95 0 95 0 0 290 0 290
110 0 110 0 0 90 0 90 -315 0 -315 0 0 -90z"/>
<path d="M4952 2078 l3 -263 28 -57 c111 -225 453 -252 601 -46 52 72 56 99
56 375 l0 254 -92 -3 -93 -3 -5 -242 c-5 -224 -6 -244 -26 -269 -36 -49 -70
-68 -122 -68 -65 0 -111 28 -139 84 -22 42 -23 56 -23 272 l0 228 -96 0 -95 0
3 -262z"/>
<path d="M5832 1963 l3 -378 93 -3 92 -3 0 381 0 380 -95 0 -95 0 2 -377z"/>
<path d="M6140 2250 l0 -90 110 0 110 0 2 -287 3 -288 93 -3 92 -3 0 291 0
290 115 0 116 0 -3 88 -3 87 -317 3 -318 2 0 -90z"/>
<path d="M5193 1379 c-38 -11 -83 -62 -83 -94 0 -34 45 -83 88 -95 103 -28
224 3 248 65 29 74 -45 135 -161 134 -33 0 -74 -5 -92 -10z"/>
<path d="M5540 890 l0 -500 135 0 135 0 0 500 0 500 -135 0 -135 0 0 -500z"/>
<path d="M6690 890 l0 -500 135 0 135 0 0 225 c0 239 5 270 49 309 32 29 79
25 108 -8 23 -27 23 -29 23 -277 l0 -249 140 0 140 0 0 258 c0 143 -5 285 -11
318 -14 77 -55 151 -99 180 -50 33 -132 47 -197 34 -80 -17 -126 -57 -168
-145 -55 -113 -75 -93 -34 35 16 48 22 93 23 175 0 61 -2 118 -6 128 -6 15
-22 17 -123 17 l-115 0 0 -500z"/>
<path d="M7564 1376 c-38 -17 -74 -61 -74 -92 0 -33 46 -82 88 -94 58 -16 142
-12 189 9 45 21 63 45 63 86 0 41 -18 65 -63 86 -50 22 -157 25 -203 5z"/>
<path d="M3411 1176 c-60 -20 -118 -78 -152 -153 -28 -61 -49 -81 -49 -47 0 9
7 40 16 70 8 29 14 68 12 86 l-3 33 -102 3 -103 3 0 -391 0 -390 140 0 140 0
0 240 c0 262 5 291 56 310 38 15 53 12 79 -15 l25 -24 0 -256 0 -255 140 0
139 0 3 251 3 251 33 29 c39 35 62 36 99 5 l28 -24 3 -256 3 -256 135 0 134 0
0 270 c0 275 -7 344 -41 411 -66 130 -257 157 -356 51 -14 -15 -40 -57 -59
-94 -44 -87 -64 -90 -64 -10 0 71 -24 119 -73 145 -48 25 -131 30 -186 13z"/>
<path d="M4519 1175 c-42 -8 -105 -25 -140 -38 l-64 -24 -3 -111 c-1 -62 1
-112 6 -112 5 0 45 16 89 35 104 45 206 63 262 46 50 -14 81 -46 81 -80 l0
-26 -117 0 c-136 -1 -206 -18 -272 -65 -110 -80 -123 -270 -25 -362 46 -43
101 -62 179 -61 126 0 229 68 271 179 25 65 39 51 36 -34 -2 -39 -1 -85 3
-102 l7 -30 99 0 99 0 0 248 c0 272 -8 330 -58 412 -30 49 -102 103 -160 120
-61 19 -210 21 -293 5z m221 -479 c0 -22 -44 -79 -79 -103 -41 -27 -111 -31
-135 -7 -21 21 -20 67 2 87 29 27 64 36 140 36 54 1 72 -2 72 -13z"/>
<path d="M6173 1175 c-157 -43 -248 -147 -273 -314 -29 -180 54 -372 190 -441
22 -11 68 -28 103 -36 107 -28 249 -3 346 61 l41 28 0 129 c0 71 -3 128 -7
126 -5 -2 -35 -17 -68 -35 -83 -43 -169 -59 -241 -45 -70 14 -109 42 -124 88
-33 99 48 184 177 184 52 0 142 -24 203 -55 27 -14 52 -25 55 -25 3 0 5 61 5
135 l0 135 -51 26 c-100 50 -251 67 -356 39z"/>
<path d="M8305 1178 c-57 -16 -137 -99 -159 -166 -10 -30 -24 -52 -32 -52 -18
0 -18 27 1 80 8 23 15 62 15 86 l0 44 -105 0 -105 0 0 -390 0 -390 140 0 140
0 0 240 c0 258 4 281 51 306 39 20 56 18 84 -11 l25 -24 0 -256 0 -255 140 0
140 0 0 233 c0 201 2 237 18 267 9 19 29 41 45 50 24 13 30 13 54 0 50 -28 53
-46 53 -307 l0 -243 136 0 135 0 -4 303 c-4 336 -6 351 -70 423 -28 32 -52 48
-90 59 -28 8 -62 15 -76 15 -88 0 -164 -57 -217 -162 -45 -88 -64 -90 -64 -5
0 75 -22 116 -75 141 -46 22 -128 28 -180 14z"/>
<path d="M9577 1176 c-66 -24 -104 -62 -142 -141 -20 -41 -40 -75 -45 -75 -18
0 -19 19 -5 60 8 23 15 67 15 96 l0 54 -105 0 -105 0 0 -490 0 -490 140 0 140
0 0 145 0 146 30 -35 c66 -74 195 -92 297 -40 99 51 152 142 173 297 24 172
-33 356 -132 428 -75 54 -182 73 -261 45z m73 -254 c34 -35 50 -87 50 -167 0
-120 -41 -195 -108 -195 -47 0 -69 14 -94 63 -16 32 -21 61 -22 122 -1 44 5
96 12 117 31 87 107 115 162 60z"/>
<path d="M5150 764 l0 -374 135 0 135 0 0 376 c0 355 -1 376 -17 368 -29 -13
-191 -17 -223 -6 l-30 11 0 -375z"/>
<path d="M7530 764 l0 -374 135 0 135 0 0 376 c0 355 -1 376 -17 368 -29 -13
-191 -17 -223 -6 l-30 11 0 -375z"/>
</g>
</svg>
         assets/images/providers/klaviyo.svg                                                                 0000644                 00000015143 15266052552 0013543 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <svg width="8782" height="2582" viewBox="0 0 8782 2582" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4750.55 344.437C4750.31 298.348 4768.14 254 4800.22 220.902C4832.3 187.805 4876.06 168.592 4922.14 167.382C4968.93 167.928 5013.64 186.757 5046.73 219.844C5079.82 252.931 5098.65 297.649 5099.19 344.437C5098.09 390.547 5078.91 434.376 5045.79 466.475C5012.67 498.573 4968.26 516.369 4922.14 516.025C4876.77 515.586 4833.38 497.368 4801.29 465.283C4769.21 433.199 4750.99 389.809 4750.55 344.437V344.437ZM6124.09 717.893C6208.2 717.893 6262.04 764.575 6262.04 866.35C6252.54 965.648 6228.01 1062.93 6189.28 1154.85C6121.57 1336.95 6051.34 1545.13 5975.64 1781.91C5941.99 1667.51 5876.8 1487.51 5796.06 1277.24L5725.82 1084.62C5693.35 1006.77 5673.21 924.33 5666.1 840.275C5663.9 824.814 5665 809.063 5669.32 794.057C5673.65 779.05 5681.1 765.13 5691.19 753.211C5701.28 741.293 5713.78 731.646 5727.87 724.906C5741.96 718.166 5757.31 714.486 5772.93 714.108V680.043H5184.14V714.108C5264.47 721.678 5329.66 798.219 5415.45 1026.16L5790.17 2017.42C5814.86 2073.62 5826.3 2134.75 5823.61 2196.08C5820.91 2257.4 5804.14 2317.29 5774.61 2371.11C5732.55 2464.89 5670.31 2511.57 5597.55 2511.57C5506.71 2511.57 5459.61 2472.46 5459.61 2392.14C5459.61 2360.59 5519.75 2298.35 5519.75 2238.63C5519.75 2154.52 5454.56 2116.25 5381.81 2116.25C5358.74 2115.45 5335.76 2119.55 5314.4 2128.29C5293.03 2137.03 5273.76 2150.2 5257.86 2166.93C5241.97 2183.67 5229.8 2203.58 5222.16 2225.37C5214.53 2247.15 5211.61 2270.31 5213.58 2293.3C5216.98 2372.85 5251.69 2447.82 5310.15 2501.87C5368.61 2555.92 5446.07 2584.66 5525.64 2581.81C5564.4 2583.14 5603.04 2576.81 5639.34 2563.17C5675.64 2549.53 5708.89 2528.85 5737.18 2502.32C5768.3 2479.19 5807.41 2398.44 5828.44 2359.33C5844.1 2328.36 5857.19 2296.15 5867.55 2263.02C5885.64 2218.87 5896.15 2182.7 5903.72 2159.15C5911.29 2135.59 5927.27 2096.48 5945.78 2036.76L5987.83 1904.29C6099.7 1555.64 6206.52 1251.16 6307.88 993.779C6367.6 845.322 6445.82 757.005 6484.93 738.921C6505.95 727.497 6528.67 719.545 6552.22 715.37V681.304H6124.09V717.893ZM2016.92 1948.45C1941.22 1935.41 1876.45 1867.7 1876.45 1724.71V0.420548L1447.49 93.3639V129.952C1520.24 121.962 1593.42 187.148 1593.42 325.091V1724.71C1593.42 1860.13 1520.24 1937.93 1447.49 1948.45L1426.88 1950.97C1389.66 1953.63 1352.37 1946.99 1318.37 1931.62C1256.47 1903.18 1205.1 1855.94 1171.6 1796.62L972.675 1477C930.814 1410.08 868.411 1358.51 794.8 1330C721.189 1301.5 640.328 1297.59 564.313 1318.87L788.05 1071.58C956.273 884.434 1113.56 764.575 1261.6 715.37V681.304H770.807V715.37C896.974 764.575 890.666 873.92 744.732 1045.51L430.155 1412.66V0L0.764648 93.3639V129.952C73.5212 129.952 146.698 202.709 146.698 330.138V1724.71C146.698 1878.21 76.4651 1937.93 0.764648 1948.45V1982.09H570.621V1948.45C476.837 1935.41 430.155 1864.33 430.155 1724.71V1467.33L552.537 1334.01L849.031 1820.6C919.264 1937.51 984.03 1984.19 1088.33 1984.19H2078.32V1957.7C2078.32 1957.7 2049.72 1953.91 2016.92 1948.45ZM3337.47 1952.23V1986.3C3337.47 1986.3 3045.61 2091.02 2957.29 1913.12C2933.75 1866.18 2920.83 1814.63 2919.44 1762.14C2879.32 1839.72 2818.44 1904.63 2743.59 1949.63C2668.73 1994.63 2582.85 2017.95 2495.52 2017C2285.24 2017 2133.84 1918.17 2133.84 1702C2133.71 1647.25 2150.13 1593.75 2180.94 1548.5C2243.18 1455.13 2315.94 1405.51 2451.36 1353.36C2519.07 1327.7 2577.53 1309.2 2619.58 1296.16C2661.64 1283.12 2720.94 1270.09 2787.8 1257.47L2919.44 1225.93V1069.9C2919.44 809.995 2807.57 691.398 2651.54 691.398C2529.16 691.398 2458.93 772.145 2458.93 865.929C2458.93 917.658 2495.52 992.097 2495.52 1048.03C2495.52 1120.79 2430.33 1174.2 2339.49 1174.2C2248.65 1174.2 2204.49 1103.97 2204.49 1010.18C2204.49 916.396 2251.17 830.602 2347.48 757.846C2438.92 686.915 2551.38 648.444 2667.1 648.501C3032.57 648.501 3191.96 821.771 3197.85 1202.38V1775.18V1783.17C3199.11 1820.6 3209.62 1972.84 3337.47 1952.23ZM2919.44 1265.04C2903.88 1273.03 2867.71 1286.07 2807.57 1307.09L2688.13 1351.25C2657.85 1365.55 2635.98 1377.33 2591.82 1400.88C2557.82 1417.09 2526.93 1439.15 2500.56 1466.07C2456.54 1515.71 2430.86 1578.94 2427.81 1645.22C2427.81 1798.73 2511.92 1882 2638.09 1882C2707.53 1879.83 2773.97 1853.16 2825.65 1806.72C2855.91 1782.31 2880.12 1751.23 2896.38 1715.93C2912.65 1680.62 2920.54 1642.03 2919.44 1603.17V1265.04ZM7736.93 1334.01C7738.14 1423.73 7721.51 1512.8 7688.01 1596.04C7654.51 1679.28 7604.81 1755.04 7541.79 1818.91C7482 1882.25 7409.91 1932.71 7329.92 1967.19C7249.94 2001.68 7163.76 2019.47 7076.66 2019.47C6989.56 2019.47 6903.37 2001.68 6823.39 1967.19C6743.41 1932.71 6671.31 1882.25 6611.52 1818.91C6485.64 1689.13 6416.24 1514.8 6418.48 1334.01C6417.14 1244.89 6433.51 1156.4 6466.65 1073.66C6499.78 990.927 6549.03 915.599 6611.52 852.051C6670.89 787.997 6742.85 736.895 6822.89 701.948C6902.93 667.002 6989.32 648.963 7076.66 648.963C7163.99 648.963 7250.39 667.002 7330.43 701.948C7410.47 736.895 7482.43 787.997 7541.79 852.051C7604.99 915.185 7654.84 990.392 7688.36 1073.19C7721.89 1155.99 7738.41 1244.69 7736.93 1334.01V1334.01ZM7311.75 871.397C7260.02 770.463 7192.31 711.584 7113.25 695.603C6952.17 663.22 6810.02 828.5 6757.03 1091.77C6734.17 1209.77 6728.65 1330.48 6740.63 1450.08C6752.05 1570.58 6785.25 1688 6838.62 1796.62C6890.35 1897.14 6958.06 1956.02 7037.54 1972C7198.2 2004.38 7344.55 1831.95 7397.96 1566.16C7442.96 1342 7419.41 1071.58 7311.75 871.397ZM5073.12 1724.71V681.304H4157.56V712.846C4279.94 730.93 4337.98 823.032 4283.73 972.751C3996.07 1750.78 4012.89 1715.88 3996.07 1778.12C3977.98 1718.4 3936.35 1571.21 3869.9 1386.58C3803.45 1201.96 3758.03 1079.57 3739.95 1024.9C3669.71 809.154 3693.27 730.51 3807.66 715.37V681.304H3213.83V715.37C3302.57 733.454 3382.05 834.808 3445.56 1016.91L3536.4 1253.68C3636.49 1509.38 3754.25 1862.65 3793.36 1982.09H3990.6C4054.11 1797.47 4308.96 1056.86 4343.03 983.265C4371.57 910.415 4414.48 844.052 4469.2 788.126V788.126C4516.8 742.377 4579.82 716.119 4645.83 714.528C4664.53 713.155 4683.31 715.741 4700.94 722.119C4718.57 728.496 4734.66 738.522 4748.16 751.54C4761.65 764.558 4772.25 780.276 4779.26 797.668C4786.26 815.06 4789.52 833.734 4788.82 852.471V969.387C4788.82 969.387 4788.82 971.489 4788.82 972.751V1724.71C4788.82 1870.22 4718.59 1937.93 4645.41 1948.45V1982.09H5212.74V1948.45C5135.36 1937.93 5073.12 1870.22 5073.12 1724.71Z" fill="#232426"/>
<path d="M8651.28 0H7656.24V681.304H8651.28L8454.88 340.652L8651.28 0Z" fill="#232426"/>
<path d="M8705.83 633.361V681.304H8700.79V633.361H8681.02V628.734H8723.08V633.361H8705.83Z" fill="#232426"/>
<path d="M8754.99 681.304L8736.9 635.884V681.304H8732.28V628.734H8739.01L8757.09 674.575L8775.18 628.734H8781.9V681.304H8776.86V635.884L8759.19 681.304H8754.99Z" fill="#232426"/>
</svg>
                                                                                                                                                                                                                                                                                                                                                                                                                             assets/images/providers/brevo.svg                                                                   0000644                 00000002627 15266052557 0013212 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <svg fill="none" height="732" viewBox="0 0 81.177 24" width="2500" xmlns="http://www.w3.org/2000/svg"><path d="m66.587 14.86c0-3.633 2.3-6.188 5.57-6.188s5.607 2.554 5.607 6.188c0 3.635-2.336 6.045-5.607 6.045-3.27 0-5.57-2.52-5.57-6.045zm-3.414 0c0 5.362 3.738 9.14 8.984 9.14s9.02-3.778 9.02-9.14c0-5.36-3.738-9.282-9.02-9.282s-8.984 3.85-8.984 9.282zm-17.428-9.067 7.043 17.99h3.307l7.043-17.99h-3.557l-5.103 13.89h-.072l-5.103-13.89zm-13.692 8.42c.215-3.312 2.408-5.541 5.39-5.541 2.587 0 4.528 1.655 4.816 4.138h-5.966c-2.12 0-3.27.252-4.132 1.404h-.108zm-3.413.539c0 5.361 3.773 9.246 8.983 9.246 3.522 0 6.611-1.798 7.94-4.642l-2.873-1.44c-1.006 1.872-2.947 2.987-5.067 2.987-2.552 0-4.852-1.943-4.852-3.814 0-.971.647-1.404 1.581-1.404h11.391v-.97c0-5.398-3.45-9.14-8.408-9.14s-8.696 3.85-8.696 9.176m-9.774 9.03h3.235v-11.044c0-2.374 1.472-4.066 3.522-4.066.862 0 1.76.288 2.192.683.324-.864.827-1.726 1.58-2.59-.862-.72-2.334-1.188-3.772-1.188-3.953 0-6.757 2.95-6.757 7.16v11.046zm-15.63-11.907v-8.78h5.318c1.796 0 2.982 1.043 2.982 2.626 0 1.799-1.544 3.166-4.707 4.21-2.156.682-3.125 1.258-3.485 1.943zm0 8.816v-3.67c0-1.62 1.365-3.202 3.27-3.814 1.69-.576 3.09-1.152 4.276-1.763 1.581.936 2.55 2.554 2.55 4.246 0 2.878-2.73 5-6.432 5zm-3.235 3.093h7.187c5.463 0 9.558-3.417 9.558-7.951 0-2.483-1.257-4.713-3.485-6.153 1.15-1.152 1.689-2.483 1.689-4.102 0-3.347-2.409-5.577-6.037-5.577h-8.912z" fill="#0b996e"/></svg>
                                                                                                         assets/images/providers/mailerlite.svg                                                              0000644                 00000005560 15266052564 0014221 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <svg enable-background="new 0 0 437.4 110.3" viewBox="0 0 437.4 110.3" xmlns="http://www.w3.org/2000/svg"><path d="m57.2 58.5c-7.3 0-13.1 2.9-17.2 8.6-2.8-5.4-8.4-8.7-14.5-8.6-5.8-.2-11.3 2.3-15 6.8v-1c0-2.9-2.4-5.3-5.3-5.3s-5.2 2.4-5.2 5.3v40.3c.1 2.9 2.6 5.2 5.5 5 2.7-.1 4.9-2.3 5-5v-27.9c2.4-3.9 6-8.3 12.5-8.3 6.2 0 8.6 3 8.6 10.8v25.4c.1 2.9 2.6 5.2 5.5 5 2.7-.1 4.9-2.3 5-5v-27.9c2.4-3.9 6-8.3 12.5-8.3 6.2 0 8.6 3 8.6 10.8v25.4c.1 2.9 2.6 5.2 5.5 5 2.7-.1 4.9-2.3 5-5v-27.1c.2-9.2-5-19-16.5-19z"/><path d="m107.6 58.5c-5.7 0-11.4 1.2-16.6 3.5-1.9.7-3.1 2.5-3.1 4.5 0 2.5 1.9 4.5 4.4 4.6h.1c.5 0 1-.1 1.5-.2 3.9-1.4 8-2.2 12.2-2.2 8.1 0 11.5 3 11.6 10.4h-10.4c-14.1-.1-22.6 5.9-22.6 16.1 0 9.9 8.2 15.1 16.3 15.1 6.1.2 12-1.9 16.7-5.8v.1c.1 2.9 2.6 5.2 5.5 5 2.7-.1 4.9-2.3 5-5v-26.6c0-9.7-6.4-19.5-20.6-19.5zm-3.4 42.3c-5.9 0-9-2.2-9-6.5 0-1.6 0-6.4 13.2-6.4h9.3v6.5c-2.8 3-7.9 6.4-13.5 6.4z"/><path d="m148.3 39.7h.6c3.2 0 5.8 2.6 5.8 5.8v.4c0 3.2-2.6 5.8-5.8 5.8h-.6c-3.2 0-5.8-2.6-5.8-5.8v-.4c0-3.2 2.6-5.8 5.8-5.8z"/><path d="m148.5 58.9c-2.9 0-5.2 2.3-5.2 5.2v.1 40.3c.1 2.9 2.6 5.2 5.5 5 2.7-.1 4.9-2.3 5-5v-40.3c0-2.9-2.3-5.3-5.3-5.3z"/><path d="m175.2 38.2c-2.9 0-5.2 2.3-5.2 5.2v.1 61c.1 2.9 2.6 5.2 5.5 5 2.7-.1 4.9-2.3 5-5v-61c0-2.9-2.3-5.3-5.2-5.3-.1 0-.1 0-.1 0z"/><path d="m216.9 58.5c-7.5 0-13.7 2.8-17.9 8.1-3.6 4.6-5.6 10.9-5.6 17.8 0 16.2 9.5 25.9 25.3 25.9 8.7 0 13-2 17-4.3 1.7-.8 2.8-2.5 3-4.4 0-2.6-2.1-4.7-4.6-4.7h-.1c-.7 0-1.5.1-2.1.5-3.8 2-8 2.9-12.2 2.8-8.8 0-14-4.1-15.3-11.9h30.6c2.8.1 5.2-2.1 5.3-4.9 0-.1 0-.2 0-.4 0-16.8-12.1-24.5-23.4-24.5zm0 9.2c4.4 0 11.8 2.5 12.8 11.7h-25.6c1-8.1 7-11.7 12.8-11.7z"/><path d="m276.8 58.5h-.5c-5 0-9.6 2.4-13.8 7v-1.2c0-2.9-2.4-5.3-5.3-5.3s-5.3 2.4-5.2 5.3v40.3c.1 2.9 2.6 5.2 5.5 5 2.7-.1 4.9-2.3 5-5v-28.3c3.4-4.9 8.2-7.6 13.6-7.6h.5c2.8.2 5.1-1.9 5.3-4.7 0-.1 0-.2 0-.3.1-2.8-2.2-5.2-5-5.3 0 .1-.1.1-.1.1z"/><g fill="#21c16c"><path d="m422.4 0h-106.4c-8.3 0-15 6.7-15 15v95.3l20.7-20.5h100.8c8.3 0 15-6.7 15-15v-59.8c-.1-8.3-6.8-15-15.1-15zm-89.5 67.2c0 2.2-1.8 4-4 4s-4-1.8-4-4v-42.5c0-2.2 1.8-4 4-4s4 1.8 4 4zm18.6 0c0 2.2-1.8 4-4 4s-4-1.8-4-4v-28.1c0-2.2 1.8-4 4-4s4 1.8 4 4zm.5-40.9c0 2.4-1.9 4.3-4.3 4.3h-.4c-2.4 0-4.3-1.9-4.3-4.3v-.3c0-2.4 1.9-4.3 4.3-4.3h.4c2.4 0 4.3 1.9 4.3 4.3zm28.2 43.9c-1.8.9-3.8 1.3-5.9 1.3-6.9 0-10.6-3.3-10.6-9.6v-19.3h-3.6c-1.2 0-2.2-.9-2.2-2.1v-.1c0-.7.4-1.4 1-1.9l8.9-8.7c.5-.5 1.1-.8 1.8-.9 1.2 0 2.3 1 2.3 2.2v.1 4.2h6.4c2 0 3.6 1.6 3.6 3.6s-1.6 3.6-3.6 3.6h-6.4v18.8c0 2.7 1.4 2.9 3.3 2.9.8 0 1.6-.1 2.3-.3.5-.2 1.1-.2 1.6-.3 1.8 0 3.3 1.5 3.4 3.4-.2 1.3-1.1 2.6-2.3 3.1zm24.5-6.2c2.9.1 5.8-.6 8.4-1.9.5-.3 1.1-.4 1.6-.4 2 0 3.6 1.5 3.6 3.5v.1c-.1 1.4-.9 2.7-2.2 3.3-2.8 1.6-5.8 3-12 3-11.2 0-18-6.9-18-18.4 0-13.5 9-18.4 16.6-18.4 11.4 0 16.6 9.1 16.6 17.5.1 2.1-1.6 3.9-3.7 4-.1 0-.2 0-.3 0h-21c1.1 5 4.7 7.7 10.4 7.7z"/><path d="m402.9 41.9c-4.4-.1-8.1 3.2-8.5 7.6h17.1c-.5-4.4-4.2-7.7-8.6-7.6z"/></g></svg>
                                                                                                                                                assets/js/chimpmatic.js                                                                             0000644                 00000131033 15266052571 0011150 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       /**
 * Admin JavaScript.
 *
 * @package   contact-form-7-mailchimp-extension
 * @author    renzo.johnson@gmail.com
 * @copyright 2014-2026 https://renzojohnson.com
 * @license   GPL-3.0+
 */

/**
 * Fix CF7's beforeunload detection for dynamically populated selects.
 *
 * CF7 compares defaultValue vs value to detect changes. PHP-generated selects
 * with selected="selected" have undefined defaultValue causing false positives.
 * This syncs defaultValue to current value on page load.
 */
document.addEventListener('DOMContentLoaded', function() {
    const form = document.getElementById('wpcf7-admin-form-element');
    if (!form) return;

    form.querySelectorAll('select').forEach(function(select) {
        if (select.value && select.defaultValue !== select.value) {
            Array.from(select.options).forEach(function(opt) {
                opt.defaultSelected = opt.selected;
            });
        }
    });

    form.querySelectorAll('input[type="checkbox"]').forEach(function(cb) {
        if (cb.value && !cb.defaultValue) {
            cb.defaultValue = cb.value;
        }
    });
});

function getFormId() {
    const dataContainer = document.getElementById('cmatic_data');
    if (dataContainer && dataContainer.dataset.formId) {
        return parseInt(dataContainer.dataset.formId, 10) || 0;
    }
    return 0;
}

function getApiValid() {
    const dataContainer = document.getElementById('cmatic_data');
    return dataContainer?.dataset?.apiValid || '0';
}

/**
 * Show inline status message near an element (non-invasive alternative to alert).
 *
 * @param {HTMLElement} targetElement Element to show message near.
 * @param {string}      message       Message text.
 * @param {string}      type          'error', 'warning', or 'success'.
 * @param {number}      duration      Auto-hide after ms (0 = manual close).
 */
function showInlineMessage(targetElement, message, type = 'warning', duration = 5000) {
    const existingMsg = targetElement.parentNode.querySelector('.cmatic-inline-msg');
    if (existingMsg) existingMsg.remove();

    const msg = document.createElement('span');
    msg.className = `cmatic-inline-msg cmatic-msg-${type}`;
    msg.textContent = message;
    msg.style.cssText = `
        display: inline-block;
        margin-left: 10px;
        padding: 4px 10px;
        border-radius: 3px;
        font-size: 13px;
        animation: cmatic-fade-in 0.3s ease;
    `;

    if (type === 'error') {
        msg.style.background = '#f8d7da';
        msg.style.color = '#721c24';
        msg.style.border = '1px solid #f5c6cb';
    } else if (type === 'warning') {
        msg.style.background = '#fff3cd';
        msg.style.color = '#856404';
        msg.style.border = '1px solid #ffeeba';
    } else {
        msg.style.background = '#d4edda';
        msg.style.color = '#155724';
        msg.style.border = '1px solid #c3e6cb';
    }

    targetElement.parentNode.insertBefore(msg, targetElement.nextSibling);

    if (duration > 0) {
        setTimeout(() => {
            msg.style.opacity = '0';
            msg.style.transition = 'opacity 0.3s ease';
            setTimeout(() => msg.remove(), 300);
        }, duration);
    }
}

/**
 * Get a newly entered API key. Stored credentials resolve server-side and
 * never enter JavaScript.
 * @returns {Promise<string>} The API key.
 */
async function getApiKey() {
    const apiInput = document.getElementById('cmatic-api');
    if (!apiInput) return '';

    const isMasked = apiInput.dataset.isMasked === '1';
    const hasKey = apiInput.dataset.hasKey === '1';
    const inputValue = apiInput.value.trim();

    if (!isMasked) {
        return inputValue;
    }

    if (!hasKey) {
        return '';
    }

    return '';
}

function setApiValid(value) {
    const dataContainer = document.getElementById('cmatic_data');
    if (dataContainer) {
        dataContainer.dataset.apiValid = value ? '1' : '0';
    }
}

function getRestUrl() {
    if (typeof wpApiSettings !== 'undefined' && wpApiSettings.root) {
        return wpApiSettings.root + 'chimpmatic/v1/';
    }
    return '/wp-json/chimpmatic/v1/';
}

const REST_ENDPOINTS = {
    SETTINGS_SAVE: 'settings/save',
    SETTINGS_CONFIG: 'settings/config',
    AUDIENCES: 'mailchimp/audiences',
    FIELDS: 'mailchimp/fields',
    GROUPS: 'mailchimp/groups',
    INTERESTS: 'mailchimp/interests',
    EXPORT_USERS: 'export/users',
    NOTICES_DISMISS: 'notices/dismiss',
    TELEMETRY_TOGGLE: 'telemetry/toggle'
};

const actionToEndpoint = {
    'wpcf7_chm_savetool': 'settings/save',
    'wpcf7_chm_savetool_cfg': 'settings/config',
    'wpcf7_chm_loadlistas': 'mailchimp/audiences',
    'wpcf7_chm_loadcampos': 'mailchimp/fields',
    'wpcf7_chm_loadgrupos': 'mailchimp/groups',
    'wpcf7_chm_get_interest': 'mailchimp/interests',
    'wpcf7_chm_exporuser': 'export/users'
};

async function chmRequest(actionOrEndpoint, data = {}) {
    const endpoint = actionToEndpoint[actionOrEndpoint] || actionOrEndpoint;
    return await chmRestRequest(endpoint, data);
}

async function chmRestRequest(endpoint, data = {}) {
    const url = getRestUrl() + endpoint;

    try {
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-WP-Nonce': wpApiSettings?.nonce || ''
            },
            body: JSON.stringify(data),
            credentials: 'same-origin'
        });

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        const result = await response.json();
        return result.html || result.data || '';
    } catch (error) {
        console.error('[Chimpmatic] Error:', error);
        throw error;
    }
}

document.addEventListener('click', function(event) {
    if (event.target && event.target.id === 'chm_submitme') {
        event.preventDefault();

        const data = {
            tool_key: document.getElementById('wpcf7-mailchimp-tool_key')?.value || '',
            chm_idformxx: getFormId(),
            uemail: document.getElementById('wpcf7-mailchimp-uemail')?.value || '',
            prod_id: document.getElementById('wpcf7-mailchimp-prod-id')?.value || ''
        };

        chmRequest('wpcf7_chm_savetool', data)
            .then(response => {
                const panel = document.getElementById('chm_panel_principal');
                if (panel) panel.innerHTML = response;
            })
            .catch(error => {
                alert('Error: ' + error.message);
            });
    }
});

document.addEventListener('click', function(event) {
    if (event.target && event.target.id === 'chm_submitme_cfg') {
        event.preventDefault();

        const data = {
            tool_key: document.getElementById('wpcf7-mailchimp-tool_key')?.value || '',
            uemail: document.getElementById('wpcf7-mailchimp-uemail')?.value || '',
            prod_id: document.getElementById('wpcf7-mailchimp-prod-id')?.value || ''
        };

        chmRequest('wpcf7_chm_savetool_cfg', data)
            .then(response => {
                const liveElements = document.querySelectorAll('.chimp-live');
                liveElements.forEach(el => el.innerHTML = response);
            })
            .catch(error => {
                alert('Error: ' + error.message);
            });
    }
});

document.addEventListener('click', function(event) {
    if (event.target && event.target.id === 'chm_activalist') {
        event.preventDefault();

        const button = event.target;
        if (button.disabled) return;

        const panel = button.closest('.cmatic-field-group') || button.closest('.mce-custom-fields');
        let apiKeyElement = panel ? panel.querySelector('#cmatic-api') : null;
        if (!apiKeyElement) {
            apiKeyElement = document.getElementById('cmatic-api');
        }

        const apiKey = apiKeyElement?.dataset?.isMasked === '1' ? '' : (apiKeyElement?.value || '');
        const dataContainer = document.getElementById('cmatic_data');
        const authType = dataContainer?.dataset?.authType || '';
        if ((!apiKey || apiKey.trim() === '') && authType !== 'oauth') {
            showInlineMessage(button, 'Enter API key first', 'warning');
            return;
        }

        const formId = getFormId();
        if (!formId || formId <= 0) {
            showInlineMessage(button, 'Save form first', 'warning');
            return;
        }

        const data = {
            tool_key: document.getElementById('wpcf7-mailchimp-tool_key')?.value || '',
            chm_idformxx: formId,
            chimpapi: apiKey
        };

        const originalText = button.value;
        button.value = 'Syncing...';
        button.disabled = true;

        chmRequest('wpcf7_chm_loadlistas', data)
            .then(response => {
                const listPanel = document.getElementById('chm_panel_listamail');
                if (listPanel) {
                    listPanel.innerHTML = response;
                }

                const valor = getApiValid();
                let attrclass = '';
                let chm_valid = '';

                if (valor === '1') {
                    attrclass = 'spt-response-out spt-valid';
                    chm_valid = '<h3 class="title">Chimpmatic<span><span class="chmm valid">API Connected</span></span></h3>';

                    button.value = 'Synced';
                    button.disabled = false;
                    setTimeout(() => {
                        button.value = 'Connected';
                        const fieldsBtn = document.getElementById('chm_selgetcampos');
                        if (fieldsBtn && !fieldsBtn.disabled) {
                            fieldsBtn.click();
                        }
                    }, 800);
                } else {
                    attrclass = 'spt-response-out';
                    chm_valid = '<h3 class="title">Chimpmatic<span><span class="chmm invalid">API Inactive</span></span></h3>';

                    button.value = originalText;
                    button.disabled = false;

                    const configPanel = document.getElementById('chm_panel_configcampos');
                    if (configPanel) {
                        configPanel.innerHTML = '<span> </span>';
                    }
                }

                if (listPanel) {
                    listPanel.className = attrclass;
                }

                const validPanel = document.getElementById('chm_apivalid');
                if (validPanel) {
                    validPanel.innerHTML = chm_valid;
                }
            })
            .catch(error => {
                button.value = originalText;
                button.disabled = false;
                alert('Error loading audiences: ' + error.message);
            });
    }
});

document.addEventListener('paste', function(event) {
    const apiInput = document.getElementById('cmatic-api');
    if (event.target !== apiInput) return;

    setTimeout(function() {
        const apiKey = apiInput.value.trim();
        if (apiKey && apiKey.length >= 30 && apiKey.includes('-')) {
            const connectBtn = document.getElementById('chm_activalist');
            if (connectBtn && !connectBtn.disabled) {
                connectBtn.click();
            }
        }
    }, 100);
});

document.addEventListener('change', function(event) {
    if (event.target && event.target.id === 'wpcf7-mailchimp-list') {
        const fieldsBtn = document.getElementById('chm_selgetcampos') || document.getElementById('mce_fetch_fields');
        if (fieldsBtn && !fieldsBtn.disabled) {
            fieldsBtn.click();
        }
    }
});

document.addEventListener('click', async function(event) {
    if (event.target && (event.target.id === 'chm_selgetcampos' || event.target.id === 'mce_fetch_fields')) {
        event.preventDefault();

        const button = event.target;
        if (button.disabled) return;

        const data = {
            tool_key: document.getElementById('wpcf7-mailchimp-tool_key')?.value || '',
            chm_idformxx: getFormId(),
            chm_listid: document.getElementById('wpcf7-mailchimp-list')?.value || '',
            chimpapi: await getApiKey()
        };

        const isInputButton = button.tagName === 'INPUT';
        const originalText = isInputButton ? button.value : button.textContent;
        if (isInputButton) {
            button.value = 'Syncing...';
        } else {
            button.textContent = 'Syncing...';
        }
        button.disabled = true;

        chmRequest('wpcf7_chm_loadcampos', data)
            .then(response => {
                const genPanel = document.getElementById('chm_panel_gencamposygrupos');

                if (genPanel) {
                    genPanel.innerHTML = response;
                    genPanel.className = 'spt-response-out';

                    const listPanel = document.getElementById('chm_panel_listamail');
                    const attrclass = listPanel ? listPanel.className : '';

                    if (attrclass === 'spt-response-out') {
                        genPanel.className = 'spt-response-out';
                    } else {
                        genPanel.className = 'spt-response-out spt-valid';
                    }

                    setTimeout(function() {
                        if (typeof applyFuzzyMatchingPro === 'function') {
                            applyFuzzyMatchingPro();
                        }
                    }, 100);
                }

                if (isInputButton) {
                    button.value = 'Synced!';
                } else {
                    button.textContent = 'Synced!';
                }
                setTimeout(() => {
                    if (isInputButton) {
                        button.value = originalText;
                    } else {
                        button.textContent = originalText;
                    }
                    button.disabled = false;
                }, 800);
            })
            .catch(error => {
                if (isInputButton) {
                    button.value = originalText;
                } else {
                    button.textContent = originalText;
                }
                button.disabled = false;
                alert('Error: ' + error.message);
            });
    }
});

document.addEventListener('click', async function(event) {
    if (event.target && event.target.id === 'chm_activagroups') {
        event.preventDefault();

        const data = {
            tool_key: document.getElementById('wpcf7-mailchimp-tool_key')?.value || '',
            chm_idformxx: getFormId(),
            chm_listid: document.getElementById('wpcf7-mailchimp-list')?.value || '',
            chimpapi: await getApiKey()
        };

        chmRequest('wpcf7_chm_loadgrupos', data)
            .then(response => {
                const groupPanel = document.getElementById('chm_panel_listgroup');
                if (groupPanel) {
                    groupPanel.innerHTML = response;
                    groupPanel.className = 'spt-response-out';

                    const listPanel = document.getElementById('chm_panel_listamail');
                    const attrclass = listPanel ? listPanel.className : '';

                    if (attrclass === 'spt-response-out') {
                        groupPanel.className = 'spt-response-out';
                    } else {
                        groupPanel.className = 'spt-response-out spt-valid';
                    }
                }
            })
            .catch(error => {
                alert('Error: ' + error.message);
            });
    }
});

document.addEventListener('click', async function(event) {
    if (event.target && event.target.id === 'chm_userexport') {
        event.preventDefault();

        const valuesChecked = [];
        let icont = 1;

        document.querySelectorAll("input[type='checkbox'][name='usercheck']:checked").forEach(checkbox => {
            const idListInput = document.getElementById(`wpcf7-mailchimp-idlistexport${icont}`);
            valuesChecked.push([
                checkbox.value,
                idListInput ? idListInput.value : ''
            ]);
            icont++;
        });

        const data = {
            tool_key: document.getElementById('wpcf7-mailchimp-tool_key')?.value || '',
            chm_idformxx: getFormId(),
            chimpapi: await getApiKey(),
            cadseluser: valuesChecked
        };

        chmRequest('wpcf7_chm_exporuser', data)
            .then(response => {
                const exportPanel = document.getElementById('chm_panel_exporuser');
                if (exportPanel) {
                    exportPanel.innerHTML = response;
                }
            })
            .catch(error => {
                alert('Error: ' + error.message);
            });
    }
});

document.addEventListener('change', async function(event) {
    if (event.target && event.target.classList.contains('chimp-gg-arbirary')) {
        event.preventDefault();

        const checkbox = event.target;
        const itag = checkbox.getAttribute('data-tag');
        const xchk = checkbox.checked ? 1 : 0;
        const ggKeyInput = document.getElementById(`wpcf7-mailchimp-ggCustomKey${itag}`);

        const data = {
            valcheck: xchk,
            chm_idformxx: getFormId(),
            chm_listid: document.getElementById('wpcf7-mailchimp-list')?.value || '',
            chimpapi: await getApiKey(),
            indtag: itag,
            ggid: ggKeyInput ? ggKeyInput.value : ''
        };

        chmRequest('wpcf7_chm_get_interest', data)
            .then(response => {
                const selectElement = document.getElementById(`wpcf7-mailchimp-ggCustomValue${itag}`);
                if (selectElement) {
                    selectElement.outerHTML = response;

                    const newSelect = document.getElementById(`wpcf7-mailchimp-ggCustomValue${itag}`);
                    if (newSelect) {
                        if (xchk === 1 && newSelect.options.length > 1) {
                            newSelect.selectedIndex = 1;
                        }
                        Array.from(newSelect.options).forEach(opt => {
                            opt.defaultSelected = opt.selected;
                        });
                    }
                }

                checkbox.defaultChecked = checkbox.checked;
            })
            .catch(error => {
                alert('Error loading interests: ' + error.message);
            });
    }
});

function togglePanel(panelSelector, buttonElement, showText, hideText) {
    const panel = typeof panelSelector === 'string' ?
        (panelSelector.startsWith('.') ? document.querySelector(panelSelector) : document.getElementById(panelSelector)) :
        panelSelector;

    if (panel) {
        const isHidden = panel.style.display === 'none' || !panel.style.display || window.getComputedStyle(panel).display === 'none';
        panel.style.display = isHidden ? 'block' : 'none';

        if (buttonElement && showText && hideText) {
            buttonElement.textContent = isHidden ? hideText : showText;
        }
    }
}

document.addEventListener('DOMContentLoaded', function() {
    const connectBtn = document.getElementById('chm_activalist');
    const isApiValid = getApiValid() === '1';

    if (connectBtn && isApiValid && connectBtn.value !== 'Connected') {
        connectBtn.value = 'Connected';
    }
});

function findBestMatchPro(mergeTag, fieldName, cf7Tags) {
    if (!mergeTag || !cf7Tags || cf7Tags.length === 0) return null;

    const normalize = (str) => String(str).toLowerCase().replace(/[^a-z0-9]/g, '');
    const normalizedTag = normalize(mergeTag);
    const normalizedName = normalize(fieldName);

    const keywordMappings = {
        email: ['email', 'mail', 'correo'],
        emailaddress: ['email', 'mail'],
        fname: ['name', 'firstname', 'first', 'nombre', 'your-name'],
        firstname: ['name', 'firstname', 'first', 'nombre'],
        lname: ['lastname', 'last', 'apellido', 'surname'],
        lastname: ['lastname', 'last', 'apellido'],
        name: ['name', 'nombre', 'your-name'],
        fullname: ['name', 'fullname', 'nombre'],
        phone: ['phone', 'tel', 'telefono', 'mobile', 'cell'],
        mobilephone: ['phone', 'tel', 'mobile', 'cell'],
        address: ['address', 'direccion', 'street'],
        address1: ['address', 'address1', 'street'],
        address2: ['address2', 'apt', 'suite'],
        city: ['city', 'ciudad'],
        state: ['state', 'province', 'region', 'estado'],
        zip: ['zip', 'postal', 'postcode'],
        country: ['country', 'pais'],
        company: ['company', 'organization', 'empresa', 'org'],
        website: ['website', 'url', 'web', 'sitio'],
        birthday: ['birthday', 'birth', 'dob', 'cumpleanos'],
        message: ['message', 'comments', 'mensaje', 'nota', 'your-message']
    };

    for (const [mcKeyword, cf7Keywords] of Object.entries(keywordMappings)) {
        if (normalizedTag.includes(mcKeyword) || normalizedName.includes(mcKeyword)) {
            for (const cf7Keyword of cf7Keywords) {
                const match = cf7Tags.find(tag => {
                    const tagName = normalize(tag.name || tag);
                    return tagName.includes(cf7Keyword);
                });
                if (match) {
                    return match.name || match;
                }
            }
        }
    }

    for (const tag of cf7Tags) {
        const tagName = normalize(tag.name || tag);

        if (normalizedTag.includes(tagName) || tagName.includes(normalizedTag)) {
            return tag.name || tag;
        }

        if (normalizedName.includes(tagName) || tagName.includes(normalizedName)) {
            return tag.name || tag;
        }
    }

    return null;
}

function applyFuzzyMatchingPro() {
    const genPanel = document.getElementById('chm_panel_gencamposygrupos');
    const camposPanel = document.getElementById('chm_panel_camposforma');

    let fieldRows = document.querySelectorAll('#chm_panel_camposforma .mcee-container');

    if (fieldRows.length === 0) {
        fieldRows = document.querySelectorAll('#chm_panel_gencamposygrupos .mcee-container');
    }

    if (fieldRows.length === 0) {
        fieldRows = document.querySelectorAll('.mcee-container');
    }

    if (!fieldRows || fieldRows.length === 0) {
        return;
    }

    const cf7TagsSet = new Set();
    const allDropdowns = document.querySelectorAll('[id^="wpcf7-mailchimp-CustomValue"]');

    allDropdowns.forEach(dropdown => {
        Array.from(dropdown.options).forEach(option => {
            if (option.value && option.value.trim() !== '' && option.value !== ' ') {
                cf7TagsSet.add(option.value);
            }
        });
    });

    const cf7Tags = Array.from(cf7TagsSet).map(name => ({ name }));

    if (cf7Tags.length === 0) {
        return;
    }

    const changedFields = [];

    fieldRows.forEach((row, index) => {
        const keyInput = row.querySelector('[id^="wpcf7-mailchimp-CustomKey"]');
        const dropdown = row.querySelector('[id^="wpcf7-mailchimp-CustomValue"]');

        if (!keyInput || !dropdown) return;

        if (dropdown.value && dropdown.value.trim() !== '' && dropdown.value !== ' ') {
            return;
        }

        const dropdownOptions = [];
        Array.from(dropdown.options).forEach(option => {
            if (option.value && option.value.trim() !== '' && option.value !== ' ') {
                dropdownOptions.push({ name: option.value });
            }
        });

        if (dropdownOptions.length === 0) {
            return;
        }

        const mergeTag = keyInput.value;
        const label = row.querySelector('label');
        const fieldName = label ? label.textContent.split('*|')[0].trim() : mergeTag;

        const bestMatch = findBestMatchPro(mergeTag, fieldName, dropdownOptions);
        if (bestMatch) {
            dropdown.value = bestMatch;
            const fieldMatch = dropdown.id.match(/wpcf7-mailchimp-CustomValue(\d+)/);
            if (fieldMatch) {
                changedFields.push({ field: 'CustomValue' + fieldMatch[1], value: bestMatch });
            }
            Array.from(dropdown.options).forEach(opt => {
                opt.defaultSelected = (opt.value === bestMatch);
            });
        }
    });

    if (changedFields.length > 0) {
        saveFieldMappingsPro(changedFields);
    }
}

async function saveFieldMappingsPro(fields) {
    const formId = getFormId();
    if (!formId || fields.length === 0) return;

    const restUrl = (typeof wpApiSettings !== 'undefined' && wpApiSettings.root)
        ? wpApiSettings.root + 'chimpmatic-lite/v1/form/field'
        : '/wp-json/chimpmatic-lite/v1/form/field';

    for (const { field, value } of fields) {
        try {
            await fetch(restUrl, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-WP-Nonce': wpApiSettings?.nonce || ''
                },
                body: JSON.stringify({ form_id: formId, field, value })
            });
        } catch (error) {
            console.error('Failed to save field mapping:', field, error);
        }
    }
}

(function() {
    document.addEventListener('change', function(e) {
        if (e.target.type !== 'checkbox') return;
        const chip = e.target.closest('.cmatic-tag-chip');
        if (!chip) return;

        const checkbox = e.target;
        chip.classList.toggle('selected', checkbox.checked);

        const nameMatch = checkbox.name.match(/\[labeltags\]\[([^\]]+)\]/);
        if (!nameMatch) return;

        const tagName = nameMatch[1];
        const formId = getFormId();

        if (!formId) return;

        const restUrl = (typeof wpApiSettings !== 'undefined' && wpApiSettings.root)
            ? wpApiSettings.root + 'chimpmatic-lite/v1/form/field'
            : '/wp-json/chimpmatic-lite/v1/form/field';

        fetch(restUrl, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-WP-Nonce': wpApiSettings?.nonce || ''
            },
            body: JSON.stringify({
                form_id: formId,
                field: 'labeltags.' + tagName,
                value: checkbox.checked
            })
        })
        .then(response => response.json())
        .then(data => {
            if (data.success) {
                checkbox.defaultChecked = checkbox.checked;
            }
        })
        .catch(error => {
            console.error('Tag auto-save failed:', error);
        });
    });
})();

(function() {
    document.addEventListener('change', function(e) {
        if (e.target.tagName !== 'SELECT') return;

        const nameMatch = e.target.name.match(/wpcf7-mailchimp\[GDPRCustomValue(\d+)\]/);
        if (!nameMatch) return;

        const fieldIndex = nameMatch[1];
        const formId = getFormId();
        if (!formId) return;

        const restUrl = (typeof wpApiSettings !== 'undefined' && wpApiSettings.root)
            ? wpApiSettings.root + 'chimpmatic-lite/v1/form/field'
            : '/wp-json/chimpmatic-lite/v1/form/field';

        fetch(restUrl, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-WP-Nonce': wpApiSettings?.nonce || ''
            },
            body: JSON.stringify({
                form_id: formId,
                field: 'GDPRCustomValue' + fieldIndex,
                value: e.target.value.trim()
            })
        })
        .then(response => response.json())
        .then(data => {
            if (data.success) {
                e.target.dataset.savedValue = e.target.value;
            }
        })
        .catch(error => {
            console.error('GDPR auto-save failed:', error);
        });
    });
})();

(function() {
    document.addEventListener('change', function(e) {
        if (e.target.tagName !== 'SELECT') return;

        const nameMatch = e.target.name.match(/wpcf7-mailchimp\[ggCustomValue(\d+)\]/);
        if (!nameMatch) return;

        const fieldIndex = nameMatch[1];
        const formId = getFormId();
        if (!formId) return;

        const restUrl = (typeof wpApiSettings !== 'undefined' && wpApiSettings.root)
            ? wpApiSettings.root + 'chimpmatic-lite/v1/form/field'
            : '/wp-json/chimpmatic-lite/v1/form/field';

        fetch(restUrl, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-WP-Nonce': wpApiSettings?.nonce || ''
            },
            body: JSON.stringify({
                form_id: formId,
                field: 'ggCustomValue' + fieldIndex,
                value: e.target.value.trim()
            })
        })
        .then(response => response.json())
        .then(data => {
            if (data.success) {
                e.target.dataset.savedValue = e.target.value;
            }
        })
        .catch(error => {
            console.error('Groups auto-save failed:', error);
        });
    });
})();

(function() {
    const globalFields = ['debug', 'backlink', 'auto_update', 'telemetry'];

    document.addEventListener('change', function(e) {
        if (e.target.type !== 'checkbox') return;

        const fieldName = e.target.dataset.field;
        if (!fieldName) return;

        if (globalFields.includes(fieldName)) return;

        const toggleTarget = e.target.dataset.toggleTarget;
        if (toggleTarget) {
            const target = document.querySelector(toggleTarget);
            if (target) {
                target.classList.toggle('cmatic-hidden', !e.target.checked);
            }
        }

        const formId = getFormId();
        if (!formId) return;

        const toggle = e.target.closest('.cmatic-toggle');
        if (toggle) {
            toggle.classList.add('is-saving');
        }

        const restUrl = (typeof wpApiSettings !== 'undefined' && wpApiSettings.root)
            ? wpApiSettings.root + 'cmatic/form/setting'
            : '/wp-json/cmatic/form/setting';

        fetch(restUrl, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-WP-Nonce': wpApiSettings?.nonce || ''
            },
            body: JSON.stringify({
                form_id: formId,
                field: fieldName,
                value: e.target.checked
            })
        })
        .then(response => response.json())
        .then(data => {
            if (toggle) {
                toggle.classList.remove('is-saving');
            }
            if (data.success) {
                e.target.defaultChecked = e.target.checked;
            } else {
                e.target.checked = !e.target.checked;
                if (toggleTarget) {
                    const target = document.querySelector(toggleTarget);
                    if (target) {
                        target.classList.toggle('cmatic-hidden', !e.target.checked);
                    }
                }
                console.error('Setting save failed:', data.message);
            }
        })
        .catch(error => {
            if (toggle) {
                toggle.classList.remove('is-saving');
            }
            e.target.checked = !e.target.checked;
            if (toggleTarget) {
                const target = document.querySelector(toggleTarget);
                if (target) {
                    target.classList.toggle('cmatic-hidden', !e.target.checked);
                }
            }
            console.error('Setting auto-save failed:', error);
        });
    });
})();

document.addEventListener('DOMContentLoaded', function() {
    const activationForm = document.getElementById('chimpmatic-activation-form');
    const deactivateBtn = document.getElementById('chimpmatic-deactivate-btn');

    function showFeedback(message, type = 'info') {
        const feedbackDiv = document.getElementById('chimpmatic-license-feedback');
        const messageP = document.getElementById('chimpmatic-license-feedback-message');

        if (!feedbackDiv || !messageP) return;

        messageP.textContent = message;

        if (type === 'success') {
            feedbackDiv.style.backgroundColor = '#d4edda';
            feedbackDiv.style.borderColor = '#c3e6cb';
            feedbackDiv.style.color = '#155724';
            feedbackDiv.style.border = '1px solid #c3e6cb';
        } else if (type === 'error') {
            feedbackDiv.style.backgroundColor = '#f8d7da';
            feedbackDiv.style.borderColor = '#f5c6cb';
            feedbackDiv.style.color = '#721c24';
            feedbackDiv.style.border = '1px solid #f5c6cb';
        } else {
            feedbackDiv.style.backgroundColor = '#d1ecf1';
            feedbackDiv.style.borderColor = '#bee5eb';
            feedbackDiv.style.color = '#0c5460';
            feedbackDiv.style.border = '1px solid #bee5eb';
        }

        feedbackDiv.style.display = 'block';
    }

    function hideFeedback() {
        const feedbackDiv = document.getElementById('chimpmatic-license-feedback');
        if (feedbackDiv) {
            feedbackDiv.style.display = 'none';
        }
    }

    if (activationForm) {
        activationForm.addEventListener('submit', async function(e) {
            e.preventDefault();

            const licenseKey = document.getElementById('license_key').value.trim();
            const productId = document.getElementById('product_id').value.trim();
            const activateBtn = document.getElementById('chimpmatic-activate-btn');

            if (!licenseKey || !productId) {
                showFeedback('Please enter both license key and product ID', 'error');
                return;
            }

            activateBtn.disabled = true;
            activateBtn.textContent = 'Activating...';
            hideFeedback();

            try {
                showFeedback('Checking activation status...', 'info');
                await new Promise(resolve => setTimeout(resolve, 500));

                const restUrl = (typeof wpApiSettings !== 'undefined' && wpApiSettings.root) ?
                    wpApiSettings.root + 'chimpmatic/v1/license/activate' :
                    '/wp-json/chimpmatic/v1/license/activate';

                const response = await fetch(restUrl, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-WP-Nonce': wpApiSettings?.nonce || ''
                    },
                    body: JSON.stringify({
                        license_key: licenseKey,
                        product_id: productId
                    }),
                    credentials: 'same-origin'
                });

                const result = await response.json();

                if (result.success) {
                    if (result.was_deactivated) {
                        showFeedback('Site already activated... deactivating previous activation...', 'info');
                        await new Promise(resolve => setTimeout(resolve, 800));
                    }

                    showFeedback('Activating license...', 'info');
                    await new Promise(resolve => setTimeout(resolve, 500));

                    showFeedback('Success! License activated.', 'success');
                    await new Promise(resolve => setTimeout(resolve, 1000));

                    window.location.reload();
                } else {
                    showFeedback(result.message || 'Activation failed', 'error');
                    activateBtn.disabled = false;
                    activateBtn.textContent = 'Activate License';
                }
            } catch (error) {
                showFeedback('Error: ' + error.message, 'error');
                activateBtn.disabled = false;
                activateBtn.textContent = 'Activate License';
            }
        });
    }

    if (deactivateBtn) {
        deactivateBtn.addEventListener('click', async function(e) {
            if (!e.target.onclick || e.target.onclick.call(e.target) === false) {
                return;
            }

            const btn = e.target;
            btn.disabled = true;
            btn.textContent = 'Deactivating...';

            try {
                const restUrl = (typeof wpApiSettings !== 'undefined' && wpApiSettings.root) ?
                    wpApiSettings.root + 'chimpmatic/v1/license/deactivate' :
                    '/wp-json/chimpmatic/v1/license/deactivate';

                const response = await fetch(restUrl, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-WP-Nonce': wpApiSettings?.nonce || ''
                    },
                    credentials: 'same-origin'
                });

                const result = await response.json();

                if (result.success) {
                    window.location.reload();
                } else {
                    alert('Deactivation failed: ' + (result.message || 'Unknown error'));
                    btn.disabled = false;
                    btn.textContent = 'Deactivate License For This Site';
                }
            } catch (error) {
                alert('Error: ' + error.message);
                btn.disabled = false;
                btn.textContent = 'Deactivate License For This Site';
            }
        });
    }
});

(function() {
    'use strict';

    const activateBtn = document.getElementById('cmatic-cf7-activate-btn');
    const deactivateBtn = document.getElementById('cmatic-cf7-deactivate-btn');
    const feedbackDiv = document.getElementById('cmatic-cf7-license-feedback');

    let form = activateBtn ? activateBtn.closest('form') : (deactivateBtn ? deactivateBtn.closest('form') : null);

    if (!form && activateBtn) {
        let element = activateBtn.parentElement;
        while (element && element.tagName !== 'FORM') {
            element = element.parentElement;
        }
        form = element;
    }

    function showFeedback(message, type = 'info') {
        if (!feedbackDiv) return;

        feedbackDiv.textContent = message;
        feedbackDiv.style.display = 'block';

        if (type === 'success') {
            feedbackDiv.style.backgroundColor = '#d4edda';
            feedbackDiv.style.color = '#155724';
            feedbackDiv.style.border = '1px solid #c3e6cb';
        } else if (type === 'error') {
            feedbackDiv.style.backgroundColor = '#f8d7da';
            feedbackDiv.style.color = '#721c24';
            feedbackDiv.style.border = '1px solid #f5c6cb';
        } else {
            feedbackDiv.style.backgroundColor = '#d1ecf1';
            feedbackDiv.style.color = '#0c5460';
            feedbackDiv.style.border = '1px solid #bee5eb';
        }
    }

    function hideFeedback() {
        if (feedbackDiv) {
            feedbackDiv.style.display = 'none';
        }
    }

    if (activateBtn) {
        activateBtn.addEventListener('click', async function(e) {
            e.preventDefault();

            const licenseKey = document.getElementById('license_key').value.trim();
            const productId = document.getElementById('product_id').value.trim();

            if (!licenseKey || !productId) {
                showFeedback('Please enter both License Key and Product ID', 'error');
                return;
            }

            activateBtn.disabled = true;
            activateBtn.textContent = 'Activating...';
            hideFeedback();

            try {
                const restUrl = (typeof wpApiSettings !== 'undefined' && wpApiSettings.root) ?
                    wpApiSettings.root + 'chimpmatic/v1/license/activate' :
                    '/wp-json/chimpmatic/v1/license/activate';

                const response = await fetch(restUrl, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-WP-Nonce': wpApiSettings?.nonce || ''
                    },
                    credentials: 'same-origin',
                    body: JSON.stringify({
                        license_key: licenseKey,
                        product_id: productId
                    })
                });

                const result = await response.json();

                if (result.success) {
                    showFeedback('License activated successfully!', 'success');

                    setTimeout(function() {
                        window.location.reload();
                    }, 1000);
                } else {
                    showFeedback(result.message || 'Activation failed', 'error');
                    activateBtn.disabled = false;
                    activateBtn.textContent = 'Activate License';
                }
            } catch (error) {
                showFeedback('Network error: ' + error.message, 'error');
                activateBtn.disabled = false;
                activateBtn.textContent = 'Activate License';
            }
        });
    }

    if (deactivateBtn) {
        deactivateBtn.addEventListener('click', async function(e) {
            e.preventDefault();

            deactivateBtn.disabled = true;
            deactivateBtn.textContent = 'Deactivating...';
            hideFeedback();

            try {
                const restUrl = (typeof wpApiSettings !== 'undefined' && wpApiSettings.root) ?
                    wpApiSettings.root + 'chimpmatic/v1/license/deactivate' :
                    '/wp-json/chimpmatic/v1/license/deactivate';

                const response = await fetch(restUrl, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-WP-Nonce': wpApiSettings?.nonce || ''
                    },
                    credentials: 'same-origin'
                });

                const result = await response.json();

                if (result.success) {
                    showFeedback('License deactivated successfully!', 'success');

                    setTimeout(function() {
                        window.location.reload();
                    }, 1000);
                } else {
                    showFeedback(result.message || 'Deactivation failed', 'error');
                    deactivateBtn.disabled = false;
                    deactivateBtn.textContent = 'Deactivate License';
                }
            } catch (error) {
                showFeedback('Network error: ' + error.message, 'error');
                deactivateBtn.disabled = false;
                deactivateBtn.textContent = 'Deactivate License';
            }
        });
    }
})();

(function() {
    'use strict';

    const updateBtn = document.querySelector('.cmatic-update-dependencies-btn');
    if (updateBtn) {
        updateBtn.addEventListener('click', function(e) {
            updateBtn.disabled = true;
            updateBtn.textContent = 'Updating...';
            updateBtn.style.opacity = '0.6';

            const dismissBtn = document.querySelector('.cmatic-dismiss-notice-btn');
            if (dismissBtn) {
                dismissBtn.disabled = true;
                dismissBtn.style.opacity = '0.6';
            }

            const noticeDiv = updateBtn.closest('.notice');
            if (noticeDiv) {
                const dismissX = noticeDiv.querySelector('.notice-dismiss');
                if (dismissX) {
                    dismissX.style.pointerEvents = 'none';
                    dismissX.style.opacity = '0.3';
                }
            }
        });
    }

    const dismissBtn = document.querySelector('.cmatic-dismiss-notice-btn');
    if (dismissBtn) {
        dismissBtn.addEventListener('click', function(e) {
            e.preventDefault();

            const nonce = dismissBtn.getAttribute('data-nonce');
            if (!nonce) {
                return;
            }

            dismissBtn.disabled = true;
            dismissBtn.textContent = 'Dismissing...';

            const restUrl = getRestUrl() + REST_ENDPOINTS.NOTICES_DISMISS;

            fetch(restUrl, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-WP-Nonce': wpApiSettings?.nonce || ''
                },
                credentials: 'same-origin',
                body: JSON.stringify({})
            })
            .then(response => response.json())
            .then(result => {
                if (result.success) {
                    const noticeDiv = dismissBtn.closest('.notice');
                    if (noticeDiv) {
                        noticeDiv.style.transition = 'opacity 0.3s';
                        noticeDiv.style.opacity = '0';
                        setTimeout(function() {
                            noticeDiv.remove();
                        }, 300);
                    }
                } else {
                    dismissBtn.disabled = false;
                    dismissBtn.textContent = 'Dismiss';
                }
            })
            .catch(error => {
                dismissBtn.disabled = false;
                dismissBtn.textContent = 'Dismiss';
            });
        });
    }
})();
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     assets/js/chimpmatic-lite-deactivate.js                                                             0000644                 00000030377 15266052572 0014224 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       /**
 * Deactivation survey modal.
 *
 * @package   contact-form-7-mailchimp-extension
 * @author    renzo.johnson@gmail.com
 * @copyright 2014-2026 https://renzojohnson.com
 * @license   GPL-3.0+
 */

(function () {
	'use strict';

	const config = window.cmaticDeactivate || {};
	let deactivateUrl = '';
	let modalElement = null;
	let pluginsCache = null;
	const MAX_RETRIES = 3;

	document.addEventListener('DOMContentLoaded', () => {
		init();
	});

	function init() {
		const deactivateLink = findDeactivateLink();
		if (!deactivateLink) return;

		deactivateUrl = deactivateLink.href;
		buildModal();
		attachEventListeners(deactivateLink);
	}

	function findDeactivateLink() {
		const row = document.querySelector(`tr[data-slug="${config.pluginSlug}"]`);
		return row ? row.querySelector('.deactivate a') : null;
	}

	function buildModal() {
		modalElement = document.getElementById('cmatic-deactivate-modal');
		if (!modalElement) return;

		modalElement.innerHTML = `
			<div class="cmatic-modal__overlay"></div>
			<div class="cmatic-modal__dialog">
				<div class="cmatic-modal__header">
					<h2 id="cmatic-modal-title">${escapeHtml(config.strings.title)}</h2>
					<button type="button" class="cmatic-modal__close" aria-label="${escapeHtml(config.strings.closeLabel)}">
						<span aria-hidden="true">&times;</span>
					</button>
				</div>
				<div class="cmatic-modal__body">
					<h3 id="cmatic-modal-description">${escapeHtml(config.strings.description)}</h3>
					<form id="cmatic-deactivate-form">
						<div class="cmatic-reasons" role="radiogroup" aria-labelledby="cmatic-modal-description">
							${buildReasonsList()}
						</div>
						<div class="cmatic-error-message" role="alert" aria-live="assertive"></div>
					</form>
				</div>
				<div class="cmatic-modal__footer">
					<a href="#" class="cmatic-skip-link" style="display: none;">${escapeHtml(config.strings.skipButton)}</a>
					<button type="submit" form="cmatic-deactivate-form" class="button button-primary cmatic-submit-button">
						${escapeHtml(config.strings.submitButton)}
					</button>
				</div>
			</div>
		`;
	}

	function buildReasonsList() {
		return (Array.isArray(config.reasons) ? config.reasons : []).map(reason => {
			return `
				<button type="button" class="cmatic-reason-btn" data-reason-id="${Number(reason.id) || 0}" data-input-type="${escapeHtml(reason.input_type || '')}" aria-pressed="false">
					${escapeHtml(reason.text || '')}
				</button>
			`;
		}).join('');
	}

	function escapeHtml(value) {
		const element = document.createElement('div');
		element.textContent = String(value || '');
		return element.innerHTML;
	}

	function buildInputField(reasonId, inputType) {
		const reason = config.reasons.find(r => r.id === reasonId);
		if (!reason) return '';

		const wrapper = document.createElement('div');
		wrapper.className = 'cmatic-input-wrapper';
		let input = null;
		if (inputType === 'plugin-dropdown') {
			input = document.createElement('select');
			input.disabled = true;
			const loading = document.createElement('option');
			loading.textContent = 'Loading plugins...';
			input.appendChild(loading);
		} else if (inputType === 'textfield') {
			input = document.createElement('input');
			input.type = 'text';
			input.placeholder = String(reason.placeholder || '');
			input.maxLength = Number(reason.max_length) || 200;
		}

		if (!input) return null;
		input.className = 'cmatic-input-field';
		input.setAttribute('aria-label', String(reason.placeholder || 'Select a plugin'));
		wrapper.appendChild(input);
		return wrapper;
	}

	async function fetchPluginsList() {
		if (pluginsCache) return pluginsCache;

		try {
			const response = await fetch(config.pluginsUrl, {
				method: 'GET',
				headers: {
					'X-WP-Nonce': config.restNonce,
				},
			});

			if (response.ok) {
				pluginsCache = await response.json();
				return pluginsCache;
			}
		} catch (error) {
			console.error('Chimpmatic: Failed to fetch plugins list', error);
		}
		return [];
	}

	function populatePluginDropdown(selectElement, plugins) {
		const options = [];
		const prompt = document.createElement('option');
		prompt.textContent = '-- Select Plugin --';
		options.push(prompt);
		(Array.isArray(plugins) ? plugins : []).forEach(plugin => {
			const option = document.createElement('option');
			option.value = String(plugin.value || '');
			option.textContent = String(plugin.label || '');
			options.push(option);
		});
		selectElement.replaceChildren(...options);
		selectElement.disabled = false;
	}

	function attachEventListeners(deactivateLink) {
		deactivateLink.addEventListener('click', handleDeactivateClick);
		modalElement.querySelector('.cmatic-modal__close').addEventListener('click', closeModal);
		modalElement.querySelector('.cmatic-skip-link').addEventListener('click', handleSkip);
		modalElement.querySelector('#cmatic-deactivate-form').addEventListener('submit', handleSubmit);
		modalElement.querySelectorAll('.cmatic-reason-btn').forEach(btn => btn.addEventListener('click', handleReasonClick));
		document.addEventListener('keydown', handleKeydown);
		modalElement.querySelector('.cmatic-modal__overlay').addEventListener('click', handleOverlayClick);
	}

	function handleDeactivateClick(evt) {
		evt.preventDefault();
		openModal();
	}

	function openModal() {
		modalElement.classList.add('cmatic-modal--active');
		document.body.classList.add('cmatic-modal-open');
		const firstBtn = modalElement.querySelector('.cmatic-reason-btn');
		if (firstBtn) firstBtn.focus();
		trapFocus();

		setTimeout(() => {
			const skipLink = modalElement.querySelector('.cmatic-skip-link');
			if (skipLink) {
				skipLink.style.display = 'inline';
				skipLink.style.animation = 'fadeIn 0.3s ease';
			}
		}, 10000);
	}

	function closeModal() {
		modalElement.classList.add('cmatic-modal--closing');

		setTimeout(() => {
			modalElement.classList.remove('cmatic-modal--active', 'cmatic-modal--closing');
			document.body.classList.remove('cmatic-modal-open');
			resetForm();
		}, 300);
	}

	function handleSkip(evt) {
		evt.preventDefault();

		const skipData = {
			reason_id: 0,
			reason_text: '',
		};

		const submitBtn = modalElement.querySelector('.cmatic-submit-button');
		const skipLink = modalElement.querySelector('.cmatic-skip-link');
		if (submitBtn) {
			submitBtn.disabled = true;
			submitBtn.textContent = config.strings.deactivating;
		}
		skipLink.style.display = 'none';

		fetch(config.restUrl, {
			method: 'POST',
			headers: {
				'Content-Type': 'application/json',
				'X-WP-Nonce': config.restNonce,
			},
			body: JSON.stringify(skipData),
		}).finally(() => {
			window.location.href = deactivateUrl;
		});
	}

	async function handleReasonClick(evt) {
		const clickedBtn = evt.currentTarget;
		const reasonId = parseInt(clickedBtn.dataset.reasonId, 10);
		const inputType = clickedBtn.dataset.inputType;

		modalElement.querySelectorAll('.cmatic-reason-btn').forEach(btn => {
			btn.classList.remove('selected');
			btn.setAttribute('aria-pressed', 'false');
			const nextEl = btn.nextElementSibling;
			if (nextEl && nextEl.classList.contains('cmatic-input-wrapper')) {
				nextEl.remove();
			}
		});

		clickedBtn.classList.add('selected');
		clickedBtn.setAttribute('aria-pressed', 'true');

		if (inputType && inputType !== '') {
			const inputWrapper = buildInputField(reasonId, inputType);
			if (inputWrapper) {
				clickedBtn.insertAdjacentElement('afterend', inputWrapper);
				const input = inputWrapper.querySelector('.cmatic-input-field');

				if (inputType === 'plugin-dropdown' && input) {
					const plugins = await fetchPluginsList();
					populatePluginDropdown(input, plugins);
					input.focus();
				} else if (input) {
					setTimeout(() => input.focus(), 100);
				}
			}
		}

		hideValidationError();
	}

	async function handleSubmit(evt) {
		evt.preventDefault();
		if (!validateForm()) return;

		const selectedBtn = modalElement.querySelector('.cmatic-reason-btn.selected');
		const reasonId = parseInt(selectedBtn.dataset.reasonId, 10);
		const inputWrapper = selectedBtn.nextElementSibling;
		const inputField = inputWrapper && inputWrapper.classList.contains('cmatic-input-wrapper')
			? inputWrapper.querySelector('.cmatic-input-field')
			: null;
		const reasonText = inputField ? inputField.value.trim() : '';

		setButtonsDisabled(true);

		try {
			await submitFeedback(reasonId, reasonText);
		} catch (error) {
			console.error('Chimpmatic: Failed to submit feedback', error);
		}
		window.location.href = deactivateUrl;
	}

	function validateForm() {
		const selectedBtn = modalElement.querySelector('.cmatic-reason-btn.selected');
		if (!selectedBtn) {
			showValidationError(config.strings.errorRequired);
			return false;
		}

		const inputWrapper = selectedBtn.nextElementSibling;
		if (!inputWrapper || !inputWrapper.classList.contains('cmatic-input-wrapper')) {
			hideValidationError();
			return true;
		}

		const inputField = inputWrapper.querySelector('.cmatic-input-field');
		if (!inputField) {
			hideValidationError();
			return true;
		}

		if (inputField.type === 'text' && inputField.value.trim() === '') {
			showValidationError(config.strings.errorDetails);
			inputField.focus();
			return false;
		}

		if (inputField.tagName === 'SELECT' && inputField.value === '') {
			showValidationError(config.strings.errorDropdown);
			inputField.focus();
			return false;
		}

		hideValidationError();
		return true;
	}

	async function submitFeedback(reasonId, reasonText, retry = 0) {
		const response = await fetch(config.restUrl, {
			method: 'POST',
			headers: {
				'Content-Type': 'application/json',
				'X-WP-Nonce': config.restNonce,
			},
			body: JSON.stringify({ reason_id: reasonId, reason_text: reasonText }),
		});

		if (!response.ok) {
			if (retry < MAX_RETRIES) {
				const delay = Math.pow(2, retry) * 1000;
				await new Promise(resolve => setTimeout(resolve, delay));
				console.warn(`Chimpmatic: Retry ${retry + 1}/${MAX_RETRIES}`);
				return submitFeedback(reasonId, reasonText, retry + 1);
			}
			throw new Error('Failed to submit feedback');
		}

		return response.json();
	}

	function showValidationError(message) {
		const errorEl = modalElement.querySelector('.cmatic-error-message');
		errorEl.textContent = message;
		errorEl.classList.add('cmatic-error-message--visible');
	}

	function hideValidationError() {
		const errorEl = modalElement.querySelector('.cmatic-error-message');
		errorEl.textContent = '';
		errorEl.classList.remove('cmatic-error-message--visible');
	}

	function handleKeydown(evt) {
		if (!modalElement.classList.contains('cmatic-modal--active')) return;
		if (evt.key === 'Escape') {
			evt.preventDefault();
			closeModal();
		}
	}

	function handleOverlayClick() {
		const textInputs = modalElement.querySelectorAll('input[type="text"]');
		const selects = modalElement.querySelectorAll('select');
		const hasContent = Array.from(textInputs).some(input => input.value.trim() !== '') ||
		                   Array.from(selects).some(select => select.value !== '');
		if (hasContent) {
			const confirmed = confirm('You have unsaved feedback. Close anyway?');
			if (confirmed) closeModal();
		} else {
			closeModal();
		}
	}

	function trapFocus() {
		const focusableElements = modalElement.querySelectorAll('button, input, select, [tabindex]:not([tabindex="-1"])');
		if (focusableElements.length === 0) return;

		const firstFocusable = focusableElements[0];
		const lastFocusable = focusableElements[focusableElements.length - 1];

		function handleTabKey(evt) {
			if (evt.key !== 'Tab') return;
			if (evt.shiftKey && document.activeElement === firstFocusable) {
				evt.preventDefault();
				lastFocusable.focus();
			} else if (!evt.shiftKey && document.activeElement === lastFocusable) {
				evt.preventDefault();
				firstFocusable.focus();
			}
		}

		modalElement.addEventListener('keydown', handleTabKey);
	}

	function resetForm() {
		modalElement.querySelectorAll('.cmatic-reason-btn').forEach(btn => {
			btn.classList.remove('selected');
			btn.setAttribute('aria-pressed', 'false');
			const nextEl = btn.nextElementSibling;
			if (nextEl && nextEl.classList.contains('cmatic-input-wrapper')) {
				nextEl.remove();
			}
		});
		hideValidationError();
		setButtonsDisabled(false);
		const skipLink = modalElement.querySelector('.cmatic-skip-link');
		if (skipLink) skipLink.style.display = 'none';
	}

	function setButtonsDisabled(disabled) {
		modalElement.querySelectorAll('.button').forEach(button => button.disabled = disabled);
	}
})();
                                                                                                                                                                                                                                                                 assets/js/chimpmatic-lite.js                                                                        0000644                 00000207236 15266052572 0012115 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       /**
 * Console logging handler.
 *
 * @package   contact-form-7-mailchimp-extension
 * @author    renzo.johnson@gmail.com
 * @copyright 2014-2026 https://renzojohnson.com
 * @license   GPL-3.0+
 */

'use strict';

(function() {
	if (typeof chimpmaticLite === 'undefined' || !chimpmaticLite.loggingEnabled) {
		return;
	}

	const originalConsole = {
		log: console.log,
		info: console.info,
		warn: console.warn,
		error: console.error,
		debug: console.debug
	};

	async function sendLogToServer(level, message, ...args) {
		let formattedMessage = message;
		let dataString = '';

		if (args.length > 0) {
			try {
				dataString = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)).join(' | ');
				formattedMessage += ' ' + dataString;
			} catch (e) {
				dataString = '[Unable to stringify arguments]';
			}
		}

		try {
			await fetch(`${chimpmaticLite.restUrl}logs/browser`, {
				method: 'POST',
				headers: {
					'Content-Type': 'application/json',
					'X-WP-Nonce': chimpmaticLite.restNonce
				},
				body: JSON.stringify({ level, message: formattedMessage, data: dataString })
			});
		} catch (error) {
			originalConsole.error('[Chimpmatic Lite] Failed to send log to server:', error);
		}
	}

	['log', 'info', 'warn', 'error', 'debug'].forEach(level => {
		console[level] = function(...args) {
			originalConsole[level].apply(console, args);
			const message = args[0] ? String(args[0]) : '';
			sendLogToServer(level, message, ...args.slice(1));
		};
	});
})();

function chimpmaticLiteInit() {
	if (typeof chimpmaticLite === 'undefined') {
		return;
	}

	const isProFieldPanelActive = !!document.getElementById('chm_panel_gencamposygrupos');
	let cachedLists = chimpmaticLite.lists && chimpmaticLite.lists.length > 0 ? chimpmaticLite.lists : [];

	function getFormId() {
		const dataContainer = document.getElementById('cmatic_data');
		return dataContainer?.dataset?.formId ? parseInt(dataContainer.dataset.formId, 10) || 0 : 0;
	}

	async function fetchMailchimpLists(formId, apiKey) {
		const response = await fetch(`${chimpmaticLite.restUrl}lists`, {
			method: 'POST',
			headers: {
				'Content-Type': 'application/json',
				'X-WP-Nonce': chimpmaticLite.restNonce
			},
			body: JSON.stringify({ form_id: formId, api_key: apiKey })
		});

		const data = await response.json();
		if (!response.ok) throw new Error(data.message || 'Failed to fetch lists');
		return data;
	}

	async function getDebugLog(filtered = true) {
		const url = filtered
			? `${chimpmaticLite.restUrl}logs`
			: `${chimpmaticLite.restUrl}logs?filter=0`;

		const response = await fetch(url, {
			method: 'GET',
			headers: { 'X-WP-Nonce': chimpmaticLite.restNonce }
		});

		const data = await response.json();
		if (!response.ok) throw new Error(data.message || 'Failed to fetch log');
		return data;
	}

	async function clearDebugLog() {
		const response = await fetch(`${chimpmaticLite.restUrl}logs/clear`, {
			method: 'POST',
			headers: { 'X-WP-Nonce': chimpmaticLite.restNonce }
		});

		const data = await response.json();
		if (!response.ok) throw new Error(data.message || 'Failed to clear log');
		return data;
	}

	function findBestMatch(mergeTag, fieldName, cf7Tags) {
		if (!mergeTag || !cf7Tags || cf7Tags.length === 0) return null;

		const normalize = str => String(str).toLowerCase().replace(/[^a-z0-9]/g, '');
		const normalizedTag = normalize(mergeTag);
		const normalizedName = normalize(fieldName);

		const keywordMappings = {
			email: ['email', 'mail', 'correo'],
			emailaddress: ['email', 'mail'],
			fname: ['name', 'firstname', 'first', 'nombre', 'your-name'],
			firstname: ['name', 'firstname', 'first', 'nombre'],
			lname: ['lastname', 'last', 'apellido', 'surname'],
			lastname: ['lastname', 'last', 'apellido'],
			name: ['name', 'nombre', 'your-name'],
			fullname: ['name', 'fullname', 'nombre'],
			phone: ['phone', 'tel', 'telefono', 'mobile', 'cell'],
			mobilephone: ['phone', 'tel', 'mobile', 'cell'],
			address: ['address', 'direccion', 'street'],
			address1: ['address', 'address1', 'street'],
			address2: ['address2', 'apt', 'suite'],
			city: ['city', 'ciudad'],
			state: ['state', 'province', 'region', 'estado'],
			zip: ['zip', 'postal', 'postcode'],
			country: ['country', 'pais'],
			company: ['company', 'organization', 'empresa', 'org'],
			website: ['website', 'url', 'web', 'sitio'],
			birthday: ['birthday', 'birth', 'dob', 'cumpleanos'],
			message: ['message', 'comments', 'mensaje', 'nota', 'your-message']
		};

		for (const [mcKeyword, cf7Keywords] of Object.entries(keywordMappings)) {
			if (normalizedTag.includes(mcKeyword) || normalizedName.includes(mcKeyword)) {
				for (const cf7Keyword of cf7Keywords) {
					const match = cf7Tags.find(tag => normalize(tag.name || tag).includes(cf7Keyword));
					if (match) return match.name || match;
				}
			}
		}

		for (const tag of cf7Tags) {
			const tagName = normalize(tag.name || tag);
			if (normalizedTag.includes(tagName) || tagName.includes(normalizedTag)) return tag.name || tag;
			if (normalizedName.includes(tagName) || tagName.includes(normalizedName)) return tag.name || tag;
		}

		return null;
	}

	function applyFuzzyMatching(mergeFields) {
		const cf7Tags = [];
		const sampleDropdown = document.getElementById('wpcf7-mailchimp-field4');
		if (sampleDropdown) {
			Array.from(sampleDropdown.options).forEach(option => {
				if (option.value && option.value.trim() !== '' && option.value !== ' ') {
					cf7Tags.push({ name: option.value });
				}
			});
		}

		if (cf7Tags.length === 0) return;

		const fieldMappings = [
			{ id: 'field3', index: 0 },
			{ id: 'field4', index: 1 },
			{ id: 'field5', index: 2 },
			{ id: 'field6', index: 3 }
		];

		const changedFields = [];

		fieldMappings.forEach(mapping => {
			const mergeField = mergeFields[mapping.index];
			if (!mergeField) return;

			const dropdown = document.getElementById(`wpcf7-mailchimp-${mapping.id}`);
			if (!dropdown) return;

			if (dropdown.value && dropdown.value.trim() !== '' && dropdown.value !== ' ') return;

			const bestMatch = findBestMatch(mergeField.tag, mergeField.name, cf7Tags);
			if (bestMatch) {
				dropdown.value = bestMatch;
				changedFields.push({ field: mapping.id, value: bestMatch });

				Array.from(dropdown.options).forEach(opt => {
					opt.defaultSelected = (opt.value === bestMatch);
				});
			}
		});

		if (changedFields.length > 0) {
			saveFieldMappings(changedFields);
		}
	}

	async function saveFieldMappings(fields) {
		const formId = getFormId();
		if (!formId || fields.length === 0) return;

		for (const { field, value } of fields) {
			try {
				await fetch(chimpmaticLite.restUrl + 'form/field', {
					method: 'POST',
					headers: {
						'Content-Type': 'application/json',
						'X-WP-Nonce': chimpmaticLite.restNonce
					},
					body: JSON.stringify({ form_id: formId, field, value })
				});
			} catch (error) {
				console.error('Failed to save field mapping:', field, error);
			}
		}
	}

	function updateFieldLabels(mergeFields) {
		const fieldMappings = [
			{ id: 'field3', index: 0 },
			{ id: 'field4', index: 1 },
			{ id: 'field5', index: 2 },
			{ id: 'field6', index: 3 }
		];

		fieldMappings.forEach(mapping => {
			const label = document.querySelector(`label[for="wpcf7-mailchimp-${mapping.id}"]`);
			const container = label ? label.closest('.mcee-container') : null;

				if (mergeFields[mapping.index]) {
					const field = mergeFields[mapping.index];
					if (label) {
						const type = document.createElement('span');
						type.className = 'mce-type';
						type.textContent = String(field.type || '');
						const nodes = [document.createTextNode(`${field.name} - *|${field.tag}|* `), type];
						if (field.tag === 'EMAIL') {
							const required = document.createElement('span');
							required.className = 'mce-required';
							required.textContent = 'Required';
							nodes.push(document.createTextNode(' '), required);
						}
						label.replaceChildren(...nodes);
				}
				if (container) container.style.display = '';
			} else {
				if (container) container.style.display = 'none';
			}
		});

		applyFuzzyMatching(mergeFields);
	}

	function updateFieldsNotice(totalMergeFields, liteLimit) {
		const notice = document.getElementById('cmatic-fields-notice');
		if (!notice) return;

		if (totalMergeFields > liteLimit) {
			notice.classList.remove('cmatic-hidden');
			notice.classList.add('cmatic-visible');
		} else {
			notice.classList.remove('cmatic-visible');
			notice.classList.add('cmatic-hidden');
		}
	}

	function renderListsDropdown(listsData, currentSelection) {
		const { api_valid, lists, total } = listsData;

		if (lists && lists.length > 0) {
			cachedLists = lists;
		}

		const dataContainer = document.getElementById('cmatic_data');
		if (dataContainer) dataContainer.dataset.apiValid = api_valid ? '1' : '0';

		const label = document.getElementById('cmatic-audiences-label');
		if (label) {
			label.textContent = api_valid && total > 0 ? `Total Mailchimp Audiences: ${total}` : 'Mailchimp Audiences';
			const caption = label.closest('.cmatic-audiences') ?
				label.closest('.cmatic-audiences').querySelector('small.description') : null;
			if (caption && caption.firstChild && caption.firstChild.nodeType === Node.TEXT_NODE) {
				caption.firstChild.nodeValue = (api_valid && total > 0
					? 'Contacts from this form join the selected audience. Sync Fields refreshes its merge fields from Mailchimp. '
					: (api_valid
						? 'No audiences found in this account. Create one in Mailchimp, then click Sync Fields. '
						: 'Connect your Mailchimp account above to load your audiences. '));
			}
		}

		const options = [];
		let selectedAudience = '';

		if (api_valid && total > 0) {
			selectedAudience = currentSelection;
			if (!selectedAudience && lists.length > 0) selectedAudience = lists[0].id;

			lists.forEach(list => {
				const optionText = `${list.name} (${Number(list.member_count).toLocaleString()} contacts, ${list.field_count} fields)`;
				const option = document.createElement('option');
				option.value = String(list.id || '');
				option.textContent = optionText;
				option.selected = selectedAudience === list.id;
				options.push(option);
			});

			const selectedList = lists.find(l => l.id === selectedAudience) || lists[0];
			updateFieldsNotice(selectedList.field_count, chimpmaticLite.liteFieldsLimit || 4);
		}

		return options;
	}

	function updateApiStatus(isValid) {
		const versionInfo = document.getElementById('chimpmatic-version-info');
		if (versionInfo) {
			const statusText = versionInfo.querySelector('.chmm');
			if (statusText) {
				if (isValid) {
					statusText.classList.remove('invalid');
					statusText.classList.add('valid');
					statusText.textContent = 'API Connected';
				} else {
					statusText.classList.remove('valid');
					statusText.classList.add('invalid');
					statusText.textContent = 'API Inactive';
				}
			}
		}

		const headerDot = document.querySelector('.cmatic-header__status-dot');
		const headerText = document.querySelector('.cmatic-header__status-text');
		if (headerDot) {
			if (isValid) {
				headerDot.classList.remove('cmatic-header__status-dot--disconnected');
				headerDot.classList.add('cmatic-header__status-dot--connected');
			} else {
				headerDot.classList.remove('cmatic-header__status-dot--connected');
				headerDot.classList.add('cmatic-header__status-dot--disconnected');
			}
		}
		if (headerText) {
			headerText.textContent = isValid ? 'API Connected' : 'API Inactive';
		}
	}

	function updateLiteBadgeStatus(status) {
		const liteBadge = document.querySelector('.cm-lite');
		if (!liteBadge) return;

		liteBadge.classList.remove('cm-status-neutral', 'cm-status-connected', 'cm-status-error');

		if (status === 'connected') liteBadge.classList.add('cm-status-connected');
		else if (status === 'error') liteBadge.classList.add('cm-status-error');
		else liteBadge.classList.add('cm-status-neutral');
	}

	function getSecureApiKey(apiKeyInput) {
		const isMasked = apiKeyInput.dataset.isMasked === '1';
		const hasKey = apiKeyInput.dataset.hasKey === '1';
		const inputValue = apiKeyInput.value.trim();

		return isMasked && hasKey ? '' : inputValue;
	}

	const fetchListsButton = document.getElementById('chm_activalist');
	if (fetchListsButton) {
		fetchListsButton.addEventListener('click', async function(event) {
			event.preventDefault();

			const apiKeyInput = document.getElementById('cmatic-api');
			const selectElement = document.getElementById('wpcf7-mailchimp-list');

			var dataContainer = document.getElementById('cmatic_data');
			var authType = (dataContainer && dataContainer.dataset.authType) || '';

			if (!selectElement) return;
			if (!apiKeyInput && authType !== 'oauth') return;

			const formId = getFormId();
			const apiKey = apiKeyInput ? await getSecureApiKey(apiKeyInput, formId) : '';

			if (!apiKey && authType !== 'oauth') {
				if (typeof showInlineMessage === 'function') {
					showInlineMessage(fetchListsButton, 'Enter API key first', 'warning');
				}
				updateApiStatus(false);
				updateLiteBadgeStatus('neutral');
				return;
			}

			if (!formId || formId <= 0) {
				if (typeof showInlineMessage === 'function') {
					showInlineMessage(fetchListsButton, 'Save form first', 'warning');
				}
				updateApiStatus(false);
				updateLiteBadgeStatus('neutral');
				return;
			}

			const originalText = fetchListsButton.value || fetchListsButton.textContent;
			fetchListsButton.disabled = true;
			if (fetchListsButton.tagName === 'INPUT') fetchListsButton.value = 'Syncing Audiences...';
			else fetchListsButton.textContent = 'Syncing Audiences...';

			try {
				const data = await fetchMailchimpLists(formId, apiKey);

				const currentSelection = selectElement.value || '';
					selectElement.replaceChildren(...renderListsDropdown(data, currentSelection));

				attachFetchFieldsListeners();

				const newListDropdown = document.getElementById('wpcf7-mailchimp-list');
				if (newListDropdown && newListDropdown.value) {
					if (isProFieldPanelActive) {
						newListDropdown.dispatchEvent(new Event('change', { bubbles: true }));
					} else {
						fetchFieldsForSelectedList();
					}
				}

				if (data.api_valid) {
					updateApiStatus(true);
					updateLiteBadgeStatus('connected');

					const chooser = document.querySelector('.cmatic-connect-options');
					if (chooser) chooser.classList.add('cmatic-hidden');

					document.querySelectorAll('.chmp-inactive').forEach(el => {
						el.classList.remove('chmp-inactive');
						el.classList.add('chmp-active');
					});

					const newUserSection = document.getElementById('chmp-new-user');
					if (newUserSection) {
						newUserSection.classList.remove('chmp-active');
						newUserSection.classList.add('chmp-inactive');
					}
				} else {
					updateApiStatus(false);
					updateLiteBadgeStatus('error');

					document.querySelectorAll('.chmp-active').forEach(el => {
						el.classList.remove('chmp-active');
						el.classList.add('chmp-inactive');
					});

					const newUserSection = document.getElementById('chmp-new-user');
					if (newUserSection) {
						newUserSection.classList.remove('chmp-inactive');
						newUserSection.classList.add('chmp-active');
					}
				}

				if (fetchListsButton.tagName === 'INPUT') fetchListsButton.value = 'Synced âœ“';
				else fetchListsButton.textContent = 'Synced âœ“';

				setTimeout(() => {
					if (fetchListsButton.tagName === 'INPUT') fetchListsButton.value = originalText;
					else fetchListsButton.textContent = originalText;
					fetchListsButton.disabled = false;
				}, 1000);

			} catch (error) {
				if (fetchListsButton.tagName === 'INPUT') fetchListsButton.value = originalText;
				else fetchListsButton.textContent = originalText;
				fetchListsButton.disabled = false;
				alert(chimpmaticLite.i18n.error);
			}
		});
	}

	const apiKeyInput = document.getElementById('cmatic-api');
	if (apiKeyInput && fetchListsButton) {
		function isValidApiKey(key) {
			if (key.length !== 36 || key.charAt(32) !== '-') return false;
			if (!/^[a-f0-9]{32}$/i.test(key.substring(0, 32))) return false;
			const dc = key.substring(33).toLowerCase();
			const validDCs = ['us1','us2','us3','us4','us5','us6','us7','us8','us9','us10','us11','us12','us13','us14','us15','us16','us17','us18','us19','us20','us21'];
			return validDCs.includes(dc);
		}

		function debounce(func, wait) {
			let timeout;
			return function(...args) {
				clearTimeout(timeout);
				timeout = setTimeout(() => func.apply(this, args), wait);
			};
		}

		apiKeyInput.addEventListener('paste', function() {
			setTimeout(() => {
				if (isValidApiKey(apiKeyInput.value.trim())) fetchListsButton.click();
			}, 50);
		});

		apiKeyInput.addEventListener('input', function() {
			if (apiKeyInput.dataset.isMasked === '1' && apiKeyInput.value !== apiKeyInput.dataset.maskedKey) {
				apiKeyInput.dataset.isMasked = '0';
			}
			updateLiteBadgeStatus('neutral');
		});

		apiKeyInput.addEventListener('input', debounce(function() {
			const apiKey = apiKeyInput.value.trim();
			if (isValidApiKey(apiKey)) {
				fetchListsButton.click();
			} else if (apiKey === '') {
				const formId = getFormId();
				if (formId) {
					fetch(`${chimpmaticLite.restUrl}settings/reset`, {
						method: 'POST',
						headers: {
							'Content-Type': 'application/json',
							'X-WP-Nonce': chimpmaticLite.restNonce
						},
						body: JSON.stringify({ form_id: formId })
					})
					.then(response => response.json())
					.then(() => {
						const selectElement = document.getElementById('wpcf7-mailchimp-list');
						if (selectElement) selectElement.innerHTML = '';
						const label = document.getElementById('cmatic-audiences-label');
						if (label) label.textContent = 'Mailchimp Audiences';
						const fieldsContainer = document.getElementById('cmatic-fields');
						if (fieldsContainer) fieldsContainer.innerHTML = '';
						updateLiteBadgeStatus('neutral');

						document.querySelectorAll('.chmp-active').forEach(el => {
							el.classList.remove('chmp-active');
							el.classList.add('chmp-inactive');
						});

						const newUserSection = document.getElementById('chmp-new-user');
						if (newUserSection) {
							newUserSection.classList.remove('chmp-inactive');
							newUserSection.classList.add('chmp-active');
						}
					});
				}
			}
		}, 500));
	}

	function initToggleAutoSave() {
		const globalFields = ['debug', 'backlink', 'auto_update', 'telemetry'];
		const toggles = document.querySelectorAll('.cmatic-toggle input[data-field]');
		if (toggles.length === 0) return;

		toggles.forEach(function(toggle) {
			toggle.addEventListener('change', async function() {
				const field = this.dataset.field;

				if (!globalFields.includes(field)) return;

				const enabled = this.checked;
				const wrapper = this.closest('.cmatic-toggle');

				if (wrapper) wrapper.classList.add('is-saving');

				try {
					const response = await fetch(chimpmaticLite.restUrl + 'settings/toggle', {
						method: 'POST',
						headers: {
							'Content-Type': 'application/json',
							'X-WP-Nonce': chimpmaticLite.restNonce
						},
						body: JSON.stringify({ field, enabled })
					});

					const data = await response.json();
					if (wrapper) wrapper.classList.remove('is-saving');
					if (data.success) {
						this.checked = enabled;
						this.defaultChecked = enabled;
					} else {
						this.checked = !enabled;
					}
				} catch (error) {
					if (wrapper) wrapper.classList.remove('is-saving');
					this.checked = !enabled;
				}
			});
		});
	}

	initToggleAutoSave();

	function initSelectAutoSave() {
		const selects = document.querySelectorAll('select.chm-select[data-field]');
		if (selects.length === 0) return;

		const perFormFields = ['double_optin', 'sync_tags'];

		selects.forEach(function(select) {
			select.addEventListener('change', async function() {
				const field = this.dataset.field;
				const value = this.value === '1';
				const wrapper = this.closest('.mcee-container');

				if (wrapper) wrapper.classList.add('is-saving');

				try {
					let url, body;

					if (perFormFields.includes(field)) {
						const formId = getFormId();
						if (!formId) {
							if (wrapper) wrapper.classList.remove('is-saving');
							this.value = value ? '0' : '1';
							return;
						}
						const rootUrl = chimpmaticLite.restUrl.replace('chimpmatic-lite/v1/', '');
						url = rootUrl + 'cmatic/form/setting';
						body = JSON.stringify({ form_id: formId, field, value });
					} else {
						url = chimpmaticLite.restUrl + 'settings/toggle';
						body = JSON.stringify({ field, enabled: value });
					}

					const response = await fetch(url, {
						method: 'POST',
						headers: {
							'Content-Type': 'application/json',
							'X-WP-Nonce': chimpmaticLite.restNonce
						},
						body
					});

					const data = await response.json();
					if (wrapper) wrapper.classList.remove('is-saving');
					if (data.success) {
						Array.from(this.options).forEach(function(opt) {
							opt.defaultSelected = opt.selected;
						});
					} else {
						this.value = value ? '0' : '1';
					}
				} catch (error) {
					if (wrapper) wrapper.classList.remove('is-saving');
					this.value = value ? '0' : '1';
				}
			});
		});
	}

	initSelectAutoSave();

	const debugLogButton = document.querySelector('.cme-trigger-log:not(.cmatic-accordion-btn)');
	if (debugLogButton) {
		debugLogButton.addEventListener('click', async function(event) {
			event.preventDefault();

			const logPanelContainer = document.getElementById('eventlog-sys');
			const logPanel = document.getElementById('log_panel');
			const advancedSettings = document.querySelector('.vc-advanced-settings');
			const advancedToggleButton = document.querySelector('.vc-view-advanced');
			const testContainer = document.getElementById('cmatic-test-container');
			const testSubmissionBtn = document.querySelector('.vc-test-submission');

			if (!logPanelContainer || !logPanel) return;

			const isLogVisible = window.getComputedStyle(logPanelContainer).display !== 'none';

			if (isLogVisible) {
				logPanelContainer.style.transition = 'opacity 0.5s ease-out';
				logPanelContainer.style.opacity = '0';

				setTimeout(() => {
					logPanelContainer.style.display = 'none';
					logPanelContainer.style.removeProperty('opacity');
					logPanelContainer.style.removeProperty('transition');
				}, 500);

				this.setAttribute('aria-expanded', 'false');
			} else {
				if (advancedSettings) {
					const isAdvancedVisible = window.getComputedStyle(advancedSettings).display !== 'none';
					if (isAdvancedVisible) {
						advancedSettings.style.transition = 'opacity 0.5s ease-out';
						advancedSettings.style.opacity = '0';

						setTimeout(() => {
							advancedSettings.style.display = 'none';
							advancedSettings.style.removeProperty('opacity');
							advancedSettings.style.removeProperty('transition');
						}, 500);

						if (advancedToggleButton) advancedToggleButton.setAttribute('aria-expanded', 'false');
					}
				}

				if (testContainer) {
					const isTestVisible = window.getComputedStyle(testContainer).display !== 'none';
					if (isTestVisible) {
						testContainer.style.transition = 'opacity 0.5s ease-out';
						testContainer.style.opacity = '0';

						setTimeout(() => {
							testContainer.style.display = 'none';
							testContainer.style.removeProperty('opacity');
							testContainer.style.removeProperty('transition');
						}, 500);

						if (testSubmissionBtn) testSubmissionBtn.setAttribute('aria-expanded', 'false');
					}
				}

				logPanel.textContent = chimpmaticLite.i18n.loading;
				logPanelContainer.style.opacity = '0';
				logPanelContainer.style.display = 'block';
				logPanelContainer.style.transition = 'opacity 0.5s ease-in';

				setTimeout(() => { logPanelContainer.style.opacity = '1'; }, 50);
				this.setAttribute('aria-expanded', 'true');

				try {
					const data = await getDebugLog();
					logPanel.textContent = data.success ? (data.logs || data.message) : 'Error: ' + (data.message || 'Unknown error');
				} catch (error) {
					logPanel.textContent = chimpmaticLite.i18n.error;
				}
			}
		});
	}

	const advancedToggleButton = document.querySelector('.vc-view-advanced:not(.cmatic-accordion-btn)');
	if (advancedToggleButton) {
		advancedToggleButton.addEventListener('click', function(event) {
			event.preventDefault();

			const advancedSettings = document.querySelector('.vc-advanced-settings');
			const logPanelContainer = document.getElementById('eventlog-sys');
			const debugLogBtn = document.querySelector('.cme-trigger-log');
			const testContainer = document.getElementById('cmatic-test-container');
			const testSubmissionBtn = document.querySelector('.vc-test-submission');

			if (!advancedSettings) return;

			const isVisible = window.getComputedStyle(advancedSettings).display !== 'none';

			if (isVisible) {
				advancedSettings.style.transition = 'opacity 0.5s ease-out';
				advancedSettings.style.opacity = '0';

				setTimeout(() => {
					advancedSettings.style.display = 'none';
					advancedSettings.style.removeProperty('opacity');
					advancedSettings.style.removeProperty('transition');
				}, 500);

				this.setAttribute('aria-expanded', 'false');
			} else {
				if (logPanelContainer) {
					const isLogVisible = window.getComputedStyle(logPanelContainer).display !== 'none';
					if (isLogVisible) {
						logPanelContainer.style.transition = 'opacity 0.5s ease-out';
						logPanelContainer.style.opacity = '0';

						setTimeout(() => {
							logPanelContainer.style.display = 'none';
							logPanelContainer.style.removeProperty('opacity');
							logPanelContainer.style.removeProperty('transition');
						}, 500);

						if (debugLogBtn) debugLogBtn.setAttribute('aria-expanded', 'false');
					}
				}

				if (testContainer) {
					const isTestVisible = window.getComputedStyle(testContainer).display !== 'none';
					if (isTestVisible) {
						testContainer.style.transition = 'opacity 0.5s ease-out';
						testContainer.style.opacity = '0';

						setTimeout(() => {
							testContainer.style.display = 'none';
							testContainer.style.removeProperty('opacity');
							testContainer.style.removeProperty('transition');
						}, 500);

						if (testSubmissionBtn) testSubmissionBtn.setAttribute('aria-expanded', 'false');
					}
				}

				advancedSettings.style.opacity = '0';
				advancedSettings.style.display = 'block';
				advancedSettings.style.transition = 'opacity 0.5s ease-in';

				setTimeout(() => { advancedSettings.style.opacity = '1'; }, 50);
				this.setAttribute('aria-expanded', 'true');
			}
		});
	}

	const clearLogsButton = document.querySelector('.vc-clear-logs');
	if (clearLogsButton) {
		clearLogsButton.addEventListener('click', async function(event) {
			event.preventDefault();

			const logPanel = document.getElementById('log_panel');
			const originalText = this.textContent;

			this.disabled = true;
			this.textContent = 'Clearing Logs...';

			try {
				const data = await clearDebugLog();

				if (data.success && data.cleared) {
					this.textContent = 'Cleared';
					if (logPanel) logPanel.textContent = 'Debug log cleared.';
				} else {
					this.textContent = 'Cleared';
					if (logPanel) logPanel.textContent = data.message || 'Debug log was already empty.';
				}

				setTimeout(() => {
					this.textContent = 'Clear Logs';
					this.disabled = false;
				}, 2000);
			} catch (error) {
				this.textContent = 'Clearing Log Error';
				setTimeout(() => {
					this.textContent = 'Clear Logs';
					this.disabled = false;
				}, 3000);
			}
		});
	}

	(function initAccordionPanels() {
		const accordionContainer = document.querySelector('.cmatic-panel-toggles');
		if (!accordionContainer) return;

		const panelConfig = {
			'eventlog-sys': {
				filtered: true,
				onOpen: async function(panel) {
					const logPanel = document.getElementById('log_panel');
					const toggleLink = panel.querySelector('.vc-toggle-filter');
					const config = panelConfig['eventlog-sys'];

					if (toggleLink && !toggleLink.hasAttribute('data-listener')) {
						toggleLink.setAttribute('data-listener', 'true');
						toggleLink.addEventListener('click', async function(e) {
							e.preventDefault();
							config.filtered = !config.filtered;
							this.textContent = config.filtered ? 'Show All' : 'Plugin Only';
							this.setAttribute('data-filtered', config.filtered ? '1' : '0');

							if (logPanel) {
								logPanel.textContent = chimpmaticLite.i18n?.loading || 'Loading...';
								try {
									const data = await getDebugLog(config.filtered);
									logPanel.textContent = data.success ? (data.logs || data.message) : 'Error: ' + (data.message || 'Unknown error');
								} catch (error) {
									logPanel.textContent = chimpmaticLite.i18n?.error || 'Error loading logs';
								}
							}
						});
					}

					if (logPanel && typeof chimpmaticLite !== 'undefined') {
						logPanel.textContent = chimpmaticLite.i18n?.loading || 'Loading...';
						try {
							const data = await getDebugLog(config.filtered);
							logPanel.textContent = data.success ? (data.logs || data.message) : 'Error: ' + (data.message || 'Unknown error');
						} catch (error) {
							logPanel.textContent = chimpmaticLite.i18n?.error || 'Error loading logs';
						}
					}
				}
			},
			'cmatic-test-container': {
				useModal: true
			}
		};

		function hidePanel(panel, button) {
			if (!panel) return;
			const isVisible = window.getComputedStyle(panel).display !== 'none';
			if (!isVisible) return;

			panel.style.transition = 'opacity 0.5s ease-out';
			panel.style.opacity = '0';

			setTimeout(() => {
				panel.style.display = 'none';
				panel.style.removeProperty('opacity');
				panel.style.removeProperty('transition');
			}, 500);

			if (button) button.setAttribute('aria-expanded', 'false');
		}

		function showPanel(panel, button, config) {
			if (!panel) return;

			panel.style.opacity = '0';
			panel.style.display = 'block';
			panel.style.transition = 'opacity 0.5s ease-in';

			setTimeout(() => { panel.style.opacity = '1'; }, 50);
			if (button) button.setAttribute('aria-expanded', 'true');

			if (config && config.onOpen) {
				config.onOpen(panel);
			}
		}

		function closeAllPanels() {
			const buttons = accordionContainer.querySelectorAll('.cmatic-accordion-btn');
			buttons.forEach(btn => {
				const panelId = btn.getAttribute('aria-controls');
				if (panelId) {
					const panel = document.getElementById(panelId);
					hidePanel(panel, btn);
				}
			});
		}

		accordionContainer.addEventListener('click', function(event) {
			const button = event.target.closest('.cmatic-accordion-btn');
			if (!button) return;

			event.preventDefault();

			const panelId = button.getAttribute('aria-controls');
			if (!panelId) return;

			const config = panelConfig[panelId] || {};

			if (config.useModal) {
				return;
			}

			const panel = document.getElementById(panelId);
			if (!panel) return;

			const isExpanded = button.getAttribute('aria-expanded') === 'true';

			if (isExpanded) {
				hidePanel(panel, button);
			} else {
				closeAllPanels();

				setTimeout(() => {
					showPanel(panel, button, config);
				}, 100);
			}
		});

		const buttons = accordionContainer.querySelectorAll('.cmatic-accordion-btn');
		buttons.forEach(btn => {
			const panelId = btn.getAttribute('aria-controls');
			const config = panelConfig[panelId] || {};
			if (panelId && !config.useModal) {
				const panel = document.getElementById(panelId);
				if (panel) {
					panel.style.display = 'none';
				}
			}
			btn.setAttribute('aria-expanded', 'false');
		});
	})();

	function relocateSidebarElements() {
		const moveElements = document.querySelectorAll('.mce-move');
		const submitDiv = document.getElementById('submitdiv');
		const postboxContainer = document.querySelector('.postbox-container');

		if (!moveElements.length || !submitDiv || !postboxContainer) return;

		let insertAfter = submitDiv;

		moveElements.forEach((el) => {
			if (insertAfter.nextSibling) {
				postboxContainer.insertBefore(el, insertAfter.nextSibling);
			} else {
				postboxContainer.appendChild(el);
			}
			insertAfter = el;

			el.classList.add('mce-fade-in');
			el.classList.remove('mce-hidden');
		});
	}

	relocateSidebarElements();

	async function fetchFieldsForSelectedList() {
		if (isProFieldPanelActive) return;

		const listDropdown = document.getElementById('wpcf7-mailchimp-list');
		const fetchFieldsButton = document.getElementById('mce_fetch_fields');
		const formId = getFormId();

		if (!listDropdown || !listDropdown.value || !formId) return;

		const listId = listDropdown.value;
		const originalText = fetchFieldsButton ? (fetchFieldsButton.value || fetchFieldsButton.textContent) : 'Sync Fields';

		if (fetchFieldsButton) {
			fetchFieldsButton.disabled = true;
			if (fetchFieldsButton.tagName === 'INPUT') fetchFieldsButton.value = 'Syncing Fields...';
			else fetchFieldsButton.textContent = 'Syncing Fields...';
		}

		try {
			const response = await fetch(`${chimpmaticLite.restUrl}merge-fields`, {
				method: 'POST',
				headers: {
					'Content-Type': 'application/json',
					'X-WP-Nonce': chimpmaticLite.restNonce
				},
				body: JSON.stringify({ form_id: formId, list_id: listId })
			});

			const data = await response.json();

			if (data.success && data.merge_fields) {
				updateFieldLabels(data.merge_fields);
				applyFuzzyMatching(data.merge_fields);
				const listDropdownEl = document.getElementById('wpcf7-mailchimp-list');
				const selectedId = listDropdownEl ? listDropdownEl.value : '';
				const selectedList = cachedLists.find(l => l.id === selectedId);
				const audienceName = data.audience_name || (selectedList ? selectedList.name : '');
				const totalFields = (typeof data.total_merge_fields === 'number')
					? data.total_merge_fields
					: (selectedList ? selectedList.field_count : 0);
				updateFieldsNotice(totalFields, data.lite_limit || chimpmaticLite.liteFieldsLimit || 4);
				document.querySelectorAll('.audience-name').forEach(el => { el.textContent = audienceName; });
				document.getElementById('cmatic-stale-cache-notice')?.remove();

				if (fetchFieldsButton) {
					if (fetchFieldsButton.tagName === 'INPUT') fetchFieldsButton.value = 'Synced âœ“';
					else fetchFieldsButton.textContent = 'Synced âœ“';

					setTimeout(() => {
						if (fetchFieldsButton.tagName === 'INPUT') fetchFieldsButton.value = originalText;
						else fetchFieldsButton.textContent = originalText;
						fetchFieldsButton.disabled = false;
					}, 1000);
				}
			} else {
				alert('Failed to load fields. Please try again.');
				if (fetchFieldsButton) {
					if (fetchFieldsButton.tagName === 'INPUT') fetchFieldsButton.value = originalText;
					else fetchFieldsButton.textContent = originalText;
					fetchFieldsButton.disabled = false;
				}
			}
		} catch (error) {
			alert('Error loading fields. Check console for details.');
			if (fetchFieldsButton) {
				if (fetchFieldsButton.tagName === 'INPUT') fetchFieldsButton.value = originalText;
				else fetchFieldsButton.textContent = originalText;
				fetchFieldsButton.disabled = false;
			}
		}
	}

	function attachFetchFieldsListeners() {
		const listDropdown = document.getElementById('wpcf7-mailchimp-list');
		if (listDropdown) {
			listDropdown.removeEventListener('change', handleListChange);
			listDropdown.addEventListener('change', handleListChange);
		}

		const fetchFieldsButton = document.getElementById('mce_fetch_fields');
		if (fetchFieldsButton) {
			fetchFieldsButton.removeEventListener('click', handleFetchFieldsClick);
			fetchFieldsButton.addEventListener('click', handleFetchFieldsClick);
		}
	}

	function handleListChange(e) {
		const selectedList = e.target.value;
		const fetchFieldsButton = document.getElementById('mce_fetch_fields');
		const listDropdown = e.target;

		if (isProFieldPanelActive) return;

		Array.from(listDropdown.options).forEach(opt => {
			opt.defaultSelected = (opt.value === selectedList);
		});

		if (selectedList) {
			if (fetchFieldsButton) fetchFieldsButton.disabled = false;

			for (let i = 3; i <= 8; i++) {
				const dropdown = document.getElementById(`wpcf7-mailchimp-field${i}`);
				if (dropdown) dropdown.value = ' ';
			}

			fetchFieldsForSelectedList();
		} else {
			if (fetchFieldsButton) fetchFieldsButton.disabled = true;
		}
	}

	async function handleFetchFieldsClick(event) {
		if (isProFieldPanelActive) return;
		event.preventDefault();
		await fetchFieldsForSelectedList();
	}

	attachFetchFieldsListeners();

	const initialListDropdown = document.getElementById('wpcf7-mailchimp-list');
	const initialFetchButton = document.getElementById('mce_fetch_fields');
	if (initialListDropdown && initialListDropdown.options.length > 0) {
		if (!initialListDropdown.value || initialListDropdown.value === '' || initialListDropdown.value === ' ') {
			initialListDropdown.value = initialListDropdown.options[0].value;
			if (initialFetchButton) initialFetchButton.disabled = false;
		} else {
			if (initialFetchButton && initialFetchButton.disabled) initialFetchButton.disabled = false;
		}
	}

	if (chimpmaticLite.mergeFields && chimpmaticLite.mergeFields.length > 0) {
		updateFieldLabels(chimpmaticLite.mergeFields);
	}

	function initLicenseResetButton() {
		const button = document.getElementById('cmatic-license-reset-btn');
		const messageDiv = document.getElementById('cmatic-license-reset-message');

		if (!button || !messageDiv) return;

		button.addEventListener('click', async function(e) {
			e.preventDefault();

			button.disabled = true;
			button.textContent = 'Resetting...';
			messageDiv.innerHTML = '';

			try {
				const response = await fetch(chimpmaticLite.licenseResetUrl, {
					method: 'POST',
					headers: {
						'Content-Type': 'application/json',
						'X-WP-Nonce': chimpmaticLite.nonce
					},
					credentials: 'same-origin',
					body: JSON.stringify({ type: 'nuclear' })
				});

				const data = await response.json();

				if (data.success) {
					button.textContent = 'Done Resetting';
					messageDiv.innerHTML = '<span style="color: #46b450;">âœ“ ' + escapeHtml(data.message) + '</span><br>' +
						'<small style="color: #666;">Deleted ' + data.deleted_counts.options + ' options and ' +
						data.deleted_counts.transients + ' transients</small>';

					setTimeout(function() {
						button.textContent = 'Reset License Data';
						button.disabled = false;
						messageDiv.innerHTML = '';
					}, 3000);
				} else {
					button.textContent = 'Reset License Data';
					button.disabled = false;
					messageDiv.innerHTML = '<span style="color: #dc3232;">âœ— Error: ' +
						escapeHtml(data.message || 'Unknown error occurred') + '</span>';

					setTimeout(function() { messageDiv.innerHTML = ''; }, 5000);
				}
			} catch (error) {
				button.textContent = 'Reset License Data';
				button.disabled = false;
				messageDiv.innerHTML = '<span style="color: #dc3232;">âœ— Network error: ' +
					escapeHtml(error.message) + '</span>';

				setTimeout(function() { messageDiv.innerHTML = ''; }, 5000);
			}
		});
	}

	function escapeHtml(text) {
		const div = document.createElement('div');
		div.textContent = text;
		return div.innerHTML;
	}

	initLicenseResetButton();

	let formHandlerAttached = false;

	function openTestModal() {
		const modal = document.getElementById('cmatic-test-modal');
		if (!modal) return;

		modal.classList.add('cmatic-modal--active');
		document.body.classList.add('cmatic-modal-open');

		const form = modal.querySelector('.wpcf7 form');
		if (form && !form.querySelector('input[name="_cmatic_test_modal"]')) {
			const hidden = document.createElement('input');
			hidden.type = 'hidden';
			hidden.name = '_cmatic_test_modal';
			hidden.value = '1';
			form.appendChild(hidden);
		}

		if (!formHandlerAttached) {
			if (form) {
				attachTestFormHandler(form);
				formHandlerAttached = true;
			}
		}

		const closeBtn = modal.querySelector('.cmatic-modal__close');
		if (closeBtn) closeBtn.focus();
	}

	function closeTestModal() {
		const modal = document.getElementById('cmatic-test-modal');
		if (!modal) return;

		modal.classList.remove('cmatic-modal--active');
		document.body.classList.remove('cmatic-modal-open');
	}

	function attachTestFormHandler(form) {
		form.addEventListener('submit', async function(e) {
			e.preventDefault();

			const submitBtn = form.querySelector('input[type="submit"], button[type="submit"]');
			const headerSubmitBtn = document.querySelector('.cmatic-modal__submit');
			const responseOutput = form.querySelector('.wpcf7-response-output') || createResponseOutput(form);
			const formId = form.querySelector('input[name="_wpcf7"]')?.value;

			if (!formId) {
				showResponse(responseOutput, 'error', 'Form ID not found.');
				return;
			}

			const originalBtnText = submitBtn?.value || submitBtn?.textContent;
			if (submitBtn) {
				submitBtn.disabled = true;
				if (submitBtn.tagName === 'INPUT') {
					submitBtn.value = 'Sending...';
				} else {
					submitBtn.textContent = 'Sending...';
				}
			}

			if (headerSubmitBtn) {
				headerSubmitBtn.disabled = true;
				headerSubmitBtn.textContent = 'Submitting...';
				headerSubmitBtn.classList.remove('cmatic-modal__submit--success', 'cmatic-modal__submit--error');
			}

			responseOutput.textContent = '';
			responseOutput.className = 'wpcf7-response-output';
			responseOutput.style.display = 'none';
			hideChimpmaticFeedback();

			let isSuccess = false;

			try {
				const formData = new FormData(form);
				const restUrl = chimpmaticLite.restUrl.replace('chimpmatic-lite/v1/', '') + 'contact-form-7/v1/contact-forms/' + formId + '/feedback';

				const response = await fetch(restUrl, {
					method: 'POST',
					body: formData,
					credentials: 'same-origin'
				});

				const data = await response.json();

				if (data.chimpmatic) {
					showChimpmaticFeedback(data.chimpmatic);
				}

				if (data.status === 'mail_sent') {
					isSuccess = true;
					showResponse(responseOutput, 'success', data.message || 'Message sent successfully.');
					form.reset();
					refreshDebugLogsAfterSubmission();
				} else if (data.status === 'validation_failed' || data.status === 'mail_failed') {
					showResponse(responseOutput, 'error', data.message || 'There was an error sending your message.');
					if (data.invalid_fields) {
						data.invalid_fields.forEach(field => {
							const wrap = form.querySelector(`.wpcf7-form-control-wrap[data-name="${field.field}"]`);
							if (wrap) {
								const tip = document.createElement('span');
								tip.className = 'wpcf7-not-valid-tip';
								tip.textContent = field.message;
								wrap.appendChild(tip);
							}
						});
					}
				} else {
					showResponse(responseOutput, 'error', data.message || 'An error occurred.');
				}
			} catch (error) {
				console.error('Test form submission error:', error);
				showResponse(responseOutput, 'error', 'Network error. Please try again.');
			} finally {
				if (submitBtn) {
					submitBtn.disabled = false;
					if (submitBtn.tagName === 'INPUT') {
						submitBtn.value = originalBtnText;
					} else {
						submitBtn.textContent = originalBtnText;
					}
				}

				if (headerSubmitBtn) {
					headerSubmitBtn.disabled = false;
					if (isSuccess) {
						headerSubmitBtn.textContent = 'Success!';
						headerSubmitBtn.classList.add('cmatic-modal__submit--success');
					} else {
						headerSubmitBtn.textContent = 'Error';
						headerSubmitBtn.classList.add('cmatic-modal__submit--error');
					}
					setTimeout(() => {
						headerSubmitBtn.textContent = 'Submit';
						headerSubmitBtn.classList.remove('cmatic-modal__submit--success', 'cmatic-modal__submit--error');
					}, 2000);
				}
			}
		});

		form.addEventListener('input', function(e) {
			const wrap = e.target.closest('.wpcf7-form-control-wrap');
			if (wrap) {
				const tip = wrap.querySelector('.wpcf7-not-valid-tip');
				if (tip) tip.remove();
			}
		});
	}

	function createResponseOutput(form) {
		const output = document.createElement('div');
		output.className = 'wpcf7-response-output';
		output.setAttribute('aria-live', 'polite');
		form.appendChild(output);
		return output;
	}

	function showResponse(element, type, message) {
		element.textContent = message;
		element.style.display = 'block';
		element.className = 'wpcf7-response-output';
		if (type === 'success') {
			element.classList.add('wpcf7-mail-sent-ok');
			element.style.borderColor = '#00a32a';
			element.style.background = '#edfaef';
		} else {
			element.classList.add('wpcf7-mail-sent-ng');
			element.style.borderColor = '#d63638';
			element.style.background = '#fcf0f1';
		}
	}

	function showChimpmaticFeedback(chimpmatic) {
		const modal = document.getElementById('cmatic-test-modal');
		if (!modal) return;

		const feedback = modal.querySelector('.cmatic-modal__feedback');
		if (!feedback) return;

		const icon = feedback.querySelector('.cmatic-modal__feedback-icon');
		const title = feedback.querySelector('.cmatic-modal__feedback-title');
		const details = feedback.querySelector('.cmatic-modal__feedback-details');

		feedback.classList.remove('cmatic-modal__feedback--success', 'cmatic-modal__feedback--error', 'cmatic-modal__feedback--skipped');

		if (chimpmatic.success === true) {
			feedback.classList.add('cmatic-modal__feedback--success');
			icon.innerHTML = '<span class="dashicons dashicons-yes-alt"></span>';
			title.textContent = chimpmatic.message;

			const sent = chimpmatic.merge_vars || {};
			const received = chimpmatic.received || {};
			const allKeys = new Set([...Object.keys(sent), ...Object.keys(received)]);

			if (allKeys.size > 0) {
				let tableHtml = '<table class="cmatic-modal__feedback-table">';
				tableHtml += '<thead><tr><th>Field</th><th>Sent</th><th>Received</th></tr></thead><tbody>';

				for (const key of allKeys) {
					const sentVal = sent[key] !== undefined ? escapeHtml(String(sent[key])) : '<span class="field-empty">â€”</span>';
					const recvVal = received[key] !== undefined ? escapeHtml(String(received[key])) : '<span class="field-empty">â€”</span>';
					const mismatch = sent[key] !== undefined && received[key] !== undefined && String(sent[key]) !== String(received[key]);
					const rowClass = mismatch ? ' class="field-mismatch"' : '';
					tableHtml += '<tr' + rowClass + '><td class="field-key">' + escapeHtml(key) + '</td><td>' + sentVal + '</td><td>' + recvVal + '</td></tr>';
				}

				tableHtml += '</tbody></table>';
				details.innerHTML = tableHtml;
			} else {
				details.innerHTML = '';
			}
		} else if (chimpmatic.skipped === true) {
			feedback.classList.add('cmatic-modal__feedback--skipped');
			icon.innerHTML = '<span class="dashicons dashicons-info-outline"></span>';
			title.textContent = 'Subscription skipped';
			details.textContent = chimpmatic.message;
		} else {
			feedback.classList.add('cmatic-modal__feedback--error');
			icon.innerHTML = '<span class="dashicons dashicons-dismiss"></span>';
			title.textContent = 'Subscription failed';
			details.textContent = chimpmatic.message;
		}

		feedback.style.display = 'flex';
	}

	function hideChimpmaticFeedback() {
		const modal = document.getElementById('cmatic-test-modal');
		if (!modal) return;

		const feedback = modal.querySelector('.cmatic-modal__feedback');
		if (feedback) {
			feedback.style.display = 'none';
		}
	}

	async function refreshDebugLogsAfterSubmission() {
		await new Promise(resolve => setTimeout(resolve, 500));

		const logPanelContainer = document.getElementById('eventlog-sys');
		const logPanel = document.getElementById('log_panel');
		const debugLogBtn = document.querySelector('.cme-trigger-log');

		if (!logPanelContainer || !logPanel) return;

		const isLogVisible = window.getComputedStyle(logPanelContainer).display !== 'none';
		if (!isLogVisible) {
			logPanel.textContent = chimpmaticLite.i18n.loading;
			logPanelContainer.style.opacity = '0';
			logPanelContainer.style.display = 'block';
			logPanelContainer.style.transition = 'opacity 0.5s ease-in';
			setTimeout(() => { logPanelContainer.style.opacity = '1'; }, 50);
			if (debugLogBtn) debugLogBtn.setAttribute('aria-expanded', 'true');
		}

		try {
			const data = await getDebugLog();
			logPanel.textContent = data.success ? (data.logs || data.message) : 'Error: ' + (data.message || 'Unknown error');
		} catch (error) {
			logPanel.textContent = chimpmaticLite.i18n.error;
		}
	}

	document.addEventListener('click', function(event) {
		const btn = event.target.closest('.vc-test-submission');
		if (!btn) return;

		event.preventDefault();
		openTestModal();
	});

	document.addEventListener('click', function(event) {
		const closeBtn = event.target.closest('#cmatic-test-modal .cmatic-modal__close');
		if (!closeBtn) return;

		event.preventDefault();
		closeTestModal();
	});

	document.addEventListener('click', function(event) {
		const overlay = event.target.closest('#cmatic-test-modal .cmatic-modal__overlay');
		if (!overlay) return;

		closeTestModal();
	});

	document.addEventListener('keydown', function(event) {
		if (event.key === 'Escape') {
			const modal = document.getElementById('cmatic-test-modal');
			if (modal && modal.classList.contains('cmatic-modal--active')) {
				closeTestModal();
			}
		}
	});

	document.addEventListener('click', function(event) {
		const submitBtn = event.target.closest('#cmatic-test-modal .cmatic-modal__submit');
		if (!submitBtn) return;

		event.preventDefault();
		const modal = document.getElementById('cmatic-test-modal');
		if (!modal) return;

		const form = modal.querySelector('.wpcf7 form');
		if (!form) return;

		form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
	});

	document.addEventListener('click', function(event) {
		if (!event.target.classList.contains('notice-dismiss')) return;
		const noticeElement = event.target.closest('#mce-notice');
		if (!noticeElement) return;

		event.preventDefault();
		fetch(chimpmaticLite.restUrl + 'notices/dismiss', {
			method: 'POST',
			headers: {
				'Content-Type': 'application/json',
				'X-WP-Nonce': chimpmaticLite.restNonce
			}
		}).then(function(response) {
			return response.json();
		}).then(function(data) {
			if (data.success) {
				noticeElement.style.transition = 'opacity 0.3s ease-out';
				noticeElement.style.opacity = '0';
				setTimeout(function() { noticeElement.style.display = 'none'; }, 300);
			}
		});
	});

	const eye = document.querySelector('.cmatic-eye');
	const input = document.getElementById('cmatic-api');
	if (eye && input && input.dataset.hasKey !== '1') {
		eye.addEventListener('click', function(e) {
			e.preventDefault();
			const icon = this.querySelector('.dashicons');
			const reveal = input.type === 'password';

			input.type = reveal ? 'text' : 'password';
			if (icon) {
				icon.classList.toggle('dashicons-visibility', !reveal);
				icon.classList.toggle('dashicons-hidden', reveal);
			}
		});
	}

	(function initContactLookup() {
		const lookupBtn = document.getElementById('cmatic-lookup-btn');
		const emailInput = document.getElementById('cmatic-lookup-email');
		const resultsContainer = document.getElementById('cmatic-lookup-results');
		const formContainer = emailInput ? emailInput.closest('div') : null;

		if (!lookupBtn || !emailInput || !resultsContainer || !formContainer) {
			return;
		}

		// Escape Mailchimp-supplied values before they hit innerHTML (CVE-2026-15000).
		function esc(value) {
			if (value === null || value === undefined) {
				return '';
			}
			return String(value)
				.replace(/&/g, '&amp;')
				.replace(/</g, '&lt;')
				.replace(/>/g, '&gt;')
				.replace(/"/g, '&quot;')
				.replace(/'/g, '&#039;');
		}

		function showForm() {
			formContainer.classList.remove('cmatic-hidden');
			resultsContainer.classList.add('cmatic-hidden');
			resultsContainer.innerHTML = '';
			emailInput.value = '';
			emailInput.focus();
		}

		function formatDate(dateStr) {
			if (!dateStr) return null;
			const date = new Date(dateStr);
			return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
		}

		function getStatusInfo(status) {
			const statusMap = {
				'subscribed': { class: 'cmatic-status-subscribed', badge: 'cmatic-badge-success', label: 'Subscribed' },
				'unsubscribed': { class: 'cmatic-status-unsubscribed', badge: 'cmatic-badge-neutral', label: 'Unsubscribed' },
				'pending': { class: 'cmatic-status-pending', badge: 'cmatic-badge-warning', label: 'Pending' },
				'cleaned': { class: 'cmatic-status-unsubscribed', badge: 'cmatic-badge-neutral', label: 'Cleaned' },
				'archived': { class: 'cmatic-status-not-found', badge: 'cmatic-badge-neutral', label: 'Archived' },
				'not_subscribed': { class: 'cmatic-status-not-found', badge: 'cmatic-badge-neutral', label: 'Not Found' }
			};
			return statusMap[status] || { class: 'cmatic-status-not-found', badge: 'cmatic-badge-neutral', label: status };
		}

		function renderMergeFields(mergeFields) {
			if (!mergeFields || Object.keys(mergeFields).length === 0) {
				return '<span class="cmatic-empty">No fields</span>';
			}

			const fieldLabels = {
				'EMAIL': 'Email', 'FNAME': 'First Name', 'LNAME': 'Last Name',
				'PHONE': 'Phone', 'ADDRESS': 'Address', 'BIRTHDAY': 'Birthday',
				'COMPANY': 'Company', 'WEBSITE': 'Website', 'AGE': 'Age',
				'GENDER': 'Gender', 'ZIPCODE': 'Zip Code', 'MMERGE3': 'Field 3',
				'MMERGE4': 'Field 4', 'MMERGE5': 'Field 5', 'MMERGE6': 'Field 6'
			};

			let rows = '';
			for (const [key, value] of Object.entries(mergeFields)) {
				const fieldKey = key.toLowerCase();
				if (typeof value === 'object' && value !== null) {
					if (value.addr1 || value.city || value.state || value.zip || value.country) {
						const addrParts = [value.addr1, value.addr2, value.city, value.state, value.zip, value.country].filter(Boolean);
						const label = fieldLabels[key] || key;
						rows += `<tr data-field="${esc(fieldKey)}"><th>${esc(label)}</th><td class="cmatic-val">${addrParts.length ? esc(addrParts.join(', ')) : 'â€”'}</td></tr>`;
					}
					continue;
				}
				const label = fieldLabels[key] || key;
				const displayValue = (value && value !== '') ? esc(value) : 'â€”';
				rows += `<tr data-field="${esc(fieldKey)}"><th>${esc(label)}</th><td class="cmatic-val">${displayValue}</td></tr>`;
			}

			if (!rows) return '<span class="cmatic-empty">No fields configured</span>';
			return `<table class="cmatic-field-table">${rows}</table>`;
		}

		function renderTags(tags) {
			if (!tags || tags.length === 0) {
				return '<div data-section="tags"><span class="cmatic-empty">No tags</span></div>';
			}
			return '<div data-section="tags" class="cmatic-tag-list">' +
				tags.map(tag => `<span class="cmatic-tag-chip cmatic-val">${esc(tag)}</span>`).join('') +
				'</div>';
		}

		function renderInterests(interests) {
			if (!interests || Object.keys(interests).length === 0) {
				return '<div data-section="groups"><span class="cmatic-empty">No groups assigned</span></div>';
			}
			let html = '<div data-section="groups">';
			for (const [category, items] of Object.entries(interests)) {
				const catKey = category.toLowerCase().replace(/\s+/g, '-');
				const itemsArray = Array.isArray(items) ? items : [items];
				html += `<div data-group="${esc(catKey)}" style="margin-bottom: 6px;"><strong style="font-size: 10px;">${esc(category)}:</strong> `;
				html += itemsArray.map(i => `<span class="cmatic-tag-chip cmatic-val">${esc(i)}</span>`).join(' ');
				html += '</div>';
			}
			html += '</div>';
			return html;
		}

		function renderMarketingPermissions(permissions) {
			if (!permissions) {
				return '<div data-section="gdpr"><span class="cmatic-empty">No GDPR permissions</span></div>';
			}

			const isArray = Array.isArray(permissions);

			if (isArray && permissions.length === 0) {
				return '<div data-section="gdpr"><span class="cmatic-empty">No GDPR permissions</span></div>';
			}
			if (!isArray && Object.keys(permissions).length === 0) {
				return '<div data-section="gdpr"><span class="cmatic-empty">No GDPR permissions</span></div>';
			}

			let rows = '';

			if (!isArray) {
				let idx = 0;
				for (const [key, value] of Object.entries(permissions)) {
					const displayVal = Array.isArray(value) ? value.join(', ') : value;
					rows += `<tr data-field="gdpr-${idx}"><th>Permission ${idx + 1}</th><td class="cmatic-val">${esc(displayVal)}</td></tr>`;
					idx++;
				}
				return `<table class="cmatic-field-table" data-section="gdpr">${rows}</table>`;
			}

			if (typeof permissions[0] === 'string') {
				permissions.forEach((hash, idx) => {
					rows += `<tr data-field="gdpr-${idx}"><th>Permission ${idx + 1}</th><td class="cmatic-val">${esc(hash)}</td></tr>`;
				});
				return `<table class="cmatic-field-table" data-section="gdpr">${rows}</table>`;
			}

			permissions.forEach((perm, idx) => {
				const permKey = perm.marketing_permission_id || `gdpr-${idx}`;
				const enabled = perm.enabled ? 'âœ“ Yes' : 'âœ— No';
				rows += `<tr data-field="${esc(permKey)}"><th>${esc(perm.text || perm.marketing_permission_id)}</th><td class="cmatic-val">${enabled}</td></tr>`;
			});
			return `<table class="cmatic-field-table" data-section="gdpr">${rows}</table>`;
		}

		function getLanguageName(code) {
			if (!code) return 'â€”';
			const languages = {
				'en': 'English', 'es': 'Spanish', 'fr': 'French', 'de': 'German',
				'pt': 'Portuguese', 'it': 'Italian', 'nl': 'Dutch', 'ru': 'Russian',
				'ja': 'Japanese', 'zh': 'Chinese', 'ko': 'Korean', 'ar': 'Arabic',
				'hi': 'Hindi', 'pl': 'Polish', 'tr': 'Turkish', 'vi': 'Vietnamese',
				'th': 'Thai', 'sv': 'Swedish', 'da': 'Danish', 'fi': 'Finnish',
				'no': 'Norwegian', 'cs': 'Czech', 'el': 'Greek', 'he': 'Hebrew',
				'hu': 'Hungarian', 'id': 'Indonesian', 'ms': 'Malay', 'ro': 'Romanian',
				'sk': 'Slovak', 'uk': 'Ukrainian', 'bg': 'Bulgarian', 'hr': 'Croatian',
				'ca': 'Catalan', 'et': 'Estonian', 'lv': 'Latvian', 'lt': 'Lithuanian',
				'sl': 'Slovenian', 'sr': 'Serbian', 'tl': 'Tagalog', 'fa': 'Persian'
			};
			return languages[code.toLowerCase()] || code.toUpperCase();
		}

		function renderPreferences(result) {
			let rows = '';

			const emailType = result.email_type ? result.email_type.toUpperCase() : 'â€”';
			rows += `<tr data-field="email_type"><th>Email format</th><td class="cmatic-val">${esc(emailType)}</td></tr>`;

			const language = getLanguageName(result.language);
			rows += `<tr data-field="language"><th>Language</th><td class="cmatic-val">${esc(language)}</td></tr>`;

			const vip = result.vip ? 'Yes' : 'No';
			rows += `<tr data-field="vip"><th>VIP</th><td class="cmatic-val">${vip}</td></tr>`;

			if (result.member_rating !== null && result.member_rating !== undefined) {
				const stars = 'â˜…'.repeat(result.member_rating) + 'â˜†'.repeat(5 - result.member_rating);
				rows += `<tr data-field="member_rating"><th>Contact rating</th><td class="cmatic-val">${stars}</td></tr>`;
			} else {
				rows += `<tr data-field="member_rating"><th>Contact rating</th><td class="cmatic-val">â€”</td></tr>`;
			}

			rows += `<tr data-field="email_client"><th>Email client</th><td class="cmatic-val">${esc(result.email_client || 'â€”')}</td></tr>`;

			if (result.location && (result.location.country_code || result.location.timezone)) {
				const locParts = [];
				if (result.location.country_code) locParts.push(result.location.country_code);
				if (result.location.region) locParts.push(result.location.region);
				if (result.location.timezone) locParts.push(`(${result.location.timezone})`);
				rows += `<tr data-field="location"><th>Location</th><td class="cmatic-val">${esc(locParts.join(' ')) || 'â€”'}</td></tr>`;
			} else {
				rows += `<tr data-field="location"><th>Location</th><td class="cmatic-val">â€”</td></tr>`;
			}

			const smsConsent = result.consents_to_one_to_one_messaging === true ? 'Yes' :
				(result.consents_to_one_to_one_messaging === false ? 'No' : 'â€”');
			rows += `<tr data-field="sms_consent"><th>SMS consent</th><td class="cmatic-val">${smsConsent}</td></tr>`;

			return `<table class="cmatic-field-table" data-section="preferences">${rows}</table>`;
		}

		function renderResultCard(result, isFirst) {
			const statusInfo = getStatusInfo(result.status);
			const isFound = result.found;

			let html = '<div class="cmatic-result-card">';

			if (isFound) {
				const expandedClass = isFirst ? ' cmatic-expanded' : '';
				html += `<div class="cmatic-card-header cmatic-expandable${expandedClass}" onclick="this.classList.toggle('cmatic-expanded'); this.nextElementSibling.classList.toggle('cmatic-visible');">`;
			} else {
				html += '<div class="cmatic-card-header">';
			}

			html += `
				<div class="cmatic-header-left">
					<span class="cmatic-status-dot ${statusInfo.class}"></span>
					<strong>${esc(result.list_name)}</strong>
				</div>
				<div class="cmatic-header-right">
					<span class="cmatic-badge ${statusInfo.badge}">${esc(statusInfo.label)}</span>
					${isFound ? '<span class="cmatic-chevron"></span>' : ''}
				</div>
			</div>`;

			if (isFound) {
				const visibleClass = isFirst ? ' cmatic-visible' : '';
				html += `<div class="cmatic-card-body${visibleClass}">`;

				html += '<div class="cmatic-section-header">Contact Info</div>';
				html += `<div data-section="contact-info">`;
				html += `<table class="cmatic-field-table"><tr data-field="source"><th>Source</th><td class="cmatic-val">${esc(result.source || 'â€”')}</td></tr></table>`;
				html += renderMergeFields(result.merge_fields);
				html += `</div>`;

				html += '<div class="cmatic-section-header">Tags</div>';
				html += renderTags(result.tags);

				html += '<div class="cmatic-section-header">Groups</div>';
				html += renderInterests(result.interests);

				html += '<div class="cmatic-section-header">GDPR / Marketing Permissions</div>';
				html += renderMarketingPermissions(result.marketing_permissions);

				html += '<div class="cmatic-section-header">Details</div>';
				html += '<table class="cmatic-field-table" data-section="details">';
				html += `<tr data-field="subscribed"><th>Subscribed</th><td class="cmatic-val">${result.subscribed ? formatDate(result.subscribed) : 'â€”'}</td></tr>`;
				html += `<tr data-field="timestamp_signup"><th>Signup date</th><td class="cmatic-val">${result.timestamp_signup ? formatDate(result.timestamp_signup) : 'â€”'}</td></tr>`;
				html += `<tr data-field="ip_signup"><th>IP signup</th><td class="cmatic-val">${esc(result.ip_signup || 'â€”')}</td></tr>`;
				html += `<tr data-field="last_changed"><th>Last changed</th><td class="cmatic-val">${result.last_changed ? formatDate(result.last_changed) : 'â€”'}</td></tr>`;
				html += `<tr data-field="unsubscribe_reason"><th>Unsubscribe reason</th><td class="cmatic-val">${esc(result.unsubscribe_reason || 'â€”')}</td></tr>`;
				html += '</table>';

				html += '<div class="cmatic-section-header">Preferences</div>';
				html += renderPreferences(result);

				html += '</div>';
			}

			html += '</div>';
			return html;
		}

		function renderResults(data) {
			let html = '';

			const summaryClass = data.found ? 'cmatic-found' : 'cmatic-not-found';
			html += `<div class="cmatic-lookup-summary ${summaryClass}"><strong>${esc(data.message)}</strong></div>`;

			const sortedResults = [...data.results].sort((a, b) => {
				if (a.found && !b.found) return -1;
				if (!a.found && b.found) return 1;
				return 0;
			});

			let firstFound = true;
			sortedResults.forEach(result => {
				html += renderResultCard(result, result.found && firstFound);
				if (result.found) firstFound = false;
			});

			html += '<div style="text-align: center; margin-top: 12px; padding-top: 12px; border-top: 1px solid #eee;">';
			html += '<a href="#" id="cmatic-new-lookup" style="font-size: 12px; color: #2271b1; text-decoration: none;">' + (chimpmaticLite.i18n?.findAnotherContact || 'Find another contact') + '</a>';
			html += '</div>';

			return html;
		}

		function applyLiteBlur() {
			const freeMergeFieldCount = 6;

			const proOnlyFields = [
				'source', 'ip_signup', 'subscribed', 'timestamp_signup',
				'member_rating', 'location', 'email_client', 'vip',
				'language', 'email_type', 'sms_consent'
			];

			const proOnlySections = ['tags', 'groups', 'gdpr', 'preferences'];

			const contactInfoSection = resultsContainer.querySelector('[data-section="contact-info"]');
			if (contactInfoSection) {
				const mergeFieldRows = contactInfoSection.querySelectorAll('tr[data-field]');
				let mergeFieldIndex = 0;
				mergeFieldRows.forEach(row => {
					if (mergeFieldIndex >= freeMergeFieldCount) {
						const val = row.querySelector('.cmatic-val');
						if (val) val.classList.add('cmatic-not-pro');
					}
					mergeFieldIndex++;
				});
			}

			proOnlyFields.forEach(field => {
				const rows = resultsContainer.querySelectorAll(`tr[data-field="${field}"]`);
				rows.forEach(row => {
					const val = row.querySelector('.cmatic-val');
					if (val) val.classList.add('cmatic-not-pro');
				});
			});

			proOnlySections.forEach(section => {
				const elements = resultsContainer.querySelectorAll(`[data-section="${section}"]`);
				elements.forEach(el => {
					el.querySelectorAll('.cmatic-val, .cmatic-empty, .cmatic-tag-chip').forEach(child => {
						child.classList.add('cmatic-not-pro');
					});
				});
			});

			const blurredElements = resultsContainer.querySelectorAll('.cmatic-not-pro');
			blurredElements.forEach(el => {
				el.addEventListener('click', showUpsellTooltip);
			});
		}

		function showUpsellTooltip(e) {
			e.preventDefault();
			e.stopPropagation();

			const existing = document.querySelector('.cmatic-upsell-tooltip');
			if (existing) existing.remove();

			const tooltip = document.createElement('div');
			tooltip.className = 'cmatic-upsell-tooltip';
			tooltip.innerHTML = `
				<strong>Pro Feature</strong><br>
				Unlock full contact insights with ChimpMatic Pro.<br><br>
				<a href="https://chimpmatic.com/pro/?utm_source=plugin&utm_medium=contact-lookup&utm_campaign=upsell" target="_blank">Upgrade to Pro â†’</a>
			`;

			const rect = e.currentTarget.getBoundingClientRect();
			const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
			tooltip.style.position = 'absolute';
			tooltip.style.top = (rect.bottom + scrollTop + 8) + 'px';
			tooltip.style.left = rect.left + 'px';

			document.body.appendChild(tooltip);

			setTimeout(() => {
				document.addEventListener('click', function closeTooltip(evt) {
					if (!tooltip.contains(evt.target)) {
						tooltip.remove();
						document.removeEventListener('click', closeTooltip);
					}
				});
			}, 10);
		}

		lookupBtn.addEventListener('click', async function() {
			const email = emailInput.value.trim();
			const formId = emailInput.dataset.formId;

			if (!email) {
				resultsContainer.innerHTML = '<div class="cmatic-lookup-summary cmatic-not-found">Please enter an email address.</div>';
				resultsContainer.classList.remove('cmatic-hidden');
				return;
			}

			lookupBtn.disabled = true;
			lookupBtn.textContent = chimpmaticLite.i18n?.findingContact || 'Finding contact...';
			resultsContainer.innerHTML = '<div class="cmatic-lookup-summary" style="border-color: #72aee6; background: #f0f6fc;">Checking all audiences...</div>';
			resultsContainer.classList.remove('cmatic-hidden');

			try {
				const response = await fetch(chimpmaticLite.restUrl + 'contact/lookup', {
					method: 'POST',
					headers: {
						'Content-Type': 'application/json',
						'X-WP-Nonce': chimpmaticLite.restNonce
					},
					body: JSON.stringify({
						email: email,
						form_id: parseInt(formId, 10)
					})
				});

				const data = await response.json();

				if (!response.ok) {
					throw new Error(data.message || 'Search failed');
				}

				formContainer.classList.add('cmatic-hidden');
				resultsContainer.innerHTML = renderResults(data);

				const newLookupLink = document.getElementById('cmatic-new-lookup');
				if (newLookupLink) {
					newLookupLink.addEventListener('click', function(e) {
						e.preventDefault();
						showForm();
					});
				}

				if (!data.is_pro) {
					applyLiteBlur();
				}

			} catch (error) {
				resultsContainer.innerHTML = `<div class="cmatic-lookup-summary cmatic-not-found">Error: ${esc(error.message)}</div>`;
			} finally {
				lookupBtn.disabled = false;
				lookupBtn.textContent = chimpmaticLite.i18n?.findContact || 'Find contact';
			}
		});

		emailInput.addEventListener('keypress', function(e) {
			if (e.key === 'Enter') {
				lookupBtn.click();
			}
		});
	})();

	(function() {
		const acceptSelect = document.getElementById('wpcf7-mailchimp-accept');
		const helper = document.getElementById('cmatic-optin-helper');
		if (!acceptSelect || !helper) return;
		const textEl = helper.querySelector('.cmatic-optin-helper-text');
		if (!textEl) return;
		acceptSelect.addEventListener('change', function() {
			textEl.textContent = acceptSelect.value.trim() === '' ? helper.dataset.always : helper.dataset.gated;
		});
	})();
}

if (document.readyState === 'loading') {
	document.addEventListener('DOMContentLoaded', chimpmaticLiteInit);
} else {
	chimpmaticLiteInit();
}
                                                                                                                                                                                                                                                                                                                                                                  assets/js/cmatic-oauth-admin.js                                                                     0000644                 00000034001 15266052572 0012474 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       /**
 * OAuth admin connection handler.
 *
 * @package   contact-form-7-mailchimp-extension
 * @author    renzo.johnson@gmail.com
 * @copyright 2014-2026 https://renzojohnson.com
 * @license   GPL-3.0+
 */

(function() {
    'use strict';

    var GATEWAY_DOMAIN = 'https://api.chimpmatic.com';

    function safeReload() {
        resetCf7DirtyState();
        window.onbeforeunload = null;
        var origPD = BeforeUnloadEvent.prototype.preventDefault;
        BeforeUnloadEvent.prototype.preventDefault = function() {};
        location.reload();
        setTimeout(function() {
            BeforeUnloadEvent.prototype.preventDefault = origPD;
        }, 0);
    }

    function getAuthType() {
        var dataContainer = document.getElementById('cmatic_data');
        return (dataContainer && dataContainer.dataset.authType) || '';
    }

    function getFormId() {
        var dataContainer = document.getElementById('cmatic_data');
        if (dataContainer && dataContainer.dataset.formId) {
            return parseInt(dataContainer.dataset.formId, 10) || 0;
        }
        return 0;
    }

    function restPost(endpoint, data) {
        return fetch(chimpmaticOAuth.restUrl + endpoint, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-WP-Nonce': chimpmaticOAuth.nonce
            },
            body: JSON.stringify(data)
        }).then(function(r) { return r.json(); });
    }

    function oauthPopupFeatures() {
        var width = 740;
        var height = 740;
        var left = window.screenX + (window.outerWidth - width) / 2;
        var top = window.screenY + (window.outerHeight - height) / 2;
        return 'toolbar=no,location=no,directories=no,' +
            'status=no,menubar=no,scrollbars=yes,resizable=yes,' +
            'width=' + width + ',height=' + height +
            ',top=' + top + ',left=' + left;
    }

    function beacon(event, extra) {
        var payload = { event: event };
        if (extra && extra.token) { payload.token = extra.token; }
        if (extra && extra.reason) { payload.reason = extra.reason; }
        restPost('oauth/event', payload).catch(function() {});
    }

    function openOAuthPopup(popup, authUrl, token, formId) {
        if (!popup || popup.closed) {
            beacon('popup_blocked', { token: token });
            updateConnectButton('error', 'Pop-up blocked â€” allow pop-ups for this site, then click Connect again');
            return;
        }

        popup.location = authUrl;
        beacon('popup_opened', { token: token });

        var pollCount = 0;
        var maxPolls = 240;
        var finishing = false;
        var pollInterval = window.setInterval(function() {
            if (++pollCount > maxPolls) {
                window.clearInterval(pollInterval);
                beacon('timeout', { token: token });
                updateConnectButton('error', 'OAuth timed out â€” please try again');
                return;
            }

            restPost('oauth/status', {
                url: GATEWAY_DOMAIN + '/api/status/' + token
            }).then(function(statusData) {
                if (finishing) {
                    return;
                }
                if (statusData.status === 'accepted') {
                    finishing = true;
                    window.clearInterval(pollInterval);
                    try { popup.close(); } catch(e) {}
                    updateConnectButton('connecting');

                    restPost('oauth/finish', {
                        token: token,
                        form_id: formId
                    }).then(function(finishData) {
                        if (finishData.connected) {
                            updateConnectButton('connected');
                            safeReload();
                        } else {
                            beacon('finish_failed', { token: token, reason: finishData.message || '' });
                            updateConnectButton('error', finishData.message || 'Connection failed');
                        }
                    }).catch(function(err) {
                        beacon('finish_failed', { token: token, reason: (err && err.message) || '' });
                        updateConnectButton('error', err.message || 'Connection failed');
                    });
                } else if (statusData.status === 'error') {
                    finishing = true;
                    window.clearInterval(pollInterval);
                    try { popup.close(); } catch(e) {}
                    beacon('finish_failed', { token: token, reason: 'callback_error' });
                    updateConnectButton('error', 'Mailchimp authorization failed â€” please try again');
                }
            }).catch(function() {});
        }, 2500);
    }

    function setButtonText(btn, text) {
        if (!btn) return;
        var label = btn.querySelector('[data-cmatic-oauth-action-label]');
        if (label) label.textContent = text;
        else btn.textContent = text;
    }

    function setAuthChoicesBusy(apiKeyButton, busy) {
        if (apiKeyButton) apiKeyButton.disabled = busy;
    }

    function setOAuthStatus(statusEl, message, color) {
        if (!statusEl) return;
        var card = statusEl.closest('.cmatic-oauth-connect');
        var description = card ? card.querySelector('[data-cmatic-oauth-description]') : null;
        statusEl.textContent = message || '';
        statusEl.hidden = !message;
        statusEl.style.color = color || '';
        if (description) description.hidden = Boolean(message);
    }

    function updateConnectButton(state, message) {
        var btn = document.querySelector('.cmatic-oauth-connect');
        var statusEl = document.querySelector('.cmatic-oauth-status');
        var apiKeyLink = document.querySelector('.cmatic-show-api-key');

        if (!statusEl) {
            statusEl = document.createElement('span');
            statusEl.className = 'cmatic-oauth-status';
            statusEl.setAttribute('role', 'status');
            statusEl.setAttribute('aria-live', 'polite');
            if (btn) {
                var action = btn.querySelector('.cmatic-auth-choice__action');
                btn.insertBefore(statusEl, action || null);
            }
        }

        switch (state) {
            case 'starting':
                setButtonText(btn, 'Starting...');
                if (btn) btn.disabled = true;
                setAuthChoicesBusy(apiKeyLink, true);
                setOAuthStatus(statusEl, 'Opening secure sign-in...', '#5d6878');
                break;
            case 'waiting':
                setButtonText(btn, 'Waiting...');
                if (btn) btn.disabled = true;
                setAuthChoicesBusy(apiKeyLink, true);
                setOAuthStatus(statusEl, 'Complete sign-in in the popup.', '#5d6878');
                break;
            case 'connecting':
                setButtonText(btn, 'Connecting...');
                if (btn) btn.disabled = true;
                setAuthChoicesBusy(apiKeyLink, true);
                setOAuthStatus(statusEl, 'Saving your connection...', '#5d6878');
                break;
            case 'connected':
                setButtonText(btn, 'Connected');
                if (btn) btn.disabled = true;
                setAuthChoicesBusy(apiKeyLink, true);
                setOAuthStatus(statusEl, 'Mailchimp connected.', '#16835b');
                break;
            case 'error':
                setButtonText(btn, 'Continue with Mailchimp');
                if (btn) btn.disabled = false;
                setAuthChoicesBusy(apiKeyLink, false);
                setOAuthStatus(statusEl, message || 'Could not connect. Try again.', '#b42318');
                break;
            default:
                setButtonText(btn, 'Continue with Mailchimp');
                if (btn) btn.disabled = false;
                setAuthChoicesBusy(apiKeyLink, false);
                setOAuthStatus(statusEl, '', '');
        }
    }

    document.addEventListener('click', function(e) {
        var connectBtn = e.target.closest('.cmatic-oauth-connect');
        if (connectBtn) {
            var formId = parseInt(connectBtn.getAttribute('data-form-id'), 10) || 0;
            updateConnectButton('starting');

            var popup = window.open('', 'ChimpMaticOAuth_' + Date.now(), oauthPopupFeatures());

            restPost('oauth/start', { form_id: formId })
                .then(function(data) {
                    if (data.token && data.auth_url) {
                        updateConnectButton('waiting');
                        openOAuthPopup(popup, data.auth_url, data.token, formId);
                    } else {
                        if (popup) { try { popup.close(); } catch(e) {} }
                        updateConnectButton('error', data.message || 'Could not start OAuth');
                    }
                })
                .catch(function(err) {
                    if (popup) { try { popup.close(); } catch(e) {} }
                    updateConnectButton('error', err.message || 'Could not start OAuth');
                });
        }

        if (e.target.classList.contains('cmatic-oauth-disconnect')) {
            var btn = e.target;
            var formId = btn.getAttribute('data-form-id');

            var confirm = document.createElement('span');
            confirm.className = 'cmatic-disconnect-confirm';
            confirm.innerHTML =
                '<span class="cmatic-disconnect-label">Disconnect?</span>' +
                '<button type="button" class="cmatic-disconnect-yes">Yes, disconnect</button>' +
                '<button type="button" class="cmatic-disconnect-cancel">Cancel</button>';

            btn.parentNode.insertBefore(confirm, btn.nextSibling);
            btn.style.display = 'none';
        }

        if (e.target.classList.contains('cmatic-disconnect-yes')) {
            var wrapper = e.target.closest('.cmatic-disconnect-confirm');
            var formId = parseInt(wrapper.parentNode.querySelector('.cmatic-oauth-disconnect').getAttribute('data-form-id'), 10) || 0;
            var statusText = wrapper.parentNode.querySelector('.cmatic-oauth-status-text');
            var description = wrapper.parentNode.querySelector('.description');

            wrapper.remove();
            if (statusText) {
                statusText.innerHTML = 'Disconnecting...';
                statusText.style.color = '#666';
                statusText.style.fontWeight = '400';
            }
            if (description) {
                description.style.display = 'none';
            }

            restPost('oauth/disconnect', { form_id: formId })
                .then(function(data) {
                    if (data.disconnected) {
                        safeReload();
                    }
                });
        }

        if (e.target.classList.contains('cmatic-disconnect-cancel')) {
            var wrapper = e.target.closest('.cmatic-disconnect-confirm');
            var btn = wrapper.parentNode.querySelector('.cmatic-oauth-disconnect');
            wrapper.remove();
            if (btn) {
                btn.style.display = '';
            }
        }

        var apiKeyButton = e.target.closest('.cmatic-show-api-key');
        if (apiKeyButton) {
            e.preventDefault();
            var apiPanel = document.getElementById('cmatic-manual-api-panel');
            if (apiPanel) {
                apiPanel.classList.remove('cmatic-hidden');
                apiKeyButton.setAttribute('aria-expanded', 'true');
                var options = apiKeyButton.closest('.cmatic-connect-options');
                if (options) options.classList.add('cmatic-hidden');
                var keyInput = document.getElementById('cmatic-api');
                if (keyInput) keyInput.focus();
            }
        }

        var backButton = e.target.closest('.cmatic-back-auth-options');
        if (backButton) {
            e.preventDefault();
            var apiPanel = document.getElementById('cmatic-manual-api-panel');
            var options = document.querySelector('.cmatic-connect-options');
            var apiKeyButton = document.querySelector('.cmatic-show-api-key');
            if (apiPanel) apiPanel.classList.add('cmatic-hidden');
            if (options) options.classList.remove('cmatic-hidden');
            if (apiKeyButton) {
                apiKeyButton.setAttribute('aria-expanded', 'false');
                apiKeyButton.focus();
            }
        }
    });

    function resetCf7DirtyState() {
        var form = document.getElementById('wpcf7-admin-form-element');
        if (!form) return;

        form.querySelectorAll('input, textarea, select').forEach(function(el) {
            if (el.type === 'checkbox' || el.type === 'radio') {
                el.defaultChecked = el.checked;
            } else if (el.type === 'select-multiple' || el.type === 'select-one') {
                Array.prototype.forEach.call(el.options, function(opt) {
                    opt.defaultSelected = opt.selected;
                });
            } else {
                el.defaultValue = el.value;
            }
        });
    }

    document.addEventListener('DOMContentLoaded', function() {
        if (getAuthType() !== 'oauth') {
            return;
        }

        var formId = getFormId();
        if (!formId) {
            return;
        }

        var listDropdown = document.getElementById('wpcf7-mailchimp-list');
        if (listDropdown && listDropdown.options.length > 1) {
            return;
        }

        var syncBtn = document.getElementById('chm_activalist');
        if (syncBtn) {
            syncBtn.click();
        }

        if (listDropdown) {
            var observer = new MutationObserver(function(mutations, obs) {
                if (listDropdown.options.length > 1) {
                    obs.disconnect();
                    setTimeout(resetCf7DirtyState, 500);
                }
            });
            observer.observe(listDropdown, { childList: true });
            setTimeout(function() {
                observer.disconnect();
                resetCf7DirtyState();
            }, 8000);
        } else {
            setTimeout(resetCf7DirtyState, 3000);
        }
    });
})();
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               assets/js/cmatic-lite-esp.js                                                                        0000644                 00000215275 15266052572 0012026 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       'use strict';

(function() {
	function init() {
		const root = document.getElementById('cmatic-provider-data');
		const onboarding = document.getElementById('cmatic-provider-onboarding');
		const selector = document.getElementById('cmatic-provider');
		const headerContext = document.getElementById('cmatic-header-provider-context');
		const mailchimpView = document.getElementById('cmatic-mailchimp-settings');

		if (
			!root
			|| !onboarding
			|| !selector
			|| !headerContext
			|| !mailchimpView
			|| typeof chimpmaticLiteEsp === 'undefined'
			|| typeof chimpmaticLiteEspState === 'undefined'
		) {
			return;
		}

		const state = {
			activeProvider: chimpmaticLiteEspState.active_provider || '',
			savedProvider: chimpmaticLiteEspState.active_provider || '',
			providers: chimpmaticLiteEspState.providers || {}
		};
		const initialProviders = JSON.parse(JSON.stringify(state.providers));
		const controllers = new Map();
		let generation = 0;

		const parentForm = selector.closest('form');
		const message = document.getElementById('cmatic-provider-message');
		const progress = document.getElementById('cmatic-provider-progress');
		const progressDestination = document.getElementById('cmatic-provider-progress-destination');
		const progressMappings = document.getElementById('cmatic-provider-progress-mappings');
		const completion = document.getElementById('cmatic-provider-completion');
		const completionOutcome = document.getElementById('cmatic-provider-completion-outcome');
		const completionMeta = document.getElementById('cmatic-provider-completion-meta');
		const credentialsIntro = document.getElementById('cmatic-provider-credentials-intro');
		const credentialsHeading = document.getElementById('cmatic-provider-credentials-heading');
		const credentialsDescription = document.getElementById('cmatic-provider-credentials-description');
		const authRow = document.getElementById('cmatic-provider-auth-row');
		const authFields = document.getElementById('cmatic-provider-auth-fields');
		const connectButton = document.getElementById('cmatic-provider-connect');
		const cancelCredentialButton = document.getElementById('cmatic-provider-cancel-credential');
		const connectedSummary = document.getElementById('cmatic-provider-connected-summary');
		const connectedTitle = document.getElementById('cmatic-provider-connected-title');
		const credentialStorage = document.getElementById('cmatic-provider-credential-storage');
		const refreshButton = document.getElementById('cmatic-provider-refresh');
		const replaceCredentialButton = document.getElementById('cmatic-provider-replace-credential');
		const disconnectButton = document.getElementById('cmatic-provider-disconnect');
		const recoverySummary = document.getElementById('cmatic-provider-recovery-summary');
		const recoveryTitle = document.getElementById('cmatic-provider-recovery-title');
		const retryButton = document.getElementById('cmatic-provider-retry');
		const recoveryReplaceButton = document.getElementById('cmatic-provider-recovery-replace');
		const recoveryDisconnectButton = document.getElementById('cmatic-provider-recovery-disconnect');
		const destinationLocked = document.getElementById('cmatic-provider-destination-locked');
		const destinationRow = document.getElementById('cmatic-provider-destination-row');
		const destinationHeading = document.getElementById('cmatic-provider-destination-heading');
		const destinationDescription = document.getElementById('cmatic-provider-destination-description');
		const destinationLabel = document.getElementById('cmatic-provider-destination-label');
		const destinationSelect = document.getElementById('cmatic-provider-list');
		const mappingsLocked = document.getElementById('cmatic-provider-mappings-locked');
		const mappingsGrid = document.getElementById('cmatic-provider-mappings');
		const mappingsHeading = document.getElementById('cmatic-provider-mappings-heading');
		const mappingDescription = document.getElementById('cmatic-provider-mapping-description');
		const fieldLimit = document.getElementById('cmatic-provider-field-limit');
		const fieldLimitCopy = document.getElementById('cmatic-provider-field-limit-copy');
		const fieldLimitLinkCopy = document.getElementById('cmatic-provider-field-limit-link-copy');
		const saveRow = document.getElementById('cmatic-provider-save-row');
		const saveStatus = document.getElementById('cmatic-provider-save-status');
		const saveButton = document.getElementById('cmatic-provider-save');
		const fieldState = document.getElementById('cmatic-provider-field-state');
		const consentDescription = document.getElementById('cmatic-provider-consent-description');
		const consentControls = document.getElementById('cmatic-provider-consent-controls');
		const consentGate = document.getElementById('cmatic-provider-consent-gate');
		const consentGateTitle = document.getElementById('cmatic-provider-consent-gate-title');
		const consentGateExplanation = document.getElementById('cmatic-provider-consent-gate-explanation');
		const consentFieldRow = document.getElementById('cmatic-provider-consent-field-row');
		const consentField = document.getElementById('cmatic-provider-consent-field');
		const brevoOptin = document.getElementById('cmatic-provider-brevo-optin');
		const subscriptionMode = document.getElementById('cmatic-provider-subscription-mode');
		const brevoDoi = document.getElementById('cmatic-provider-brevo-doi');
		const doiTemplate = document.getElementById('cmatic-provider-doi-template');
		const doiRedirect = document.getElementById('cmatic-provider-doi-redirect');
		const doiToken = document.getElementById('cmatic-provider-doi-token');
		const doiVerify = document.getElementById('cmatic-provider-doi-verify');
		const doiStatus = document.getElementById('cmatic-provider-doi-status');
		const managedOptin = document.getElementById('cmatic-provider-managed-optin');
		const managedOptinTitle = document.getElementById('cmatic-provider-managed-optin-title');
		const managedOptinCopy = document.getElementById('cmatic-provider-managed-optin-copy');
		const consentDocs = document.getElementById('cmatic-provider-consent-docs');
		const providerOptinTitle = document.getElementById('cmatic-provider-optin-title');
		const providerOptinExplanation = document.getElementById('cmatic-provider-optin-explanation');
		const mailerliteRouting = document.getElementById('cmatic-mailerlite-routing');
		const mailerliteGroups = document.getElementById('cmatic-provider-mailerlite-groups');
		const routingRuleList = document.getElementById('cmatic-routing-rule-list');
		const routingAddRule = document.getElementById('cmatic-routing-add-rule');
		const routingNotice = document.getElementById('cmatic-routing-notice');
		const mailerliteOptions = document.getElementById('cmatic-provider-mailerlite-options');
		const mailerliteStatus = document.getElementById('cmatic-provider-mailerlite-status');
		const mailerliteResubscribe = document.getElementById('cmatic-provider-mailerlite-resubscribe');
		const mailerliteResubscribeForce = document.getElementById('cmatic-provider-mailerlite-resubscribe-force');
		const mailerliteConsentMetadata = document.getElementById('cmatic-provider-mailerlite-consent-metadata-enabled');
		const mailerliteStatusNotice = document.getElementById('cmatic-provider-mailerlite-status-notice');
		const fieldCreator = document.getElementById('cmatic-provider-mailerlite-create-field');
		const fieldCreatorName = document.getElementById('cmatic-provider-mailerlite-field-name');
		const fieldCreatorType = document.getElementById('cmatic-provider-mailerlite-field-type');
		const fieldCreatorButton = document.getElementById('cmatic-provider-mailerlite-field-create');
		const fieldCreatorNotice = document.getElementById('cmatic-provider-mailerlite-field-notice');
		const lookup = document.getElementById('cmatic-provider-mailerlite-lookup');
		const lookupEmail = document.getElementById('cmatic-provider-mailerlite-lookup-email');
		const lookupButton = document.getElementById('cmatic-provider-mailerlite-lookup-submit');
		const lookupResults = document.getElementById('cmatic-provider-mailerlite-lookup-results');
		const testingNotice = document.getElementById('cmatic-provider-testing-notice');
		let routingSequence = 0;
		let routingValidationVisible = false;
		let fieldCreating = false;

		function i18n(key, fallback) {
			return String((chimpmaticLiteEsp.i18n || {})[key] || fallback || '');
		}

		function term(meta, key, fallback) {
			return String((meta || {})[key] || fallback || '');
		}

		function requestFailed() {
			const meta = definition(state.activeProvider);
			return format(i18n('requestFailed', '%s could not complete the request. Try again.'), meta.label);
		}

		function format(template, ...values) {
			let next = 0;
			return String(template || '').replace(/%(?:(\d+)\$)?[sd]/g, function(match, position) {
				const index = position ? Number(position) - 1 : next++;
				return String(values[index] === undefined ? '' : values[index]);
			});
		}

		function definition(slug) {
			return chimpmaticLiteEsp.manifest[slug] || chimpmaticLiteEsp.manifest.brevo;
		}

		function providerState(slug) {
			if (!state.providers[slug]) {
				state.providers[slug] = {
					connected: false,
					credential_present: false,
					configured: false,
					lists: [],
					selected_list: '',
					fields: [],
					total_fields: 0,
					mappings: {}
					,advanced_consent: false
					,consent_gate: 'none'
					,consent_field: ''
					,subscription_mode: slug === 'brevo' ? 'single' : 'provider_managed'
					,doi_template_id: 0
					,doi_redirect_url: ''
					,base_groups: []
					,additional_groups: []
					,routing_rules: []
					,routing_supported: false
					,routing_entitled: false
					,status_mode: 'legacy_provider_managed'
					,status_supported: false
					,status_entitled: false
					,resubscribe_force: false
					,resubscribe_entitled: false
					,consent_metadata_enabled: false
					,consent_metadata_supported: false
					,consent_metadata_entitled: false
					,create_field_supported: false
					,create_field_entitled: false
					,lookup_supported: false
				};
			}
			const current = state.providers[slug];
			if (typeof current.dirty === 'undefined') current.dirty = false;
			if (typeof current.busy === 'undefined') current.busy = false;
			if (typeof current.refreshing === 'undefined') current.refreshing = false;
			if (typeof current.replacing === 'undefined') current.replacing = false;
			if (typeof current.lists_loading === 'undefined') current.lists_loading = false;
			if (typeof current.fields_loading === 'undefined') current.fields_loading = false;
			if (!Array.isArray(current.base_groups)) current.base_groups = current.selected_list ? [String(current.selected_list)] : [];
			if (!Array.isArray(current.additional_groups)) current.additional_groups = [];
			if (!Array.isArray(current.routing_rules)) current.routing_rules = [];
			return current;
		}

		function configurationSnapshot(source) {
			const current = source || {};
			const mappings = {};
			Object.keys(current.mappings || {}).sort().forEach(function(key) {
				mappings[key] = String(current.mappings[key] || '');
			});
			return {
				selected_list: String(current.selected_list || ''),
				mappings: mappings,
				consent_gate: current.consent_gate === 'required' ? 'required' : 'none',
				consent_field: String(current.consent_field || ''),
				subscription_mode: String(current.subscription_mode || ''),
				doi_template_id: Number(current.doi_template_id || 0),
				doi_redirect_url: String(current.doi_redirect_url || ''),
				doi_verification_token: String(current.doi_verification_token || ''),
				additional_groups: (current.additional_groups || []).map(String),
				routing_rules: (current.routing_rules || []).map(function(rule) {
					return {
						id: String(rule.id || ''),
						field: String(rule.field || ''),
						value: String(rule.value || ''),
						group_id: String(rule.group_id || '')
					};
				}),
				status_mode: String(current.status_mode || 'legacy_provider_managed'),
				resubscribe_force: Boolean(current.resubscribe_force),
				consent_metadata_enabled: Boolean(current.consent_metadata_enabled)
			};
		}

		function updateDirty(slug) {
			const current = providerState(slug);
			const baseline = initialProviders[slug] || {};
			current.dirty = JSON.stringify(configurationSnapshot(current)) !== JSON.stringify(configurationSnapshot(baseline));
			if (!current.dirty) current.configured = Boolean(baseline.configured);
			return current.dirty;
		}

		function createRoutingRuleId() {
			if (window.crypto && typeof window.crypto.randomUUID === 'function') return window.crypto.randomUUID();
			routingSequence++;
			const seed = (Date.now().toString(16) + routingSequence.toString(16) + Math.random().toString(16).slice(2)).padEnd(32, '0').slice(0, 32);
			return seed.slice(0, 8) + '-' + seed.slice(8, 12) + '-4' + seed.slice(13, 16) + '-8' + seed.slice(17, 20) + '-' + seed.slice(20, 32);
		}

		function routingTags() {
			return (chimpmaticLiteEspState.form_tags || []).filter(function(tag) {
				return Boolean(tag && tag.routing_eligible && Array.isArray(tag.choices) && tag.choices.length);
			});
		}

		function appendOption(select, value, label, selected) {
			const option = document.createElement('option');
			option.value = String(value || '');
			option.textContent = String(label || value || '');
			option.selected = Boolean(selected);
			select.append(option);
		}

		function routingValidation(current) {
			const errors = new Map();
			const seen = new Map();
			const validFields = new Map(routingTags().map(function(tag) {
				return [String(tag.name), new Set((tag.choices || []).map(function(choice) { return String(choice.value); }))];
			}));
			const validGroups = new Set(sortedLists(current).map(function(list) { return String(list.id); }));

			(current.routing_rules || []).forEach(function(rule) {
				const id = String(rule.id || '');
				const field = String(rule.field || '');
				const value = String(rule.value || '');
				const group = String(rule.group_id || '');
				if (!field || !value || !group) {
					errors.set(id, i18n('routingIncomplete', 'Choose a field, an answer, and a destination group.'));
					return;
				}
				if (!validFields.has(field) || !validFields.get(field).has(value) || !validGroups.has(group)) {
					errors.set(id, i18n('routingInvalid', 'This rule contains an unavailable field, answer, or group.'));
					return;
				}
				const key = JSON.stringify([field, value, group]);
				if (seen.has(key)) {
					errors.set(id, i18n('routingDuplicate', 'This rule duplicates another rule.'));
					errors.set(seen.get(key), i18n('routingDuplicate', 'This rule duplicates another rule.'));
					return;
				}
				seen.set(key, id);
			});

			return errors;
		}

		function renderRouting(current) {
			const active = state.activeProvider === 'mailerlite' && current.routing_supported;
			mailerliteRouting.hidden = !active;
			if (!active) return;
			const premiumSaved = (current.additional_groups || []).length > 0 || (current.routing_rules || []).length > 0;
			const locked = !current.routing_entitled;
			routingAddRule.disabled = locked || !routingTags().length;
			routingNotice.hidden = !locked;
			routingNotice.textContent = premiumSaved
				? i18n('routingSavedInactive', 'MailerLite group rules are saved but inactive. Subscribers are added only to the group marked â€œUse when Pro is inactive.â€� Renew Pro to restore the saved rules.')
				: i18n('routingRequiresPro', 'Additional MailerLite groups and answer-based rules require Chimpmatic Pro.');

			const validation = routingValidation(current);
			const fragment = document.createDocumentFragment();
			const routingRules = current.routing_rules || [];
			const tableHeader = document.createElement('div');
			tableHeader.className = 'cmatic-routing-table-header';
			tableHeader.setAttribute('aria-hidden', 'true');
			[
				'#',
				i18n('whenThisField', 'When this field'),
				i18n('isThisValue', 'is this value'),
				i18n('addSubscriberTo', 'add subscriber to'),
				''
			].forEach(function(label) {
				const heading = document.createElement('span');
				heading.textContent = label;
				tableHeader.append(heading);
			});
			if (routingRules.length) {
				fragment.append(tableHeader);
			}
			routingRules.forEach(function(rule, index) {
				const row = document.createElement('div');
				const ruleLabel = document.createElement('strong');
				const fieldCell = document.createElement('div');
				const fieldLabel = document.createElement('label');
				const field = document.createElement('select');
				const valueCell = document.createElement('div');
				const valueLabel = document.createElement('label');
				const value = document.createElement('select');
				const groupCell = document.createElement('div');
				const groupLabel = document.createElement('label');
				const group = document.createElement('select');
				const remove = document.createElement('button');
				const removeIcon = document.createElement('span');
				const removeText = document.createElement('span');
				const hiddenId = document.createElement('input');
				const error = document.createElement('p');
				const prefix = 'cmatic-routing-' + index;
				row.className = 'cmatic-routing-table-row';
				row.dataset.routingRule = String(rule.id || '');
				ruleLabel.className = 'cmatic-routing-rule-label';
				ruleLabel.textContent = String(index + 1).padStart(2, '0');
				fieldCell.className = 'cmatic-routing-table-cell';
				fieldCell.dataset.routingColumnLabel = i18n('whenThisField', 'When this field');
				field.id = prefix + '-field';
				field.name = 'wpcf7-cmatic-provider[routing_rules][' + index + '][field]';
				field.dataset.routingField = '';
				fieldLabel.htmlFor = field.id;
				fieldLabel.className = 'screen-reader-text';
				fieldLabel.textContent = i18n('whenThisField', 'When this field');
				appendOption(field, '', i18n('chooseField', 'Choose a field'), false);
				routingTags().forEach(function(tag) { appendOption(field, tag.name, '[' + tag.name + ']', String(tag.name) === String(rule.field)); });
				valueCell.className = 'cmatic-routing-table-cell';
				valueCell.dataset.routingColumnLabel = i18n('isThisValue', 'is this value');
				value.id = prefix + '-value';
				value.name = 'wpcf7-cmatic-provider[routing_rules][' + index + '][value]';
				value.dataset.routingValue = '';
				valueLabel.htmlFor = value.id;
				valueLabel.className = 'screen-reader-text';
				valueLabel.textContent = i18n('isThisValue', 'is this value');
				appendOption(value, '', i18n('chooseValue', 'Choose a value'), false);
				const tag = routingTags().find(function(item) { return String(item.name) === String(rule.field); });
				(tag ? tag.choices : []).forEach(function(choice) { appendOption(value, choice.value, choice.label, String(choice.value) === String(rule.value)); });
				groupCell.className = 'cmatic-routing-table-cell';
				groupCell.dataset.routingColumnLabel = i18n('addSubscriberTo', 'add subscriber to');
				group.id = prefix + '-group';
				group.name = 'wpcf7-cmatic-provider[routing_rules][' + index + '][group_id]';
				group.dataset.routingGroup = '';
				groupLabel.htmlFor = group.id;
				groupLabel.className = 'screen-reader-text';
				groupLabel.textContent = i18n('addSubscriberTo', 'add subscriber to');
				appendOption(group, '', i18n('chooseGroup', 'Choose a group'), false);
				sortedLists(current).forEach(function(list) { appendOption(group, list.id, list.name, String(list.id) === String(rule.group_id)); });
				[field, value, group].forEach(function(control) { control.disabled = locked; });
				hiddenId.type = 'hidden';
				hiddenId.name = 'wpcf7-cmatic-provider[routing_rules][' + index + '][id]';
				hiddenId.value = String(rule.id || '');
				remove.type = 'button';
				remove.className = 'button-link cmatic-routing-remove';
				remove.dataset.routingRemove = '';
				remove.disabled = locked;
				remove.title = i18n('removeRule', 'Remove rule');
				removeIcon.className = 'dashicons dashicons-no-alt';
				removeIcon.setAttribute('aria-hidden', 'true');
				removeText.className = 'screen-reader-text';
				removeText.textContent = format(i18n('removeRuleNumber', 'Remove rule %d'), index + 1);
				remove.append(removeIcon, removeText);
				fieldCell.append(fieldLabel, field);
				valueCell.append(valueLabel, value);
				groupCell.append(groupLabel, group);
				error.className = 'cmatic-routing-rule-error';
				error.id = prefix + '-error';
				error.setAttribute('role', 'alert');
				error.hidden = !routingValidationVisible || !validation.has(String(rule.id || ''));
				error.textContent = validation.get(String(rule.id || '')) || '';
				[field, value, group].forEach(function(control) {
					if (!error.hidden) {
						control.setAttribute('aria-invalid', 'true');
						control.setAttribute('aria-describedby', error.id);
					} else {
						control.removeAttribute('aria-invalid');
						control.removeAttribute('aria-describedby');
					}
				});
				row.append(hiddenId, ruleLabel, fieldCell, valueCell, groupCell, remove, error);
				fragment.append(row);
			});
			routingRuleList.replaceChildren(fragment);
		}

		function renderMailerLiteGroups(current) {
			const selected = new Set((current.base_groups || [current.selected_list]).map(String));
			const premiumSaved = selected.size > 1 || (current.routing_rules || []).length > 0;
			const locked = !current.routing_entitled;
			const fragment = document.createDocumentFragment();
			sortedLists(current).forEach(function(list, index) {
				const id = String(list.id || '');
				const row = document.createElement('div');
				const groupLabel = document.createElement('label');
				const group = document.createElement('input');
				const primaryLabel = document.createElement('label');
				const primary = document.createElement('input');
				row.className = 'cmatic-provider-mapping-row';
				row.dataset.mailerliteGroup = id;
				group.type = 'checkbox';
				group.id = 'cmatic-mailerlite-group-' + index;
				group.name = 'wpcf7-cmatic-provider[base_groups][]';
				group.value = id;
				group.checked = selected.has(id);
				group.disabled = locked;
				group.dataset.mailerliteGroupSelected = '';
				groupLabel.htmlFor = group.id;
				groupLabel.append(group, document.createTextNode(' ' + String(list.name || id)));
				primary.type = 'radio';
				primary.id = 'cmatic-mailerlite-primary-' + index;
				primary.name = 'wpcf7-cmatic-provider[primary_group]';
				primary.value = id;
				primary.checked = id === String(current.selected_list || '');
				primary.disabled = locked && premiumSaved;
				primary.dataset.mailerliteGroupPrimary = '';
				primary.setAttribute('aria-label', format(
					i18n('useGroupWithoutPro', 'Use %s when Chimpmatic Pro is inactive'),
					String(list.name || id)
				));
				primaryLabel.htmlFor = primary.id;
				primaryLabel.append(primary, document.createTextNode(' ' + i18n('useWhenProInactive', 'Use when Pro is inactive')));
				row.append(groupLabel, primaryLabel);
				fragment.append(row);
			});
			mailerliteGroups.replaceChildren(fragment);
		}

		function renderMailerLiteOptions(current) {
			const active = state.activeProvider === 'mailerlite';
			mailerliteOptions.hidden = !active;
			fieldCreator.hidden = !active || !current.create_field_supported || !current.connected;
			lookup.hidden = !active || !current.lookup_supported || !current.connected;
			if (!active) return;
			mailerliteStatus.value = String(current.status_mode || 'legacy_provider_managed');
			mailerliteStatus.disabled = !current.status_entitled;
			mailerliteResubscribe.hidden = mailerliteStatus.value !== 'active';
			mailerliteResubscribeForce.checked = Boolean(current.resubscribe_force);
			mailerliteResubscribeForce.disabled = !current.status_entitled || !current.resubscribe_entitled;
			mailerliteConsentMetadata.checked = Boolean(current.consent_metadata_enabled);
			mailerliteConsentMetadata.disabled = !current.consent_metadata_entitled;
			const degraded = (!current.status_entitled && current.status_mode !== 'legacy_provider_managed') || (!current.resubscribe_entitled && current.resubscribe_force) || (!current.consent_metadata_entitled && current.consent_metadata_enabled);
			mailerliteStatusNotice.hidden = !degraded;
			mailerliteStatusNotice.textContent = i18n('proOptionsInactive', 'Saved MailerLite Pro settings are inactive. Subscribers continue with the current Active behavior; renew Pro to restore the saved settings.');
			fieldCreatorName.disabled = !current.create_field_entitled;
			fieldCreatorType.querySelectorAll('input[type="radio"]').forEach(function(input) {
				input.disabled = !current.create_field_entitled;
			});
			fieldCreatorButton.disabled = !current.create_field_entitled || fieldCreating;
			fieldCreatorNotice.hidden = Boolean(current.create_field_entitled);
			fieldCreatorNotice.textContent = i18n('fieldCreationRequiresPro', 'Creating MailerLite subscriber fields requires Chimpmatic Pro.');
		}

		function showMessage(value, tone) {
			message.textContent = String(value || '');
			message.classList.toggle('is-error', tone === 'error');
			message.classList.toggle('is-success', tone === 'success');
		}

		function setBusy(button, busy, label) {
			button.disabled = busy;
			button.classList.toggle('is-busy', busy);
			button.setAttribute('aria-busy', busy ? 'true' : 'false');
			if (label) button.textContent = label;
		}

		function setHeaderStatus(slug, current) {
			const dot = headerContext.querySelector('.cmatic-header__status-dot');
			const text = headerContext.querySelector('.cmatic-header__status-text');
			if (!dot || !text) return;

			const label = definition(slug).label;
			dot.classList.remove(
				'cmatic-header__status-dot--connected',
				'cmatic-header__status-dot--neutral',
				'cmatic-header__status-dot--disconnected'
			);
			if (current.connected) {
				dot.classList.add('cmatic-header__status-dot--connected');
				text.textContent = format(i18n('connected', '%s connected'), label);
			} else if (current.credential_present) {
				dot.classList.add('cmatic-header__status-dot--disconnected');
				text.textContent = format(i18n('inactive', 'Reconnect %s'), label);
			} else {
				dot.classList.add('cmatic-header__status-dot--neutral');
				text.textContent = format(i18n('notConnected', '%s not connected'), label);
			}
		}

		function authField() {
			return authFields.querySelector('[data-auth-field="api_key"]');
		}

		function credentialName(meta) {
			const field = (meta.auth_fields || [])[0] || {};
			return String(field.label || i18n('credential', 'credential')).replace(String(meta.label) + ' ', '');
		}

		function renderAuthFields(meta) {
			const fragment = document.createDocumentFragment();
			(meta.auth_fields || []).forEach(function(field) {
				const wrapper = document.createElement('div');
				const label = document.createElement('label');
				const input = document.createElement('input');
				const description = document.createElement('small');
				const readiness = document.createElement('small');
				const id = 'cmatic-provider-auth-' + field.id;

				wrapper.className = 'cmatic-provider-auth-field';
				label.htmlFor = id;
				label.textContent = field.label;
				input.type = field.type || 'password';
				input.id = id;
				input.value = '';
				input.placeholder = field.placeholder || '';
				input.autocomplete = field.autocomplete || 'new-password';
				input.spellcheck = false;
				input.dataset.authField = field.id;
				description.className = 'description';
				description.textContent = field.description || '';
				readiness.className = 'cmatic-provider-credential-readiness';
				readiness.dataset.credentialReadiness = '';
				readiness.setAttribute('role', 'status');
				readiness.setAttribute('aria-live', 'polite');

				wrapper.append(label, input, description, readiness);
				fragment.append(wrapper);
			});
			authFields.replaceChildren(fragment);
		}

		function sortedLists(current) {
			return (current.lists || []).slice().sort(function(first, second) {
				return String(first.name || '').localeCompare(String(second.name || ''), undefined, { sensitivity: 'base' });
			});
		}

		function renderDestinations(meta, current) {
			const fragment = document.createDocumentFragment();
			const empty = document.createElement('option');
			empty.value = '';
			empty.textContent = format(
				i18n('selectDestination', 'Select a %s...'),
				String(meta.destination_singular || '').toLowerCase()
			);
			fragment.append(empty);

			sortedLists(current).forEach(function(list) {
				const option = document.createElement('option');
				option.value = String(list.id);
				option.textContent = String(list.name);
				fragment.append(option);
			});
			destinationSelect.replaceChildren(fragment);
			destinationSelect.value = String(current.selected_list || '');
		}

		function firstEmailOption(select) {
			return Array.from(select.options).find(function(option) {
				return option.dataset.basetype === 'email';
			});
		}

		function compatibleType(providerType, formType) {
			const types = {
				email: ['email'],
				phone: ['tel'],
				text: ['text', 'textarea'],
				boolean: ['acceptance']
			};
			return (types[String(providerType || '').toLowerCase()] || []).includes(formType);
		}

		function mappingWarning(field, select) {
			const previous = select.parentElement.querySelector('.cmatic-provider-mapping-warning');
			if (previous) previous.remove();
			if (!select.value) return;
			const option = select.options[select.selectedIndex];
			const formType = option ? String(option.dataset.basetype || '') : '';
			if (!formType || compatibleType(field.type, formType)) return;
			const warning = document.createElement('p');
			warning.className = 'cmatic-provider-mapping-warning';
			warning.id = select.id + '-warning';
			warning.setAttribute('role', 'status');
			warning.textContent = format(
				i18n('mappingTypeWarning', '%1$s is a %2$s field, but %3$s expects %4$s. Review this mapping.'),
				select.value,
				formType,
				field.name || field.tag,
				field.type || 'text'
			);
			select.setAttribute('aria-describedby', warning.id);
			select.insertAdjacentElement('afterend', warning);
		}

		function renderFieldState(current) {
			const fragment = document.createDocumentFragment();
			const total = document.createElement('input');
			total.type = 'hidden';
			total.name = 'wpcf7-cmatic-provider[total_merge_fields]';
			total.value = String(current.total_fields || 0);
			fragment.append(total);

			(current.fields || []).slice(0, Number(chimpmaticLiteEsp.fieldLimit)).forEach(function(field, index) {
				['tag', 'name', 'type', 'display_order'].forEach(function(property) {
					const input = document.createElement('input');
					input.type = 'hidden';
					input.name = 'wpcf7-cmatic-provider[merge_fields][' + index + '][' + property + ']';
					input.value = String(field[property] === undefined ? '' : field[property]);
					fragment.append(input);
				});
			});
			fieldState.replaceChildren(fragment);
		}

		function renderMappings(current) {
			const rows = Array.from(root.querySelectorAll('[data-mapping-slot]'));
			rows.forEach(function(select, offset) {
				const row = select.closest('.cmatic-provider-mapping-row');
				const field = (current.fields || [])[offset];
				const slot = select.dataset.mappingSlot;

				if (!row || !field) {
					if (row) row.hidden = true;
					select.disabled = true;
					select.value = '';
					return;
				}

				const isEmail = String(field.tag || '').toUpperCase() === 'EMAIL';
				const currentValue = String((current.mappings || {})[slot] || '');
				const placeholder = select.options[0];
				const options = Array.from(select.options).slice(1).sort(function(first, second) {
					return Number(compatibleType(field.type, second.dataset.basetype))
						- Number(compatibleType(field.type, first.dataset.basetype));
				});
				select.replaceChildren(placeholder, ...options);
				row.hidden = false;
				row.dataset.remoteTag = String(field.tag || '');
				row.dataset.required = isEmail ? '1' : '0';
				row.querySelector('[data-field-label]').textContent = String(field.name || field.tag || slot);
				row.querySelector('[data-field-type]').textContent = String(field.tag || '') + ' Â· ' + String(field.type || 'text');
				row.querySelector('[data-required-label]').hidden = !isEmail;
				select.disabled = false;
				select.setAttribute('aria-required', isEmail ? 'true' : 'false');

				Array.from(select.options).forEach(function(option) {
					if (!option.value) {
						option.disabled = false;
						return;
					}
					option.disabled = (isEmail && option.dataset.basetype !== 'email')
						|| (String(field.type || '').toLowerCase() === 'boolean' && option.dataset.basetype !== 'acceptance');
				});

				let mapping = currentValue;
				if (!mapping && isEmail) {
					const match = firstEmailOption(select);
					mapping = match ? match.value : '';
					current.mappings[slot] = mapping;
				}
				select.value = mapping;
				mappingWarning(field, select);
			});

			const total = Number(current.total_fields || 0);
			fieldLimit.hidden = total <= Number(chimpmaticLiteEsp.fieldLimit);
			renderFieldState(current);
		}

		function requiredEmailMapped() {
			const row = root.querySelector('[data-required="1"]:not([hidden])');
			const select = row ? row.querySelector('[data-mapping-slot]') : null;
			return Boolean(select && select.value);
		}

		function mappedCount(current) {
			return Object.values(current.mappings || {}).filter(Boolean).length;
		}

		function selectedList(current) {
			return (current.lists || []).find(function(list) {
				return String(list.id) === String(current.selected_list || '');
			}) || null;
		}

		function consentReady(slug, current) {
			if (!current.advanced_consent) return true;
			if (current.consent_gate === 'required' && !current.consent_field) return false;
			if (slug !== 'brevo' || current.subscription_mode !== 'double') return true;
			return Boolean(current.doi_verification_token);
		}

		function renderConsent(meta, current) {
			const slug = state.activeProvider;
			const consentMeta = meta.consent || {};
			const advanced = Boolean(current.advanced_consent);
			consentGateTitle.textContent = format(i18n('sendToProvider', 'Send to %s'), meta.label);
			consentGateExplanation.textContent = format(
				i18n('consentGateExplanation', 'Choose whether every valid form submission is sent to %s or only submissions with affirmative consent.'),
				meta.label
			);
			providerOptinTitle.textContent = format(i18n('confirmationInProvider', 'Confirmation in %s'), meta.label);
			providerOptinExplanation.textContent = format(i18n('confirmationExplanation', 'Controls whether %s requires confirmation after the form is submitted.'), meta.label);
			consentDescription.textContent = advanced
				? String(consentMeta.description || '')
				: i18n('consentRequiresPro', 'Advanced consent controls are available with an active Chimpmatic Pro license.');
			Array.from(consentControls.querySelectorAll('input, select, button')).forEach(function(control) {
				control.disabled = !advanced;
			});
			consentGate.value = current.consent_gate === 'required' ? 'required' : 'none';
			consentFieldRow.hidden = consentGate.value !== 'required';
			consentField.value = String(current.consent_field || '');
			brevoOptin.hidden = slug !== 'brevo';
			managedOptin.hidden = slug === 'brevo';
			if (slug === 'brevo') {
				subscriptionMode.value = current.subscription_mode === 'double' ? 'double' : 'single';
				brevoDoi.hidden = subscriptionMode.value !== 'double';
				doiTemplate.value = current.doi_template_id ? String(current.doi_template_id) : '';
				doiRedirect.value = String(current.doi_redirect_url || '');
				doiToken.value = String(current.doi_verification_token || '');
				doiStatus.textContent = current.doi_verification_token
					? i18n('doiVerified', 'DOI settings verified.')
					: '';
				doiVerify.disabled = !advanced || !doiTemplate.value || !doiRedirect.value || current.doi_verifying;
			} else if (slug === 'mailerlite') {
				managedOptinTitle.textContent = i18n('managedByMailerLite', 'Managed by MailerLite');
				managedOptinCopy.textContent = i18n('mailerLiteOptin', 'MailerLite uses the Double opt-in for API and integrations setting in your account.');
			} else {
				const list = selectedList(current);
				const process = String(list && list.opt_in_process || '');
				managedOptinTitle.textContent = process === 'double_opt_in'
					? i18n('doubleOptin', 'Double opt-in')
					: process === 'single_opt_in'
						? i18n('singleOptin', 'Single opt-in')
						: i18n('optinUnavailable', 'Opt-in setting unavailable');
				managedOptinCopy.textContent = i18n('klaviyoOptin', 'The selected Klaviyo list controls whether confirmation is required.');
			}
			consentDocs.href = String(consentMeta.docs_url || '#');
			consentDocs.textContent = format(i18n('openConfirmationSettings', 'Open %s confirmation settings'), meta.label);
		}

		function renderProgress(meta, current) {
			const complete = state.activeProvider === state.savedProvider && current.configured && !current.dirty;
			completion.hidden = !complete;
			progress.hidden = complete;
			if (complete) {
				const baseGroupNames = (current.base_groups || [current.selected_list]).map(function(groupId) {
					const group = (current.lists || []).find(function(list) { return String(list.id) === String(groupId); });
					return group ? String(group.name) : String(groupId);
				}).filter(Boolean);
				completionOutcome.textContent = format(
					i18n('setupOutcome', 'New %1$s from this form will be added to %2$s in %3$s.'),
					term(meta, 'person_plural', 'contacts').toLowerCase(),
					baseGroupNames.join(', ') || current.selected_list_name || current.selected_list,
					meta.label
				);
				completionMeta.textContent = state.activeProvider === 'mailerlite'
					? format(i18n('mailerLiteSummary', 'Always-used groups: %1$d Â· Answer-based rules: %2$d Â· Subscriber fields mapped: %3$d Â· Saved'), baseGroupNames.length, (current.routing_rules || []).length, mappedCount(current))
					: format(i18n('mappedCount', '%1$d fields mapped Â· Saved'), mappedCount(current));
				return;
			}

			progressDestination.textContent = format(
				i18n('chooseDestination', 'Choose %s'),
				String(meta.destination_singular || '').toLowerCase()
			);
			progressMappings.textContent = format(i18n('mapData', 'Map %s'), term(meta, 'data_plural', 'fields').toLowerCase());
			const items = Array.from(progress.children);
			const destinationDone = Boolean(current.selected_list && current.fields && current.fields.length);
			const statuses = [current.connected, destinationDone, false];
			let currentIndex = current.connected ? (destinationDone ? 2 : 1) : 0;
			items.forEach(function(item, index) {
				item.classList.toggle('is-done', statuses[index]);
				item.classList.toggle('is-current', index === currentIndex);
				if (index === currentIndex) item.setAttribute('aria-current', 'step');
				else item.removeAttribute('aria-current');
				item.querySelector('span').textContent = statuses[index] ? 'âœ“' : String(index + 1);
			});
		}

		function renderConnection(meta, current) {
			const fresh = !current.connected && !current.credential_present;
			const recovering = !current.connected && current.credential_present && !current.replacing;
			const editing = fresh || current.replacing;
			credentialsIntro.hidden = current.connected && !current.replacing || recovering;
			authRow.hidden = !editing || current.busy;
			connectedSummary.hidden = !current.connected || current.replacing;
			recoverySummary.hidden = !recovering;

			if (editing) {
				renderAuthFields(meta);
				credentialsHeading.textContent = current.replacing
					? format(i18n('updateProvider', 'Update %s connection'), meta.label)
					: format(i18n('connectProvider', 'Connect %s'), meta.label);
				credentialsDescription.textContent = format(
					i18n('connectDescription', 'Connect %1$s to choose a %2$s and map its %3$s.'),
					meta.label,
					String(meta.destination_singular || '').toLowerCase(),
					term(meta, 'data_plural', 'fields').toLowerCase()
				);
				connectButton.textContent = current.replacing
					? i18n('updateConnection', 'Update connection')
					: format(i18n('connectProvider', 'Connect %s'), meta.label);
				connectButton.disabled = true;
				cancelCredentialButton.hidden = !current.replacing;
			}

			if (current.busy) {
				credentialsIntro.hidden = false;
				credentialsHeading.textContent = format(i18n('checkingConnection', 'Checking your %s connection...'), meta.label);
				credentialsDescription.textContent = '';
			}

			connectedTitle.textContent = format(i18n('connected', '%s connected'), meta.label);
			credentialStorage.textContent = format(
				i18n('credentialStored', '%s stored securely'),
				String(((meta.auth_fields || [])[0] || {}).label || i18n('credential', 'Credential'))
			);
			refreshButton.textContent = current.lists_loading || current.refreshing
				? format(i18n('loadingDestinations', 'Loading %s...'), String(meta.destination_plural || '').toLowerCase())
				: format(i18n('refreshDestinations', 'Refresh %s'), String(meta.destination_plural || '').toLowerCase());
			refreshButton.disabled = current.lists_loading || current.refreshing;
			replaceCredentialButton.textContent = format(i18n('replaceCredential', 'Replace %s'), credentialName(meta));
			recoveryTitle.textContent = format(i18n('reconnectProvider', 'Reconnect %s'), meta.label);
			recoveryReplaceButton.textContent = format(i18n('replaceCredential', 'Replace %s'), credentialName(meta));
		}

		function renderDestination(meta, current) {
			destinationHeading.textContent = state.activeProvider === 'mailerlite'
				? i18n('groupsForEverySubscriber', 'Groups for every subscriber')
				: format(i18n('chooseDestination', 'Choose a %s'), String(meta.destination_singular || '').toLowerCase());
			destinationLabel.textContent = meta.label + ' ' + String(meta.destination_singular || '').toLowerCase();
			if (!current.connected || current.lists_loading) {
				destinationLocked.hidden = false;
				destinationRow.hidden = true;
				mailerliteGroups.hidden = true;
				destinationDescription.textContent = '';
				destinationLocked.querySelector('p').textContent = current.lists_loading
					? format(i18n('loadingDestinationsFrom', 'Loading %1$s from %2$s...'), String(meta.destination_plural || '').toLowerCase(), meta.label)
					: format(i18n('connectToContinue', 'Connect %s to continue.'), meta.label);
				return;
			}

			destinationLocked.hidden = true;
			renderDestinations(meta, current);
			const isMailerLite = state.activeProvider === 'mailerlite';
			destinationRow.hidden = isMailerLite;
			destinationSelect.disabled = isMailerLite;
			mailerliteGroups.hidden = !isMailerLite;
			if (isMailerLite) renderMailerLiteGroups(current);
			renderRouting(current);
			const listCount = (current.lists || []).length;
			if (isMailerLite) {
				destinationDescription.textContent = i18n('mailerLiteGroupsHelp', 'Every subscriber successfully sent to MailerLite is added to each selected group. Mark one selected group â€œUse when Pro is inactive.â€�');
				return;
			}
			if (1 === listCount && current.selected_list) {
				destinationDescription.textContent = format(
					i18n('onlyDestination', '%1$s was selected because it is your only %2$s %3$s.'),
					current.selected_list_name || current.selected_list,
					meta.label,
					String(meta.destination_singular || '').toLowerCase()
				);
			} else {
				destinationDescription.textContent = 1 === listCount
					? format(
						i18n('oneDestinationFound', '1 %s found. Choose where new submissions from this form should go.'),
						String(meta.destination_singular || '').toLowerCase()
					)
					: format(
						i18n('destinationsFound', '%1$d %2$s found. Choose where new submissions from this form should go.'),
						listCount,
						String(meta.destination_plural || '').toLowerCase()
					);
			}
		}

		function renderMappingStage(meta, current) {
			const visible = Boolean(current.connected && current.selected_list && current.fields && current.fields.length);
			mappingsHeading.textContent = format(
				i18n('mapProviderFields', 'Map %1$s %2$s'),
				meta.label,
				term(meta, 'data_plural', 'fields').toLowerCase()
			);
			mappingDescription.textContent = format(
				i18n('mappedFields', 'Match each %1$s %2$s to a Contact Form 7 field. Email address mapping is required.'),
				meta.label,
				term(meta, 'data_singular', 'field').toLowerCase()
			);
			const dataPlural = term(meta, 'data_plural', 'fields').toLowerCase();
			const dataSingular = term(meta, 'data_singular', 'field').toLowerCase();
			fieldLimitCopy.textContent = fieldLimit.dataset.proActive === '1'
				? format(i18n('proDataLimit', 'Up to %1$d %2$s are available with your active Chimpmatic Pro license.'), Number(fieldLimit.dataset.currentLimit || 0), dataPlural)
				: format(i18n('liteDataLimit', 'Your Lite setup includes %1$d %2$s. Email address is always included.'), Number(fieldLimit.dataset.liteLimit || 0), dataPlural);
			if (fieldLimitLinkCopy) {
				fieldLimitLinkCopy.textContent = format(i18n('unlockData', 'Unlock every available %s and advanced features with Chimpmatic Pro'), dataSingular);
			}
			testingNotice.textContent = format(
				i18n('testingWarning', 'Real submission: may create or update a %1$s in %2$s and trigger confirmation emails or automations.'),
				term(meta, 'person_singular', 'contact').toLowerCase(),
				meta.label
			);
			mappingsGrid.hidden = !visible;
			mappingsLocked.hidden = visible;
			saveRow.hidden = !visible;
			if (!visible) {
				mappingsLocked.querySelector('p').textContent = current.lists_loading
					? format(i18n('waitForDestinations', 'Wait while %s load.'), String(meta.destination_plural || '').toLowerCase())
					: format(
						i18n('chooseToLoadFields', 'Choose a %1$s to load its %2$s.'),
						String(meta.destination_singular || '').toLowerCase(),
						term(meta, 'data_plural', 'fields').toLowerCase()
					);
				return;
			}

			renderMappings(current);
			renderMailerLiteOptions(current);
			const complete = state.activeProvider === state.savedProvider && current.configured && !current.dirty;
			if (current.dirty) {
				saveStatus.textContent = i18n('unsavedChanges', 'Unsaved changes');
				saveButton.textContent = i18n('saveChanges', 'Save changes');
				saveButton.disabled = !requiredEmailMapped() || !consentReady(state.activeProvider, current);
			} else if (complete) {
				saveStatus.textContent = i18n('savedJustNow', 'Saved');
				saveButton.textContent = i18n('saveChanges', 'Save changes');
				saveButton.disabled = true;
			} else {
				saveStatus.textContent = i18n('saveToActivate', 'Save to activate this configuration.');
				saveButton.textContent = i18n('saveConfiguration', 'Save configuration');
				saveButton.disabled = !requiredEmailMapped() || !consentReady(state.activeProvider, current);
			}
		}

		function render() {
			const slug = state.activeProvider;
			selector.value = slug;
			headerContext.hidden = !slug;
			onboarding.hidden = Boolean(slug);
			showMessage('', '');

			if (!slug) {
				root.hidden = true;
				mailchimpView.hidden = true;
				return;
			}

			const current = providerState(slug);
			setHeaderStatus(slug, current);
			if ('mailchimp' === slug) {
				root.hidden = true;
				mailchimpView.hidden = false;
				return;
			}

			const meta = definition(slug);
			root.hidden = false;
			root.dataset.provider = slug;
			mailchimpView.hidden = true;
			renderProgress(meta, current);
			renderConnection(meta, current);
			renderDestination(meta, current);
			renderMappingStage(meta, current);
			renderConsent(meta, current);
			renderRouting(current);
			renderMailerLiteOptions(current);
		}

		function abortRequests() {
			controllers.forEach(function(controller) {
				controller.abort();
			});
			controllers.clear();
		}

		async function request(channel, action, payload) {
			if (controllers.has(channel)) controllers.get(channel).abort();
			const controller = new AbortController();
			controllers.set(channel, controller);

			try {
				const response = await fetch(chimpmaticLiteEsp.restUrl + 'providers/' + action, {
					method: 'POST',
					headers: {
						'Content-Type': 'application/json',
						'X-WP-Nonce': chimpmaticLiteEsp.nonce
					},
					body: JSON.stringify(payload),
					signal: controller.signal
				});
				const data = await response.json().catch(function() {
					return {};
				});
				if (!response.ok) {
					throw new Error(data.message || requestFailed());
				}
				return data;
			} finally {
				if (controllers.get(channel) === controller) controllers.delete(channel);
			}
		}

		function discardDraft(slug) {
			const current = providerState(slug);
			const baseline = initialProviders[slug] || {};
			current.selected_list = String(baseline.selected_list || '');
			current.selected_list_name = String(baseline.selected_list_name || '');
			current.fields = Array.isArray(baseline.fields) ? JSON.parse(JSON.stringify(baseline.fields)) : [];
			current.total_fields = Number(baseline.total_fields || 0);
			current.mappings = baseline.mappings ? JSON.parse(JSON.stringify(baseline.mappings)) : {};
			current.base_groups = Array.isArray(baseline.base_groups) ? JSON.parse(JSON.stringify(baseline.base_groups)) : [];
			current.additional_groups = Array.isArray(baseline.additional_groups) ? JSON.parse(JSON.stringify(baseline.additional_groups)) : [];
			current.routing_rules = Array.isArray(baseline.routing_rules) ? JSON.parse(JSON.stringify(baseline.routing_rules)) : [];
			current.status_mode = String(baseline.status_mode || 'legacy_provider_managed');
			current.resubscribe_force = Boolean(baseline.resubscribe_force);
			current.consent_metadata_enabled = Boolean(baseline.consent_metadata_enabled);
			current.configured = Boolean(baseline.configured);
			current.dirty = false;
		}

		function switchProvider(nextSlug) {
			const previousSlug = state.activeProvider;
			if (previousSlug && 'mailchimp' !== previousSlug && providerState(previousSlug).dirty) {
				if (!window.confirm(i18n('discardChanges', 'Discard unsaved changes and switch providers?'))) {
					selector.value = previousSlug;
					return;
				}
				discardDraft(previousSlug);
			}
			abortRequests();
			lookupEmail.value = '';
			lookupResults.replaceChildren();
			generation++;
			state.activeProvider = nextSlug;
			render();
			autoLoadOnlyDestination();
		}

		function autoLoadOnlyDestination() {
			const slug = state.activeProvider;
			if (!slug || 'mailchimp' === slug) return;
			const current = providerState(slug);
			const lists = sortedLists(current);
			if (current.connected && !current.selected_list && !current.fields_loading && 1 === lists.length) {
				loadFields(String(lists[0].id), true);
			}
		}

		async function connectProvider(options) {
			const settings = options || {};
			const slug = state.activeProvider;
			const current = providerState(slug);
			const input = authField();
			const apiKey = settings.useSaved ? '' : input ? input.value.trim() : '';
			if (!settings.useSaved && !apiKey) return;

			abortRequests();
			const token = ++generation;
			current.busy = true;
			current.lists_loading = true;
			render();
			try {
				const data = await request('connect', 'connect', {
					form_id: Number(root.dataset.formId),
					provider: slug,
					api_key: apiKey
				});
				if (token !== generation || data.provider !== state.activeProvider) return;

				current.connected = true;
				current.credential_present = true;
				current.lists = Array.isArray(data.lists) ? data.lists : [];
				if (data.key_changed) {
					current.selected_list = '';
					current.selected_list_name = '';
					current.fields = [];
					current.total_fields = 0;
					current.mappings = {};
					current.base_groups = [];
					current.additional_groups = [];
					current.routing_rules = [];
					current.configured = false;
					current.dirty = false;
					initialProviders[slug] = JSON.parse(JSON.stringify(current));
				}
				current.replacing = false;
				current.busy = false;
				current.lists_loading = false;
				const lists = sortedLists(current);
				if (1 === lists.length && !current.selected_list) {
					await loadFields(String(lists[0].id), true);
					return;
				}
				if (current.selected_list && lists.some(function(list) { return String(list.id) === String(current.selected_list); })) {
					await loadFields(String(current.selected_list), true);
					return;
				}
				render();
				showMessage(
					format(
						i18n('connectedDestinationsReady', '%1$s connected. %2$d %3$s ready.'),
						definition(slug).label,
						lists.length,
						term(definition(slug), 'destination_plural', 'destinations').toLowerCase()
					),
					'success'
				);
			} catch (error) {
				current.busy = false;
				current.lists_loading = false;
				if (error.name !== 'AbortError') {
					render();
					showMessage(error.message || requestFailed(), 'error');
				}
			}
		}

		async function loadFields(listId, automatic, focusFieldKey) {
			const slug = state.activeProvider;
			const current = providerState(slug);
			const meta = definition(slug);
			const previous = {
				selected_list: current.selected_list,
				selected_list_name: current.selected_list_name,
				fields: current.fields,
				total_fields: current.total_fields,
				mappings: current.mappings
				,base_groups: current.base_groups
				,additional_groups: current.additional_groups
			};
			current.selected_list = listId;
			const selected = (current.lists || []).find(function(list) {
				return String(list.id) === String(listId);
			});
			current.selected_list_name = selected ? String(selected.name) : '';
			current.additional_groups = (current.additional_groups || []).filter(function(groupId) { return String(groupId) !== String(listId); });
			current.base_groups = listId ? [String(listId), ...current.additional_groups.map(String)] : [];
			current.fields_loading = Boolean(listId);
			current.fields = [];
			render();
			if (!listId) {
				current.total_fields = 0;
				current.mappings = {};
				updateDirty(slug);
				return;
			}

			abortRequests();
			const token = ++generation;
			showMessage(format(
				i18n('loadingProviderFields', 'Loading %1$s %2$s...'),
				meta.label,
				term(meta, 'data_plural', 'fields').toLowerCase()
			), '');
			try {
				const data = await request('fields', 'fields', {
					form_id: Number(root.dataset.formId),
					provider: slug,
					list_id: listId
				});
				if (token !== generation || data.provider !== state.activeProvider || String(data.list_id || '') !== String(listId)) return;
				current.fields = Array.isArray(data.merge_fields) ? data.merge_fields : [];
				current.total_fields = Number(data.total_merge_fields || 0);
				current.mappings = data.mappings || {};
				current.fields_loading = false;
				current.configured = false;
				updateDirty(slug);
				render();
				if (focusFieldKey) {
					const row = Array.from(root.querySelectorAll('.cmatic-provider-mapping-row')).find(function(item) { return String(item.dataset.remoteTag || '') === String(focusFieldKey); });
					const select = row ? row.querySelector('[data-mapping-slot]') : null;
					if (select) {
						row.scrollIntoView({ block: 'nearest' });
						select.focus();
					} else {
						showMessage(i18n('fieldRefreshRequired', 'Subscriber field created; refresh MailerLite subscriber fields before mapping it.'), 'error');
					}
				}
				showMessage(
					automatic
						? format(i18n('onlyDestinationSelected', '%1$s connected. %2$s was selected automatically.'), meta.label, current.selected_list_name)
						: format(
							i18n('providerFieldsReady', '%1$s %2$s are ready.'),
							meta.label,
							term(meta, 'data_plural', 'fields').toLowerCase()
						),
					'success'
				);
			} catch (error) {
				current.selected_list = previous.selected_list;
				current.selected_list_name = previous.selected_list_name;
				current.fields = previous.fields;
				current.total_fields = previous.total_fields;
				current.mappings = previous.mappings;
				current.base_groups = previous.base_groups;
				current.additional_groups = previous.additional_groups;
				current.fields_loading = false;
				if (error.name !== 'AbortError') {
					render();
					showMessage(error.message || requestFailed(), 'error');
				}
			}
		}

		async function disconnectProvider() {
			const slug = state.activeProvider;
			const meta = definition(slug);
			if (!window.confirm(format(i18n('disconnectConfirm', 'Disconnect %s? Your field mappings will be kept.'), meta.label))) return;
			abortRequests();
			const token = ++generation;
			try {
				const data = await request('disconnect', 'disconnect', {
					form_id: Number(root.dataset.formId),
					provider: slug
				});
				if (token !== generation || data.provider !== state.activeProvider) return;
				const current = providerState(slug);
				current.connected = false;
				current.credential_present = false;
				current.configured = false;
				current.replacing = false;
				current.dirty = state.savedProvider === slug;
				render();
			} catch (error) {
				if (error.name !== 'AbortError') showMessage(error.message || requestFailed(), 'error');
			}
		}

		onboarding.addEventListener('click', function(event) {
			const choice = event.target.closest('[data-provider-choice]');
			if (!choice) return;
			switchProvider(String(choice.dataset.providerChoice || ''));
			const heading = document.getElementById('cmatic-provider-credentials-heading');
			if (heading && !root.hidden) {
				heading.setAttribute('tabindex', '-1');
				heading.focus({ preventScroll: true });
			}
		});

		selector.addEventListener('change', function() {
			switchProvider(selector.value);
		});

		authFields.addEventListener('input', function(event) {
			const input = event.target.closest('[data-auth-field="api_key"]');
			if (!input) return;
			const ready = Boolean(input.value.trim());
			const readiness = authFields.querySelector('[data-credential-readiness]');
			connectButton.disabled = !ready;
			if (readiness) readiness.textContent = ready ? i18n('readyToVerify', 'Ready to verify.') : '';
			showMessage('', '');
		});

		authFields.addEventListener('keydown', function(event) {
			if (event.key !== 'Enter' || connectButton.disabled) return;
			event.preventDefault();
			connectProvider({ useSaved: false });
		});

		connectButton.addEventListener('click', function() {
			connectProvider({ useSaved: false });
		});
		cancelCredentialButton.addEventListener('click', function() {
			providerState(state.activeProvider).replacing = false;
			render();
		});
		refreshButton.addEventListener('click', function() {
			const current = providerState(state.activeProvider);
			current.refreshing = true;
			render();
			connectProvider({ useSaved: true }).finally(function() {
				current.refreshing = false;
				if (state.activeProvider) render();
			});
		});
		replaceCredentialButton.addEventListener('click', function() {
			providerState(state.activeProvider).replacing = true;
			render();
			authField()?.focus();
		});
		recoveryReplaceButton.addEventListener('click', function() {
			providerState(state.activeProvider).replacing = true;
			render();
			authField()?.focus();
		});
		retryButton.addEventListener('click', function() {
			connectProvider({ useSaved: true });
		});
		disconnectButton.addEventListener('click', disconnectProvider);
		recoveryDisconnectButton.addEventListener('click', disconnectProvider);

		destinationSelect.addEventListener('change', function() {
			const current = providerState(state.activeProvider);
			current.doi_verification_token = '';
			loadFields(destinationSelect.value, false);
		});

		routingAddRule.addEventListener('click', function() {
			const current = providerState(state.activeProvider);
			if (!routingTags().length || !sortedLists(current).length) return;
			current.routing_rules.push({
				id: createRoutingRuleId(),
				field: '',
				value: '',
				group_id: ''
			});
			routingValidationVisible = false;
			current.configured = false;
			updateDirty(state.activeProvider);
			render();
			const rows = routingRuleList.querySelectorAll('[data-routing-rule]');
			const last = rows[rows.length - 1];
			if (last) last.querySelector('[data-routing-field]')?.focus();
		});

		mailerliteStatus.addEventListener('change', function() {
			const current = providerState(state.activeProvider);
			current.status_mode = mailerliteStatus.value;
			if (current.status_mode !== 'active') current.resubscribe_force = false;
			updateDirty(state.activeProvider);
			render();
			mailerliteStatus.focus();
		});
		mailerliteResubscribeForce.addEventListener('change', function() {
			const current = providerState(state.activeProvider);
			current.resubscribe_force = mailerliteResubscribeForce.checked;
			updateDirty(state.activeProvider);
			render();
		});
		mailerliteConsentMetadata.addEventListener('change', function() {
			const current = providerState(state.activeProvider);
			current.consent_metadata_enabled = mailerliteConsentMetadata.checked;
			updateDirty(state.activeProvider);
			render();
		});

		consentGate.addEventListener('change', function() {
			const current = providerState(state.activeProvider);
			current.consent_gate = consentGate.value === 'required' ? 'required' : 'none';
			current.consent_field = current.consent_gate === 'required' ? String(consentField.value || '') : '';
			updateDirty(state.activeProvider);
			render();
		});
		consentField.addEventListener('change', function() {
			const current = providerState(state.activeProvider);
			current.consent_field = String(consentField.value || '');
			updateDirty(state.activeProvider);
			render();
		});
		subscriptionMode.addEventListener('change', function() {
			const current = providerState(state.activeProvider);
			current.subscription_mode = subscriptionMode.value === 'double' ? 'double' : 'single';
			current.doi_verification_token = '';
			updateDirty(state.activeProvider);
			render();
		});
		[doiTemplate, doiRedirect].forEach(function(input) {
			input.addEventListener('input', function() {
				const current = providerState(state.activeProvider);
				current.doi_template_id = Number(doiTemplate.value || 0);
				current.doi_redirect_url = String(doiRedirect.value || '');
				current.doi_verification_token = '';
				updateDirty(state.activeProvider);
				render();
			});
		});
		doiVerify.addEventListener('click', async function() {
			const slug = state.activeProvider;
			const current = providerState(slug);
			current.doi_verifying = true;
			render();
			try {
				const data = await request('consent', 'consent/validate', {
					form_id: Number(root.dataset.formId),
					provider: slug,
					list_id: String(current.selected_list || ''),
					subscription_mode: 'double',
					doi_template_id: Number(current.doi_template_id || 0),
					doi_redirect_url: String(current.doi_redirect_url || '')
				});
				if (slug !== state.activeProvider) return;
				current.doi_verification_token = String(data.verification_token || '');
				showMessage(i18n('doiVerified', 'DOI settings verified.'), 'success');
			} catch (error) {
				if (error.name !== 'AbortError') showMessage(error.message || i18n('doiFailed', 'DOI settings could not be verified.'), 'error');
			} finally {
				current.doi_verifying = false;
				if (slug === state.activeProvider) render();
			}
		});

		root.addEventListener('change', function(event) {
			const groupControl = event.target.closest('[data-mailerlite-group-selected], [data-mailerlite-group-primary]');
			if (groupControl) {
				const current = providerState(state.activeProvider);
				const row = groupControl.closest('[data-mailerlite-group]');
				const groupId = String(row?.dataset.mailerliteGroup || '');
				const checkedGroups = Array.from(mailerliteGroups.querySelectorAll('[data-mailerlite-group-selected]:checked')).map(function(input) { return input.value; });
				let primary = String(current.selected_list || '');
				if (groupControl.matches('[data-mailerlite-group-primary]')) {
					primary = groupId;
					if (!checkedGroups.includes(primary)) checkedGroups.push(primary);
				} else if (!checkedGroups.includes(primary)) {
					primary = String(checkedGroups[0] || '');
				}
				if (!primary) {
					groupControl.checked = true;
					return;
				}
				current.additional_groups = checkedGroups.filter(function(id) { return String(id) !== primary; });
				current.base_groups = [primary, ...current.additional_groups];
				current.configured = false;
				if (primary !== String(current.selected_list || '')) {
					loadFields(primary, false);
					return;
				}
				updateDirty(state.activeProvider);
				render();
				const replacement = mailerliteGroups.querySelector('[data-mailerlite-group="' + CSS.escape(groupId) + '"] ' + (groupControl.matches('[data-mailerlite-group-primary]') ? '[data-mailerlite-group-primary]' : '[data-mailerlite-group-selected]'));
				if (replacement) replacement.focus();
				return;
			}
			const routingControl = event.target.closest('[data-routing-field], [data-routing-value], [data-routing-group]');
			if (routingControl) {
				const row = routingControl.closest('[data-routing-rule]');
				const current = providerState(state.activeProvider);
				const rule = (current.routing_rules || []).find(function(item) { return String(item.id) === String(row.dataset.routingRule); });
				if (!rule) return;
				if (routingControl.matches('[data-routing-field]')) {
					rule.field = routingControl.value;
					rule.value = '';
				} else if (routingControl.matches('[data-routing-value]')) {
					rule.value = routingControl.value;
				} else {
					rule.group_id = routingControl.value;
				}
				current.configured = false;
				updateDirty(state.activeProvider);
				render();
				const replacement = routingRuleList.querySelector('[data-routing-rule="' + CSS.escape(String(rule.id)) + '"] ' + (routingControl.matches('[data-routing-field]') ? '[data-routing-field]' : routingControl.matches('[data-routing-value]') ? '[data-routing-value]' : '[data-routing-group]'));
				if (replacement) replacement.focus();
				return;
			}
			const select = event.target.closest('[data-mapping-slot]');
			if (!select || !root.contains(select)) return;
			const current = providerState(state.activeProvider);
			current.mappings = current.mappings || {};
			current.mappings[select.dataset.mappingSlot] = select.value;
			current.configured = false;
			updateDirty(state.activeProvider);
			render();
			document.getElementById(select.id)?.focus();
		});

		root.addEventListener('click', function(event) {
			const remove = event.target.closest('[data-routing-remove]');
			if (!remove) return;
			const row = remove.closest('[data-routing-rule]');
			const current = providerState(state.activeProvider);
			current.routing_rules = (current.routing_rules || []).filter(function(rule) { return String(rule.id) !== String(row.dataset.routingRule); });
			current.configured = false;
			updateDirty(state.activeProvider);
			render();
			routingAddRule.focus();
		});

		fieldCreatorButton.addEventListener('click', async function() {
			if (fieldCreating || !fieldCreatorName.value.trim()) return;
			fieldCreating = true;
			let finalMessage = '';
			let finalTone = '';
			const current = providerState(state.activeProvider);
			setBusy(fieldCreatorButton, true, i18n('creating', 'Creating...'));
			try {
				const data = await request('field-create', 'fields/create', {
					form_id: Number(root.dataset.formId),
					provider: 'mailerlite',
					name: fieldCreatorName.value.trim(),
					type: fieldCreatorType.querySelector('input[type="radio"]:checked')?.value || 'text'
				});
				const key = String((data.field || {}).key || '');
				fieldCreatorName.value = '';
				await loadFields(String(current.selected_list || ''), false, key);
				finalMessage = i18n('fieldCreated', 'MailerLite subscriber field created. Choose its Contact Form 7 source and save.');
				finalTone = 'success';
			} catch (error) {
				if (error.name !== 'AbortError') {
					finalMessage = error.message || i18n('fieldCreateUnconfirmed', 'Field creation was not confirmed. Refresh MailerLite subscriber fields before retrying.');
					finalTone = 'error';
				}
			} finally {
				fieldCreating = false;
				setBusy(fieldCreatorButton, false, i18n('createField', 'Create field'));
				render();
				if (finalMessage) showMessage(finalMessage, finalTone);
			}
		});

		lookupButton.addEventListener('click', async function() {
			const email = lookupEmail.value.trim();
			if (!email) return;
			setBusy(lookupButton, true, i18n('findingSubscriber', 'Finding subscriber...'));
			lookupResults.replaceChildren();
			try {
				const data = await request('lookup', 'lookup', { form_id: Number(root.dataset.formId), provider: 'mailerlite', email: email });
				const summary = document.createElement('p');
				summary.className = 'cmatic-lookup-summary';
				summary.textContent = data.found
					? format(i18n('subscriberFound', 'Subscriber found Â· %s'), String(data.status || i18n('statusUnavailable', 'status unavailable')))
					: i18n('noSubscriberFound', 'No subscriber found.');
				lookupResults.append(summary);
				if (data.found) {
					const card = document.createElement('div');
					card.className = 'cmatic-result-card';
					const groups = document.createElement('p');
					groups.textContent = format(
						i18n('groupsLabel', 'Groups: %s'),
						(data.groups || []).map(function(group) { return String(group.name || group.id || ''); }).filter(Boolean).join(', ')
					);
					card.append(groups);
					Object.entries(data.fields || {}).forEach(function(entry) {
						const item = document.createElement('p');
						item.textContent = String(entry[0]) + ': ' + String(entry[1]);
						card.append(item);
					});
					lookupResults.append(card);
				}
			} catch (error) {
				if (error.name !== 'AbortError') lookupResults.textContent = error.message || i18n('lookupFailed', 'MailerLite could not find the subscriber. Try again.');
			} finally {
				setBusy(lookupButton, false, i18n('findSubscriber', 'Find subscriber'));
			}
		});

		if (parentForm) {
			parentForm.addEventListener('submit', function(event) {
				if (!state.activeProvider || 'mailchimp' === state.activeProvider) return;
				const current = providerState(state.activeProvider);
				if (!current.connected || !current.selected_list || !current.fields.length) {
					event.preventDefault();
					const meta = definition(state.activeProvider);
					showMessage(format(
						i18n('missingDestination', 'Connect %1$s and choose a %2$s before saving.'),
						meta.label,
						String(meta.destination_singular || '').toLowerCase()
					), 'error');
					return;
				}
				if (!requiredEmailMapped()) {
					event.preventDefault();
					showMessage(i18n('missingEmailMapping', 'Select a Contact Form 7 field for the required email address.'), 'error');
					const emailRow = root.querySelector('[data-required="1"]:not([hidden])');
					const emailSelect = emailRow ? emailRow.querySelector('[data-mapping-slot]') : null;
					if (emailSelect) emailSelect.focus();
					return;
				}
				if (!consentReady(state.activeProvider, current)) {
					event.preventDefault();
					showMessage(i18n('consentIncomplete', 'Complete the consent and opt-in settings before saving.'), 'error');
					return;
				}
				if ('mailerlite' === state.activeProvider && current.routing_entitled) {
					const validation = routingValidation(current);
					if (validation.size) {
						event.preventDefault();
						routingValidationVisible = true;
						render();
						showMessage(i18n('routingFixBeforeSave', 'Complete or remove the highlighted routing rules before saving.'), 'error');
						const firstInvalid = routingRuleList.querySelector('[aria-invalid="true"]');
						if (firstInvalid) firstInvalid.focus();
						return;
					}
				}
				setBusy(saveButton, true, i18n('saving', 'Saving...'));
			});
		}

		render();
		autoLoadOnlyDestination();
	}

	if (document.readyState === 'loading') {
		document.addEventListener('DOMContentLoaded', init);
	} else {
		init();
	}
})();
                                                                                                                                                                                                                                                                                                                                   assets/css/chimpmatic-lite.css                                                                      0000644                 00000136746 15266052574 0012456 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       /**
 * Admin panel styles.
 *
 * @package   contact-form-7-mailchimp-extension
 * @author    renzo.johnson@gmail.com
 * @copyright 2014-2026 https://renzojohnson.com
 * @license   GPL-3.0+
 */

.chimpmatic-lite {
	#contact-form-editor-tabs {
	border-color: #b5bec7;
	}

	section#Chimpmatic {
	.metabox-holder {
		padding: 0;
	}
	}
}

#contact-form-editor-tabs {
	#Chimpmatic-tab[aria-selected="true"] {
	background-color: #fff;
	}
}

.chimpmatic-lite {
	section#Chimpmatic {
	border-color: #b5bec7;
	padding: 0;
	background-color: #f2f4f6;
	}
}

.cmatic-content {
	padding: 0 20px;
}

.cmatic-field-group {
	background-color: rgba(255, 255, 255, .95);
	border: 1px solid #e5e5e5;
	border-radius: 8px;
	margin-bottom: 20px;
	padding: 12px 20px 20px;
}

.cmatic-panel-toggles,
.mce-custom-fields {
	background-color: rgba(255, 255, 255, .95);
	border: 1px solid #e5e5e5;
	border-radius: 8px;
	padding: 12px 20px 20px;
	box-shadow: 0 1px 1px rgba(0, 0, 0, .04);
	margin-bottom: 20px;
}

.cmatic-panel-toggles {
	display: flex;
	flex-wrap: wrap;
	gap: 10px;
}

#chimpmatic-lite-panel .highlight {
	color: #999;
	padding: 9px 20px;
	margin-bottom: 14px;
	background-color: #fafafc;
	border: 1px solid #e1e1e8;
	border-radius: 4px;
}

#chimpmatic-lite-panel .highlight,
#chimpmatic-lite-panel pre,
#chimpmatic-lite-panel pre code {
	font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}

#chimpmatic-lite-panel pre code {
	padding: 2px 4px;
	font-size: 90%;
	color: #c7254e;
	background-color: #f9f2f4;
	word-break: normal;
	white-space: pre-wrap;
	line-height: 1.42857143;
}

#chimpmatic-lite-panel .button {
	padding: 0 25px;
}

#cmatic-api {
	font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}

#chimpmatic-lite-panel #cme-container th {
	width: 170px;
}

.description p {
	font-size: 12px !important;
	line-height: 1.25em !important;
	margin-top: 0 !important;
	font-style: italic !important;
}

.mc-code {
	font-size: 11px;
	color: #999;
	display: block;
}

#MailChimp-Extension {
	position: relative;
}

.mce-main-fields {
	position: relative;
}

.mce-container {
	padding: 16px;
}

.mce-container small {
	font-style: italic;
}

.postbox-inner {
	padding: 5px 20px;
}

#cme-container .form-table th {
	width: 200px;
	padding: 19px 10px 10px 0;
}

#cme-container .form-table td {
	padding-bottom: 10px;
}

#cme-container.vc-advanced-settings .form-table th {
	width: 150px;
}

.cm-form-table {
	padding-top: 20px;
	margin-top: 18px;
}

.cm-form-table th,
.cm-form-table td {
	padding: 8px 5px;
}

.wp-core-ui .cm-submit {
	background-color: #460479;
	border: 1px solid #6f1a3f;
	color: #fff;
	width: 100%;
	margin-bottom: 8px;
	padding: 1px 5px;
	font-size: 24px;
	border-radius: 53px;
	transition: all 0.4s ease-in-out;
  text-align: center;
}

.wp-core-ui .cm-submit:hover {
	background-color: #ee3455;
	border-color: #ee3455;
	color: #fff;
}

li#chimpmatic-lite-panel-tab,
#chimpmatic-lite-panel-tab {
	border-top: 1px solid #01b7ff;
	background: rgba(255, 255, 255, 0.5) url(../images/postal-marks.png) center top no-repeat;
}

.mc-lateral {
	box-shadow: 0 0 0 1px rgba(200, 215, 225, .5), 0 1px 2px #e9eff3;
}

.contact-form-editor-panel .mc-lateral {
	display: none;
}

.mc-lateral::after {
	content: '';
	display: table;
	clear: both;
}

.metabox-holder .mc-lateral h3 {
	font-size: 19px;
	padding: 25px 15px 13px !important;
	background-color: #f9f9f9;
	text-align: center;
	color: #000;
}

.metabox-holder .mc-lateral h3::before {
	display: block;
	position: absolute;
	right: 0;
	left: 0;
	background-size: 70px 9px;
	width: 100%;
	height: 9px;
	content: '';
	top: -1px;
	z-index: 2;
}

.metabox-holder .mc-lateral .inside {
	padding: 0 15px 25px !important;
	margin-top: 0;
}

.holder-img {
	position: relative;
	background-color: rgba(0, 0, 0, 0.05);
}

.holder-img img {
	display: block;
	max-width: 100%;
}

.audience-name a {
	text-decoration: none;
}

.lin-to-pro {
	position: absolute;
	width: 100%;
	height: 100%;
	z-index: -99;
	text-align: center;
	text-decoration: none;
	background-color: rgba(0, 0, 0, 0.45);
	box-sizing: border-box;
	opacity: 0;
	color: #fff !important;
	border-radius: 3px;
}

.lin-to-pro span {
	margin: auto;
	padding: 1rem 3rem;
	text-align: center;
	font-size: 4em;
}

.lin-to-pro span span {
	display: block;
	margin-top: 25px;
	background: #00be28;
	border: 1px solid #00a523;
	border-width: 1px 1px 2px;
	padding: 5px 20px;
	border-radius: 3px;
	font-size: 14px;
	line-height: 21px;
	font-weight: 500;
}

.holder-img:hover .lin-to-pro {
	top: 0;
	left: 0;
	z-index: 999;
	opacity: 1;
	display: flex;
}

.mce-main-fields,
.cmatic-field-group {
	.description {
	display: block;
	color: #777;
	padding-left: 0.7em;
	}
}

.need-api {
	margin-bottom: 10px;
}

#cmatic-fields label {
	display: inline-block;
	vertical-align: bottom;
}

#wpcf7-mailchimp-list {
	max-width: unset;
}

.chm-select {
	margin-bottom: 7px;
}

.atags {
	display: block;
}

.mcee-container label {
  display: block;
  margin-bottom: 5px;
}

.mcee-container .glocks {
	display: inline-block;
	margin-left: 8px;
	vertical-align: middle;
}

.atags {
	margin-top: 20px;
}

.atags input {
	width: 90% !important;
	margin-top: 5px;
}

.cm-form {
	display: block;
	width: 100%;
}

.cm-form input {
	max-width: 100%;
	margin-bottom: 10px;
}

.cm-pricing {
	text-align: center;
	display: block;
	width: 100%;
}

.a-support {
	color: #222;
	background: #f7f7f7;
	box-shadow: inset 0 1px 0 #fff, 0 1px 0 rgba(0, 0, 0, .08);
	vertical-align: top;
	display: inline-block;
	text-decoration: none;
	font-size: 13px;
	line-height: 26px;
	height: 28px;
	margin: 0;
	padding: 0 10px 1px;
	cursor: pointer;
	border: 1px solid #ccc;
	border-radius: 3px;
	white-space: nowrap;
	box-sizing: border-box;
}

ul#contact-form-editor-tabs>li a:focus,
.a-support:focus,
a.helping-field {
	outline: none;
	box-shadow: none;
}

.a-support:focus {
	color: #0073aa;
}

.a-support::after {
	right: 0;
	content: '\f333';
	font: 400 20px/1 dashicons;
	speak: none;
	display: inline-block;
	padding: 3px 0 0 5px;
	bottom: 2px;
	position: relative;
	vertical-align: bottom;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
	text-decoration: none !important;
	color: #0073aa;
}

.donate {
	border: 1px solid #46b450 !important;
	background-color: #46b450 !important;
	display: inline-block;
	padding: 3px 10px;
	text-decoration: none;
	color: #fff;
	border-radius: 3px;
	border-color: #43ab4b #3c9943 #3c9943 !important;
	box-shadow: 0 2px 0 #3c9943 !important;
	text-shadow: 0 -1px 1px #3c9943, 1px 0 1px #3c9943, 0 1px 1px #3c9943, -1px 0 1px #3c9943 !important;
}

.donate:hover {
	background-color: #4cc255;
	color: #fff;
}

.donate-2019 {
	font-size: 17px;
	position: absolute;
	right: 15px;
	top: 0;
	padding: 5px 30px 3px !important;
	letter-spacing: 1px;
}

.dops-button {
	background: #fff;
	border: solid #d5d5d5;
	border-width: 1px 1px 2px;
	color: #414141;
	cursor: pointer;
	display: inline-block;
	margin: 0;
	outline: 0;
	overflow: hidden;
	font-weight: 500;
	text-overflow: ellipsis;
	text-decoration: none;
	vertical-align: top;
	box-sizing: border-box;
	font-size: 14px;
	line-height: 21px;
	border-radius: 4px;
	padding: 7px 14px 9px;
	appearance: none;
}

.dops-button.is-primary {
	background: #00be28;
	border-color: #00a523;
	color: #fff;
	padding: 5px 20px;
	margin-top: 15px;
	margin-bottom: 15px;
	float: right;
}

.dops-button.is-primary:hover {
	background: #00a523;
	border-color: #008b1d;
}

span.chmm {
	font-size: 14px;
	line-height: 1.2em;
	margin-left: 1em;
	border-radius: 3px;
}

span.chmm .dashicons,
span.chmm .dashicons-no {
	line-height: 1.37em;
}

span.valid {
	background-color: #46b450;
	color: #fff;
}

span.invalid {
	background-color: #dd3d36;
	color: #fff;
}

.misc-pub-section.chimpmatic-info {
	line-height: 1.6;
}

.chimpmatic-info .chmm {
	font-weight: 500;
	font-size: 13px;
	margin-left: 0;
}

.chimpmatic-info .chmm.valid {
	color: #00a32a;
	background-color: transparent;
}

.chimpmatic-info .chmm.invalid {
	color: #d63638;
	background-color: transparent;
}

.chimpmatic-info .mc-stats {
	font-family: Monaco, Consolas, monospace;
	font-size: 12px;
}

.mc-tool-activated {
	background-color: #00be28 !important;
	color: #fff !important;
	font-size: 13px;
}

.mc-tool-not-activated {
	background-color: #dd3d36 !important;
	color: #fff !important;
	font-size: 13px;
}

.mce-red,
.mce-required {
	color: #dd3d36;
	font-weight: 400;
}

.mce-type {
	color: #999 !important;
}

.cm-lite {
	color: #121212;
	font-size: 0.87em;
	background-color: rgba(255, 255, 255, 0.85);
	border-radius: 3px;
	padding: 0 10px;
	box-shadow: inset 0 0 3px rgba(0, 0, 0, .15);
	transition: all 0.4s ease-in-out;
	display: inline-block;
}

.cm-lite.cm-status-neutral {
	color: #646970;
	background-color: rgba(255, 255, 255, 0.85);
	border: 1px solid #dcdcde;
}

.cm-lite.cm-status-connected {
	color: #fff;
	background-color: #00a32a;
	border: 1px solid #008a20;
	box-shadow: 0 0 8px rgba(0, 163, 42, 0.4);
	animation: badge-pulse 0.6s ease-in-out;
}

.cm-lite.cm-status-error {
	color: #fff;
	background-color: #d63638;
	border: 1px solid #b32d2e;
	box-shadow: 0 0 8px rgba(214, 54, 56, 0.4);
	animation: badge-pulse 0.6s ease-in-out;
}

.cmatic-out {
	display: none;
}

@keyframes badge-pulse {
	0% { transform: scale(1); }
	50% { transform: scale(1.05); }
	100% { transform: scale(1); }
}

.promo-2022 {
	padding: 15px;
	background-color: #f0f4f5;
}

.promo-2022 h1 {
	text-align: center;
	font-size: 43px;
	font-weight: 900;
	display: block;
	clear: both;
	color: #ee3455;
	margin: 0 0 0.2407911001em;
	background-image: linear-gradient(to right, #ee3455 0%, #bb2c6b 50%, #460479 100%);
	-webkit-background-clip: text;
	-webkit-text-fill-color: transparent;
	-webkit-box-decoration-break: clone;
}

.promo-2022 h1 span {
	font-size: 35px;
}

.dev-cta {
	margin-top: 35px;
	padding: 40px 10px 30px;
	border: 1px solid #efefef;
	background: unset;
}

.dev-cta::before {
	display: block;
	position: absolute;
	right: 0;
	left: 0;
	background: #fff url(../images/newsletter-stripe.svg) repeat-x;
	background-size: 70px 9px;
	width: 100%;
	height: 9px;
	content: '';
	top: 0;
	z-index: 2;
}

.dev-cta .welcome-panel-content {
	padding-left: 9em;
	min-height: unset;
}

.dev-cta .welcome-panel-content::before {
	content: "\f488";
	color: #ddd;
	display: inline-block;
	position: absolute;
	z-index: 1;
	left: 0;
	top: 50%;
	margin-top: -0.5em;
	font-size: 7em;
	line-height: 1;
	font-family: dashicons;
	text-decoration: inherit;
	font-weight: 400;
	font-style: normal;
	vertical-align: top;
	text-align: center;
	transition: color .1s ease-in 0;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

.bg-f2 {
	display: block;
	margin-top: 0 !important;
	background-color: #1a212b;
}

.bg-f2 * {
	color: #fff !important;
}

.bg-f2::before,
.bg-f2::after {
	content: ' ';
	display: table;
	width: 100%;
	clear: both;
}

.bg-f2 ul {
	list-style-type: square;
	list-style-position: inside;
	list-style-image: none;
}

.metabox-holder .bg-f2 h3 {
	background-color: transparent !important;
	padding: 10px 0 10px !important;
	font-size: 25px;
	font-weight: 900;
	margin-bottom: 0;
	letter-spacing: 1px;
}

.vc-notice {
	position: relative;
	z-index: 0;
	padding-top: 32px;
	padding-bottom: 22px;
	background-color: #fff;
}

.vc-notice h2 {
	color: #000;
}

.vc-notice::before {
	display: block;
	position: absolute;
	right: 0;
	left: 0;
	background: #fff url(../images/newsletter-stripe.svg) repeat-x;
	background-size: 70px 9px;
	width: 100%;
	height: 9px;
	content: '';
	top: 0;
}

.vc-notice .welcome-panel-content {
	position: relative;
	z-index: 2;
	justify-content: initial;
	min-height: unset;
}

.vc-notice .welcome-panel-column-container {
	grid-template-columns: repeat(2, 1fr);
	margin-top: 0;
	padding: 0;
}

.vc-notice .welcome-panel-column {
	background-color: #f6f6f6;
	display: block;
	padding: 15px 20px;
}

.vc-notice .welcome-panel-column>* {
	display: block;
}

.vc-notice::after {
	content: "\f488";
	color: #ddd;
	display: inline-block;
	position: absolute;
	z-index: 1;
	right: 5%;
	bottom: 10%;
	font-size: 7em;
	transform: scaleX(-1);
	filter: fliph;
	line-height: 1;
	font-family: dashicons;
	text-decoration: inherit;
	font-weight: 400;
	font-style: normal;
	vertical-align: top;
	text-align: center;
	transition: color .1s ease-in 0;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

.vc-advanced-settings,
.vc-logs {
	display: none;
}

.vc-logs .mce-custom-fields {
	background: #0d1b2a;
	font-family: 'JetBrains Mono', 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', 'Droid Sans Mono', 'Source Code Pro', monospace;
	font-size: 12px;
	color: #e0e1dd;
	border-radius: 6px;
	padding: 0;
	overflow: hidden;
}

.vc-logs .vc-logs-header {
	display: flex;
	justify-content: space-between;
	align-items: center;
	padding: 10px 16px;
	border-bottom: 1px solid #1b2838;
	background: #0a1628;
}

.vc-logs .vc-logs-title {
	color: #778da9;
	font-weight: 500;
	font-size: 12px;
}

.vc-logs .vc-logs-actions {
	display: flex;
	gap: 8px;
	align-items: center;
}

.vc-logs .vc-logs-separator {
	color: #415a77;
	font-size: 12px;
}

.vc-logs .vc-toggle-filter {
	color: #00a32a;
	text-decoration: none;
	font-size: 12px;
}

.vc-logs .vc-toggle-filter:hover {
	color: #00c853;
	text-decoration: underline;
}

.vc-logs .vc-clear-logs {
	color: #e63946;
	text-decoration: none;
	font-size: 12px;
}

.vc-logs .vc-clear-logs:hover {
	color: #ff6b6b;
	text-decoration: underline;
}

.vc-logs .mce-custom-fields pre {
	margin: 0;
	padding: 16px;
	background: transparent;
	overflow-x: auto;
	max-height: 400px;
	overflow-y: auto;
	white-space: pre-wrap;
	word-wrap: break-word;
}

.vc-logs .mce-custom-fields code {
	background: transparent;
	color: #e0e1dd;
	font-family: inherit;
	font-size: inherit;
}

#chimpmatic-response {
	background-color: rgba(0, 0, 0, 0.05);
}

#chimpmatic-response h5 {
	font-size: 15px;
	margin-bottom: 0;
}

#chimpmatic-response ul {
	margin-top: 10px;
}

div#plugin-information,
div#section-holder {
	padding-right: 10px;
}

#plugin-information .section,
#section-holder .section {
	overflow: hidden;
}

#plugin-information .section img,
#section-holder .section img {
	position: relative;
	max-width: 100%;
}

.blue,
.spartan-blue {
	color: #0073aa;
}

.red-icon {
	color: #dd3d36;
}

.reds {
	color: #ff4500 !important;
	font-weight: 900 !important;
}

.anew {
	color: #ff4500;
	font-weight: 800;
}

.smiles {
	font-weight: 700;
	font-style: normal;
}

.pr10 {
	padding-right: 10px;
}

.mt0 {
	margin-top: 0;
}

.vc-hidden-start,
.to-hide {
	display: none;
}

.chmp-inactive {
	display: none;
	height: 0;
	width: 100%;
}

.chmp-inactive::before .chmp-inactive::after {
	content: '';
	display: table;
	clear: both;
}

.chmp-active {
	display: block;
	height: auto;
}

.chm-titles {
	margin-bottom: 0.15em;
}

.chm-legend {
	margin-bottom: 0.45em;
}

.mce-support {
	border-top: 1px solid rgba(0, 0, 0, 0);
}

.p-hr-s {
	margin-top: 0.5rem;
	height: 0.5rem;
	border: 0;
	border-top: 1px solid rgba(0, 0, 0, 0.05);
}

a.helping-field {
	font-weight: 700;
	padding-left: 4px;
	padding-right: 20px;
	text-decoration: none;
}

a.helping-hand:hover,
a.helping-field:hover,
a.helping-field:hover .red-icon::before {
	color: #444;
}

#chm_panel_principal .spinner,
.mce-custom-fields .spinner {
	float: none;
	margin-top: 0;
}

.cmatic-api-wrap {
	position: relative;
	display: inline-block;
	width: 364px;
}

.cmatic-api-wrap input {
	padding-right: 30px;
	width: 100%;
	box-sizing: border-box;
}

.cmatic-eye {
	position: absolute;
	right: 5px;
	top: 50%;
	transform: translateY(-50%);
	background: none;
	border: none;
	padding: 0;
	cursor: pointer;
	color: #a7aaad;
}

.cmatic-api-wrap input:not(:placeholder-shown) + .cmatic-eye {
	color: #3c434a;
}

.cmatic-eye:hover {
	color: #1d2327;
}

.cmatic-audiences select {
	width: 364px;
}

#chm_activalist,
#mce_fetch_fields {
	min-width: 160px;
}

.cmatic-oauth-status-text {
	display: inline-block;
	width: 364px;
	height: 30px;
	line-height: 30px;
	color: #16a34a;
	font-weight: 600;
	font-size: 13px;
	box-sizing: border-box;
	vertical-align: middle;
}

.cmatic-oauth-status-text .dashicons {
	color: #16a34a;
	font-size: 16px;
	width: 16px;
	height: 16px;
	vertical-align: middle;
	margin-right: 4px;
	position: relative;
	top: -1px;
}

.cmatic-oauth-connect-wrap {
	display: flex;
	flex-direction: column;
}

.cmatic-connect-options {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 12px;
}

.cmatic-mailchimp-connect-intro {
	margin-bottom: 16px;
}

.cmatic-mailchimp-connect-intro h2 {
	margin: 0 0 5px;
	color: #18212f;
	font-size: 18px;
	line-height: 1.3;
}

.cmatic-mailchimp-connect-intro p {
	margin: 0;
	color: #5d6878;
	font-size: 13px;
}

.cmatic-auth-choice-wrap {
	display: flex;
	flex-direction: column;
}

.cmatic-auth-choice-wrap[hidden] {
	display: none;
}

.cmatic-auth-choice {
	position: relative;
	display: flex;
	width: 100%;
	min-height: 166px;
	padding: 17px;
	flex: 1;
	flex-direction: column;
	border: 1px solid #d9e0e8;
	border-radius: 11px;
	background: #f8fafc;
	color: #18212f;
	cursor: pointer;
	font: inherit;
	text-align: left;
	transition: border-color .16s, box-shadow .16s, transform .16s;
}

.cmatic-auth-choice:hover {
	transform: translateY(-2px);
	border-color: #9db1ee;
	background: #fff;
	box-shadow: 0 9px 22px rgba(45, 70, 115, .09);
}

.cmatic-auth-choice:focus-visible {
	outline: 3px solid rgba(47, 91, 231, .24);
	outline-offset: 2px;
}

.cmatic-auth-choice:disabled {
	transform: none;
	cursor: wait;
	opacity: .68;
	box-shadow: none;
}

.cmatic-auth-choice--recommended {
	border-color: #b9c8f7;
	background: #f8faff;
}

.cmatic-auth-choice__top {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	margin-bottom: 16px;
}

.cmatic-auth-choice__icon {
	display: grid;
	width: 34px;
	height: 34px;
	place-items: center;
	border: 1px solid #d5ddea;
	border-radius: 9px;
	background: #fff;
	color: #3d526b;
	font-size: 16px;
	font-weight: 800;
	line-height: 32px;
	text-align: center;
}

.cmatic-auth-choice--recommended .cmatic-auth-choice__icon {
	border-color: #c9d5fb;
	background: #edf2ff;
	color: #2f5be7;
}

.cmatic-recommended-badge {
	display: inline-block;
	padding: 3px 7px;
	border-radius: 999px;
	background: #e8f7f0;
	color: #167752;
	font-size: 10px;
	font-weight: 800;
	letter-spacing: .04em;
}

.cmatic-auth-choice__title {
	display: block;
	margin-bottom: 5px;
	font-size: 15px;
}

.cmatic-auth-choice__description {
	display: block;
	max-width: 430px;
	min-height: 34px;
	color: #5d6878;
	font-size: 12px;
}

.cmatic-auth-choice__description[hidden] {
	display: none;
}

.cmatic-auth-choice__action {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	margin-top: auto;
	padding-top: 15px;
	color: #2f5be7;
	font-size: 12px;
	font-weight: 700;
}

.cmatic-auth-choice__action > :last-child {
	transition: transform .16s;
}

.cmatic-auth-choice:disabled .cmatic-auth-choice__action > :last-child {
	display: none;
}

.cmatic-auth-choice:hover .cmatic-auth-choice__action > :last-child {
	transform: translateX(2px);
}

.cmatic-oauth-status {
	display: block;
	max-width: 430px;
	min-height: 34px;
	margin: 0;
	font-size: 12px;
	font-style: normal;
	text-align: left;
}

.cmatic-oauth-status[hidden] {
	display: none;
}

.cmatic-manual-api-panel {
	margin-top: 12px;
}

.cmatic-back-auth-options {
	display: block;
	margin: 0 0 14px;
	padding: 0;
	border: 0;
	background: transparent;
	color: #2f5be7;
	cursor: pointer;
	font: inherit;
	font-size: 12px;
	font-weight: 700;
}

.cmatic-back-auth-options:hover {
	color: #2548bd;
	text-decoration: underline;
	text-underline-offset: 3px;
}

.cmatic-back-auth-options:focus-visible {
	outline: 3px solid rgba(47, 91, 231, .24);
	outline-offset: 2px;
}

.cmatic-disconnect-confirm {
	display: inline-flex;
	align-items: center;
	gap: 10px;
	height: 30px;
	font-size: 13px;
	vertical-align: middle;
}

.cmatic-disconnect-label {
	color: #d63638;
	font-weight: 600;
}

.cmatic-disconnect-yes {
	background: #d63638;
	color: #fff;
	border: 1px solid #d63638;
	border-radius: 3px;
	padding: 0 14px;
	height: 28px;
	cursor: pointer;
	font-size: 13px;
}

.cmatic-disconnect-yes:hover {
	background: #b32d2e;
	border-color: #b32d2e;
}

.cmatic-disconnect-cancel {
	background: none;
	border: none;
	color: #2271b1;
	cursor: pointer;
	font-size: 13px;
	padding: 0;
	text-decoration: underline;
}

.cmatic-disconnect-cancel:hover {
	color: #135e96;
}

@media only screen and (min-width: 768px) {
	.mcee-container {
	display: inline-block;
	width: 49.5%;
	box-sizing: border-box;
	padding-right: 2rem;
	margin-bottom: 1.25rem;
	}

	.mcee-container > div[id^="gg-select"],
	.mcee-container > div[id^="GDPR-select"] {
	width: 100%;
	}

	.mcee-container select {
	margin-top: 0;
	margin-bottom: 0;
	width: 100%;
	max-width: 100%;
	}

	.mce-custom-fields .md-half {
	display: inline-block;
	width: 49%;
	vertical-align: top;
	}
}

.cmatic-header {
	background: #fff;
	border-bottom: 1px solid #c5d1d9;
	margin: 0 0 20px 0;
}

.cmatic-header__inner {
	display: flex;
	align-items: center;
	justify-content: space-between;
	flex-wrap: wrap;
	gap: 16px;
	padding: 14px 20px;
}

.cmatic-header__brand {
	display: flex;
	align-items: center;
	gap: 10px;
}

.cmatic-header__title {
	font-size: 18px;
	font-weight: 600;
	color: #1d2327;
	letter-spacing: -0.3px;
}

.cmatic-header__badge {
	font-size: 10px;
	font-weight: 700;
	padding: 3px 8px;
	border-radius: 4px;
	text-transform: uppercase;
	letter-spacing: 0.5px;
}

.cmatic-header__badge--pro,
.cmatic-header__badge--lite {
	background: #4d5d6d;
	color: #fff;
}

.cmatic-header__version {
	font-size: 12px;
	color: #646970;
	font-weight: 400;
}

.cmatic-header__actions {
	display: flex;
	align-items: center;
}

.cmatic-header__context {
	display: flex;
	align-items: center;
	gap: 12px;
	margin-left: auto;
}

.cmatic-header__provider-control {
	display: flex;
	align-items: center;
	gap: 8px;
}

.cmatic-header__provider-control label {
	color: #50575e;
	font-size: 12px;
	font-weight: 600;
	white-space: nowrap;
}

.cmatic-header__provider-control select {
	min-width: 150px;
	max-width: 220px;
}

.cmatic-header__review {
	display: flex;
	align-items: center;
	gap: 8px;
	color: #646970;
	text-decoration: none;
	font-size: 13px;
	transition: color 0.2s;
}

.cmatic-header__review:hover {
	color: #1d2327;
}

.cmatic-sparkles {
	display: inline-block;
	vertical-align: middle;
	margin-left: 6px;
	width: 80px;
	height: 16px;
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 80 16'%3E%3Cpath fill='%23EFBF04' d='M8 0l2.5 5 5.5.8-4 3.9.9 5.3L8 12.5 3.1 15l.9-5.3-4-3.9L5.5 5zM24 0l2.5 5 5.5.8-4 3.9.9 5.3-4.9-2.5-4.9 2.5.9-5.3-4-3.9 5.5-.8zM40 0l2.5 5 5.5.8-4 3.9.9 5.3-4.9-2.5-4.9 2.5.9-5.3-4-3.9 5.5-.8zM56 0l2.5 5 5.5.8-4 3.9.9 5.3-4.9-2.5-4.9 2.5.9-5.3-4-3.9 5.5-.8zM72 0l2.5 5 5.5.8-4 3.9.9 5.3-4.9-2.5-4.9 2.5.9-5.3-4-3.9 5.5-.8z'/%3E%3C/svg%3E");
	background-repeat: no-repeat;
	background-size: contain;
}

.cmatic-header__status {
	display: flex;
	align-items: center;
	gap: 6px;
	margin-left: 12px;
	padding-left: 12px;
	border-left: 1px solid #c5d1d9;
}

.cmatic-header__status-dot {
	width: 8px;
	height: 8px;
	border-radius: 50%;
}

.cmatic-header__status-dot--connected {
	background: #00a32a;
}

.cmatic-header__status-dot--neutral {
	background: #a7aaad;
}

.chimpmatic-info .chmm.neutral {
	color: #646970;
}

.cmatic-header__status-dot--disconnected {
	background: #d63638;
}

.cmatic-header__status-text {
	font-size: 12px;
	color: #646970;
}

.cmatic-tags-grid {
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
	margin-bottom: 18px;
}

.cmatic-tag-chip {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	/* padding: 4px 10px; */
	border: 1px solid #c3c4c7;
	border-radius: 25px;
	background: #f6f7f7;
	cursor: pointer;
	font-size: 12px;
	transition: all 0.15s ease;
	user-select: none;
	color: #50575e;
	line-height: 1em;
}

.cmatic-tag-chip:hover {
	border-color: #2271b1;
	background: #f6f7f7;
}

.cmatic-tag-chip input[type="checkbox"] {
	display: none;
}

.cmatic-tag-chip.selected {
	background: #f0f6fc;
	border-color: #2271b1;
	color: #2271b1;
}

.cmatic-tag-name {
	font-weight: 500;
	font-family: monospace;
}

.cmatic-tag-type {
	font-size: 9px;
	text-transform: uppercase;
	font-weight: 600;
	padding: 2px 2px;
	border-radius: 15px;
	letter-spacing: 0.3px;
	color: #000;
}

.cmatic-toggle {
	position: relative;
	display: inline-block;
	width: 44px;
	height: 24px;
	vertical-align: middle;
}

.cmatic-toggle input {
	opacity: 0;
	width: 0;
	height: 0;
}

.cmatic-toggle-slider {
	position: absolute;
	cursor: pointer;
	inset: 0;
	background: #c3c4c7;
	border-radius: 24px;
	transition: .3s;
}

.cmatic-toggle-slider:before {
	content: "";
	position: absolute;
	height: 18px;
	width: 18px;
	left: 3px;
	bottom: 3px;
	background: #fff;
	border-radius: 50%;
	transition: .3s;
}

.cmatic-toggle input:checked + .cmatic-toggle-slider {
	background: #2271b1;
}

.cmatic-toggle input:checked + .cmatic-toggle-slider:before {
	transform: translateX(20px);
}

.cmatic-toggle.is-saving .cmatic-toggle-slider {
	opacity: 0.6;
	cursor: wait;
}

.cmatic-toggle-label {
	margin-left: 10px;
	vertical-align: middle;
	color: #1d2327;
	font-size: 12px;
	font-weight: normal;
}

/* Privacy link beside the toggle label matches the label's size/weight
   (the shared a.helping-field is 13px/700 for the API-key link's arrows). */
.cmatic-toggle-label + a.helping-field {
	font-size: 12px;
	font-weight: normal;
	padding-right: 4px;
}

.cmatic-toggle-row {
	display: flex;
	align-items: center;
	gap: 10px;
}

.cmatic-title-with-toggle {
	display: flex;
	justify-content: space-between;
	align-items: center;
}

.cmatic-help-icon {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 16px;
	height: 16px;
	font-size: 11px;
	font-weight: 600;
	color: #646970;
	background: #f0f0f1;
	border-radius: 50%;
	text-decoration: none;
	line-height: 1;
}

.cmatic-help-icon:hover {
	color: #2271b1;
	background: #f0f6fc;
}

/* The modal's header Submit drives the test flow (REST + feedback panel).
   The CF7 preview form renders its own submit too: two buttons, one job.
   Hide the inner one so there is exactly one unambiguous action. */
.cmatic-test-form-wrap .wpcf7-submit,
.cmatic-test-form-wrap input[type="submit"] {
	display: none !important;
}

.cmatic-test-form-wrap {
	padding: 16px;
	background: #fafafa;
	border: 1px solid #dcdcde;
	border-radius: 4px;
}

.cmatic-test-form-wrap .wpcf7 {
	max-width: 100%;
}

.cmatic-test-form-wrap .wpcf7-form-control-wrap {
	display: block;
	margin-bottom: 12px;
}

.cmatic-test-form-wrap input[type="text"],
.cmatic-test-form-wrap input[type="email"],
.cmatic-test-form-wrap input[type="tel"],
.cmatic-test-form-wrap input[type="url"],
.cmatic-test-form-wrap input[type="number"],
.cmatic-test-form-wrap input[type="date"],
.cmatic-test-form-wrap textarea,
.cmatic-test-form-wrap select {
	width: 100%;
	max-width: 100%;
}

.cmatic-test-form-wrap input[type="submit"] {
	background: #2271b1;
	border-color: #2271b1;
	color: #fff;
	padding: 8px 20px;
	font-size: 14px;
	border-radius: 4px;
	cursor: pointer;
	transition: background 0.15s;
}

.cmatic-test-form-wrap input[type="submit"]:hover {
	background: #135e96;
	border-color: #135e96;
}

.cmatic-test-form-wrap .wpcf7-response-output {
	margin: 16px 0 0;
	padding: 12px 16px;
	border-radius: 4px;
}

.cmatic-test-form-wrap .wpcf7-mail-sent-ok {
	border-color: #00a32a;
	background: #edfaef;
}

.cmatic-test-form-wrap .wpcf7-mail-sent-ng,
.cmatic-test-form-wrap .wpcf7-validation-errors {
	border-color: #d63638;
	background: #fcf0f1;
}

.cmatic-hidden,
.mce-hidden {
	display: none !important;
}

.cmatic-defaults-fields-notice {
	margin: 12px 0 4px;
	padding: 10px 14px;
	background: #f0f6fc;
	border-left: 4px solid #72aee6;
	border-radius: 2px;
}

.cmatic-defaults-fields-notice.cmatic-visible {
	display: block;
}

.cmatic-defaults-fields-notice .cmatic-notice {
	margin: 0;
	font-size: 13px;
	color: #1d2327;
	line-height: 1.5;
}

.cmatic-defaults-fields-notice .helping-field {
	margin-left: 4px;
}

.mce-fade-in {
	animation: mceFadeIn 0.5s ease-in forwards;
}

@keyframes mceFadeIn {
	from { opacity: 0; }
	to { opacity: 1; }
}

.cmatic-lookup-summary {
	padding: 8px 10px;
	margin: 10px 0;
	border-left: 4px solid;
	border-radius: 2px;
	font-size: 12px;
}
.cmatic-lookup-summary.cmatic-found {
	border-color: #00a32a;
	background: #edfaef;
}
.cmatic-lookup-summary.cmatic-not-found {
	border-color: #dba617;
	background: #fcf9e8;
}

.cmatic-result-card {
	border: 1px solid #ddd;
	border-radius: 4px;
	margin: 8px 0;
	overflow: hidden;
}

.cmatic-card-header {
	padding: 10px 12px;
	background: #f6f7f7;
	font-size: 12px;
	display: flex;
	justify-content: space-between;
	align-items: center;
}

.cmatic-card-header.cmatic-expandable {
	cursor: pointer;
	transition: background 0.2s;
}
.cmatic-card-header.cmatic-expandable:hover {
	background: #eef0f2;
}

.cmatic-chevron {
	width: 0;
	height: 0;
	border-left: 4px solid transparent;
	border-right: 4px solid transparent;
	border-top: 5px solid #50575e;
	transition: transform 0.2s;
	margin-left: 8px;
}
.cmatic-card-header.cmatic-expanded .cmatic-chevron {
	transform: rotate(180deg);
}

.cmatic-card-body {
	padding: 12px;
	border-top: 1px solid #eee;
	display: none;
	font-size: 11px;
}
.cmatic-card-body.cmatic-visible {
	display: block;
}

.cmatic-status-dot {
	display: inline-block;
	width: 8px;
	height: 8px;
	border-radius: 50%;
	margin-right: 6px;
}
.cmatic-status-subscribed { background: #00a32a; }
.cmatic-status-unsubscribed { background: #d63638; }
.cmatic-status-pending { background: #dba617; }
.cmatic-status-not-found { background: #c3c4c7; }

.cmatic-header-left,
.cmatic-header-right {
	display: flex;
	align-items: center;
}

.cmatic-badge {
	display: inline-block;
	padding: 2px 8px;
	border-radius: 3px;
	font-size: 11px;
	font-weight: 500;
}
.cmatic-badge-success { background: #00a32a; color: #fff; }
.cmatic-badge-warning { background: #dba617; color: #fff; }
.cmatic-badge-neutral { background: #dcdcde; color: #50575e; }

.cmatic-section-header {
	font-size: 10px;
	font-weight: 600;
	color: #50575e;
	margin: 12px 0 4px;
	text-transform: uppercase;
	letter-spacing: 0.5px;
}

.cmatic-field-table {
	width: 100%;
	border-collapse: collapse;
	margin: 8px 0;
}
.cmatic-field-table th,
.cmatic-field-table td {
	padding: 4px 6px;
	text-align: left;
	border-bottom: 1px solid #f0f0f1;
	font-size: 11px;
}
.cmatic-field-table th {
	font-weight: 600;
	color: #50575e;
	width: 35%;
}
.cmatic-field-table tr:last-child th,
.cmatic-field-table tr:last-child td {
	border-bottom: none;
}

.cmatic-tag-list {
	display: flex;
	flex-wrap: wrap;
	gap: 4px;
	margin: 6px 0;
}
.cmatic-tag-chip {
	display: inline-block;
	padding: 7px 6px;
	background: #f0f0f1;
	border: 1px solid #c3c4c7;
	border-radius: 10px;
	font-size: 10px;
}

.cmatic-empty {
	color: #787c82;
	font-style: italic;
	font-size: 10px;
}

.cmatic-meta-list {
	margin: 0;
	padding: 0;
	list-style: none;
}
.cmatic-meta-list li {
	padding: 3px 0;
	font-size: 10px;
	color: #50575e;
}

.cmatic-not-pro {
	filter: blur(4px);
	user-select: none;
	cursor: pointer;
}

tr.cmatic-not-pro:hover,
[data-section].cmatic-not-pro:hover {
	background: #f9f9f9;
}

.cmatic-upsell-tooltip {
	position: absolute;
	background: #1d2327;
	color: #fff;
	padding: 10px 14px;
	border-radius: 4px;
	font-size: 12px;
	line-height: 1.4;
	max-width: 220px;
	z-index: 10000;
	box-shadow: 0 2px 8px rgba(0,0,0,0.2);
	animation: cmatic-fade-in 0.2s ease;
}

.cmatic-upsell-tooltip::before {
	content: '';
	position: absolute;
	top: -6px;
	left: 20px;
	border-left: 6px solid transparent;
	border-right: 6px solid transparent;
	border-bottom: 6px solid #1d2327;
}

.cmatic-upsell-tooltip a {
	color: #72aee6;
	text-decoration: none;
	font-weight: 500;
}

.cmatic-upsell-tooltip a:hover {
	text-decoration: underline;
}

@keyframes cmatic-fade-in {
	from { opacity: 0; transform: translateY(-4px); }
	to { opacity: 1; transform: translateY(0); }
}


/* â”€â”€ Groups + GDPR native previews (Pro showcase) â”€â”€ */
.cmatic-showcase-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
	gap: 14px 28px;
	margin-top: 6px;
}

.cmatic-showcase-row {
	display: flex;
	flex-direction: column;
	gap: 6px;
	font-size: 13px;
	cursor: not-allowed;
}

.cmatic-showcase-row select {
	max-width: 100%;
	opacity: .6;
}

.cmatic-showcase-meta {
	color: #787c82;
	font-weight: 400;
}

.cmatic-showcase-locked {
	opacity: .55;
	cursor: not-allowed;
}

.cmatic-showcase-unlock {
	margin: 14px 0 0;
	font-size: 13px;
}

.cmatic-showcase-off {
	display: inline-block;
	margin-left: 6px;
	padding: 1px 7px;
	background: #fff;
	border: 1px dashed #2271b1;
	border-radius: 3px;
	font-weight: 700;
	font-size: 12px;
	color: #2271b1;
}

.cmatic-field-offer {
	display: inline-block;
	margin-left: 6px;
	padding: 1px 7px;
	border: 1px dashed currentColor;
	border-radius: 3px;
	font-size: 12px;
	font-weight: 700;
	white-space: nowrap;
}

.cmatic-showcase-note {
	margin: 8px 0 0;
	font-size: 11px;
	color: #a7aaad;
}

.cmatic-provider-panel[hidden],
[data-cmatic-provider-view][hidden] {
	display: none !important;
}

.cmatic-provider-section {
	background: rgba(255, 255, 255, .95);
	border: 1px solid #e5e5e5;
	border-radius: 8px;
	margin-bottom: 20px;
	padding: 16px 20px 20px;
}

.cmatic-provider-section h3 {
	margin: 0 0 12px;
}

.cmatic-provider-auth-row,
.cmatic-provider-destination-row {
	display: flex;
	align-items: flex-end;
	flex-wrap: wrap;
	gap: 8px;
}

#cmatic-provider-auth-fields {
	display: flex;
	flex: 1 1 420px;
	flex-wrap: wrap;
	gap: 12px;
}

.cmatic-provider-auth-field {
	flex: 1 1 320px;
	max-width: 520px;
}

.cmatic-provider-auth-field label,
.cmatic-provider-auth-field input,
.cmatic-provider-auth-field small {
	display: block;
}

.cmatic-provider-auth-field input,
#cmatic-provider-list {
	box-sizing: border-box;
	width: 100%;
	max-width: 520px;
}

.cmatic-provider-actions {
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
}

.cmatic-provider-actions--end {
	justify-content: flex-end;
	margin-top: 12px;
}

.cmatic-provider-tools-description {
	margin: -8px 0 14px;
}

.cmatic-provider-tool-list {
	overflow: hidden;
	border: 1px solid #e1e6ec;
	border-radius: 9px;
	background: #fbfcfe;
}

.cmatic-provider-tool-row {
	display: grid;
	grid-template-columns: minmax(190px, 240px) minmax(280px, 1fr) auto;
	align-items: center;
	gap: 18px;
	padding: 14px;
}

.cmatic-provider-tool-row + .cmatic-provider-tool-row {
	border-top: 1px solid #e1e6ec;
}

.cmatic-provider-tool-row--lookup {
	grid-template-columns: minmax(190px, 240px) minmax(280px, 1fr);
}

.cmatic-provider-tool-copy strong,
.cmatic-provider-tool-copy p {
	display: block;
}

.cmatic-provider-tool-copy strong {
	color: #1d2327;
}

.cmatic-provider-tool-copy p,
.cmatic-provider-tool-warning {
	margin: 2px 0 0;
	color: #646970;
}

.cmatic-provider-tool-control {
	display: grid;
	grid-template-columns: minmax(220px, 1fr) auto;
	align-items: end;
	gap: 8px;
}

.cmatic-provider-tool-field label,
.cmatic-provider-tool-field input {
	display: block;
}

.cmatic-provider-tool-field input {
	box-sizing: border-box;
	width: 100%;
	max-width: none;
}

#cmatic-provider-mailerlite-lookup-results {
	grid-column: 1 / -1;
}

.cmatic-provider-tool-row > .cmatic-provider-actions {
	justify-content: flex-end;
}

.cmatic-provider-mapping-grid {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 16px 24px;
}

.cmatic-routing-table-list:not(:empty) {
	overflow: hidden;
	border: 1px solid #e1e6ec;
	border-radius: 9px;
	background: #fbfcfe;
}

.cmatic-routing-table-header,
.cmatic-routing-table-row {
	display: grid;
	grid-template-columns: 42px minmax(145px, 0.85fr) minmax(145px, 0.75fr) minmax(220px, 1fr) 34px;
	align-items: center;
}

.cmatic-routing-table-header {
	background: #f6f7f7;
	border-bottom: 1px solid #e1e6ec;
}

.cmatic-routing-table-header span {
	padding: 9px 10px;
	color: #50575e;
	font-weight: 600;
}

.cmatic-routing-table-row {
	background: #fbfcfe;
}

.cmatic-routing-table-row + .cmatic-routing-table-row {
	border-top: 1px solid #e1e6ec;
}

.cmatic-routing-rule-label {
	padding: 10px;
	color: #1d2327;
	font-weight: 600;
	white-space: nowrap;
}

.cmatic-routing-table-cell {
	padding: 10px;
}

.cmatic-routing-table-cell select {
	display: block;
	width: 100%;
	min-width: 0;
	max-width: 100%;
}

.cmatic-routing-rule-error {
	grid-column: 2 / 5;
	margin: 0 10px 10px;
	color: #b32d2e;
}

.cmatic-routing-table-row [aria-invalid="true"] {
	border-color: #d63638;
	box-shadow: 0 0 0 1px #d63638;
}

#cmatic-provider-data .cmatic-routing-remove {
	display: inline-flex;
	width: 32px;
	height: 32px;
	align-items: center;
	justify-content: center;
	padding: 0;
	color: #646970;
	text-decoration: none;
}

#cmatic-provider-data .cmatic-routing-remove:hover {
	color: #b32d2e;
}

#cmatic-provider-data .cmatic-routing-remove:focus {
	color: #3858e9;
	text-decoration: none;
}

#cmatic-provider-data .cmatic-routing-remove:disabled {
	cursor: default;
	opacity: .45;
}

.cmatic-provider-mapping-row label,
.cmatic-provider-mapping-row select {
	display: block;
	width: 100%;
	max-width: 100%;
}

.cmatic-provider-mapping-row label {
	margin-bottom: 5px;
}

.cmatic-provider-message:empty {
	display: none;
}

.cmatic-provider-message.is-error {
	color: #b32d2e;
}

.cmatic-provider-message.is-success {
	color: #008a20;
}

.cmatic-header__context[hidden],
.cmatic-provider-onboarding[hidden],
.cmatic-provider-completion[hidden],
.cmatic-provider-locked[hidden],
.cmatic-provider-connection-summary[hidden],
.cmatic-provider-recovery-summary[hidden],
.cmatic-provider-auth-row[hidden],
.cmatic-provider-destination-row[hidden],
.cmatic-provider-mapping-grid[hidden],
.cmatic-provider-save-row[hidden] {
	display: none !important;
}

.cmatic-provider-onboarding {
	margin: 0 0 20px;
	padding: clamp(26px, 4vw, 46px);
	border: 1px solid #d8e0e8;
	border-radius: 12px;
	background: #fff;
	text-align: center;
}

.cmatic-provider-onboarding__kicker {
	display: inline-block;
	margin-bottom: 8px;
	color: #3153b9;
	font-size: 11px;
	font-weight: 700;
	letter-spacing: .08em;
	text-transform: uppercase;
}

.cmatic-provider-onboarding h2 {
	margin: 0 0 7px;
	font-size: clamp(21px, 2.4vw, 28px);
}

.cmatic-provider-onboarding > p {
	margin: 0;
	color: #5d6878;
}

.cmatic-provider-choices {
	display: grid;
	grid-template-columns: repeat(4, minmax(0, 1fr));
	gap: 12px;
	margin-top: 25px;
	text-align: left;
}

.cmatic-provider-choice {
	min-height: 150px;
	padding: 16px;
	border: 1px solid #d8e0e8;
	border-radius: 10px;
	background: #f8fafc;
	color: #1d2734;
	text-align: left;
	transition: border-color .16s, box-shadow .16s, transform .16s;
}

.cmatic-provider-choice:hover,
.cmatic-provider-choice:focus-visible {
	transform: translateY(-2px);
	border-color: #9db1ee;
	box-shadow: 0 9px 22px rgba(45, 70, 115, .09);
}

.cmatic-provider-choice img {
	display: block;
	width: 118px;
	height: 36px;
	margin-bottom: 15px;
	object-fit: contain;
	object-position: left center;
}

.cmatic-provider-choice strong,
.cmatic-provider-choice span {
	display: block;
}

.cmatic-provider-choice strong {
	margin-bottom: 4px;
}

.cmatic-provider-choice span {
	color: #5d6878;
	font-size: 11px;
	line-height: 1.4;
}

.cmatic-provider-progress {
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	margin: 0 0 18px;
	padding: 0;
	list-style: none;
}

.cmatic-provider-progress[hidden] {
	display: none;
}

.cmatic-provider-progress li {
	position: relative;
	display: flex;
	align-items: center;
	gap: 9px;
	color: #7c8796;
	font-size: 12px;
}

.cmatic-provider-progress li::after {
	position: absolute;
	top: 13px;
	right: 10px;
	left: 42px;
	height: 2px;
	background: #dce3eb;
	content: "";
}

.cmatic-provider-progress li:last-child::after {
	display: none;
}

.cmatic-provider-progress li > span,
.cmatic-provider-progress li > b {
	position: relative;
	z-index: 1;
}

.cmatic-provider-progress li > span {
	display: grid;
	width: 28px;
	height: 28px;
	flex: 0 0 28px;
	place-items: center;
	border: 1px solid #d2dae4;
	border-radius: 50%;
	background: #fff;
}

.cmatic-provider-progress li > b {
	padding-right: 10px;
	background: #f2f4f6;
}

.cmatic-provider-progress li.is-current {
	color: #2f5be7;
}

.cmatic-provider-progress li.is-current > span {
	border-color: #2f5be7;
	background: #edf2ff;
}

.cmatic-provider-progress li.is-done {
	color: #16835b;
}

.cmatic-provider-progress li.is-done > span {
	border-color: #83ceb2;
	background: #eaf8f2;
}

.cmatic-provider-progress li.is-done::after {
	background: #9dd9c3;
}

.cmatic-provider-completion {
	display: flex;
	align-items: flex-start;
	gap: 12px;
	margin: 0 0 18px;
	padding: 15px 17px;
	border: 1px solid #a9dcc8;
	border-radius: 10px;
	background: #eaf8f2;
	color: #285e4b;
}

.cmatic-provider-completion__icon,
.cmatic-provider-connection-summary__icon {
	display: grid;
	width: 32px;
	height: 32px;
	flex: 0 0 32px;
	place-items: center;
	border-radius: 50%;
	background: #16835b;
	color: #fff;
	font-weight: 700;
}

.cmatic-provider-completion strong {
	display: block;
	margin-bottom: 2px;
	font-size: 15px;
}

.cmatic-provider-completion p {
	margin: 0 0 3px;
}

#cmatic-provider-completion-meta {
	font-size: 12px;
}

.cmatic-provider-section {
	border-color: #d8e0e8;
	border-radius: 12px;
	padding: 20px;
}

.cmatic-provider-section h3 {
	margin-bottom: 4px;
	font-size: 18px;
}

.cmatic-provider-section > .description,
#cmatic-provider-credentials-intro .description {
	margin: 0 0 18px;
	color: #5d6878;
}

.cmatic-provider-auth-field small {
	margin-top: 6px;
	color: #5d6878;
}

.cmatic-provider-auth-field .cmatic-provider-credential-readiness {
	min-height: 16px;
	margin-top: 3px;
	color: #16835b;
	font-weight: 600;
}

.cmatic-provider-connection-summary,
.cmatic-provider-recovery-summary {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
	padding: 15px;
	border: 1px solid #bfe4d5;
	border-radius: 9px;
	background: #eaf8f2;
}

.cmatic-provider-recovery-summary {
	border-color: #efd49c;
	background: #fff6df;
}

.cmatic-provider-recovery-summary p {
	margin: 3px 0 0;
	color: #795f35;
}

.cmatic-provider-connection-summary__text {
	display: flex;
	flex: 1;
	flex-direction: column;
}

.cmatic-provider-connection-summary__text span {
	color: #477163;
	font-size: 12px;
}

.cmatic-provider-locked {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 14px;
	border: 1px dashed #ccd5df;
	border-radius: 9px;
	background: #f8fafc;
	color: #5d6878;
}

.cmatic-provider-locked > span {
	display: grid;
	width: 28px;
	height: 28px;
	place-items: center;
	border-radius: 50%;
	background: #e8edf2;
}

.cmatic-provider-locked p {
	margin: 0;
}

.cmatic-provider-mapping-row {
	padding: 13px;
	border: 1px solid #e1e6ec;
	border-radius: 9px;
	background: #fbfcfe;
}

.cmatic-consent-policy-list {
	overflow: hidden;
	border: 1px solid #d8e0e8;
	border-radius: 10px;
}

.cmatic-consent-policy-provider {
	display: contents;
}

.cmatic-consent-policy-provider[hidden] {
	display: none;
}

.cmatic-consent-policy-row {
	display: grid;
	grid-template-columns: minmax(170px, 220px) minmax(220px, 310px) minmax(240px, 1fr);
	align-items: center;
	gap: 20px;
	min-height: 76px;
	padding: 13px 15px;
	background: #fff;
}

.cmatic-consent-policy-row + .cmatic-consent-policy-row {
	border-top: 1px solid #e7ebef;
}

.cmatic-consent-policy-row:nth-child(even) {
	background: #fbfcfd;
}

.cmatic-consent-policy-row--warning {
	background: #fffaf0;
}

.cmatic-consent-policy-name strong,
.cmatic-consent-policy-name span {
	display: block;
}

.cmatic-consent-policy-name strong {
	font-size: 13px;
}

.cmatic-consent-policy-name span,
.cmatic-consent-policy-explanation,
.cmatic-consent-policy-checks p,
.cmatic-consent-policy-note {
	color: #5d6878;
	font-size: 12px;
}

.cmatic-consent-policy-explanation,
.cmatic-consent-policy-checks p,
.cmatic-consent-managed-value p,
.cmatic-consent-policy-note {
	margin: 0;
}

.cmatic-consent-policy-control select,
.cmatic-consent-policy-control input[type="number"],
.cmatic-consent-policy-control input[type="url"] {
	box-sizing: border-box;
	width: 100%;
	max-width: none;
}

.cmatic-consent-managed-value {
	padding: 9px 10px;
	border: 1px solid #cbd4dc;
	border-radius: 6px;
	background: #f8fafc;
}

.cmatic-consent-managed-value p {
	margin-top: 3px;
	color: #5d6878;
	font-size: 12px;
}

.cmatic-consent-managed-value a {
	display: inline-block;
	margin-top: 5px;
}

.cmatic-consent-policy-checks > div + div {
	margin-top: 10px;
}

.cmatic-consent-policy-checks label {
	font-weight: 600;
}

.cmatic-consent-policy-checks p {
	margin-top: 2px;
	padding-left: 24px;
}

.cmatic-consent-policy-checks .cmatic-defaults-fields-notice {
	margin-top: 10px;
	padding-left: 13px;
}

.cmatic-consent-policy-note {
	margin-top: 12px;
}

.cmatic-provider-mapping-warning {
	margin: 8px 0 0;
	color: #875407;
	font-size: 11px;
}

.cmatic-defaults-fields-notice {
	margin-top: 14px;
	padding: 11px 13px;
	border-left: 3px solid #6c8ef3;
	background: #f0f4ff;
}

.cmatic-defaults-fields-notice a {
	font-weight: 600;
}

.cmatic-provider-save-row {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
	margin-top: 17px;
	padding-top: 16px;
	border-top: 1px solid #d8e0e8;
}

#cmatic-provider-save-status {
	color: #477163;
	font-size: 12px;
}

.cmatic-provider-message {
	margin: 0 0 12px;
}

.cmatic-provider-message:not(:empty) {
	padding: 10px 12px;
	border-left: 3px solid #6c8ef3;
	background: #f0f4ff;
}

.cmatic-provider-message.is-error {
	border-left-color: #d63638;
	background: #fcf0f1;
}

.cmatic-provider-message.is-success {
	border-left-color: #00a32a;
	background: #edfaef;
}

@media only screen and (max-width: 782px) {
	.cmatic-connect-options {
		grid-template-columns: minmax(0, 1fr);
	}

	.cmatic-header__context {
		flex-wrap: wrap;
		width: 100%;
		margin-left: 0;
	}

	.cmatic-header__provider-control {
		flex: 1 1 280px;
	}

	.cmatic-header__provider-control select {
		flex: 1 1 auto;
		max-width: none;
	}

	.cmatic-header__actions {
		margin-left: auto;
	}

	.cmatic-provider-mapping-grid {
		grid-template-columns: minmax(0, 1fr);
	}

	.cmatic-provider-tool-row {
		grid-template-columns: minmax(0, 1fr);
		gap: 10px;
	}

	.cmatic-provider-tool-control {
		grid-template-columns: minmax(0, 1fr);
	}

	#cmatic-provider-mailerlite-lookup-results {
		grid-column: 1;
	}

	.cmatic-consent-policy-row {
		grid-template-columns: minmax(0, 1fr);
		gap: 7px;
	}

	.cmatic-provider-choices {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}

	.cmatic-provider-connection-summary,
	.cmatic-provider-recovery-summary {
		align-items: stretch;
		flex-direction: column;
	}
}

@media only screen and (max-width: 600px) {
	.cmatic-header__provider-control {
		align-items: stretch;
		flex-direction: column;
		width: 100%;
	}

	.cmatic-header__provider-control select,
	.cmatic-provider-auth-field,
	.cmatic-provider-auth-field input,
	#cmatic-provider-list,
	.cmatic-provider-actions,
	.cmatic-provider-actions .button {
		width: 100%;
		max-width: none;
	}

	.cmatic-routing-table-header {
		display: none;
	}

	.cmatic-routing-table-row {
		grid-template-columns: 42px minmax(0, 1fr) 34px;
		align-items: start;
		padding: 10px 0;
	}

	.cmatic-routing-table-row .cmatic-routing-rule-label {
		grid-column: 1;
		grid-row: 1;
	}

	.cmatic-routing-table-cell {
		grid-column: 2;
		padding: 4px 10px;
	}

	.cmatic-routing-table-cell::before {
		display: block;
		margin-bottom: 4px;
		color: #50575e;
		font-weight: 600;
		content: attr(data-routing-column-label);
	}

	.cmatic-routing-remove {
		grid-column: 3;
		grid-row: 1;
	}

	.cmatic-routing-rule-error {
		grid-column: 2;
		margin-top: 5px;
	}

	.cmatic-header__status {
		margin-left: 0;
		padding-left: 0;
		border-left: 0;
	}

	.cmatic-provider-choices {
		grid-template-columns: minmax(0, 1fr);
	}

	.cmatic-provider-progress {
		gap: 5px;
	}

	.cmatic-provider-progress li {
		align-items: flex-start;
		flex-direction: column;
	}

	.cmatic-provider-progress li > b {
		font-size: 10px;
	}

	.cmatic-provider-save-row {
		align-items: stretch;
		flex-direction: column;
	}

	.cmatic-provider-save-row .button {
		width: 100%;
	}
}

@media (prefers-reduced-motion: reduce) {
	.cmatic-provider-panel *,
	.cmatic-header__context * {
		scroll-behavior: auto;
		transition-duration: .001ms !important;
	}
}
                          assets/css/chimpmatic-lite-deactivate.css                                                           0000644                 00000022062 15266052602 0014536 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       /**
 * Deactivation survey modal styles.
 *
 * @package   contact-form-7-mailchimp-extension
 * @author    renzo.johnson@gmail.com
 * @copyright 2014-2026 https://renzojohnson.com
 * @license   GPL-3.0+
 */

:root {
	--cmatic-modal-overlay-bg: rgba(0, 0, 0, 0.6);
	--cmatic-modal-bg: #fff;
	--cmatic-modal-max-width: 600px;
	--cmatic-modal-border-radius: 8px;
	--cmatic-modal-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
	--cmatic-primary-color: #0073aa;
	--cmatic-primary-hover: #005a87;
	--cmatic-secondary-color: #f0f0f1;
	--cmatic-secondary-hover: #dcdcde;
	--cmatic-text-color: #1e1e1e;
	--cmatic-text-light: #646970;
	--cmatic-border-color: #dcdcde;
	--cmatic-error-color: #d63638;
	--cmatic-success-color: #00a32a;
	--cmatic-transition: all 0.3s ease;
}

.cmatic-modal {
	display: none;
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	z-index: 100000;
	overflow-y: auto;
}

.cmatic-modal--active {
	display: flex;
	align-items: center;
	justify-content: center;
}

.cmatic-modal--active .cmatic-modal__overlay {
	animation: modalOverlayFadeIn 0.3s ease;
}

.cmatic-modal--active .cmatic-modal__dialog {
	animation: modalSlideUp 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes modalOverlayFadeIn {
	from { opacity: 0; }
	to { opacity: 1; }
}

@keyframes modalSlideUp {
	from { opacity: 0; transform: translateY(100px); }
	to { opacity: 1; transform: translateY(0); }
}

.cmatic-modal--closing .cmatic-modal__overlay {
	animation: modalOverlayFadeOut 0.3s ease;
}

.cmatic-modal--closing .cmatic-modal__dialog {
	animation: modalSlideDown 0.3s ease;
}

@keyframes modalOverlayFadeOut {
	from { opacity: 1; }
	to { opacity: 0; }
}

@keyframes modalSlideDown {
	from { opacity: 1; transform: translateY(0); }
	to { opacity: 0; transform: translateY(50px); }
}

.cmatic-modal-open {
	overflow: hidden;
}

.cmatic-modal__overlay {
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	background: var(--cmatic-modal-overlay-bg);
	z-index: -1;
	backdrop-filter: blur(5px);
}

.cmatic-modal__dialog {
	position: relative;
	max-width: var(--cmatic-modal-max-width);
	width: 90%;
	background: var(--cmatic-modal-bg);
	border-radius: var(--cmatic-modal-border-radius);
	box-shadow: var(--cmatic-modal-shadow);
	margin: 20px auto;
}

.cmatic-modal__header {
	display: flex;
	align-items: center;
	justify-content: space-between;
	padding: 6px 24px;
	border-bottom: 1px solid var(--cmatic-border-color);
}

.cmatic-modal__header h2 {
	margin: 0;
	font-size: 16px;
	font-weight: 600;
	color: var(--cmatic-text-color);
}

.cmatic-modal__header .button {
	margin-left: auto;
	margin-right: 12px;
}

.cmatic-modal__close {
	background: none;
	border: none;
	font-size: 28px;
	line-height: 1;
	color: var(--cmatic-text-light);
	cursor: pointer;
	padding: 0;
	width: 32px;
	height: 32px;
	display: flex;
	align-items: center;
	justify-content: center;
	border-radius: 4px;
	transition: var(--cmatic-transition);
}

.cmatic-modal__close:hover,
.cmatic-modal__close:focus {
	background: var(--cmatic-secondary-color);
	color: var(--cmatic-text-color);
}

.cmatic-modal__body {
	padding: 24px;
	max-height: calc(100vh - 300px);
	overflow-y: auto;
}

.cmatic-modal__body > h3 {
	margin: 0 0 20px;
	font-size: 16px;
	font-weight: 600;
	color: var(--cmatic-text-color);
}

.cmatic-reasons {
	display: flex;
	flex-direction: column;
	gap: 12px;
}

.cmatic-reason-btn {
	background: linear-gradient(135deg, #f5f7fa 0%, #f8f9fa 100%);
	border: 2px solid transparent;
	padding: 16px 20px;
	border-radius: 12px;
	cursor: pointer;
	transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
	font-size: 14px;
	color: var(--cmatic-text-color);
	text-align: left;
	font-family: inherit;
	width: 100%;
	line-height: 1.5;
	position: relative;
	overflow: hidden;
}

.cmatic-reason-btn:hover {
	background: linear-gradient(135deg, #e8f0fe 0%, #f1f8ff 100%);
	border-color: #f0f6fc;
}

.cmatic-reason-btn.selected {
	background: linear-gradient(135deg, #f0f6fc 0%, #f0f6fc 100%);
	border-color: #d8dde3;
	color: var(--cmatic-text-color);
	font-weight: 500;
	box-shadow: 0 6px 16px rgba(26, 115, 232, 0.3);
}

.cmatic-reason-btn:focus-visible {
	outline: 2px solid var(--cmatic-primary-color);
	outline-offset: 2px;
}

.cmatic-input-wrapper {
	margin-top: 12px;
	animation: slideDown 0.3s ease;
}

@keyframes slideDown {
	from { opacity: 0; transform: translateY(-10px); }
	to { opacity: 1; transform: translateY(0); }
}

.cmatic-input-field {
	width: 100%;
	padding: 10px 14px;
	border-radius: 6px;
	font-size: 14px;
	font-family: inherit;
}

.cmatic-input-field:focus {
	outline: none;
}

.cmatic-input-field[type="text"] {
	display: block;
}

select.cmatic-input-field {
	cursor: pointer;
	appearance: none;
	background-color: #fff;
	background-image: url(data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23646970' d='M6 9L1 4h10z'/%3E%3C/svg%3E);
	background-repeat: no-repeat;
	background-position: right 14px center;
	background-size: 12px;
	padding-right: 40px;
}

.cmatic-char-counter {
	margin-top: 4px;
	font-size: 12px;
	color: var(--cmatic-text-light);
	text-align: right;
}

.cmatic-error-message {
	display: none;
	margin-top: 16px;
	padding: 12px;
	background: #fcf0f1;
	border: 1px solid var(--cmatic-error-color);
	border-radius: 4px;
	color: var(--cmatic-error-color);
	font-size: 14px;
}

.cmatic-error-message--visible {
	display: block;
}

.cmatic-modal__footer {
	display: flex;
	align-items: center;
	justify-content: space-between;
	flex-direction: row;
	padding: 20px 24px;
	border-top: 1px solid var(--cmatic-border-color);
	gap: 16px;
}

.cmatic-modal__footer .button {
	margin-left: auto;
}

.cmatic-skip-link {
	color: var(--cmatic-text-light);
	font-size: 13px;
	text-decoration: none;
	cursor: pointer;
	transition: color 0.2s ease;
	padding: 4px 8px;
}

.cmatic-skip-link:hover {
	color: var(--cmatic-text-color);
	text-decoration: underline;
}

@media (max-width: 640px) {
	.cmatic-modal__dialog {
	width: 95%;
	margin: 10px auto;
	}

	.cmatic-modal__header,
	.cmatic-modal__body,
	.cmatic-modal__footer {
	padding: 16px;
	}

	.cmatic-modal__footer {
	flex-direction: column;
	gap: 12px;
	}

	.cmatic-modal__footer .button {
	width: 100%;
	order: 1;
	}

	.cmatic-skip-link {
	order: 2;
	margin-top: 8px;
	}

	.cmatic-reason-btn {
	padding: 14px 16px;
	}

	.cmatic-reasons {
	gap: 10px;
	}
}

.cmatic-modal__footer .button:focus-visible {
	outline: 2px solid var(--cmatic-primary-color);
	outline-offset: 2px;
}

@media (prefers-contrast: high) {
	.cmatic-modal__dialog {
	border: 2px solid currentcolor;
	}
}

@media (prefers-reduced-motion: reduce) {
	* {
	transition: none !important;
	}
}

/* ==========================================================================
   Submission Feedback
   ========================================================================== */

.cmatic-modal__feedback {
	display: flex;
	align-items: flex-start;
	gap: 12px;
	padding: 16px;
	margin-bottom: 16px;
	border-radius: 8px;
	background: #f0f6fc;
	border: 1px solid #c3c4c7;
}

.cmatic-modal__feedback-icon {
	flex-shrink: 0;
	width: 24px;
	height: 24px;
}

.cmatic-modal__feedback-icon .dashicons {
	font-size: 24px;
	width: 24px;
	height: 24px;
}

.cmatic-modal__feedback-content {
	flex: 1;
	min-width: 0;
}

.cmatic-modal__feedback-title {
	font-weight: 600;
	font-size: 14px;
	color: var(--cmatic-text-color);
	margin-bottom: 4px;
}

.cmatic-modal__feedback-details {
	font-size: 13px;
	color: var(--cmatic-text-light);
}

/* Sent vs Received comparison table */
.cmatic-modal__feedback-table {
	width: 100%;
	border-collapse: collapse;
	margin-top: 10px;
	font-size: 13px;
}

.cmatic-modal__feedback-table th,
.cmatic-modal__feedback-table td {
	padding: 6px 10px;
	text-align: left;
	border-bottom: 1px solid #e0e0e0;
}

.cmatic-modal__feedback-table th {
	background: rgba(0, 0, 0, 0.03);
	font-weight: 600;
	font-size: 12px;
	text-transform: uppercase;
	letter-spacing: 0.5px;
	color: var(--cmatic-text-light);
}

.cmatic-modal__feedback-table .field-key {
	font-weight: 600;
	color: var(--cmatic-text-color);
}

.cmatic-modal__feedback-table .field-empty {
	color: #999;
	font-style: italic;
}

.cmatic-modal__feedback-table .field-mismatch {
	background: #fff8e5;
}

.cmatic-modal__feedback-table .field-mismatch td {
	border-bottom-color: #f0c36d;
}

/* Success state */
.cmatic-modal__feedback--success {
	background: #edfaef;
	border-color: var(--cmatic-success-color);
}

.cmatic-modal__feedback--success .cmatic-modal__feedback-icon .dashicons {
	color: var(--cmatic-success-color);
}

.cmatic-modal__feedback--success .cmatic-modal__feedback-title {
	color: #006d1b;
}

/* Error state */
.cmatic-modal__feedback--error {
	background: #fcf0f1;
	border-color: var(--cmatic-error-color);
}

.cmatic-modal__feedback--error .cmatic-modal__feedback-icon .dashicons {
	color: var(--cmatic-error-color);
}

.cmatic-modal__feedback--error .cmatic-modal__feedback-title {
	color: #8a1f1f;
}

/* Skipped state */
.cmatic-modal__feedback--skipped {
	background: #fff8e5;
	border-color: #dba617;
}

.cmatic-modal__feedback--skipped .cmatic-modal__feedback-icon .dashicons {
	color: #996800;
}

.cmatic-modal__feedback--skipped .cmatic-modal__feedback-title {
	color: #614200;
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                              chimpmatic-lite.php                                                                                 0000644                 00000003652 15266052607 0010345 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Plugin Name: Connect Contact Form 7 and Mailchimp
 * Plugin URI: https://chimpmatic.com
 * Description: Connect Contact Form 7 to Mailchimp, Brevo, MailerLite, or Klaviyo with per-form destinations, field mapping, and provider-specific opt-in.
 * Version: 0.9.81.05
 * Author: Renzo Johnson
 * Author URI: https://renzojohnson.com
 * License: GPL v3 or later
 * License URI: https://www.gnu.org/licenses/gpl-3.0.html
 * Text Domain: chimpmatic-lite
 * Domain Path: /languages/
 * Requires at least: 6.4
 * Requires PHP: 7.4
 */

if ( ! defined( 'ABSPATH' ) ) {
	header( 'Status: 403 Forbidden' );
	header( 'HTTP/1.1 403 Forbidden' );
	exit();
}

if ( ! function_exists( 'add_filter' ) ) {
	header( 'Status: 403 Forbidden' );
	header( 'HTTP/1.1 403 Forbidden' );
	exit();
}

defined( 'ABSPATH' ) || exit;

if ( ! defined( 'SPARTAN_MCE_VERSION' ) ) {
	define( 'SPARTAN_MCE_VERSION', '0.9.81.05' );
	define( 'SPARTAN_MCE_RELEASE_TRAIN', '2026.07.18.17' );

	define( 'SPARTAN_MCE_PLUGIN_FILE', __FILE__ );
	define( 'SPARTAN_MCE_PLUGIN_BASENAME', plugin_basename( SPARTAN_MCE_PLUGIN_FILE ) );
	define( 'SPARTAN_MCE_PLUGIN_DIR', plugin_dir_path( SPARTAN_MCE_PLUGIN_FILE ) );
	define( 'SPARTAN_MCE_PLUGIN_URL', plugin_dir_url( SPARTAN_MCE_PLUGIN_FILE ) );

	if ( ! defined( 'CMATIC_LOG_OPTION' ) ) {
		define( 'CMATIC_LOG_OPTION', 'cmatic_log_on' );
	}

	if ( ! defined( 'CMATIC_LITE_FIELDS' ) ) {
		define( 'CMATIC_LITE_FIELDS', 4 );
	}
}


require_once SPARTAN_MCE_PLUGIN_DIR . 'includes/bootstrap.php';

if ( ! function_exists( 'mce_get_cmatic' ) ) {
	// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Legacy public compatibility alias.
	function mce_get_cmatic( $key, $default = null ) {
		return Cmatic_Options_Repository::get_option( $key, $default );
	}
}

if ( ! function_exists( 'cmatic_get_cmatic' ) ) {
	function cmatic_get_cmatic( $key, $default = null ) {
		return mce_get_cmatic( $key, $default );
	}
}
                                                                                      languages/readme.txt                                                                                0000644                 00000000224 15266052610 0010511 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       Translations have moved to
https://translate.wordpress.org/projects/wp-plugins/contact-form-7-mailchimp-extension

Thank you for your contribution.
                                                                                                                                                                                                                                                                                                                                                                            license.txt                                                                                         0000644                 00000044526 15266052610 0006745 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       Chimpmatic Lite
Copyright (C) 2010â€“2025 Chimpmatic <hello@chimpmatic.com>

This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program; if not, see https://www.gnu.org/licenses/gpl-2.0.html

-----------------------------------------------------------------------
GNU GENERAL PUBLIC LICENSE
                           Version 2, June 1991

 Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The licenses for most software are designed to take away your
freedom to share and change it.  By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users.  This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it.  (Some other Free Software Foundation software is covered by
the GNU Library General Public License instead.)  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.

  To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have.  You must make sure that they, too, receive or can get the
source code.  And you must show them these terms so they know their
rights.

  We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.

  Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software.  If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.

  Finally, any free program is threatened constantly by software
patents.  We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary.  To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.

  The precise terms and conditions for copying, distribution and
modification follow.

                    TERMS AND CONDITIONS FOR COPYING,
                  DISTRIBUTION AND MODIFICATION

  0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License.  The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language.  (Hereinafter, translation is included without limitation in
the term "modification".)  Each licensee is addressed as "you".

Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope.  The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.

  1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.

You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.

  2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:

    a) You must cause the modified files to carry prominent notices
    stating that you changed the files and the date of any change.

    b) You must cause any work that you distribute or publish, that in
    whole or in part contains or is derived from the Program or any
    part thereof, to be licensed as a whole at no charge to all third
    parties under the terms of this License.

    c) If the modified program normally reads commands interactively
    when run, you must cause it, when started running for such
    interactive use in the most ordinary way, to print or display an
    announcement including an appropriate copyright notice and a
    notice that there is no warranty (or else, saying that you provide
    a warranty) and that users may redistribute the program under
    these conditions, and telling the user how to view a copy of this
    License.  (Exception: if the Program itself is interactive but
    does not normally print such an announcement, your work based on
    the Program is not required to print an announcement.)

These requirements apply to the modified work as a whole.  If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works.  But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.

Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.

In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.

  3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:

    a) Accompany it with the complete corresponding machine-readable
    source code, which must be distributed under the terms of Sections
    1 and 2 above on a medium customarily used for software interchange; or,

    b) Accompany it with a written offer, valid for at least three
    years, to give any third party, for a charge no more than your
    cost of physically performing source distribution, a complete
    machine-readable copy of the corresponding source code, to be
    distributed under the terms of Sections 1 and 2 above on a medium
    customarily used for software interchange; or,

    c) Accompany it with the information you received as to the offer
    to distribute corresponding source code.  (This alternative is
    allowed only for noncommercial distribution and only if you
    received the program in object code or executable form with such
    an offer, in accord with Subsection b above.)

The source code for a work means the preferred form of the work for
making modifications to it.  For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable.  However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.

If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.

  4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License.  Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.

  5. You are not required to accept this License, since you have not
signed it.  However, nothing else grants you permission to modify or
distribute the Program or its derivative works.  These actions are
prohibited by law if you do not accept this License.  Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.

  6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions.  You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.

  7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all.  For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.

If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.

It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices.  Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.

This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.

  8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded.  In such case, this License incorporates
the limitation as if written in the body of this License.

  9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

Each version is given a distinguishing version number.  If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation.  If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.

  10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission.  For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this.  Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.

                            NO WARRANTY

  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.

  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, see https://www.gnu.org/licenses/gpl-2.0.html

Also add information on how to contact you by electronic and paper mail.

If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:

    Gnomovision version 69, Copyright (C) year name of author
    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.

You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary.  Here is a sample; alter the names:

  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
  `Gnomovision' (which makes passes at compilers) written by James Hacker.

  <signature of Ty Coon>, 1 April 1989
  Ty Coon, President of Vice

This General Public License does not permit incorporating your program into
proprietary programs.  If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library.  If this is what you want to do, use the GNU Library General
Public License instead of this License.
                                                                                                                                                                          readme.txt                                                                                          0000644                 00000027140 15266052610 0006551 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       === Connect Contact Form 7 to Mailchimp, Brevo, MailerLite & Klaviyo ===
Contributors: rnzo, chimpmatic
Donate link: https://chimpmatic.com/pricing
Tags: contact form 7, mailchimp, brevo, mailerlite, klaviyo
Requires at least: 6.4
Tested up to: 7.0
Stable tag: 0.9.81.05
Requires PHP: 7.4
License: GPL v3 or later
License URI: https://www.gnu.org/licenses/gpl-3.0.html

Contact Form 7 integration for Mailchimp, Brevo, MailerLite, and Klaviyo. Send form leads to email marketing lists and audiences.

== Description ==

**ChimpMatic Lite is a Mailchimp integration for Contact Form 7 that also connects forms to Brevo, MailerLite, and Klaviyo.** Send form submissions to email marketing lists, audiences, or groups without replacing Contact Form 7 or adding a separate automation platform.

Choose a provider for each form, connect the account, select a destination, and map Contact Form 7 fields to subscriber fields. Existing Mailchimp forms keep their saved audience, field mappings, credentials, and opt-in settings after upgrading.

= Contact Form 7 Email Marketing Integrations =

Choose one provider independently for each Contact Form 7 form. Each integration sends submitted subscriber data directly to the audience, list, or group selected in that form's settings.

**Mailchimp integration for Contact Form 7** - Connect through one-click OAuth or an API key, choose an audience, map merge fields, and select single or double opt-in. [Read the Mailchimp setup guide](https://chimpmatic.com/integrations/mailchimp/).

**Brevo integration for Contact Form 7** - Connect with a Brevo API key, choose a list, map contact attributes, and select direct subscription or a Brevo confirmation email. [Read the Brevo setup guide](https://chimpmatic.com/integrations/brevo/).

**MailerLite integration for Contact Form 7** - Connect with a MailerLite API token, choose a group, and map subscriber fields. Double opt-in follows the MailerLite account setting. [Read the MailerLite setup guide](https://chimpmatic.com/integrations/mailerlite/).

**Klaviyo integration for Contact Form 7** - Connect with a Klaviyo private API key, choose a list, and map profile properties. Confirmation behavior follows the selected Klaviyo list. [Read the Klaviyo setup guide](https://chimpmatic.com/integrations/klaviyo/).

= Free Features =

* Choose Mailchimp, Brevo, MailerLite, or Klaviyo independently for each Contact Form 7 form.
* Use different provider accounts and destinations on different forms.
* Load audiences, lists, or groups inside the Contact Form 7 editor.
* Map the required email address and up to four additional Contact Form 7 fields.
* Require an opt-in checkbox before subscriber data is sent.
* Control or respect provider-specific confirmation behavior.
* Keep existing Mailchimp configurations working after upgrading.
* Use unlimited Contact Form 7 forms.

= Built for Contact Form 7 =

ChimpMatic adds a focused integration panel to each Contact Form 7 form. It does not replace your forms, introduce another form builder, or require a generic automation service between WordPress and your email provider.

Brevo API keys, MailerLite API tokens, and Klaviyo private API keys are encrypted before storage and are not displayed again after saving. Mailchimp users can avoid storing an API key by using the OAuth connection.

= ChimpMatic Pro =

ChimpMatic Pro is an optional add-on for teams that need advanced provider features, expanded field mapping, advanced consent controls, subscriber tools, and priority support. ChimpMatic Lite remains a complete Contact Form 7 integration on its own.

[Compare ChimpMatic Lite and Pro](https://chimpmatic.com/pro/).

= External Services and Data Disclosure =

ChimpMatic is independently developed and is not affiliated with or endorsed by Mailchimp, Brevo, MailerLite, or Klaviyo. When a configured form is submitted, its email address, mapped fields, destination, and consent choice are sent directly to the provider you selected; an account with that provider is required.

[Read how each external service is used, what data is sent, and review provider terms and privacy policies](https://chimpmatic.com/external-services/).

When the existing **Help Us Improve** setting is enabled, ChimpMatic Lite sends a signed product snapshot to `https://signls.dev/wp-json/chimpmatic/v1/telemetry` to measure setup completion, compatibility, feature use, reliability, upgrades, and long-term adoption. Administrators can turn sharing off or on repeatedly without restriction.

The bounded snapshot contains random pseudonymous site/install/device identities; the current site URL and one snapshot user agent; WordPress, PHP, database, server, theme, Contact Form 7, and ChimpMatic facts; plugin inventory (maximum 500 rows, including name/version/author/active state); 16 fixed competitor indicators; aggregate provider/auth method, destination, mapping, feature, lifecycle, submission, performance, and scrubbed failure facts; and Contact Form 7 configuration labels (maximum 100 forms, 50 detailed forms, and 30 field/mapping rows per detailed form). Totals and truncation flags identify omitted rows.

It does not send contact details, submitted values, credentials, API or license keys, OAuth secrets, cookies, authorization headers, raw provider responses, raw administrator email, commerce records, raw IP addresses, arbitrary settings, request bodies, or file contents. Server address and hostname are SHA-256 hashes; failure samples are scrubbed and limited to 50 characters.

Active installations send at most daily and quiet installations at most weekly, with one coalesced refresh after relevant changes. Delivery receipts are retained for 90 days; current typed facts are replaced by newer observations; daily aggregates and pseudonymous adoption/lifecycle identity are retained for long-term analysis. Turning sharing off clears scheduled delivery, any pending payload, and the local delivery credential immediately without sending an opt-out event.

[Review the complete public field matrix, exact bounds, destination, purpose, exclusions, and retention](https://github.com/signls-dev/signls/blob/main/reference/wordpress-signls-sdk-v2.md). Also review the [ChimpMatic privacy policy](https://chimpmatic.com/privacy).

= Requirements =

1. WordPress 6.4 or higher.
2. Contact Form 7 5.0 or higher.
3. PHP 7.4 or higher.
4. An account with at least one supported email marketing provider.

= Support =

* [Provider integration guides](https://chimpmatic.com/integrations/)
* [Documentation and Help Center](https://chimpmatic.com/help/)
* [Contact the ChimpMatic team](https://chimpmatic.com/contact)

== Installation ==

1. Install and activate Contact Form 7.
2. Install ChimpMatic Lite from the WordPress Plugin Directory and activate it.
3. Edit a Contact Form 7 form and open its **Chimpmatic** tab.
4. Choose Mailchimp, Brevo, MailerLite, or Klaviyo.
5. Connect the provider, select an audience, list, or group, and map the email field plus any additional fields.
6. Configure consent behavior, save the form, and submit a test entry.

= Provider Credentials =

Mailchimp supports OAuth or an API key. Brevo uses an API key, MailerLite uses an API token, and Klaviyo uses a private API key. For current screenshots and exact provider navigation, use the [integration setup guides](https://chimpmatic.com/integrations/).

== Frequently Asked Questions ==

= Which email marketing providers does ChimpMatic Lite support? =

Version 0.9.81.06 supports Mailchimp, Brevo, MailerLite, and Klaviyo. You select one provider independently for each Contact Form 7 form.

= How do I connect Mailchimp to Contact Form 7? =

Open the ChimpMatic tab in the Contact Form 7 editor, select Mailchimp, connect with OAuth or an API key, choose an audience, and map the form's email field. You can then configure consent and single or double opt-in. [Follow the Mailchimp setup guide](https://chimpmatic.com/integrations/mailchimp/).

= How do I connect Brevo to Contact Form 7? =

Open the ChimpMatic tab, select Brevo, enter a Brevo API key, choose a list, and map the form fields to Brevo contact attributes. You can subscribe the contact directly or use a Brevo confirmation email. [Follow the Brevo setup guide](https://chimpmatic.com/integrations/brevo/).

= Can Contact Form 7 send subscribers to MailerLite? =

Yes. Select MailerLite in the ChimpMatic tab, connect with an API token, choose a group, and map the Contact Form 7 email field and subscriber fields. MailerLite applies the account's API double-opt-in setting. [Follow the MailerLite setup guide](https://chimpmatic.com/integrations/mailerlite/).

= Can Contact Form 7 send leads to Klaviyo? =

Yes. Select Klaviyo in the ChimpMatic tab, connect with a private API key, choose a list, and map Contact Form 7 fields to Klaviyo profile properties. Consent behavior follows the selected Klaviyo list. [Follow the Klaviyo setup guide](https://chimpmatic.com/integrations/klaviyo/).

= Will my existing Mailchimp forms keep working? =

Yes. Existing configurations default to Mailchimp and retain their saved audience, field mappings, API credentials, and opt-in behavior.

[Read more about the Contact Form 7 Mailchimp integration](https://chimpmatic.com/integrations/mailchimp/).

= Can different forms use different providers? =

Yes. A newsletter form can send to MailerLite while a lead form sends to Brevo, for example. Every form stores its own provider, credentials, destination, mappings, and opt-in configuration.

= Does ChimpMatic support double opt-in? =

Yes, but the control differs by provider. Mailchimp offers single or double opt-in per form. Brevo can send a confirmation email. MailerLite follows the account's API double-opt-in setting. Klaviyo follows the selected list's consent behavior.

[Read more about provider-specific double opt-in](https://chimpmatic.com/help/double-opt-in/).

= How many fields can I map in Lite? =

Lite maps the required email address plus up to four additional form fields. Compatible ChimpMatic Pro features can expand the mapping limit.

[Read more about Contact Form 7 field mapping](https://chimpmatic.com/help/field-mapping/).

= Where do I get help? =

Start with the [provider integration guides](https://chimpmatic.com/integrations/), visit the [Help Center](https://chimpmatic.com/help/), or [contact the ChimpMatic team](https://chimpmatic.com/contact).

== Screenshots ==

1. Choose Mailchimp, Brevo, MailerLite, or Klaviyo inside a Contact Form 7 form.
2. Select an audience, list, or group and map Contact Form 7 fields to subscriber fields.

== Upgrade Notice ==

= 0.9.81.06 =
Expands the bounded Help Us Improve snapshot and its public disclosure while preserving the established setting state and unrestricted opt-out and re-enable controls.

= 0.9.81.05 =
Managed-network build: initializes Help Us Improve as enabled while preserving unrestricted administrator opt-out and re-enable controls.

= 0.9.81.04 =
Adds optional product-insights sharing that remains off until an administrator enables it. Existing provider integrations are unchanged.

= 0.9.81.03 =
Improved MailerLite support and an updated listing. Existing Mailchimp configurations remain compatible.

= 0.9.78.07 =
Security release: fixes a stored cross-site scripting vulnerability in the Contact Lookup tool (CVE-2026-15000). All users should update.

== Changelog ==

= 0.9.81.06 =

[Version 0.9.81.06 release notes](https://chimpmatic.com/changelog#0.9.81.06)

= 0.9.81.05 =

[Version 0.9.81.05 release notes](https://chimpmatic.com/changelog#0.9.81.05)

= 0.9.81.04 =

[Version 0.9.81.04 release notes](https://chimpmatic.com/changelog#0.9.81.04)

= 0.9.81.03 =

[Version 0.9.81.03 release notes](https://chimpmatic.com/changelog#0.9.81.03)

[View the complete ChimpMatic changelog](https://chimpmatic.com/changelog)
                                                                                                                                                                                                                                                                                                                                                                                                                                includes/auth/class-cmatic-lite-auth-manager.php                                                    0000644                 00000017041 15266052610 0015677 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * OAuth credential 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_Lite_Auth_Manager {

	const OPTION_PREFIX = 'cmatic_auth_';
	const OAUTH_GATEWAY = 'https://api.chimpmatic.com';
	const CIPHER        = 'aes-256-cbc';

	/**
	 * Save encrypted OAuth credentials.
	 *
	 * @param int    $form_id Form ID.
	 * @param string $api_key Plain Mailchimp API key from gateway.
	 * @return true|WP_Error
	 */
	public function save_oauth_credentials( $form_id, $api_key ) {
		$encrypted = $this->encrypt( $api_key );
		if ( ! $encrypted ) {
			return new WP_Error( 'encryption_failed', 'Could not encrypt OAuth credentials' );
		}

		update_option( self::OPTION_PREFIX . $form_id, $encrypted, false );

		$option_name = 'cf7_mch_' . $form_id;
		$config      = get_option( $option_name, array() );
		if ( ! is_array( $config ) ) {
			$config = array();
		}

		if ( ! empty( $config['api'] ) ) {
			$config['api_key_backup'] = $config['api'];
		}

		$config['api-validation'] = 1;
		$config['auth_type']      = 'oauth';
		$config['api']            = '';

		$config['lisdata'] = array();
		$config['list']    = '';

		update_option( $option_name, $config );

		return true;
	}

	/**
	 * Get decrypted credentials for a form.
	 *
	 * @param int $form_id Form ID.
	 * @return Cmatic_Lite_Credentials|null
	 */
	public function get_credentials( $form_id ) {
		$encrypted = get_option( self::OPTION_PREFIX . $form_id );
		if ( empty( $encrypted ) ) {
			return null;
		}

		$api_key = $this->decrypt( $encrypted );
		if ( ! $api_key ) {
			return null;
		}

		return Cmatic_Lite_Credentials::from_oauth( $api_key );
	}

	/**
	 * Check if a form has OAuth credentials stored.
	 *
	 * @param int $form_id Form ID.
	 * @return bool
	 */
	public function has_oauth( $form_id ) {
		return false !== get_option( self::OPTION_PREFIX . $form_id, false );
	}

	/**
	 * Disconnect OAuth for a form. Restores backup API key if available.
	 *
	 * @param int $form_id Form ID.
	 */
	public function disconnect( $form_id ) {
		delete_option( self::OPTION_PREFIX . $form_id );

		$option_name = 'cf7_mch_' . $form_id;
		$config      = get_option( $option_name, array() );
		if ( ! is_array( $config ) ) {
			$config = array();
		}

		if ( ! empty( $config['api_key_backup'] ) ) {
			$creds = Cmatic_Lite_Credentials::from_api_key( $config['api_key_backup'] );
			if ( $creds ) {
				$config['api']            = $config['api_key_backup'];
				$config['api-validation'] = 1;
			}
		}

		if ( empty( $config['api'] ) ) {
			$config['api-validation'] = 0;
		}
		unset( $config['auth_type'] );
		unset( $config['api_key_backup'] );
		unset( $config['oauth_account_name'] );
		unset( $config['oauth_connected_by'] );
		unset( $config['oauth_connected_date'] );

		$config['lisdata'] = array();
		$config['list']    = '';

		update_option( $option_name, $config );
	}

	/**
	 * Resolve the credential for a form.
	 *
	 * Precedence: explicit key, then the stored manual key, then the OAuth
	 * envelope. Every OAuth connect clears cf7_mch['api'], so a non-empty
	 * manual key always postdates the last connect and must win.
	 *
	 * @param int    $form_id     Form ID.
	 * @param string $fallback    Optional explicit API key (from REST param).
	 * @param array  $cf7_mch     Optional pre-loaded config (avoids duplicate get_option).
	 * @return string API key or empty string.
	 */
	public function resolve_api_key( $form_id, $fallback = '', $cf7_mch = null ) {
		if ( ! empty( $fallback ) ) {
			return $fallback;
		}

		if ( null === $cf7_mch ) {
			$cf7_mch = get_option( 'cf7_mch_' . $form_id, array() );
		}

		if ( ! empty( $cf7_mch['api'] ) ) {
			return $cf7_mch['api'];
		}

		$credentials = $this->get_credentials( $form_id );
		if ( $credentials ) {
			return $credentials->get_api_key();
		}

		return '';
	}

	public static function restore_all_api_keys() {
		global $wpdb;

		$options = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
				$wpdb->esc_like( self::OPTION_PREFIX ) . '%'
			)
		);

		if ( empty( $options ) ) {
			return;
		}

		foreach ( $options as $option ) {
			$form_id = str_replace( self::OPTION_PREFIX, '', $option->option_name );

			if ( ! is_numeric( $form_id ) ) {
				continue;
			}

			$form_id = (int) $form_id;
			$config  = get_option( 'cf7_mch_' . $form_id, array() );

			if ( ! empty( $config['api_key_backup'] ) ) {
				$creds = Cmatic_Lite_Credentials::from_api_key( $config['api_key_backup'] );
				if ( $creds ) {
					$config['api']            = $config['api_key_backup'];
					$config['api-validation'] = 1;
				} else {
					$config['api-validation'] = 0;
				}
			} else {
				$config['api-validation'] = 0;
			}

			unset( $config['auth_type'] );
			unset( $config['api_key_backup'] );
			unset( $config['oauth_account_name'] );
			unset( $config['oauth_connected_by'] );
			unset( $config['oauth_connected_date'] );

			update_option( 'cf7_mch_' . $form_id, $config );
			delete_option( self::OPTION_PREFIX . $form_id );
		}
	}

	private function derive_keys() {
		$master  = wp_salt( 'auth' );
		$enc_key = hash_hkdf( 'sha256', $master, 32, 'cmatic-encrypt' );
		$mac_key = hash_hkdf( 'sha256', $master, 32, 'cmatic-mac' );
		return array( $enc_key, $mac_key );
	}

	private function derive_legacy_key() {
		return hash( 'sha256', wp_salt( 'auth' ), true );
	}

	private function encrypt( $plaintext ) {
		list( $enc_key, $mac_key ) = $this->derive_keys();
		$iv                        = openssl_random_pseudo_bytes( openssl_cipher_iv_length( self::CIPHER ) );

		$ciphertext = openssl_encrypt( $plaintext, self::CIPHER, $enc_key, OPENSSL_RAW_DATA, $iv );
		if ( false === $ciphertext ) {
			return false;
		}

		$hmac = hash_hmac( 'sha256', $iv . $ciphertext, $mac_key, true );

		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Binary authenticated ciphertext transport encoding.
		return base64_encode( "\x02" . $hmac . $iv . $ciphertext );
	}

	private function decrypt( $encoded ) {
		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Binary authenticated ciphertext transport decoding.
		$data = base64_decode( $encoded, true );
		if ( false === $data || strlen( $data ) < 48 ) {
			return false;
		}

		if ( strlen( $data ) >= 49 && "\x02" === $data[0] ) {
			$result = $this->decrypt_with_keys(
				substr( $data, 1 ),
				$this->derive_keys()
			);
			if ( false !== $result ) {
				return $result;
			}
		}

		$legacy_key = $this->derive_legacy_key();
		return $this->decrypt_with_keys( $data, array( $legacy_key, $legacy_key ) );
	}

	/**
	 * Decrypt a raw blob (no version byte) with the given key pair.
	 *
	 * @param string $data    Raw binary: HMAC(32) + IV(16) + ciphertext.
	 * @param array  $keys    [ $enc_key, $mac_key ].
	 * @return string|false   Plaintext on success, false on failure.
	 */
	private function decrypt_with_keys( $data, array $keys ) {
		if ( strlen( $data ) < 48 ) {
			return false;
		}

		list( $enc_key, $mac_key ) = $keys;

		$hmac       = substr( $data, 0, 32 );
		$iv_length  = openssl_cipher_iv_length( self::CIPHER );
		$iv         = substr( $data, 32, $iv_length );
		$ciphertext = substr( $data, 32 + $iv_length );

		$calc_hmac = hash_hmac( 'sha256', $iv . $ciphertext, $mac_key, true );
		if ( ! hash_equals( $hmac, $calc_hmac ) ) {
			return false;
		}

		$plaintext = openssl_decrypt( $ciphertext, self::CIPHER, $enc_key, OPENSSL_RAW_DATA, $iv );
		if ( false === $plaintext ) {
			return false;
		}
		return $plaintext;
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               includes/auth/class-cmatic-lite-credentials.php                                                     0000644                 00000003027 15266052610 0015622 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Credential value object.
 *
 * @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_Credentials {

	private $api_key;
	private $datacenter;
	private $source;

		const DC_PATTERN = '/^[a-z]{2}\d{1,3}$/';

	private function __construct( $api_key, $datacenter, $source ) {
		$this->api_key    = $api_key;
		$this->datacenter = $datacenter;
		$this->source     = $source;
	}

	public static function from_api_key( $api_key ) {
		if ( empty( $api_key ) || substr_count( $api_key, '-' ) !== 1 ) {
			return null;
		}
		$parts = explode( '-', $api_key );
		if ( empty( $parts[0] ) || empty( $parts[1] ) ) {
			return null;
		}
		if ( ! preg_match( self::DC_PATTERN, $parts[1] ) ) {
			return null;
		}
		return new self( $api_key, $parts[1], 'api_key' );
	}

	public static function from_oauth( $api_key ) {
		if ( empty( $api_key ) || substr_count( $api_key, '-' ) !== 1 ) {
			return null;
		}
		$parts = explode( '-', $api_key );
		if ( empty( $parts[0] ) || empty( $parts[1] ) ) {
			return null;
		}
		if ( ! preg_match( self::DC_PATTERN, $parts[1] ) ) {
			return null;
		}
		return new self( $api_key, $parts[1], 'oauth' );
	}

	public function get_api_key() {
		return $this->api_key;
	}

	public function get_datacenter() {
		return $this->datacenter;
	}

	public function get_source() {
		return $this->source;
	}

	public function is_oauth() {
		return 'oauth' === $this->source;
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         includes/services/class-cmatic-lite-service-context.php                                             0000644                 00000010174 15266052610 0017332 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Request service context.
 *
 * @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_Service_Context {

	private const PRODUCT = 'contact-form-7-mailchimp-extension';

	private const TRIGGERS = array( 'promo_pricing_refresh', 'oauth_admin_connect' );

	public static function payload( string $trigger ): array {
		if ( ! in_array( $trigger, self::TRIGGERS, true )
			|| ! class_exists( 'Cmatic_Lite_Signls_Privacy' )
			|| 'enabled' !== Cmatic_Lite_Signls_Privacy::consent_status()
			|| ! class_exists( 'Signls_Sdk_Loader' )
			|| ! class_exists( 'Signls\Sdk\V1\Runtime' )
			|| ! class_exists( 'Signls\Sdk\V1\SiteIdentity' )
			|| ! defined( 'SPARTAN_MCE_VERSION' ) ) {
			return array();
		}

		try {
			$state = \Signls\Sdk\V1\Runtime::state( self::PRODUCT );
			if ( 'enabled' !== ( $state['consent_status'] ?? '' ) ) {
				return array();
			}

			$last_acknowledged = self::state_uint( $state, 'last_acknowledged_at', time() + 300 );
			$retry_attempt     = self::state_uint( $state, 'retry_attempt', 1000000 );
			$sdk_version       = Signls_Sdk_Loader::selected_version();
			$plugin_version    = (string) SPARTAN_MCE_VERSION;
			if ( null === $last_acknowledged
				|| null === $retry_attempt
				|| ! is_string( $sdk_version )
				|| ! self::version_token( $plugin_version, 64 )
				|| ! self::version_token( $sdk_version, 32 ) ) {
				return array();
			}

			$install_id = ( new Cmatic_Install_Data( Cmatic_Options_Repository::instance() ) )->get_install_id();
			$site_id    = ( new \Signls\Sdk\V1\SiteIdentity() )->site_id();
			if ( 1 !== preg_match( '/^(?:[a-f0-9]{12}|[a-f0-9]{32})$/', $install_id )
				|| 1 !== preg_match( '/^[a-f0-9]{32}$/', $site_id ) ) {
				return array();
			}

			$heartbeat_state = $retry_attempt > 0 ? 'retrying' : ( $last_acknowledged > 0 ? 'healthy' : 'never' );

			return array(
				'context_version'                => 1,
				'install_id'                     => $install_id,
				'site_id'                        => $site_id,
				'product_slug'                   => self::PRODUCT,
				'edition'                        => 'lite',
				'plugin_version'                 => $plugin_version,
				'sdk_version'                    => $sdk_version,
				'heartbeat_state'                => $heartbeat_state,
				'heartbeat_last_acknowledged_at' => $last_acknowledged,
				'heartbeat_retry_attempt'        => $retry_attempt,
				'trigger'                        => $trigger,
			);
		} catch ( \Throwable $error ) {
			return array();
		}
	}

	public static function headers( string $trigger ): array {
		$payload = self::payload( $trigger );
		if ( empty( $payload ) ) {
			return array();
		}

		return array(
			'X-Chimpmatic-Context-Version'                => (string) $payload['context_version'],
			'X-Chimpmatic-Install-Id'                     => $payload['install_id'],
			'X-Chimpmatic-Site-Id'                        => $payload['site_id'],
			'X-Chimpmatic-Product-Slug'                   => $payload['product_slug'],
			'X-Chimpmatic-Edition'                        => $payload['edition'],
			'X-Chimpmatic-Plugin-Version'                 => $payload['plugin_version'],
			'X-Chimpmatic-Sdk-Version'                    => $payload['sdk_version'],
			'X-Chimpmatic-Heartbeat-State'                => $payload['heartbeat_state'],
			'X-Chimpmatic-Heartbeat-Last-Acknowledged-At' => (string) $payload['heartbeat_last_acknowledged_at'],
			'X-Chimpmatic-Heartbeat-Retry-Attempt'        => (string) $payload['heartbeat_retry_attempt'],
			'X-Chimpmatic-Trigger'                        => $payload['trigger'],
		);
	}

	private static function state_uint( array $state, string $key, int $maximum ): ?int {
		if ( ! array_key_exists( $key, $state ) ) {
			return 0;
		}
		$value = $state[ $key ];
		return is_int( $value ) && $value >= 0 && $value <= $maximum ? $value : null;
	}

	private static function version_token( string $value, int $maximum ): bool {
		return strlen( $value ) <= $maximum && 1 === preg_match( '/^[0-9A-Za-z][0-9A-Za-z._+-]*$/', $value );
	}

	private function __construct() {}
}
                                                                                                                                                                                                                                                                                                                                                                                                    includes/services/class-cmatic-api-key-importer.php                                                 0000644                 00000002630 15266052611 0016452 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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;
	}
}
                                                                                                        includes/services/submission/class-cmatic-status-resolver.php                                       0000644                 00000002462 15266052611 0020634 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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 {
		$consent_required = 'required' === (string) ( $cf7_mch['consent_gate'] ?? '' );
		$consent_field    = $consent_required
			? (string) ( $cf7_mch['consent_field'] ?? '' )
			: (string) ( $cf7_mch['accept'] ?? '' );

		if ( '' !== trim( $consent_field ) ) {
			$acceptance = Cmatic_Email_Extractor::replace_tags( $consent_field, $posted_data );

			if ( empty( $acceptance ) ) {
				if ( ! $consent_required && ! 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;
			}
		}

		if ( 'double' === (string) ( $cf7_mch['subscription_mode'] ?? '' ) ) {
			return 'pending';
		}

		if ( ! empty( $cf7_mch['double_optin'] ) || ! empty( $cf7_mch['confsubs'] ) ) {
			return 'pending';
		}

		return 'subscribed';
	}
}
                                                                                                                                                                                                              includes/services/submission/class-cmatic-consent-decision.php                                      0000644                 00000003224 15266052611 0020713 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * MailerLite consent decision value object factory.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

final class Cmatic_Consent_Decision {
	/**
	 * Produces the consent decision consumed by the pipeline.
	 *
	 * @param array $settings    Saved provider settings.
	 * @param array $posted_data Submitted Contact Form 7 data.
	 * @param array $form_tags   Current normalized form tags.
	 */
	public static function resolve( array $settings, array $posted_data, array $form_tags ): array {
		$required = 'required' === (string) ( $settings['consent_gate'] ?? '' );
		$field    = $required ? (string) ( $settings['consent_field'] ?? '' ) : (string) ( $settings['accept'] ?? '' );
		$field    = trim( $field, '[]' );
		$tag      = self::find_tag( $field, $form_tags );
		$accepted = '' !== $field && ! empty( $posted_data[ $field ] );
		$eligible = $required && null !== $tag && 'acceptance' === (string) ( $tag['basetype'] ?? '' ) && ! empty( $tag['required'] ) && empty( $tag['inverted'] );
		$allowed  = ! $required || $accepted;

		return array(
			'configured' => '' !== $field,
			'required'   => $required,
			'accepted'   => $accepted,
			'eligible'   => $eligible,
			'allowed'    => $allowed,
			'field'      => $field,
			'reason'     => $allowed ? '' : 'acceptance_not_checked',
		);
	}

	private static function find_tag( string $field, array $form_tags ): ?array {
		foreach ( $form_tags as $tag ) {
			if ( is_array( $tag ) && isset( $tag['name'] ) && is_scalar( $tag['name'] ) && (string) $tag['name'] === $field ) {
				return $tag;
			}
		}
		return null;
	}

	private function __construct() {}
}
                                                                                                                                                                                                                                                                                                                                                                            includes/services/submission/class-cmatic-mailerlite-routing-resolver.php                           0000644                 00000007243 15266052611 0023127 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Deterministic MailerLite group routing.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

final class Cmatic_Mailerlite_Routing_Resolver {
	public static function is_premium_configured( array $settings ): bool {
		return count( self::base_groups( $settings ) ) > 1 || ! empty( $settings['routing_rules'] );
	}

	public static function base_groups( array $settings ): array {
		if ( isset( $settings['base_groups'] ) && is_array( $settings['base_groups'] ) ) {
			return self::unique_strings( $settings['base_groups'] );
		}

		$list = $settings['list'] ?? '';
		if ( is_array( $list ) ) {
			$list = reset( $list );
		}

		return ! is_scalar( $list ) || '' === (string) $list ? array() : array( (string) $list );
	}

	/**
	 * Resolves exact submitted values against current static choices.
	 *
	 * @param array $settings    Effective provider settings.
	 * @param array $posted_data Submitted Contact Form 7 data.
	 * @param array $form_tags   Current normalized form tags.
	 */
	public static function resolve( array $settings, array $posted_data, array $form_tags ): array {
		$groups  = self::base_groups( $settings );
		$choices = self::choice_index( $form_tags );
		$matched = array();
		$rules   = isset( $settings['routing_rules'] ) && is_array( $settings['routing_rules'] ) ? $settings['routing_rules'] : array();

		if ( empty( $groups ) ) {
			return self::failure( 'routing_missing_base_group' );
		}

		foreach ( $rules as $rule ) {
			if ( ! is_array( $rule ) ) {
				return self::failure( 'routing_stale_rule' );
			}
			$field = self::scalar_string( $rule['field'] ?? '' );
			$value = self::scalar_string( $rule['value'] ?? '' );
			if ( ! isset( $choices[ $field ] ) || ! in_array( $value, $choices[ $field ], true ) ) {
				return self::failure( 'routing_stale_rule' );
			}

			$submitted = isset( $posted_data[ $field ] ) ? (array) $posted_data[ $field ] : array();
			$submitted = self::unique_strings( $submitted );
			if ( in_array( $value, $submitted, true ) ) {
				$group_id = self::scalar_string( $rule['group_id'] ?? '' );
				if ( '' === $group_id ) {
					return self::failure( 'routing_stale_rule' );
				}
				$groups[]  = $group_id;
				$matched[] = self::scalar_string( $rule['id'] ?? '' );
			}
		}

		return array(
			'success'       => true,
			'reason'        => '',
			'groups'        => self::unique_strings( $groups ),
			'matched_rules' => self::unique_strings( $matched ),
		);
	}

	private static function choice_index( array $form_tags ): array {
		$index = array();
		foreach ( $form_tags as $tag ) {
			if ( ! is_array( $tag ) || empty( $tag['routing_eligible'] ) || empty( $tag['name'] ) ) {
				continue;
			}
			$values = array();
			foreach ( (array) ( $tag['choices'] ?? array() ) as $choice ) {
				if ( is_array( $choice ) && isset( $choice['value'] ) && is_scalar( $choice['value'] ) ) {
					$values[] = (string) $choice['value'];
				}
			}
			if ( is_scalar( $tag['name'] ) ) {
				$index[ (string) $tag['name'] ] = self::unique_strings( $values );
			}
		}
		return $index;
	}

	private static function unique_strings( array $values ): array {
		$result = array();
		foreach ( $values as $value ) {
			if ( is_scalar( $value ) && '' !== (string) $value ) {
				$result[] = (string) $value;
			}
		}
		return array_values( array_unique( $result ) );
	}

	private static function scalar_string( $value ): string {
		return is_scalar( $value ) ? (string) $value : '';
	}

	private static function failure( string $reason ): array {
		return array(
			'success'       => false,
			'reason'        => $reason,
			'groups'        => array(),
			'matched_rules' => array(),
		);
	}

	private function __construct() {}
}
                                                                                                                                                                                                                                                                                                                                                             includes/services/submission/class-cmatic-mailerlite-submission-pipeline.php                        0000644                 00000012115 15266052612 0023572 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * MailerLite submission application service.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

final class Cmatic_Mailerlite_Submission_Pipeline {
	/**
	 * MailerLite provider adapter.
	 *
	 * @var Cmatic_Lite_Esp_Provider_Interface
	 */
	private Cmatic_Lite_Esp_Provider_Interface $provider;

	public function __construct( Cmatic_Lite_Esp_Provider_Interface $provider ) {
		$this->provider = $provider;
	}

	/**
	 * Validates, routes, normalizes, and submits one CF7 payload.
	 *
	 * @param int                $form_id     Contact Form 7 form ID.
	 * @param array              $settings    Saved provider settings.
	 * @param array              $posted_data Submitted Contact Form 7 data.
	 * @param array              $form_tags   Current normalized form tags.
	 * @param string             $api_key     Provider credential.
	 * @param int                $field_limit Effective mapping limit.
	 * @param Cmatic_File_Logger $logger      Operational logger.
	 */
	public function process( int $form_id, array $settings, array $posted_data, array $form_tags, string $api_key, int $field_limit, Cmatic_File_Logger $logger ): void {
		$entitlements = array(
			'mailerlite_routing'          => Cmatic_Lite_Esp_Capabilities::feature_enabled( 'mailerlite_routing', 'mailerlite', $form_id ),
			'mailerlite_status'           => Cmatic_Lite_Esp_Capabilities::feature_enabled( 'mailerlite_status', 'mailerlite', $form_id ),
			'mailerlite_resubscribe'      => Cmatic_Lite_Esp_Capabilities::feature_enabled( 'mailerlite_resubscribe', 'mailerlite', $form_id ),
			'mailerlite_consent_metadata' => Cmatic_Lite_Esp_Capabilities::feature_enabled( 'mailerlite_consent_metadata', 'mailerlite', $form_id ),
		);
		$policy       = Cmatic_Mailerlite_Runtime_Policy::apply( $settings, $entitlements );
		$effective    = $policy['effective_settings'];
		$email        = Cmatic_Email_Extractor::extract( $effective, $posted_data );
		if ( ! is_email( $email ) ) {
			Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::failure( 'invalid_email', '', $email ) );
			return;
		}

		$decision = Cmatic_Consent_Decision::resolve( $effective, $posted_data, $form_tags );
		if ( ! $decision['allowed'] ) {
			$logger->log( 'INFO', 'Subscription skipped: required acceptance was not checked.' );
			Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::skipped( 'acceptance_not_checked' ) );
			return;
		}

		$routing = Cmatic_Mailerlite_Routing_Resolver::resolve( $effective, $posted_data, $form_tags );
		if ( ! $routing['success'] ) {
			Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::failure( (string) $routing['reason'], '', $email ) );
			return;
		}

		$status_mode = self::status_mode( $effective );
		if ( ! empty( $effective['resubscribe_force'] ) && ( 'active' !== $status_mode || ! $decision['eligible'] || ! $decision['accepted'] ) ) {
			Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::failure( 'resubscribe_requires_consent', '', $email ) );
			return;
		}

		$merge_vars = Cmatic_Merge_Vars_Builder::build( $effective, $posted_data, $field_limit );
		$merge_vars = Cmatic_Mailerlite_Field_Normalizer::apply_boolean_mappings( $merge_vars, $effective, $posted_data, $field_limit );
		$normalized = Cmatic_Mailerlite_Field_Normalizer::normalize( $merge_vars, (array) ( $effective['merge_fields']['merge_fields'] ?? $effective['merge_fields'] ?? array() ) );
		if ( ! $normalized['success'] ) {
			Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::failure( 'provider_field_type_error', '', $email ) );
			return;
		}

		$options = array(
			'groups'      => $routing['groups'],
			'status_mode' => $status_mode,
		);
		if ( ! empty( $effective['resubscribe_force'] ) ) {
			$options['resubscribe'] = true;
		}
		if ( ! empty( $effective['consent_metadata_enabled'] ) ) {
			if ( ! $decision['eligible'] || ! $decision['accepted'] ) {
				Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::failure( 'consent_required', '', $email ) );
				return;
			}
			$ip = Cmatic_Request_Ip::get();
			if ( '' === $ip ) {
				Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::failure( 'consent_ip_unavailable', '', $email ) );
				return;
			}
			$options['consent_metadata'] = array(
				'ip_address'  => $ip,
				'optin_ip'    => $ip,
				'opted_in_at' => gmdate( 'Y-m-d H:i:s' ),
			);
		}

		if ( $policy['degraded'] ) {
			Cmatic_Mailerlite_Degradation_Reporter::record_submission( $form_id, $policy['suppressed_features'], $policy['suppressed_rule_ids'] );
		}

		$groups  = $routing['groups'];
		$list_id = (string) reset( $groups );
		$status  = 'unconfirmed' === $status_mode ? 'pending' : 'subscribed';
		$this->provider->subscribe( $api_key, $list_id, $email, $status, $normalized['fields'], $form_id, $options, $logger );
	}

	private static function status_mode( array $settings ): string {
		$mode = (string) ( $settings['status_mode'] ?? 'legacy_provider_managed' );
		return in_array( $mode, array( 'legacy_provider_managed', 'account', 'active', 'unconfirmed' ), true ) ? $mode : 'legacy_provider_managed';
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                   includes/services/submission/class-cmatic-mailerlite-field-normalizer.php                           0000644                 00000006513 15266052612 0023044 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * MailerLite typed field normalization.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

final class Cmatic_Mailerlite_Field_Normalizer {
	/**
	 * Adds explicit Boolean values for mapped Contact Form 7 acceptance tags.
	 *
	 * @param array $fields      Already-built MailerLite fields.
	 * @param array $settings    Effective provider settings.
	 * @param array $posted_data Raw Contact Form 7 submission data.
	 * @param int   $field_limit Effective mapping limit.
	 */
	public static function apply_boolean_mappings( array $fields, array $settings, array $posted_data, int $field_limit ): array {
		foreach ( array_slice( (array) ( $settings['merge_fields'] ?? array() ), 0, $field_limit ) as $offset => $definition ) {
			if ( ! is_array( $definition ) || 'boolean' !== sanitize_key( self::scalar_string( $definition['type'] ?? '' ) ) ) {
				continue;
			}
			$remote_tag = sanitize_text_field( self::scalar_string( $definition['tag'] ?? '' ) );
			$mapping    = self::scalar_string( $settings[ 'field' . ( $offset + 3 ) ] ?? '' );
			if ( '' === $remote_tag || 1 !== preg_match( '/^\[([A-Za-z0-9_-]+)\]$/', $mapping, $matches ) ) {
				continue;
			}
			$raw                   = $posted_data[ $matches[1] ] ?? null;
			$fields[ $remote_tag ] = is_array( $raw ) ? ! empty( $raw ) : null !== $raw && '' !== (string) $raw;
		}

		return $fields;
	}

	/**
	 * Returns normalized fields and any invalid field keys.
	 *
	 * @param array $fields      Mapped submission values.
	 * @param array $definitions MailerLite field definitions.
	 */
	public static function normalize( array $fields, array $definitions ): array {
		$types  = array();
		$result = array();
		$errors = array();
		foreach ( $definitions as $definition ) {
			if ( is_array( $definition ) && isset( $definition['tag'] ) && is_scalar( $definition['tag'] ) ) {
				$types[ (string) $definition['tag'] ] = sanitize_key( self::scalar_string( $definition['type'] ?? 'text' ) );
			}
		}

		foreach ( $fields as $key => $raw ) {
			$type = $types[ (string) $key ] ?? 'text';
			if ( 'boolean' === $type ) {
				$result[ (string) $key ] = is_bool( $raw ) ? $raw : '' !== trim( is_array( $raw ) ? implode( '', self::scalar_strings( $raw ) ) : self::scalar_string( $raw ) );
				continue;
			}
			$value = is_array( $raw ) ? implode( ', ', self::scalar_strings( $raw ) ) : self::scalar_string( $raw );
			$valid = true;
			if ( 'date' === $type ) {
				$date  = DateTimeImmutable::createFromFormat( '!Y-m-d', $value );
				$valid = 1 === preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) && false !== $date && $date->format( 'Y-m-d' ) === $value;
			} elseif ( 'number' === $type ) {
				$valid = 1 === preg_match( '/^-?(?:\d+|\d*\.\d+)$/', $value );
			}
			if ( ! $valid ) {
				$errors[] = (string) $key;
				continue;
			}
			$result[ (string) $key ] = sanitize_text_field( $value );
		}

		return array(
			'success' => empty( $errors ),
			'fields'  => $result,
			'errors'  => $errors,
		);
	}

	private static function scalar_string( $value ): string {
		return is_scalar( $value ) ? (string) $value : '';
	}

	private static function scalar_strings( array $values ): array {
		$result = array();
		foreach ( $values as $value ) {
			if ( is_scalar( $value ) ) {
				$result[] = (string) $value;
			}
		}
		return $result;
	}

	private function __construct() {}
}
                                                                                                                                                                                     includes/services/submission/class-cmatic-merge-vars-builder.php                                    0000644                 00000003071 15266052613 0021145 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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, int $field_limit = CMATIC_LITE_FIELDS ): array {
		$merge_vars  = array();
		$field_limit = max( 1, min( 50, $field_limit ) );

		if ( empty( $cf7_mch['merge_fields'] ) || ! is_array( $cf7_mch['merge_fields'] ) ) {
			return $merge_vars;
		}

		$field_index = 3;
		$max_index   = $field_limit + 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 ] = self::sanitize_value( $value );
				}
			}

			++$field_index;
			if ( $field_index > $max_index ) {
				break;
			}
		}

		return self::filter_empty( $merge_vars );
	}

	private static function sanitize_value( $value ) {
		if ( is_array( $value ) ) {
			return array_map( array( __CLASS__, 'sanitize_value' ), $value );
		}
		return sanitize_text_field( (string) $value );
	}

	private static function filter_empty( array $merge_vars ): array {
		return array_filter(
			$merge_vars,
			function ( $value ) {
				return ! empty( $value ) || 0 === $value || '0' === $value;
			}
		);
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                       includes/services/submission/class-cmatic-mailchimp-subscriber.php                                  0000644                 00000003556 15266052613 0021565 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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 ) ); // nosemgrep
	}
}
                                                                                                                                                  includes/services/submission/class-cmatic-response-handler.php                                      0000644                 00000016445 15266052613 0020733 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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 {
		if ( false === $response[0] ) {
			self::record_outcome( $form_id, false, 'transport_connect', isset( $response[1] ) ? $response[1] : '', 'remote_rejected' );
			$logger->log( 'ERROR', 'Network request failed.', array( 'response' => $response[1] ) );
			Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::failure( 'network_error', '', $email ) );
			return;
		}

		if ( empty( $api_data ) ) {
			self::record_outcome( $form_id, false, 'remote_rejected', 'Empty provider response.', 'remote_rejected' );
			$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;
		}

		if ( ! empty( $api_data['errors'] ) ) {
			self::record_outcome( $form_id, false, self::api_failure_class( $api_data ), $api_data, 'remote_rejected' );
			self::log_api_errors( $api_data['errors'] );
			Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::parse_api_error( $api_data, $email ) );
			return;
		}

		if ( isset( $api_data['status'] ) && is_int( $api_data['status'] ) && $api_data['status'] >= 400 ) {
			self::record_outcome( $form_id, false, self::api_failure_class( $api_data ), $api_data, 'remote_rejected' );
			Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::parse_api_error( $api_data, $email ) );
			return;
		}

		if ( isset( $api_data['title'] ) && stripos( $api_data['title'], 'error' ) !== false ) {
			self::record_outcome( $form_id, false, self::api_failure_class( $api_data ), $api_data, 'remote_rejected' );
			Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::parse_api_error( $api_data, $email ) );
			return;
		}

		self::handle_success( $email, $status, $merge_vars, $form_id, $api_data );
	}

	/**
	 * Convert a provider-neutral result into the released feedback/bookkeeping path.
	 *
	 * @param array<string,mixed> $result     Normalized provider result.
	 * @param string              $email      Submitted email.
	 * @param string              $status     Resolved subscription status.
	 * @param array<string,mixed> $merge_vars Submitted provider fields.
	 * @param int                 $form_id    Contact form ID.
	 */
	public static function handle_provider_result( array $result, string $email, string $status, array $merge_vars, int $form_id ): void {
		if ( ! $result['success'] ) {
			if ( 'network_error' === $result['reason'] ) {
				$reason = 'provider_network_error';
			} elseif ( 'configuration_error' === $result['reason'] ) {
				$reason = 'provider_configuration_error';
			} else {
				$reason = 'provider_api_error';
			}
			$failure_class = 'network_error' === $result['reason'] ? 'transport_connect' : ( 'configuration_error' === $result['reason'] ? 'configuration' : 'remote_rejected' );
			self::record_outcome( $form_id, false, $failure_class, $result, 'remote_rejected' );
			Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::failure( $reason, '', $email ) );
			return;
		}

		$provider_data = isset( $result['data'] ) && is_array( $result['data'] ) ? $result['data'] : array();
		self::handle_success( $email, $status, $merge_vars, $form_id, $provider_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::record_outcome( $form_id, true, 'unknown', '', 'unknown' );
		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 record_outcome( int $form_id, bool $success, string $failure_class, $value, string $fallback ): void {
		try {
			$config        = get_option( 'cf7_mch_' . $form_id, array() );
			$config        = is_array( $config ) ? $config : array();
			$provider      = Cmatic_Lite_Esp_Registry::get_selected( $config );
			$reason        = class_exists( 'Cmatic_Lite_Signls_Failure_Reason' )
				? Cmatic_Lite_Signls_Failure_Reason::from_value( $value, $fallback )
				: array(
					'code'   => $fallback,
					'sample' => '',
				);
			$failure_class = $success ? 'unknown' : self::failure_class_for_reason( $reason['code'], $failure_class );
			$recorded      = \Signls\Sdk\V1\CounterStore::record_outcome(
				'contact-form-7-mailchimp-extension',
				$provider,
				'subscribe',
				$success,
				$failure_class,
				$success ? '' : $reason['code'],
				$success ? '' : $reason['sample']
			);
			if ( $recorded ) {
				\Signls\Sdk\V1\Runtime::relevant_change( 'contact-form-7-mailchimp-extension' );
			}
		} catch ( Throwable $error ) {
			// Signals must never change form-submission behavior.
			return;
		}
	}

	private static function failure_class_for_reason( string $reason_code, string $fallback ): string {
		$map = array(
			'dns'                => 'transport_dns',
			'tls'                => 'transport_tls',
			'timeout'            => 'transport_timeout',
			'rate_limit'         => 'http_429',
			'http_4xx'           => 'http_4xx',
			'http_5xx'           => 'http_5xx',
			'revoked_credential' => 'auth',
			'permission'         => 'auth',
			'configuration'      => 'configuration',
			'validation'         => 'validation',
		);
		return isset( $map[ $reason_code ] ) ? $map[ $reason_code ] : $fallback;
	}

	private static function api_failure_class( array $api_data ): string {
		$status = isset( $api_data['status'] ) && is_numeric( $api_data['status'] ) ? (int) $api_data['status'] : 0;
		if ( 401 === $status || 403 === $status ) {
			return 'auth';
		}
		if ( 429 === $status ) {
			return 'http_429';
		}
		if ( $status >= 500 ) {
			return 'http_5xx';
		}
		if ( $status >= 400 ) {
			return 'http_4xx';
		}
		return 'remote_rejected';
	}

	private static function track_test_modal(): void {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		$test_modal = isset( $_POST['_cmatic_test_modal'] ) ? sanitize_text_field( wp_unslash( $_POST['_cmatic_test_modal'] ) ) : '';
		if ( '1' === $test_modal ) {
			Cmatic_Options_Repository::set_option( 'features.test_modal_used', true );
		}
	}

	private static function increment_counter( int $form_id ): void {
		$count = (int) Cmatic_Options_Repository::get_option( 'stats.sent', 0 );
		Cmatic_Options_Repository::set_option( 'stats.sent', $count + 1 );

		$cf7_mch               = get_option( 'cf7_mch_' . $form_id, array() );
		$cf7_mch               = is_array( $cf7_mch ) ? $cf7_mch : array();
		$cf7_mch['stats_sent'] = ( (int) ( $cf7_mch['stats_sent'] ?? 0 ) ) + 1;
		update_option( 'cf7_mch_' . $form_id, $cf7_mch );
	}
}
                                                                                                                                                                                                                           includes/services/submission/class-cmatic-email-extractor.php                                       0000644                 00000003160 15266052614 0020551 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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;
			}
			$mail_tag = class_exists( 'WPCF7_MailTag' )
				? new WPCF7_MailTag( sprintf( '[%s]', $matches[1] ), $matches[1], '' )
				: null;
			// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Contact Form 7 core hook.
			$special = apply_filters( 'wpcf7_special_mail_tags', '', $matches[1], false, $mail_tag );
			if ( is_string( $special ) && '' !== $special ) {
				return $special;
			}
			return '';
		}
		return $subject;
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                includes/services/submission/class-cmatic-request-ip.php                                            0000644                 00000001102 15266052614 0017541 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Trusted request IP boundary.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

final class Cmatic_Request_Ip {
	public static function get(): string {
		$remote = isset( $_SERVER['REMOTE_ADDR'] ) && is_string( $_SERVER['REMOTE_ADDR'] )
			? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) )
			: '';
		$ip     = apply_filters( 'cmatic_lite_client_ip', $remote, $remote );
		return is_string( $ip ) && filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : '';
	}

	private function __construct() {}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                              includes/services/submission/class-cmatic-mailerlite-runtime-policy.php                             0000644                 00000006130 15266052614 0022556 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * MailerLite paid-feature runtime degradation policy.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

final class Cmatic_Mailerlite_Runtime_Policy {
	/**
	 * Applies current entitlements and returns suppressed feature identifiers.
	 *
	 * @param array $settings     Saved provider settings.
	 * @param array $entitlements Effective paid-feature grants.
	 */
	public static function apply( array $settings, array $entitlements ): array {
		$effective           = $settings;
		$suppressed_features = array();
		$suppressed_rule_ids = array();

		if ( Cmatic_Mailerlite_Routing_Resolver::is_premium_configured( $settings ) && empty( $entitlements['mailerlite_routing'] ) ) {
			foreach ( (array) ( $settings['routing_rules'] ?? array() ) as $rule ) {
				if ( is_array( $rule ) && isset( $rule['id'] ) && is_scalar( $rule['id'] ) && '' !== (string) $rule['id'] ) {
					$suppressed_rule_ids[] = sanitize_key( (string) $rule['id'] );
				}
			}
			$groups                     = Cmatic_Mailerlite_Routing_Resolver::base_groups( $settings );
			$primary                    = self::primary_group( $settings, $groups );
			$effective['list']          = $primary;
			$effective['base_groups']   = '' === $primary ? array() : array( $primary );
			$effective['routing_rules'] = array();
			$suppressed_features[]      = 'mailerlite_routing';
		}

		$status_mode = (string) ( $settings['status_mode'] ?? 'legacy_provider_managed' );
		if ( 'legacy_provider_managed' !== $status_mode && empty( $entitlements['mailerlite_status'] ) ) {
			unset( $effective['status_mode'] );
			$suppressed_features[] = 'mailerlite_status';
		}

		if ( ! empty( $settings['resubscribe_force'] ) && ( empty( $entitlements['mailerlite_resubscribe'] ) || empty( $entitlements['mailerlite_status'] ) ) ) {
			unset( $effective['resubscribe_force'] );
			$suppressed_features[] = 'mailerlite_resubscribe';
		}

		if ( ! empty( $settings['consent_metadata_enabled'] ) && empty( $entitlements['mailerlite_consent_metadata'] ) ) {
			unset( $effective['consent_metadata_enabled'] );
			$suppressed_features[] = 'mailerlite_consent_metadata';
		}

		$suppressed_features = self::canonical( $suppressed_features );
		$suppressed_rule_ids = self::canonical( $suppressed_rule_ids );

		return array(
			'effective_settings'  => $effective,
			'degraded'            => ! empty( $suppressed_features ),
			'suppressed_features' => $suppressed_features,
			'suppressed_rule_ids' => $suppressed_rule_ids,
		);
	}

	private static function primary_group( array $settings, array $groups ): string {
		$list = $settings['list'] ?? '';
		if ( is_array( $list ) ) {
			$list = reset( $list );
		}
		return '' !== (string) $list ? (string) $list : (string) ( $groups[0] ?? '' );
	}

	private static function canonical( array $values ): array {
		$values = array_values(
			array_unique(
				array_filter(
					array_map( 'sanitize_key', $values ),
					static function ( string $value ): bool {
						return '' !== $value;
					}
				)
			)
		);
		sort( $values, SORT_STRING );
		return $values;
	}

	private function __construct() {}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                        includes/services/class-cmatic-pro-status.php                                                       0000644                 00000004130 15266052614 0015375 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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'] );
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                        includes/services/class-cmatic-options-repository.php                                               0000644                 00000004171 15266052614 0017171 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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;
	}


	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();
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                       includes/services/class-cmatic-cron.php                                                             0000644                 00000003266 15266052615 0014227 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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 {
		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 );
	}
}
                                                                                                                                                                                                                                                                                                                                          includes/services/class-cmatic-api-service.php                                                      0000644                 00000015053 15266052615 0015472 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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/";
			$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 failed.', $error );
				self::record_failure();
				return array( 'api-validation' => 0 );
			}

			$logger->log( 'INFO', 'API Key validated successfully.' );
			self::record_success();

			$result = array( 'api-validation' => 1 );

			$body = $response[0] ?? array();
			if ( ! empty( $body['account_name'] ) ) {
				$result['account_name'] = sanitize_text_field( $body['account_name'] );
			}

			return $result;

		} 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() {}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     includes/services/class-cmatic-cf7-dependency.php                                                   0000644                 00000004124 15266052615 0016053 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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() {
		add_action( 'admin_init', array( __CLASS__, 'maybe_activate_cf7' ), 1 );
		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;
		}

		$screen = get_current_screen();
		if ( $screen && 'plugins' === $screen->id ) {
			self::render_not_installed_notice();
		}
	}

	public static function activate_cf7() {
		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;
		}

		$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 )
		);
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                            includes/services/class-cmatic-cf7-tags.php                                                         0000644                 00000006450 15266052615 0014677 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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;
	}
}
                                                                                                                                                                                                                        includes/services/class-cmatic-pro-syncer.php                                                       0000644                 00000012032 15266052616 0015357 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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 ( is_string( $activation ) ) {
			$activation = maybe_unserialize( $activation );
		}
		if ( ! is_array( $activation ) ) {
			$activation = array();
		}

		$api_key     = ! empty( $activation['license_key'] ) ? $activation['license_key'] : 'unlicensed';
		$instance_id = ! empty( $activation['instance_id'] ) ? $activation['instance_id'] : hash( 'crc32', home_url() );
		$product_id  = ! empty( $activation['product_id'] ) ? $activation['product_id'] : 436;

		$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();
		}
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      includes/services/signls/class-cmatic-lite-signls-inventory-collector.php                           0000644                 00000017377 15266052616 0023047 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * ChimpMatic Lite inventory Signls collector.
 *
 * @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_Signls_Inventory_Collector {

	private const MAX_PLUGINS = 500;

	private const COMPETITORS = array(
		'mc4wp'             => 'mailchimp-for-wp/mailchimp-for-wp.php',
		'mc4wp_premium'     => 'mc4wp-premium/mc4wp-premium.php',
		'mailchimp_woo'     => 'mailchimp-for-woocommerce/mailchimp-woocommerce.php',
		'crm_perks'         => 'cf7-mailchimp/cf7-mailchimp.php',
		'easy_forms'        => 'jetwp-easy-mailchimp/jetwp-easy-mailchimp.php',
		'jetrail'           => 'jetrail-cf7-mailchimp/jetrail-cf7-mailchimp.php',
		'cf7_mailchimp_ext' => 'contact-form-7-mailchimp-extension-jetrail/cf7-mailchimp-ext.php',
		'newsletter'        => 'newsletter/plugin.php',
		'mailpoet'          => 'mailpoet/mailpoet.php',
		'fluent_forms'      => 'fluentform/fluentform.php',
		'wpforms'           => 'wpforms-lite/wpforms.php',
		'gravity_forms'     => 'gravityforms/gravityforms.php',
		'ninja_forms'       => 'ninja-forms/ninja-forms.php',
		'formidable'        => 'formidable/formidable.php',
		'hubspot'           => 'leadin/leadin.php',
		'elementor_pro'     => 'elementor-pro/elementor-pro.php',
	);

	public function collect(): array {
		$this->load_plugin_api();
		$plugins        = get_plugins();
		$mu_plugins     = get_mu_plugins();
		$active_plugins = get_option( 'active_plugins', array() );
		$active         = array();
		foreach ( is_array( $active_plugins ) ? $active_plugins : array() as $plugin_file ) {
			if ( is_scalar( $plugin_file ) ) {
				$active[ plugin_basename( (string) $plugin_file ) ] = true;
			}
		}
		$network_active = is_multisite() ? (array) get_site_option( 'active_sitewide_plugins', array() ) : array();
		$inventory      = $this->inventory_rows( $plugins, $mu_plugins, $active, $network_active );
		$competitors    = $this->competitors( $plugins, $active, $network_active );

		return array(
			'plugins'       => $this->plugin_summary( $plugins, $mu_plugins, $inventory ),
			'competitors'   => $competitors,
			'opportunities' => array( 'connector_conflict' => $competitors['competitors_active'] > 0 ),
		);
	}

	public function theme_facts(): array {
		$theme  = wp_get_theme();
		$parent = $theme->parent();
		return array(
			'theme'          => (string) $theme->get( 'Name' ),
			'theme_version'  => (string) $theme->get( 'Version' ),
			'theme_author'   => wp_strip_all_tags( (string) $theme->get( 'Author' ) ),
			'parent_theme'   => $parent ? (string) $parent->get( 'Name' ) : '',
			'is_child_theme' => (bool) $parent,
		);
	}

	private function inventory_rows( array $plugins, array $mu_plugins, array $active, array $network_active ): array {
		ksort( $plugins, SORT_STRING );
		ksort( $mu_plugins, SORT_STRING );
		$rows = array();
		foreach ( $plugins as $basename => $data ) {
			$basename = plugin_basename( (string) $basename );
			$status   = isset( $network_active[ $basename ] ) ? 'network-active' : ( isset( $active[ $basename ] ) ? 'active' : 'inactive' );
			$rows[]   = $this->plugin_row( $basename, $data, $status );
		}
		foreach ( $mu_plugins as $basename => $data ) {
			$rows[] = $this->plugin_row( plugin_basename( (string) $basename ), $data, 'mu-plugin' );
		}
		return $rows;
	}

	private function plugin_row( string $basename, array $data, string $status ): array {
		$directory = dirname( $basename );
		$slug      = \Signls\Sdk\V1\Sanitizer::slug( sanitize_key( '.' === $directory ? basename( $basename, '.php' ) : $directory ), 100 );
		$slug      = '' !== $slug ? $slug : 'unknown-' . substr( hash( 'sha256', $basename ), 0, 12 );
		$author    = isset( $data['AuthorName'] ) && '' !== $data['AuthorName'] ? $data['AuthorName'] : ( isset( $data['Author'] ) ? $data['Author'] : '' );
		return array(
			'slug'    => $slug,
			'name'    => isset( $data['Name'] ) ? wp_strip_all_tags( (string) $data['Name'] ) : '',
			'version' => isset( $data['Version'] ) ? (string) $data['Version'] : '',
			'author'  => wp_strip_all_tags( (string) $author ),
			'status'  => $status,
		);
	}

	private function plugin_summary( array $plugins, array $mu_plugins, array $inventory ): array {
		$stats        = array_fill_keys( array( 'premium_plugins', 'cf7_addons', 'mailchimp_plugins', 'security_plugins', 'cache_plugins', 'seo_plugins' ), 0 );
		$active_count = 0;
		foreach ( $inventory as $row ) {
			if ( in_array( $row['status'], array( 'active', 'network-active' ), true ) ) {
				++$active_count;
			}
			if ( 'mu-plugin' === $row['status'] ) {
				continue;
			}
			$name                        = strtolower( $row['name'] );
			$stats['premium_plugins']   += false !== strpos( $name, 'pro' ) || false !== strpos( $name, 'premium' ) ? 1 : 0;
			$stats['cf7_addons']        += false !== strpos( $name, 'contact form 7' ) ? 1 : 0;
			$stats['mailchimp_plugins'] += false !== strpos( $name, 'mailchimp' ) ? 1 : 0;
			$stats['security_plugins']  += false !== strpos( $name, 'security' ) || false !== strpos( $name, 'wordfence' ) || false !== strpos( $name, 'sucuri' ) ? 1 : 0;
			$stats['cache_plugins']     += false !== strpos( $name, 'cache' ) || false !== strpos( $name, 'wp rocket' ) || false !== strpos( $name, 'w3 total cache' ) ? 1 : 0;
			$stats['seo_plugins']       += false !== strpos( $name, 'seo' ) || false !== strpos( $name, 'yoast' ) ? 1 : 0;
		}
		$total_rows = count( $inventory );
		return array_merge(
			array(
				'total_plugins'    => count( $plugins ),
				'active_plugins'   => $active_count,
				'inactive_plugins' => max( 0, count( $plugins ) - $active_count ),
				'mu_plugins'       => count( $mu_plugins ),
				'has_woocommerce'  => $this->has_active_plugin( $inventory, 'woocommerce' ),
				'has_elementor'    => $this->has_active_plugin( $inventory, 'elementor' ),
				'has_jetpack'      => $this->has_active_plugin( $inventory, 'jetpack' ),
				'has_wordfence'    => $this->has_active_plugin( $inventory, 'wordfence' ),
				'has_yoast_seo'    => $this->has_active_plugin( $inventory, 'wordpress-seo' ),
				'plugin_list'      => array(
					'items'          => array_slice( $inventory, 0, self::MAX_PLUGINS ),
					'reported_total' => $total_rows,
					'truncated'      => $total_rows > self::MAX_PLUGINS,
				),
			),
			$stats
		);
	}

	private function has_active_plugin( array $inventory, string $slug ): bool {
		foreach ( $inventory as $row ) {
			if ( $slug === $row['slug'] && in_array( $row['status'], array( 'active', 'network-active' ), true ) ) {
				return true;
			}
		}
		return false;
	}

	private function competitors( array $plugins, array $active, array $network_active ): array {
		$result = array(
			'has_competitors'       => false,
			'competitors_installed' => 0,
			'competitors_active'    => 0,
			'churn_risk'            => 'none',
			'installed_list'        => array(),
			'active_list'           => array(),
		);
		foreach ( self::COMPETITORS as $key => $basename ) {
			$installed                     = isset( $plugins[ $basename ] );
			$is_active                     = isset( $active[ $basename ] ) || isset( $network_active[ $basename ] );
			$result[ $key . '_installed' ] = $installed;
			$result[ $key . '_active' ]    = $is_active;
			if ( $installed ) {
				++$result['competitors_installed'];
				$result['installed_list'][] = $key;
			}
			if ( $is_active ) {
				++$result['competitors_active'];
				$result['active_list'][] = $key;
			}
		}
		$result['has_competitors'] = $result['competitors_installed'] > 0;
		$result['churn_risk']      = $result['competitors_active'] > 0 ? 'high' : ( $result['competitors_installed'] > 0 ? 'medium' : 'none' );
		return $result;
	}

	private function load_plugin_api(): void {
		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
	}
}
                                                                                                                                                                                                                                                                 includes/services/signls/class-cmatic-lite-signls-site-collector.php                                0000644                 00000030420 15266052617 0021737 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * ChimpMatic Lite site Signls collector.
 *
 * @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_Signls_Site_Collector {

	private $inventory;

	public function __construct( $inventory = null ) {
		$this->inventory = $inventory;
	}

	public function collect(): array {
		return array(
			'environment'   => $this->environment(),
			'server'        => $this->server(),
			'wordpress'     => $this->wordpress(),
			'opportunities' => array(
				'environment_type' => $this->environment_type(),
				'rest_api_ok'      => $this->rest_api_ok(),
				'host_class'       => $this->host_class(),
				'cache_dropins'    => $this->cache_dropins(),
			),
		);
	}

	private function environment(): array {
		global $wpdb, $wp_version;

		$this->load_plugin_api();
		$extensions = get_loaded_extensions();
		$critical   = array( 'curl', 'json', 'mbstring', 'openssl', 'zip', 'gd', 'xml', 'dom', 'SimpleXML' );
		$theme      = is_object( $this->inventory ) && is_callable( array( $this->inventory, 'theme_facts' ) ) ? $this->inventory->theme_facts() : $this->theme_facts();
		$db_version = get_option( 'db_version', 0 );
		$db_version = is_scalar( $db_version ) ? (int) $db_version : 0;

		return array_merge(
			array(
				'php_version'                    => PHP_VERSION,
				'php_sapi'                       => php_sapi_name(),
				'php_os'                         => PHP_OS,
				'php_architecture'               => 8 === PHP_INT_SIZE ? '64-bit' : '32-bit',
				'php_memory_limit'               => (string) 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'              => (string) ini_get( 'post_max_size' ),
				'php_upload_max_filesize'        => (string) ini_get( 'upload_max_filesize' ),
				'php_default_timezone'           => (string) ini_get( 'date.timezone' ),
				'php_log_errors'                 => (string) ini_get( 'log_errors' ),
				'php_extensions_count'           => count( $extensions ),
				'php_critical_extensions'        => implode( ',', array_values( array_intersect( $critical, $extensions ) ) ),
				'php_curl_version'               => $this->curl_version(),
				'php_openssl_version'            => defined( 'OPENSSL_VERSION_TEXT' ) ? (string) OPENSSL_VERSION_TEXT : '',
				'wp_version'                     => (string) $wp_version,
				'wp_db_version'                  => $db_version,
				'wp_memory_limit'                => defined( 'WP_MEMORY_LIMIT' ) ? (string) WP_MEMORY_LIMIT : '',
				'wp_max_memory_limit'            => defined( 'WP_MAX_MEMORY_LIMIT' ) ? (string) WP_MAX_MEMORY_LIMIT : '',
				'wp_debug'                       => defined( 'WP_DEBUG' ) && WP_DEBUG,
				'wp_debug_log'                   => defined( 'WP_DEBUG_LOG' ) && (bool) 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'            => $this->scalar_text( get_option( 'auto_update_core', 'enabled' ) ),
				'mysql_version'                  => (string) $wpdb->db_version(),
				'mysql_client_version'           => (string) $wpdb->get_var( 'SELECT VERSION()' ), // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- One scalar compatibility fact is read from the active server.
				'db_charset'                     => (string) $wpdb->charset,
				'db_collate'                     => (string) $wpdb->collate,
				'db_prefix_length'               => strlen( (string) $wpdb->prefix ),
				'server_software'                => $this->server_value( 'SERVER_SOFTWARE' ),
				'server_protocol'                => $this->server_value( 'SERVER_PROTOCOL' ),
				'server_port'                    => (int) $this->server_value( 'SERVER_PORT' ),
				'https'                          => is_ssl(),
				'http_host_sha256'               => $this->hash_value( $this->server_value( 'HTTP_HOST' ) ),
				'locale'                         => get_locale(),
				'timezone'                       => wp_timezone_string(),
				'site_language'                  => get_bloginfo( 'language' ),
				'site_charset'                   => get_bloginfo( 'charset' ),
				'permalink_structure'            => $this->scalar_text( get_option( 'permalink_structure', '' ) ),
				'home_url_sha256'                => $this->hash_value( home_url() ),
				'site_url_sha256'                => $this->hash_value( site_url() ),
				'admin_email_sha256'             => $this->hash_value( $this->scalar_text( get_option( 'admin_email', '' ) ) ),
				'theme_supports_html5'           => current_theme_supports( 'html5' ),
				'theme_supports_post_thumbnails' => current_theme_supports( 'post-thumbnails' ),
				'active_plugins_count'           => count( (array) get_option( 'active_plugins', array() ) ),
				'total_plugins_count'            => count( get_plugins() ),
				'must_use_plugins_count'         => count( get_mu_plugins() ),
				'is_multisite'                   => is_multisite(),
				'is_subdomain_install'           => is_multisite() && is_subdomain_install(),
				'network_count'                  => is_multisite() && function_exists( 'get_blog_count' ) ? (int) get_blog_count() : 1,
				'is_main_site'                   => ! is_multisite() || is_main_site(),
				'cf7_version'                    => defined( 'WPCF7_VERSION' ) ? (string) WPCF7_VERSION : '',
				'cf7_installed'                  => class_exists( 'WPCF7_ContactForm' ),
				'plugin_version'                 => defined( 'SPARTAN_MCE_VERSION' ) ? (string) SPARTAN_MCE_VERSION : '',
				'user_agent'                     => $this->server_value( 'HTTP_USER_AGENT' ),
			),
			$theme
		);
	}

	private function server(): array {
		$load      = function_exists( 'sys_getloadavg' ) ? sys_getloadavg() : false;
		$disk_path = realpath( ABSPATH );
		$free      = function_exists( 'disk_free_space' ) && is_string( $disk_path ) && is_readable( $disk_path ) ? disk_free_space( $disk_path ) : false;
		$total     = function_exists( 'disk_total_space' ) && is_string( $disk_path ) && is_readable( $disk_path ) ? disk_total_space( $disk_path ) : false;
		$used      = false !== $free && false !== $total ? max( 0, $total - $free ) : null;
		$host      = function_exists( 'gethostname' ) ? gethostname() : false;
		$arch      = function_exists( 'php_uname' ) ? php_uname( 'm' ) : false;

		return array(
			'load_average_1min'      => is_array( $load ) ? round( (float) $load[0], 2 ) : null,
			'load_average_5min'      => is_array( $load ) ? round( (float) $load[1], 2 ) : null,
			'load_average_15min'     => is_array( $load ) ? round( (float) $load[2], 2 ) : null,
			'disk_usage_percent'     => null !== $used && $total > 0 ? round( ( $used / $total ) * 100, 2 ) : null,
			'disk_total_gb'          => false !== $total ? round( $total / 1073741824, 2 ) : null,
			'server_ip_sha256'       => $this->hash_value( $this->server_value( 'SERVER_ADDR' ) ),
			'server_hostname_sha256' => $this->hash_value( false !== $host ? (string) $host : '' ),
			'server_os'              => PHP_OS,
			'server_architecture'    => false !== $arch ? (string) $arch : '',
		);
	}

	private function wordpress(): array {
		global $wpdb;

		$post_counts    = wp_count_posts( 'post' );
		$page_counts    = wp_count_posts( 'page' );
		$media_counts   = wp_count_posts( 'attachment' );
		$comment_counts = wp_count_comments();
		$user_counts    = count_users();
		$categories     = wp_count_terms( array( 'taxonomy' => 'category' ) );
		$tags           = wp_count_terms( array( 'taxonomy' => 'post_tag' ) );
		$revisions      = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'revision'" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Aggregate count over the current site's posts table.

		return 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_counts['total_users'] ) ? (int) $user_counts['total_users'] : 0,
			'users_administrators' => isset( $user_counts['avail_roles']['administrator'] ) ? (int) $user_counts['avail_roles']['administrator'] : 0,
			'users_editors'        => isset( $user_counts['avail_roles']['editor'] ) ? (int) $user_counts['avail_roles']['editor'] : 0,
			'users_authors'        => isset( $user_counts['avail_roles']['author'] ) ? (int) $user_counts['avail_roles']['author'] : 0,
			'users_subscribers'    => isset( $user_counts['avail_roles']['subscriber'] ) ? (int) $user_counts['avail_roles']['subscriber'] : 0,
			'categories_count'     => is_wp_error( $categories ) ? 0 : (int) $categories,
			'tags_count'           => is_wp_error( $tags ) ? 0 : (int) $tags,
			'revisions_count'      => (int) $revisions,
			'auto_updates_enabled' => ! empty( get_option( 'auto_update_plugins', array() ) ),
		);
	}

	private function cache_dropins(): array {
		$this->load_plugin_api();
		$slugs = array();
		foreach ( array_keys( get_dropins() ) as $file ) {
			$slug = sanitize_key( basename( (string) $file, '.php' ) );
			if ( '' !== $slug ) {
				$slugs[] = $slug;
			}
		}
		sort( $slugs, SORT_STRING );
		$total = count( $slugs );
		return array(
			'items'          => array_slice( $slugs, 0, 8 ),
			'reported_total' => $total,
			'truncated'      => $total > 8,
		);
	}

	private function environment_type(): string {
		$type = function_exists( 'wp_get_environment_type' ) ? wp_get_environment_type() : 'unknown';
		return in_array( $type, array( 'production', 'staging', 'development', 'local' ), true ) ? $type : 'unknown';
	}

	private function host_class(): string {
		if ( defined( 'WPCOM_IS_VIP_ENV' ) || defined( 'VIP_GO_APP_ID' ) ) {
			return 'wpcom_vip';
		}
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
			return 'wpcom';
		}
		if ( defined( 'PRESSABLE_HOSTING' ) || defined( 'PRESSABLE_SITE_ID' ) ) {
			return 'pressable';
		}
		if ( defined( 'WPE_APIKEY' ) || defined( 'WPE_CLUSTER_ID' ) || defined( 'PWP_NAME' ) ) {
			return 'wp_engine';
		}
		return '' !== $this->server_value( 'SERVER_SOFTWARE' ) ? 'other' : 'unknown';
	}

	private function rest_api_ok(): ?bool {
		if ( ! function_exists( 'rest_url' ) || ! class_exists( 'WP_REST_Server' ) ) {
			return null;
		}
		return '' !== (string) rest_url();
	}

	private function theme_facts(): array {
		$theme  = wp_get_theme();
		$parent = $theme->parent();
		return array(
			'theme'          => (string) $theme->get( 'Name' ),
			'theme_version'  => (string) $theme->get( 'Version' ),
			'theme_author'   => wp_strip_all_tags( (string) $theme->get( 'Author' ) ),
			'parent_theme'   => $parent ? (string) $parent->get( 'Name' ) : '',
			'is_child_theme' => (bool) $parent,
		);
	}

	private function load_plugin_api(): void {
		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
	}

	private function curl_version(): string {
		if ( ! function_exists( 'curl_version' ) ) {
			return '';
		}
		$data = curl_version();
		return is_array( $data ) && isset( $data['version'] ) ? (string) $data['version'] : '';
	}

	private function server_value( string $key ): string {
		return isset( $_SERVER[ $key ] ) && is_scalar( $_SERVER[ $key ] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER[ $key ] ) ) : '';
	}

	private function hash_value( string $value ): string {
		return '' === $value ? '' : hash( 'sha256', $value );
	}

	private function scalar_text( $value ): string {
		return is_scalar( $value ) ? (string) $value : '';
	}
}
                                                                                                                                                                                                                                                includes/services/signls/class-cmatic-lite-signls-product-collector.php                             0000644                 00000116760 15266052617 0022467 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * ChimpMatic Lite product Signls collector.
 *
 * @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_Signls_Product_Collector {

	private const PRODUCT = 'contact-form-7-mailchimp-extension';

	private const INTEGRATIONS = array( 'mailchimp', 'brevo', 'mailerlite', 'klaviyo' );

	private const NARROW_FEATURES = array(
		'advanced_fields',
		'tags_management',
		'groups_segments',
		'gdpr_fields',
		'conditional_logic',
		'double_optin',
		'visitor_geolocation',
		'unsubscribe',
		'multi_group_routing',
		'status_control',
		'consent_metadata',
		'debug_logging',
		'auto_updates',
	);

	private $forms_collector;
	private $lifetime_rows;
	private $daily_rows;
	private $reason_rows;

	public function __construct( $forms_collector = null ) {
		$this->forms_collector = $forms_collector;
	}

	public function collect(): array {
		$configurations = $this->configurations();
		$analysis       = $this->analyse_forms( $configurations );
		$install        = new Cmatic_Install_Data( Cmatic_Options_Repository::instance() );
		$quest          = (int) $install->get_quest();
		$pro            = $this->pro_state();
		$lifecycle      = $this->lifecycle( $quest );
		$submissions    = $this->submissions( $quest, $configurations, $analysis['active'] );
		$rolling        = array(
			'seven'  => $submissions['_subs_7d'],
			'thirty' => $submissions['_subs_30d'],
			'median' => $submissions['_subs_median'],
		);
		unset( $submissions['_subs_7d'], $submissions['_subs_30d'], $submissions['_subs_median'] );
		$setup  = $this->setup( $analysis );
		$update = $this->update_state();
		$reason = $this->top_failure_reason();

		return array(
			'narrow'           => $this->narrow( $analysis, $pro ),
			'install'          => array(
				'plugin_slug' => self::PRODUCT,
				'quest'       => $quest,
				'pro'         => $pro,
			),
			'metadata'         => $this->metadata( $quest ),
			'lifecycle'        => $lifecycle,
			'api'              => $this->api( $analysis ),
			'submissions'      => $submissions,
			'features'         => $this->features( $configurations ),
			'legacy_lifecycle' => array(
				'event'         => 'heartbeat',
				'install_id'    => (string) $install->get_install_id(),
				'version'       => defined( 'SPARTAN_MCE_VERSION' ) ? (string) SPARTAN_MCE_VERSION : 'unknown',
				'site_url'      => site_url(),
				'timestamp'     => time(),
				'wp_version'    => get_bloginfo( 'version' ),
				'php'           => PHP_VERSION,
				'mysql_version' => $this->mysql_version(),
				'software'      => array( 'server' => $this->server_software() ),
			),
			'opportunities'    => array(
				'setup_stage'      => $setup['stage'],
				'setup_percent'    => $setup['percent'],
				'failure_reason'   => $reason,
				'update_available' => $update['available'],
				'versions_behind'  => $update['versions_behind'],
				'subs_7d'          => $rolling['seven'],
				'subs_30d'         => $rolling['thirty'],
				'subs_median'      => $rolling['median'],
				'install_source'   => $this->install_source( isset( $lifecycle['install_method'] ) ? $lifecycle['install_method'] : 'unknown' ),
			),
		);
	}

	private function configurations(): array {
		if ( is_object( $this->forms_collector ) && is_callable( array( $this->forms_collector, 'configurations' ) ) ) {
			$value = $this->forms_collector->configurations();
			return is_array( $value ) ? $value : array();
		}
		return array();
	}

	private function analyse_forms( array $configurations ): array {
		$integrations = array();
		foreach ( self::INTEGRATIONS as $slug ) {
			$integrations[ $slug ] = array(
				'slug'              => $slug,
				'configured_units'  => 0,
				'credential_units'  => 0,
				'oauth_units'       => 0,
				'api_key_units'     => 0,
				'destination_units' => 0,
				'mapping_count'     => 0,
				'attempts'          => 0,
				'successes'         => 0,
				'failures'          => 0,
			);
		}
		$feature_counts = array_fill_keys( self::NARROW_FEATURES, 0 );
		$result         = array(
			'integrations'      => &$integrations,
			'features'          => &$feature_counts,
			'configured'        => 0,
			'active'            => 0,
			'credential_units'  => 0,
			'destination_units' => 0,
			'mapping_count'     => 0,
			'manual_key_length' => 0,
			'data_center'       => '',
		);

		foreach ( $configurations as $form_id => $row ) {
			$config   = isset( $row['settings'] ) && is_array( $row['settings'] ) ? $row['settings'] : array();
			$provider = Cmatic_Lite_Esp_Registry::get_selected( $config );
			$settings = $this->provider_settings( $config, $provider );
			if ( ! $this->has_configuration( $settings, $provider ) ) {
				continue;
			}
			++$result['configured'];
			++$integrations[ $provider ]['configured_units'];
			$auth = $this->authentication( (int) $form_id, $provider, $config );
			if ( 'none' !== $auth ) {
				++$result['credential_units'];
				++$integrations[ $provider ]['credential_units'];
				if ( 'oauth' === $auth ) {
					$integrations[ $provider ]['oauth_units'] = isset( $integrations[ $provider ]['oauth_units'] ) ? $integrations[ $provider ]['oauth_units'] + 1 : 1;
				} else {
					$integrations[ $provider ]['api_key_units'] = isset( $integrations[ $provider ]['api_key_units'] ) ? $integrations[ $provider ]['api_key_units'] + 1 : 1;
				}
			}
			$destination = $this->has_destination( $settings );
			if ( $destination ) {
				++$result['destination_units'];
				++$integrations[ $provider ]['destination_units'];
			}
			$mappings                                    = $this->mapping_count( $settings );
			$result['mapping_count']                    += $mappings;
			$integrations[ $provider ]['mapping_count'] += $mappings;
			if ( 'none' !== $auth && $destination ) {
				++$result['active'];
			}
			if ( 'mailchimp' === $provider && ! empty( $config['api'] ) && is_scalar( $config['api'] ) ) {
				$key = (string) $config['api'];
				if ( 0 === $result['manual_key_length'] ) {
					$result['manual_key_length'] = strlen( $key );
				}
				if ( '' === $result['data_center'] && 1 === preg_match( '/-([a-z0-9]+)$/i', $key, $match ) ) {
					$result['data_center'] = strtolower( $match[1] );
				}
				unset( $key );
			}
			$this->count_narrow_features( $feature_counts, $settings, $provider, $mappings );
		}
		unset( $result['integrations'], $result['features'] );
		$result['integrations'] = $integrations;
		$result['features']     = $feature_counts;
		$operations             = array();
		$this->merge_outcomes( $result['integrations'], $operations );
		$result['operations']                = $operations;
		$result['features']['debug_logging'] = Cmatic_Options_Repository::get_option( 'debug', false ) ? 1 : 0;
		$result['features']['auto_updates']  = Cmatic_Options_Repository::get_option( 'auto_update', true ) ? 1 : 0;
		return $result;
	}

	private function narrow( array $analysis, array $pro ): array {
		$features = array();
		foreach ( self::NARROW_FEATURES as $slug ) {
			$features[] = array(
				'slug'             => $slug,
				'configured_units' => isset( $analysis['features'][ $slug ] ) ? $analysis['features'][ $slug ] : 0,
				'source'           => 'lite_config',
			);
		}
		return array(
			'versions'         => array(
				'wordpress' => get_bloginfo( 'version' ),
				'cf7'       => defined( 'WPCF7_VERSION' ) ? (string) WPCF7_VERSION : 'unknown',
			),
			'is_multisite'     => is_multisite(),
			'configured_units' => $analysis['configured'],
			'active_units'     => $analysis['active'],
			'integrations'     => array_values( $analysis['integrations'] ),
			'features'         => $features,
			'operation_health' => $analysis['operations'],
			'companions'       => array( $this->pro_companion( $pro ) ),
		);
	}

	private function pro_state(): array {
		$status    = new Cmatic_Pro_Status( Cmatic_Options_Repository::instance() );
		$installed = (bool) $status->is_installed();
		$active    = $installed && (bool) $status->is_activated();
		$version   = $this->pro_version( $installed );
		$licensed  = $active && (bool) $status->is_licensed();
		$expires   = Cmatic_Options_Repository::get_option( 'install.pro.license_expires', null );
		if ( null === $expires ) {
			$activation = get_option( 'chimpmatic_license_activation', array() );
			$expires    = is_array( $activation ) && isset( $activation['expires_at'] ) ? $activation['expires_at'] : null;
		}
		if ( null !== $expires && ! is_numeric( $expires ) ) {
			$expires = strtotime( (string) $expires );
		}
		return array(
			'installed'       => $installed,
			'activated'       => $active,
			'version'         => $version,
			'licensed'        => $licensed,
			'license_expires' => is_numeric( $expires ) ? max( 0, (int) $expires ) : null,
		);
	}

	private function pro_companion( array $pro ): array {
		$state           = ! $pro['installed'] ? 'not_present' : ( $pro['activated'] ? 'unknown' : 'inactive' );
		$validator_class = 'Cmatic_License_Validator';
		$validator       = array( $validator_class, 'get_license_state' );
		if ( $pro['activated'] && class_exists( $validator_class ) && is_callable( $validator ) ) {
			$reported_value = call_user_func( $validator );
			$reported       = is_scalar( $reported_value ) ? (string) $reported_value : '';
			$state          = in_array( $reported, array( 'active', 'expired' ), true ) ? $reported : 'inactive';
		} elseif ( $pro['activated'] && $pro['licensed'] ) {
			$state = 'legacy_activated';
		}
		return array(
			'slug'                   => 'chimpmatic',
			'installed'              => $pro['installed'],
			'active'                 => $pro['activated'],
			'version'                => $pro['version'],
			'license_state'          => $state,
			'source'                 => 'lite_observed',
			'observation_started_at' => (int) ( new Cmatic_Install_Data( Cmatic_Options_Repository::instance() ) )->get_quest(),
		);
	}

	private function metadata( int $quest ): array {
		$state = $this->sdk_state();
		$mode  = isset( $state['cadence_mode'] ) ? (string) $state['cadence_mode'] : (string) Cmatic_Options_Repository::get_option( 'telemetry.schedule', 'daily' );
		return array(
			'schedule'             => $mode,
			'frequent_started_at'  => isset( $state['consent_first_enabled_at'] ) ? (int) $state['consent_first_enabled_at'] : (int) Cmatic_Options_Repository::get_option( 'telemetry.frequent_started_at', 0 ),
			'is_reactivation'      => (bool) Cmatic_Options_Repository::get_option( 'telemetry.is_reactivation', false ) || $this->timestamps( 'activations' )['reported_total'] > 1,
			'disabled_count'       => (int) Cmatic_Options_Repository::get_option( 'telemetry.disabled_count', 0 ),
			'opt_in_date'          => isset( $state['consent_first_enabled_at'] ) ? (int) $state['consent_first_enabled_at'] : (int) Cmatic_Options_Repository::get_option( 'telemetry.opt_in_date', 0 ),
			'last_heartbeat'       => isset( $state['last_acknowledged_at'] ) ? (int) $state['last_acknowledged_at'] : (int) Cmatic_Options_Repository::get_option( 'telemetry.last_heartbeat', 0 ),
			'failed_heartbeats'    => isset( $state['retry_attempt'] ) ? (int) $state['retry_attempt'] : (int) Cmatic_Options_Repository::get_option( 'telemetry.failed_count', 0 ),
			'total_uptime_seconds' => $quest > 0 ? max( 0, time() - $quest ) : 0,
			'signal_version'       => defined( 'SPARTAN_MCE_VERSION' ) ? (string) SPARTAN_MCE_VERSION : 'unknown',
		);
	}

	private function lifecycle( int $quest ): array {
		$activations   = $this->timestamps( 'activations' );
		$deactivations = $this->timestamps( 'deactivations' );
		$upgrades      = $this->timestamps( 'upgrades' );
		$last_active   = ! empty( $activations['items'] ) ? max( $activations['items'] ) : 0;
		$last_inactive = ! empty( $deactivations['items'] ) ? max( $deactivations['items'] ) : 0;
		$last_upgrade  = ! empty( $upgrades['items'] ) ? max( $upgrades['items'] ) : 0;
		$history       = Cmatic_Options_Repository::get_option( 'lifecycle.version_history', array() );
		return array(
			'activation_count'            => $activations['reported_total'],
			'deactivation_count'          => $deactivations['reported_total'],
			'upgrade_count'               => $upgrades['reported_total'],
			'first_activated'             => $quest,
			'last_activated'              => $last_active,
			'last_deactivated'            => $last_inactive,
			'last_upgrade'                => $last_upgrade,
			'days_since_first_activation' => $this->age_days( $quest ),
			'days_since_last_upgrade'     => $this->age_days( $last_upgrade ),
			'avg_session_length_seconds'  => $this->average_session( $activations['items'], $deactivations['items'] ),
			'total_sessions'              => $activations['reported_total'],
			'previous_version'            => (string) Cmatic_Options_Repository::get_option( 'lifecycle.previous_version', '' ),
			'version_history_count'       => is_array( $history ) ? count( $history ) : 0,
			'install_method'              => (string) Cmatic_Options_Repository::get_option( 'lifecycle.install_method', 'unknown' ),
			'days_on_current_version'     => $this->age_days( $last_upgrade > 0 ? $last_upgrade : $quest ),
			'activation_timestamps'       => $activations,
			'deactivation_timestamps'     => $deactivations,
			'upgrade_timestamps'          => $upgrades,
			'active_session'              => 0 === $last_inactive || $last_active > $last_inactive,
		);
	}

	private function api( array $analysis ): array {
		$counters      = $this->outcome_totals( 'subscribe' );
		$old_sent      = (int) Cmatic_Options_Repository::get_option( 'stats.sent', 0 );
		$attempts      = max( (int) Cmatic_Options_Repository::get_option( 'api.total_attempts', $old_sent ), $counters['attempts'] );
		$successes     = max( (int) Cmatic_Options_Repository::get_option( 'api.total_successes', $old_sent ), $counters['successes'] );
		$failures      = max( (int) Cmatic_Options_Repository::get_option( 'api.total_failures', 0 ), $counters['failures'] );
		$last_success  = max( (int) Cmatic_Options_Repository::get_option( 'api.last_success', 0 ), $counters['last_success_at'] );
		$last_failure  = max( (int) Cmatic_Options_Repository::get_option( 'api.last_failure', 0 ), $counters['last_failure_at'] );
		$rate          = $attempts > 0 ? round( ( $successes / $attempts ) * 100, 2 ) : null;
		$errors        = Cmatic_Options_Repository::get_option( 'api.error_codes', array() );
		$error_summary = array();
		foreach ( is_array( $errors ) ? $errors : array() as $code => $count ) {
			$code  = sanitize_key( (string) $code );
			$count = self::nonnegative_int( $count );
			if ( '' !== $code && $count > 0 ) {
				$error_summary[ $code ] = $count;
			}
		}
		ksort( $error_summary, SORT_STRING );
		$consecutive = (int) Cmatic_Options_Repository::get_option( 'api.consecutive_failures', 0 );
		return array(
			'is_connected'              => $analysis['credential_units'] > 0,
			'forms_with_api'            => $analysis['credential_units'],
			'api_data_center'           => '' !== $analysis['data_center'] ? $analysis['data_center'] : 'unknown',
			'api_key_length'            => $analysis['manual_key_length'],
			'first_connected'           => (int) Cmatic_Options_Repository::get_option( 'api.first_connected', 0 ),
			'total_attempts'            => $attempts,
			'total_successes'           => $successes,
			'total_failures'            => $failures,
			'success_rate'              => $rate,
			'uptime_percentage'         => $rate,
			'last_success'              => $last_success,
			'last_failure'              => $last_failure,
			'days_since_last_success'   => $this->age_days( $last_success ),
			'days_since_last_failure'   => $this->age_days( $last_failure ),
			'avg_response_time_ms'      => (int) Cmatic_Options_Repository::get_option( 'api.avg_response_time', 0 ),
			'error_codes'               => array_slice( $error_summary, 0, 64, true ),
			'api_health_score'          => null === $rate ? null : max( 0, min( 100, $rate - ( $consecutive * 5 ) ) ),
			'setup_sync_attempted'      => (bool) 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'       => (bool) Cmatic_Options_Repository::get_option( 'api.setup_first_success', false ),
			'setup_first_failure'       => (bool) 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'   => (bool) Cmatic_Options_Repository::get_option( 'api.audience_selected', false ),
		);
	}

	private function submissions( int $quest, array $configurations, int $active_forms ): array {
		$counters     = $this->outcome_totals( 'subscribe' );
		$old_sent     = (int) Cmatic_Options_Repository::get_option( 'stats.sent', 0 );
		$sent         = max( $old_sent, $counters['successes'] );
		$failed       = max( (int) Cmatic_Options_Repository::get_option( 'submissions.failed', 0 ), $counters['failures'] );
		$total        = max( $sent + $failed, $counters['attempts'] );
		$first        = (int) Cmatic_Options_Repository::get_option( 'submissions.first', $counters['first_recorded_at'] );
		$last         = (int) Cmatic_Options_Repository::get_option( 'submissions.last', max( $counters['last_success_at'], $counters['last_failure_at'] ) );
		$last_success = max( (int) Cmatic_Options_Repository::get_option( 'submissions.last_success', 0 ), $counters['last_success_at'] );
		$last_failure = max( (int) Cmatic_Options_Repository::get_option( 'submissions.last_failure', 0 ), $counters['last_failure_at'] );
		$days         = max( 1, $this->age_days( $quest ) );
		$hourly       = Cmatic_Options_Repository::get_option( 'submissions.hourly', array() );
		$daily        = Cmatic_Options_Repository::get_option( 'submissions.daily', array() );
		$busiest_hour = $this->busiest( is_array( $hourly ) ? $hourly : array(), 0, 23 );
		$busiest_day  = $this->busiest( is_array( $daily ) ? $daily : array(), 0, 6 );
		$this_month   = (int) Cmatic_Options_Repository::get_option( 'submissions.this_month', 0 );
		$last_month   = (int) Cmatic_Options_Repository::get_option( 'submissions.last_month', 0 );
		$rolling      = $this->rolling_successes();
		$forms_used   = 0;
		foreach ( $configurations as $row ) {
			if ( isset( $row['submissions'] ) && (int) $row['submissions'] > 0 ) {
				++$forms_used;
			}
		}
		$rate = $total > 0 ? round( ( $sent / $total ) * 100, 2 ) : null;
		$data = array(
			'total_sent'                   => $sent,
			'total_failed'                 => $failed,
			'total_submissions'            => $total,
			'successful_submissions_count' => $sent,
			'failed_count'                 => $failed,
			'success_rate'                 => $rate,
			'first_submission'             => $first,
			'last_submission'              => $last,
			'last_success'                 => $last_success,
			'last_failure'                 => $last_failure,
			'days_since_first'             => $this->age_days( $first ),
			'days_since_last'              => $this->age_days( $last ),
			'hours_since_last'             => $this->age_hours( $last ),
			'avg_per_day'                  => round( $total / $days, 2 ),
			'avg_per_week'                 => round( ( $total / $days ) * 7, 2 ),
			'avg_per_month'                => round( ( $total / $days ) * 30, 2 ),
			'busiest_hour'                 => $busiest_hour['key'],
			'busiest_day'                  => $busiest_day['key'],
			'submissions_busiest_hour'     => $busiest_hour['count'],
			'submissions_busiest_day'      => $busiest_day['count'],
			'this_month'                   => $this_month,
			'last_month'                   => $last_month,
			'peak_month'                   => (int) Cmatic_Options_Repository::get_option( 'submissions.peak_month', 0 ),
			'month_over_month_change'      => $last_month > 0 ? round( ( ( $this_month - $last_month ) / $last_month ) * 100, 2 ) : null,
			'consecutive_successes'        => (int) Cmatic_Options_Repository::get_option( 'submissions.consecutive_successes', 0 ),
			'consecutive_failures'         => (int) Cmatic_Options_Repository::get_option( 'submissions.consecutive_failures', 0 ),
			'longest_success_streak'       => (int) Cmatic_Options_Repository::get_option( 'submissions.longest_success_streak', 0 ),
			'active_forms_count'           => max( 0, $active_forms ),
			'forms_with_submissions'       => $forms_used,
			'_subs_7d'                     => $rolling['seven'],
			'_subs_30d'                    => $rolling['thirty'],
			'_subs_median'                 => $rolling['median'],
		);
		return $data;
	}

	private function features( array $configurations ): array {
		$counts = array_fill_keys( array( 'double_optin', 'required_consent', 'debug_logger', 'custom_merge_fields', 'interest_groups', 'groups_total_mapped', 'tags_enabled', 'tags_total_selected', 'arbitrary_tags', 'conditional_logic' ), 0 );
		foreach ( $configurations as $row ) {
			$config                       = isset( $row['settings'] ) && is_array( $row['settings'] ) ? $row['settings'] : array();
			$provider                     = Cmatic_Lite_Esp_Registry::get_selected( $config );
			$settings                     = $this->provider_settings( $config, $provider );
			$counts['double_optin']      += ! empty( $settings['confsubs'] ) || 'double' === ( isset( $settings['subscription_mode'] ) ? $settings['subscription_mode'] : '' ) ? 1 : 0;
			$counts['required_consent']  += ! empty( $settings['accept'] ) || ! empty( $settings['consent_required'] ) || 'required' === ( isset( $settings['consent_gate'] ) ? $settings['consent_gate'] : '' ) ? 1 : 0;
			$counts['debug_logger']      += ! empty( $settings['logfileEnabled'] ) ? 1 : 0;
			$counts['conditional_logic'] += ! empty( $settings['conditional_logic'] ) || ! empty( $settings['conditions'] ) ? 1 : 0;
			$tags                         = isset( $settings['labeltags'] ) && is_array( $settings['labeltags'] ) ? array_filter( $settings['labeltags'] ) : array();
			if ( ! empty( $tags ) || ! empty( $settings['tags'] ) ) {
				++$counts['tags_enabled'];
				$counts['tags_total_selected'] += count( $tags ) + ( is_array( isset( $settings['tags'] ) ? $settings['tags'] : null ) ? count( $settings['tags'] ) : 0 );
			}
			$counts['arbitrary_tags'] += ! empty( $settings['labeltags_cm-tag'] ) || ! empty( $settings['tag'] ) ? 1 : 0;
			$custom                    = 0;
			$defaults                  = array( 'EMAIL', 'FNAME', 'LNAME', 'ADDRESS', 'PHONE' );
			$merge_fields              = isset( $settings['merge_fields'] ) && is_array( $settings['merge_fields'] ) ? $settings['merge_fields'] : array();
			if ( empty( $merge_fields ) && isset( $settings['merge-vars'] ) && is_array( $settings['merge-vars'] ) ) {
				$merge_fields = $settings['merge-vars'];
			}
			foreach ( $merge_fields as $field ) {
				if ( is_array( $field ) && isset( $field['tag'] ) && is_scalar( $field['tag'] ) && ! in_array( strtoupper( (string) $field['tag'] ), $defaults, true ) ) {
					++$custom;
				}
			}
			$counts['custom_merge_fields'] += $custom;
			$groups                         = 0;
			for ( $index = 1; $index <= 20; $index++ ) {
				if ( ! empty( $settings[ 'ggCustomKey' . $index ] ) && ! empty( trim( (string) ( isset( $settings[ 'ggCustomValue' . $index ] ) ? $settings[ 'ggCustomValue' . $index ] : '' ) ) ) ) {
					++$groups;
				}
			}
			if ( ! empty( $settings['base_groups'] ) && is_array( $settings['base_groups'] ) ) {
				$groups += count( $settings['base_groups'] );
			}
			if ( $groups > 0 ) {
				++$counts['interest_groups'];
				$counts['groups_total_mapped'] += $groups;
			}
		}
		$flags   = array(
			'double_optin'        => $counts['double_optin'] > 0,
			'required_consent'    => $counts['required_consent'] > 0,
			'debug_logger'        => $counts['debug_logger'] > 0,
			'custom_merge_fields' => $counts['custom_merge_fields'] > 0,
			'interest_groups'     => $counts['interest_groups'] > 0,
			'tags_enabled'        => $counts['tags_enabled'] > 0,
			'arbitrary_tags'      => $counts['arbitrary_tags'] > 0,
			'conditional_logic'   => $counts['conditional_logic'] > 0,
		);
		$enabled = count( array_filter( $flags ) );
		return array(
			'double_optin_count'        => $counts['double_optin'],
			'required_consent_count'    => $counts['required_consent'],
			'debug_logger_count'        => $counts['debug_logger'],
			'custom_merge_fields_count' => $counts['custom_merge_fields'],
			'interest_groups_count'     => $counts['interest_groups'],
			'groups_total_mapped'       => $counts['groups_total_mapped'],
			'tags_enabled_count'        => $counts['tags_enabled'],
			'tags_total_selected'       => $counts['tags_total_selected'],
			'arbitrary_tags_count'      => $counts['arbitrary_tags'],
			'conditional_logic_count'   => $counts['conditional_logic'],
			'double_optin'              => $flags['double_optin'],
			'required_consent'          => $flags['required_consent'],
			'debug_logger'              => $flags['debug_logger'],
			'custom_merge_fields'       => $flags['custom_merge_fields'],
			'interest_groups'           => $flags['interest_groups'],
			'tags_enabled'              => $flags['tags_enabled'],
			'arbitrary_tags'            => $flags['arbitrary_tags'],
			'conditional_logic'         => $flags['conditional_logic'],
			'auto_update'               => (bool) Cmatic_Options_Repository::get_option( 'auto_update', true ),
			'signal_sharing_enabled'    => 'enabled' === Cmatic_Options_Repository::get_option( 'signls.consent_status', 'unset' ),
			'debug'                     => (bool) Cmatic_Options_Repository::get_option( 'debug', false ),
			'backlink'                  => (bool) Cmatic_Options_Repository::get_option( 'backlink', false ),
			'total_features_enabled'    => $enabled,
			'features_usage_percentage' => round( ( $enabled / count( $flags ) ) * 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 ),
		);
	}

	private function setup( array $analysis ): array {
		$totals  = $this->outcome_totals( 'subscribe' );
		$stage   = 'installed';
		$percent = 0;
		if ( $analysis['credential_units'] > 0 ) {
			$stage   = 'connected';
			$percent = 20;
		}
		if ( $analysis['destination_units'] > 0 ) {
			$stage   = 'audience_selected';
			$percent = 40;
		}
		if ( $analysis['mapping_count'] > 0 ) {
			$stage   = 'fields_mapped';
			$percent = 60;
		}
		if ( $totals['attempts'] > 0 ) {
			$stage   = 'first_send';
			$percent = 80;
		}
		if ( $totals['successes'] > 0 ) {
			$stage   = 'first_success';
			$percent = 100;
		}
		return compact( 'stage', 'percent' );
	}

	private function merge_outcomes( array &$integrations, array &$operations ): void {
		$operations = array();
		$grouped    = array();
		foreach ( $this->lifetime_rows() as $row ) {
			$integration = isset( $row['integration_slug'] ) ? (string) $row['integration_slug'] : '';
			$operation   = isset( $row['operation_slug'] ) ? (string) $row['operation_slug'] : '';
			$counter     = isset( $row['counter_slug'] ) ? (string) $row['counter_slug'] : '';
			if ( ! isset( $integrations[ $integration ] ) || '' === $operation || '' === $counter ) {
				continue;
			}
			$grouped[ $integration ][ $operation ]['counters'][ $counter ] = max( 0, (int) ( isset( $row['counter_value'] ) ? $row['counter_value'] : 0 ) );
			foreach ( array( 'first_recorded_at', 'last_success_at', 'last_failure_at' ) as $time_key ) {
				$grouped[ $integration ][ $operation ][ $time_key ] = max( isset( $grouped[ $integration ][ $operation ][ $time_key ] ) ? $grouped[ $integration ][ $operation ][ $time_key ] : 0, (int) ( isset( $row[ $time_key ] ) ? $row[ $time_key ] : 0 ) );
			}
		}
		foreach ( $grouped as $integration => $by_operation ) {
			foreach ( $by_operation as $operation => $data ) {
				$counters                                   = $data['counters'];
				$integrations[ $integration ]['attempts']  += isset( $counters['attempts'] ) ? $counters['attempts'] : 0;
				$integrations[ $integration ]['successes'] += isset( $counters['successes'] ) ? $counters['successes'] : 0;
				$integrations[ $integration ]['failures']  += isset( $counters['failures'] ) ? $counters['failures'] : 0;
				$classes                                    = array();
				foreach ( array( 'transport_dns', 'transport_tls', 'transport_connect', 'transport_timeout', 'http_4xx', 'http_429', 'http_5xx', 'auth', 'configuration', 'validation', 'remote_rejected', 'unknown' ) as $class ) {
					$classes[ $class ] = isset( $counters[ 'failure_' . $class ] ) ? $counters[ 'failure_' . $class ] : 0;
				}
				$operations[] = array(
					'integration'      => $integration,
					'operation'        => $operation,
					'last_success_age' => $this->age_seconds_nullable( isset( $data['last_success_at'] ) ? $data['last_success_at'] : 0 ),
					'last_failure_age' => $this->age_seconds_nullable( isset( $data['last_failure_at'] ) ? $data['last_failure_at'] : 0 ),
					'failure_classes'  => $classes,
				);
			}
		}
	}

	private function outcome_totals( string $operation ): array {
		$result = array(
			'attempts'          => 0,
			'successes'         => 0,
			'failures'          => 0,
			'first_recorded_at' => 0,
			'last_success_at'   => 0,
			'last_failure_at'   => 0,
		);
		foreach ( $this->lifetime_rows() as $row ) {
			if ( $operation !== ( isset( $row['operation_slug'] ) ? (string) $row['operation_slug'] : '' ) ) {
				continue;
			}
			$counter = isset( $row['counter_slug'] ) ? (string) $row['counter_slug'] : '';
			if ( isset( $result[ $counter ] ) && in_array( $counter, array( 'attempts', 'successes', 'failures' ), true ) ) {
				$result[ $counter ] += max( 0, (int) $row['counter_value'] );
			}
			$first = (int) ( isset( $row['first_recorded_at'] ) ? $row['first_recorded_at'] : 0 );
			if ( $first > 0 ) {
				$result['first_recorded_at'] = 0 === $result['first_recorded_at'] ? $first : min( $result['first_recorded_at'], $first );
			}
			$result['last_success_at'] = max( $result['last_success_at'], (int) ( isset( $row['last_success_at'] ) ? $row['last_success_at'] : 0 ) );
			$result['last_failure_at'] = max( $result['last_failure_at'], (int) ( isset( $row['last_failure_at'] ) ? $row['last_failure_at'] : 0 ) );
		}
		return $result;
	}

	private function rolling_successes(): array {
		$by_date = array();
		foreach ( $this->daily_rows() as $row ) {
			if ( 'subscribe' !== ( isset( $row['operation_slug'] ) ? (string) $row['operation_slug'] : '' ) ) {
				continue;
			}
			$date = isset( $row['signal_date'] ) ? (string) $row['signal_date'] : '';
			if ( 1 === preg_match( '/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/', $date ) ) {
				$by_date[ $date ] = isset( $by_date[ $date ] ) ? $by_date[ $date ] + max( 0, (int) $row['successes'] ) : max( 0, (int) $row['successes'] );
			}
		}
		$values = array();
		$seven  = 0;
		for ( $offset = 29; $offset >= 0; --$offset ) {
			$date     = gmdate( 'Y-m-d', time() - $offset * DAY_IN_SECONDS );
			$value    = isset( $by_date[ $date ] ) ? $by_date[ $date ] : 0;
			$values[] = $value;
			if ( $offset <= 6 ) {
				$seven += $value;
			}
		}
		$sorted = $values;
		sort( $sorted, SORT_NUMERIC );
		$median = ( $sorted[14] + $sorted[15] ) / 2;
		return array(
			'seven'  => $seven,
			'thirty' => array_sum( $values ),
			'median' => round( $median, 2 ),
		);
	}

	private function lifetime_rows(): array {
		if ( null === $this->lifetime_rows ) {
			$this->lifetime_rows = \Signls\Sdk\V1\CounterStore::read_product( self::PRODUCT );
		}
		return is_array( $this->lifetime_rows ) ? $this->lifetime_rows : array();
	}

	private function daily_rows(): array {
		if ( null === $this->daily_rows ) {
			$this->daily_rows = \Signls\Sdk\V1\CounterStore::read_daily( self::PRODUCT, 30 );
		}
		return is_array( $this->daily_rows ) ? $this->daily_rows : array();
	}

	private function top_failure_reason(): array {
		if ( null === $this->reason_rows ) {
			$this->reason_rows = \Signls\Sdk\V1\CounterStore::read_reasons( self::PRODUCT );
		}
		$row = is_array( $this->reason_rows ) && isset( $this->reason_rows[0] ) && is_array( $this->reason_rows[0] ) ? $this->reason_rows[0] : array();
		return array(
			'code'   => isset( $row['reason_code'] ) && is_scalar( $row['reason_code'] ) ? (string) $row['reason_code'] : 'unknown',
			'sample' => isset( $row['sample'] ) && is_scalar( $row['sample'] ) ? (string) $row['sample'] : '',
		);
	}

	private function timestamps( string $key ): array {
		$source = Cmatic_Options_Repository::get_option( 'lifecycle.' . $key, array() );
		$items  = is_array( $source ) && isset( $source['items'] ) && is_array( $source['items'] ) ? $source['items'] : ( is_array( $source ) ? $source : array() );
		$items  = array_values(
			array_filter(
				array_map(
					static function ( $value ): int {
						return is_scalar( $value ) ? (int) $value : 0;
					},
					$items
				),
				static function ( int $value ): bool {
					return $value > 0;
				}
			)
		);
		sort( $items, SORT_NUMERIC );
		$reported = is_array( $source ) && isset( $source['reported_total'] ) && is_scalar( $source['reported_total'] ) ? max( count( $items ), (int) $source['reported_total'] ) : count( $items );
		$items    = array_slice( $items, -256 );
		return array(
			'items'          => $items,
			'reported_total' => $reported,
			'truncated'      => $reported > count( $items ),
		);
	}

	private function update_state(): array {
		$updates = get_site_transient( 'update_plugins' );
		$file    = 'contact-form-7-mailchimp-extension/chimpmatic-lite.php';
		$item    = is_object( $updates ) && isset( $updates->response[ $file ] ) ? $updates->response[ $file ] : null;
		return array(
			'available'       => is_object( $item ),
			'versions_behind' => is_object( $item ) ? 1 : 0,
		);
	}

	private function install_source( string $source ): string {
		$source = strtolower( sanitize_key( $source ) );
		if ( in_array( $source, array( 'wordpress', 'wordpress_org', 'wp_org', 'wporg', 'wp_org_search' ), true ) ) {
			return 'wp_org_search';
		}
		if ( in_array( $source, array( 'upload', 'manual', 'manual_upload' ), true ) ) {
			return 'manual_upload';
		}
		return in_array( $source, array( 'referral', 'migration', 'network' ), true ) ? $source : 'unknown';
	}

	private function sdk_state(): array {
		if ( ! class_exists( '\Signls\Sdk\V1\StateStore' ) ) {
			return array();
		}
		return ( new \Signls\Sdk\V1\StateStore( self::PRODUCT ) )->all();
	}

	private function provider_settings( array $config, string $provider ): array {
		return 'mailchimp' === $provider ? $config : ( isset( $config['providers'][ $provider ] ) && is_array( $config['providers'][ $provider ] ) ? $config['providers'][ $provider ] : array() );
	}

	private function has_configuration( array $settings, string $provider ): bool {
		foreach ( array( 'api-validation', 'list', 'merge_fields', 'field3', 'subscription_mode', 'doubleoptin' ) as $key ) {
			if ( ! empty( $settings[ $key ] ) ) {
				return true;
			}
		}
		return 'mailchimp' !== $provider && ! empty( $settings );
	}

	private function authentication( int $form_id, string $provider, array $config ): string {
		if ( 'mailchimp' === $provider ) {
			if ( ! empty( $config['api'] ) ) {
				return 'api_key';
			}
			return ( new Cmatic_Lite_Auth_Manager() )->has_oauth( $form_id ) ? 'oauth' : 'none';
		}
		return Cmatic_Lite_Esp_Credentials::has( $form_id, $provider ) ? 'api_key' : 'none';
	}

	private function has_destination( array $settings ): bool {
		$list = isset( $settings['list'] ) ? $settings['list'] : '';
		return is_array( $list ) ? ! empty( array_filter( $list ) ) : '' !== trim( is_scalar( $list ) ? (string) $list : '' );
	}

	private function mapping_count( array $settings ): int {
		$count = 0;
		foreach ( $settings as $key => $value ) {
			if ( 1 === preg_match( '/^field[0-9]+$/', (string) $key ) && is_scalar( $value ) && '' !== trim( (string) $value ) ) {
				++$count;
			}
		}
		if ( 0 === $count ) {
			for ( $index = 1; $index <= 50; $index++ ) {
				$count += ! empty( $settings[ 'CustomKey' . $index ] ) && ! empty( $settings[ 'CustomValue' . $index ] ) ? 1 : 0;
			}
		}
		return $count;
	}

	private function count_narrow_features( array &$features, array $settings, string $provider, int $mappings ): void {
		$checks = array(
			'advanced_fields'     => $mappings > 4,
			'tags_management'     => ! empty( $settings['tags'] ) || ! empty( $settings['tag'] ) || ! empty( $settings['labeltags'] ),
			'groups_segments'     => ! empty( $settings['groups'] ) || ! empty( $settings['interests'] ) || ! empty( $settings['base_groups'] ),
			'gdpr_fields'         => ! empty( $settings['gdpr'] ) || ! empty( $settings['marketing_permissions'] ),
			'conditional_logic'   => ! empty( $settings['conditions'] ) || ! empty( $settings['conditional_logic'] ),
			'double_optin'        => ! empty( $settings['confsubs'] ) || 'double' === ( isset( $settings['subscription_mode'] ) ? $settings['subscription_mode'] : '' ),
			'visitor_geolocation' => ! empty( $settings['visitor_geolocation'] ) || ! empty( $settings['geolocation'] ),
			'unsubscribe'         => ! empty( $settings['addunsubscr'] ),
			'multi_group_routing' => 'mailerlite' === $provider && ( ! empty( $settings['base_groups'] ) || ! empty( $settings['routing_rules'] ) ),
			'status_control'      => ! empty( $settings['status_mode'] ) || ! empty( $settings['status'] ),
			'consent_metadata'    => ! empty( $settings['consent_field'] ) || ! empty( $settings['consent_metadata_enabled'] ),
		);
		foreach ( $checks as $slug => $enabled ) {
			$features[ $slug ] += $enabled ? 1 : 0;
		}
	}

	private function pro_version( bool $installed ): string {
		if ( defined( 'CMATIC_VERSION' ) ) {
			return (string) CMATIC_VERSION;
		}
		if ( ! $installed ) {
			return 'unknown';
		}
		$data = get_file_data( WP_PLUGIN_DIR . '/chimpmatic/chimpmatic.php', array( 'version' => 'Version' ) );
		return ! empty( $data['version'] ) ? (string) $data['version'] : 'unknown';
	}

	private function age_days( int $timestamp ): int {
		return $timestamp > 0 ? (int) floor( max( 0, time() - $timestamp ) / DAY_IN_SECONDS ) : 0;
	}

	private function age_hours( int $timestamp ): int {
		return $timestamp > 0 ? (int) floor( max( 0, time() - $timestamp ) / HOUR_IN_SECONDS ) : 0;
	}

	private function age_seconds_nullable( int $timestamp ) {
		return $timestamp > 0 ? max( 0, time() - $timestamp ) : null;
	}

	private function average_session( array $activations, array $deactivations ): int {
		$total = 0;
		$count = 0;
		foreach ( $activations as $index => $activated ) {
			if ( isset( $deactivations[ $index ] ) && $deactivations[ $index ] >= $activated ) {
				$total += $deactivations[ $index ] - $activated;
				++$count;
			}
		}
		return $count > 0 ? (int) floor( $total / $count ) : 0;
	}

	private function busiest( array $distribution, int $minimum, int $maximum ): array {
		$result = array(
			'key'   => $minimum,
			'count' => 0,
		);
		foreach ( $distribution as $key => $count ) {
			$key   = (int) $key;
			$count = max( 0, (int) $count );
			if ( $key >= $minimum && $key <= $maximum && $count > $result['count'] ) {
				$result = array(
					'key'   => $key,
					'count' => $count,
				);
			}
		}
		return $result;
	}

	private function mysql_version(): string {
		global $wpdb;
		if ( ! is_object( $wpdb ) || ! is_callable( array( $wpdb, 'db_version' ) ) ) {
			return '';
		}
		return (string) $wpdb->db_version();
	}

	private static function nonnegative_int( $value ): int {
		return is_scalar( $value ) && is_numeric( $value ) ? max( 0, (int) $value ) : 0;
	}

	private function server_software(): string {
		return isset( $_SERVER['SERVER_SOFTWARE'] ) && is_scalar( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER['SERVER_SOFTWARE'] ) ) : '';
	}
}
                includes/services/signls/class-cmatic-lite-signls-forms-collector.php                               0000644                 00000045350 15266052620 0022123 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * ChimpMatic Lite forms and audiences Signls collector.
 *
 * @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_Signls_Forms_Collector {

	private const MAX_FORMS         = 100;
	private const MAX_DETAILS       = 50;
	private const MAX_FIELDS        = 30;
	private const MAX_MAPPINGS      = 30;
	private const MAX_AUDIENCES     = 100;
	private const MAX_REMOTE_FIELDS = 50;

	private $forms;
	private $configurations;

	public function collect(): array {
		$forms          = $this->forms();
		$configurations = $this->configurations();
		$total_forms    = $this->total_form_count();
		$audiences      = $this->audiences( $configurations );
		$details        = array();
		$field_types    = array();
		$total_fields   = 0;
		$total_mappings = 0;
		$total_remote   = 0;
		$active_forms   = 0;
		$with_api       = 0;
		$with_lists     = 0;
		$total_lists    = 0;
		$with_sends     = 0;
		$never_sent     = 0;
		$with_double    = 0;
		$with_consent   = 0;
		$total_sends    = 0;
		$oldest         = 0;
		$newest         = 0;
		$field_counts   = array();
		$list_counts    = array();

		foreach ( $forms as $form_id => $form ) {
			$config        = isset( $configurations[ $form_id ]['settings'] ) ? $configurations[ $form_id ]['settings'] : array();
			$provider      = Cmatic_Lite_Esp_Registry::get_selected( $config );
			$settings      = $this->provider_settings( $config, $provider );
			$authenticated = $this->has_authentication( $form_id, $provider, $config );
			$lists         = $this->cached_lists( $settings );
			$selected      = $this->selected_list( $settings );
			$tags          = is_object( $form ) && is_callable( array( $form, 'scan_form_tags' ) ) ? (array) $form->scan_form_tags() : array();
			$fields        = $this->fields( $tags, $field_types );
			$mappings      = $this->mappings( $settings, $fields );
			$features      = $this->features( $settings );
			$submissions   = isset( $configurations[ $form_id ]['submissions'] ) ? max( 0, (int) $configurations[ $form_id ]['submissions'] ) : 0;

			if ( $authenticated ) {
				++$active_forms;
				++$with_api;
			}
			if ( ! empty( $lists ) ) {
				++$with_lists;
			}
			$total_lists += count( $lists );
			if ( $submissions > 0 ) {
				++$with_sends;
			} else {
				++$never_sent;
			}
			$with_double    += $features['double_optin'] ? 1 : 0;
			$with_consent   += $features['required_consent'] ? 1 : 0;
			$total_sends    += $submissions;
			$total_fields   += count( $fields );
			$total_mappings += $mappings['reported_total'];
			$total_remote   += $mappings['remote_total'];
			$field_counts[]  = count( $fields );
			$list_counts[]   = count( $lists );

			$created = get_post_field( 'post_date', $form_id, 'raw' );
			$created = is_string( $created ) ? (int) strtotime( $created ) : 0;
			if ( $created > 0 ) {
				$oldest = 0 === $oldest ? $created : min( $oldest, $created );
				$newest = max( $newest, $created );
			}

			if ( count( $details ) < self::MAX_DETAILS ) {
				$details[] = array(
					'form_id_sha256'            => hash( 'sha256', (string) $form_id ),
					'field_count'               => count( $fields ),
					'fields'                    => array(
						'items'          => array_slice( $fields, 0, self::MAX_FIELDS ),
						'reported_total' => count( $fields ),
						'truncated'      => count( $fields ) > self::MAX_FIELDS,
					),
					'paired_audience_id_sha256' => '' !== $selected ? hash( 'sha256', $selected ) : null,
					'mappings'                  => array(
						'items'          => array_slice( $mappings['items'], 0, self::MAX_MAPPINGS ),
						'reported_total' => $mappings['reported_total'],
						'truncated'      => $mappings['reported_total'] > self::MAX_MAPPINGS,
					),
					'unmapped_cf7_fields'       => $this->unmapped_fields( $fields, $mappings['mapped_fields'] ),
					'unmapped_mc_fields'        => max( 0, $mappings['remote_total'] - $mappings['reported_total'] ),
					'features'                  => $features,
				);
			}
		}

		usort(
			$details,
			static function ( array $left, array $right ): int {
				return strcmp( $left['form_id_sha256'], $right['form_id_sha256'] );
			}
		);
		ksort( $field_types, SORT_STRING );
		$processed      = count( $forms );
		$now            = time();
		$audience_items = array_slice( $audiences, 0, self::MAX_AUDIENCES );

		return array(
			'forms' => array(
				'total_forms'                 => $total_forms,
				'processed_forms'             => $processed,
				'active_forms'                => $active_forms,
				'forms_with_api'              => $with_api,
				'forms_with_lists'            => $with_lists,
				'inactive_forms'              => max( 0, $processed - $active_forms ),
				'total_audiences'             => count( $audiences ),
				'audiences'                   => array(
					'items'          => $audience_items,
					'reported_total' => count( $audiences ),
					'truncated'      => count( $audiences ) > self::MAX_AUDIENCES,
				),
				'total_contacts'              => array_sum( array_column( $audiences, 'member_count' ) ),
				'avg_lists_per_form'          => $processed > 0 ? round( $total_lists / $processed, 2 ) : null,
				'max_lists_per_form'          => ! empty( $list_counts ) ? max( $list_counts ) : 0,
				'total_fields_all_forms'      => $total_fields,
				'avg_fields_per_form'         => $processed > 0 ? round( $total_fields / $processed, 2 ) : null,
				'min_fields_per_form'         => ! empty( $field_counts ) ? min( $field_counts ) : 0,
				'max_fields_per_form'         => ! empty( $field_counts ) ? max( $field_counts ) : 0,
				'oldest_form_created'         => $oldest,
				'newest_form_created'         => $newest,
				'days_since_oldest_form'      => $oldest > 0 ? (int) floor( max( 0, $now - $oldest ) / DAY_IN_SECONDS ) : 0,
				'days_since_newest_form'      => $newest > 0 ? (int) floor( max( 0, $now - $newest ) / DAY_IN_SECONDS ) : 0,
				'forms_with_submissions'      => $with_sends,
				'forms_never_submitted'       => $never_sent,
				'forms_with_double_opt'       => $with_double,
				'forms_with_consent'          => $with_consent,
				'total_submissions_all_forms' => $total_sends,
				'form_utilization_rate'       => $processed > 0 ? round( ( $active_forms / $processed ) * 100, 2 ) : null,
				'forms_detail'                => array(
					'items'          => $details,
					'reported_total' => $processed,
					'truncated'      => $processed > self::MAX_DETAILS,
				),
				'forms_truncated'             => $total_forms > $processed,
				'forms_detail_truncated'      => $processed > self::MAX_DETAILS,
				'field_types_aggregate'       => $field_types,
				'mapping_stats'               => array(
					'total_cf7_fields' => $total_fields,
					'total_mc_fields'  => $total_remote,
					'mapped_fields'    => $total_mappings,
					'mapping_rate'     => $total_fields > 0 ? min( 100, round( ( $total_mappings / $total_fields ) * 100, 2 ) ) : null,
				),
			),
		);
	}

	public function configurations(): array {
		if ( null !== $this->configurations ) {
			return $this->configurations;
		}
		global $wpdb;

		$form_ids = array_keys( $this->forms() );
		$result   = array();
		foreach ( $form_ids as $form_id ) {
			$result[ $form_id ] = array(
				'settings'        => array(),
				'submissions'     => 0,
				'last_submission' => 0,
			);
		}
		if ( empty( $form_ids ) ) {
			$this->configurations = $result;
			return $result;
		}

		$names = array();
		foreach ( $form_ids as $form_id ) {
			$names[] = 'cf7_mch_' . $form_id;
			$names[] = 'cf7_mch_submissions_' . $form_id;
			$names[] = 'cf7_mch_last_submission_' . $form_id;
		}
		$placeholders = implode( ',', array_fill( 0, count( $names ), '%s' ) );
		$query        = $wpdb->prepare(
			"SELECT option_name,option_value FROM {$wpdb->options} WHERE option_name IN ({$placeholders}) ORDER BY option_name ASC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Placeholder count and table are generated from bounded local IDs.
			...$names
		);
		$rows         = $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- One bounded ordered read avoids per-form queries.
		foreach ( is_array( $rows ) ? $rows : array() as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}
			$name_candidate  = isset( $row['option_name'] ) ? $row['option_name'] : '';
			$value_candidate = isset( $row['option_value'] ) ? $row['option_value'] : null;
			$name            = is_scalar( $name_candidate ) ? (string) $name_candidate : '';
			$value           = is_scalar( $value_candidate ) ? maybe_unserialize( (string) $value_candidate ) : null;
			if ( 1 === preg_match( '/^cf7_mch_([0-9]+)$/', $name, $match ) && isset( $result[ (int) $match[1] ] ) ) {
				$result[ (int) $match[1] ]['settings'] = is_array( $value ) ? $value : array();
			} elseif ( 1 === preg_match( '/^cf7_mch_submissions_([0-9]+)$/', $name, $match ) && isset( $result[ (int) $match[1] ] ) ) {
				$result[ (int) $match[1] ]['submissions'] = self::nonnegative_int( $value );
			} elseif ( 1 === preg_match( '/^cf7_mch_last_submission_([0-9]+)$/', $name, $match ) && isset( $result[ (int) $match[1] ] ) ) {
				$result[ (int) $match[1] ]['last_submission'] = self::nonnegative_int( $value );
			}
		}
		$this->configurations = $result;
		return $result;
	}

	private function forms(): array {
		if ( null !== $this->forms ) {
			return $this->forms;
		}
		$this->forms = array();
		if ( ! class_exists( 'WPCF7_ContactForm' ) ) {
			return $this->forms;
		}
		$ids = get_posts(
			array(
				'post_type'      => 'wpcf7_contact_form',
				'posts_per_page' => self::MAX_FORMS,
				'post_status'    => 'publish',
				'fields'         => 'ids',
				'orderby'        => 'modified',
				'order'          => 'DESC',
			)
		);
		sort( $ids, SORT_NUMERIC );
		foreach ( $ids as $id ) {
			$form = WPCF7_ContactForm::get_instance( $id );
			if ( $form ) {
				$this->forms[ (int) $id ] = $form;
			}
		}
		return $this->forms;
	}

	private function total_form_count(): int {
		$counts = wp_count_posts( 'wpcf7_contact_form' );
		return is_object( $counts ) && isset( $counts->publish ) ? max( 0, (int) $counts->publish ) : count( $this->forms() );
	}

	private function audiences( array $configurations ): array {
		$rows   = array();
		$global = Cmatic_Options_Repository::get_option( 'lisdata', array() );
		$this->merge_audiences( $rows, 'mailchimp', is_array( $global ) ? $global : array(), '' );
		foreach ( $configurations as $configuration ) {
			$config   = isset( $configuration['settings'] ) && is_array( $configuration['settings'] ) ? $configuration['settings'] : array();
			$provider = Cmatic_Lite_Esp_Registry::get_selected( $config );
			$settings = $this->provider_settings( $config, $provider );
			$this->merge_audiences( $rows, $provider, isset( $settings['lisdata'] ) && is_array( $settings['lisdata'] ) ? $settings['lisdata'] : array(), $this->selected_list( $settings ) );
		}
		ksort( $rows, SORT_STRING );
		return array_values( $rows );
	}

	private function merge_audiences( array &$rows, string $provider, array $list_data, string $selected ): void {
		$lists = isset( $list_data['lists'] ) && is_array( $list_data['lists'] ) ? $list_data['lists'] : array();
		foreach ( $lists as $list ) {
			if ( ! is_array( $list ) || ! isset( $list['id'] ) || ! is_scalar( $list['id'] ) || '' === (string) $list['id'] ) {
				continue;
			}
			$id    = (string) $list['id'];
			$key   = $provider . '|' . $id;
			$stats = isset( $list['stats'] ) && is_array( $list['stats'] ) ? $list['stats'] : array();
			if ( ! isset( $rows[ $key ] ) ) {
				$member_count      = isset( $stats['member_count'] ) ? $stats['member_count'] : ( isset( $list['member_count'] ) ? $list['member_count'] : 0 );
				$merge_field_count = isset( $stats['merge_field_count'] ) ? $stats['merge_field_count'] : ( isset( $list['field_count'] ) ? $list['field_count'] : 0 );
				$campaign_count    = isset( $stats['campaign_count'] ) ? $stats['campaign_count'] : 0;
				$rows[ $key ]      = array(
					'audience_id_sha256'    => hash( 'sha256', $id ),
					'member_count'          => self::nonnegative_int( $member_count ),
					'merge_field_count'     => self::nonnegative_int( $merge_field_count ),
					'double_optin'          => ! empty( $list['double_optin'] ) || 'double' === ( isset( $list['opt_in_process'] ) ? $list['opt_in_process'] : '' ),
					'marketing_permissions' => ! empty( $list['marketing_permissions'] ),
					'campaign_count'        => self::nonnegative_int( $campaign_count ),
					'is_paired'             => false,
				);
			}
			if ( '' !== $selected && hash_equals( $selected, $id ) ) {
				$rows[ $key ]['is_paired'] = true;
			}
		}
	}

	private function cached_lists( array $settings ): array {
		return isset( $settings['lisdata']['lists'] ) && is_array( $settings['lisdata']['lists'] ) ? $settings['lisdata']['lists'] : array();
	}

	private function fields( array $tags, array &$aggregate ): array {
		$fields = array();
		foreach ( $tags as $tag ) {
			$name = is_object( $tag ) && isset( $tag->name ) ? $tag->name : ( is_array( $tag ) && isset( $tag['name'] ) ? $tag['name'] : '' );
			$type = is_object( $tag ) && isset( $tag->basetype ) ? $tag->basetype : ( is_array( $tag ) && isset( $tag['basetype'] ) ? $tag['basetype'] : '' );
			if ( ! is_scalar( $name ) || ! is_scalar( $type ) || '' === (string) $name || '' === (string) $type ) {
				continue;
			}
			$name               = (string) $name;
			$type               = (string) $type;
			$fields[]           = array(
				'name' => $name,
				'type' => $type,
			);
			$aggregate[ $type ] = isset( $aggregate[ $type ] ) ? $aggregate[ $type ] + 1 : 1;
		}
		return $fields;
	}

	private function mappings( array $settings, array $fields ): array {
		$definitions = isset( $settings['merge_fields'] ) && is_array( $settings['merge_fields'] ) ? array_values( $settings['merge_fields'] ) : array();
		if ( empty( $definitions ) ) {
			for ( $index = 1; $index <= self::MAX_REMOTE_FIELDS; $index++ ) {
				$tag = isset( $settings[ 'CustomKey' . $index ] ) && is_scalar( $settings[ 'CustomKey' . $index ] ) ? (string) $settings[ 'CustomKey' . $index ] : '';
				if ( '' !== $tag ) {
					$definitions[] = array(
						'tag'             => $tag,
						'type'            => isset( $settings[ 'CustomKeyType' . $index ] ) ? (string) $settings[ 'CustomKeyType' . $index ] : 'text',
						'_legacy_mapping' => isset( $settings[ 'CustomValue' . $index ] ) ? (string) $settings[ 'CustomValue' . $index ] : '',
					);
				}
			}
		}
		$items  = array();
		$mapped = array();
		foreach ( array_slice( $definitions, 0, self::MAX_REMOTE_FIELDS ) as $offset => $definition ) {
			if ( ! is_array( $definition ) || ! isset( $definition['tag'] ) || ! is_scalar( $definition['tag'] ) || '' === (string) $definition['tag'] ) {
				continue;
			}
			$field = isset( $definition['_legacy_mapping'] ) ? $definition['_legacy_mapping'] : ( isset( $settings[ 'field' . ( $offset + 3 ) ] ) ? $settings[ 'field' . ( $offset + 3 ) ] : '' );
			$field = is_scalar( $field ) ? trim( (string) $field ) : '';
			if ( '' === $field ) {
				continue;
			}
			$items[]  = array(
				'cf7_field' => $field,
				'mc_tag'    => (string) $definition['tag'],
				'mc_type'   => isset( $definition['type'] ) && is_scalar( $definition['type'] ) ? (string) $definition['type'] : 'text',
			);
			$mapped[] = trim( $field, "[] \t\n\r\0\x0B" );
		}
		$reported = count( $items );
		$remote   = isset( $settings['total_merge_fields'] ) ? max( count( $definitions ), self::nonnegative_int( $settings['total_merge_fields'] ) ) : count( $definitions );
		return array(
			'items'          => $items,
			'reported_total' => $reported,
			'remote_total'   => max( 0, $remote ),
			'mapped_fields'  => $mapped,
		);
	}

	private function unmapped_fields( array $fields, array $mapped ): int {
		$count = 0;
		foreach ( $fields as $field ) {
			if ( ! in_array( $field['name'], $mapped, true ) ) {
				++$count;
			}
		}
		return $count;
	}

	private function features( array $settings ): array {
		$tags   = isset( $settings['labeltags'] ) && is_array( $settings['labeltags'] ) ? array_filter( $settings['labeltags'] ) : array();
		$groups = false;
		for ( $index = 1; $index <= 20; $index++ ) {
			if ( ! empty( $settings[ 'ggCustomKey' . $index ] ) && ! empty( trim( (string) ( isset( $settings[ 'ggCustomValue' . $index ] ) ? $settings[ 'ggCustomValue' . $index ] : '' ) ) ) ) {
				$groups = true;
				break;
			}
		}
		return array(
			'double_optin'        => ! empty( $settings['confsubs'] ) || 'double' === ( isset( $settings['subscription_mode'] ) ? $settings['subscription_mode'] : '' ),
			'required_consent'    => ! empty( $settings['accept'] ) || ! empty( $settings['consent_required'] ) || 'required' === ( isset( $settings['consent_gate'] ) ? $settings['consent_gate'] : '' ),
			'debug_logger'        => ! empty( $settings['logfileEnabled'] ),
			'tags_enabled'        => ! empty( $tags ) || ! empty( $settings['tags'] ),
			'interest_groups'     => $groups || ! empty( $settings['groups'] ) || ! empty( $settings['base_groups'] ),
			'custom_merge_fields' => $this->has_custom_fields( $settings ),
			'conditional_logic'   => ! empty( $settings['conditional_logic'] ) || ! empty( $settings['conditions'] ),
		);
	}

	private function has_custom_fields( array $settings ): bool {
		$defaults = array( 'EMAIL', 'FNAME', 'LNAME', 'ADDRESS', 'PHONE' );
		$fields   = isset( $settings['merge_fields'] ) && is_array( $settings['merge_fields'] ) ? $settings['merge_fields'] : array();
		if ( empty( $fields ) && isset( $settings['merge-vars'] ) && is_array( $settings['merge-vars'] ) ) {
			$fields = $settings['merge-vars'];
		}
		foreach ( $fields as $field ) {
			if ( is_array( $field ) && isset( $field['tag'] ) && is_scalar( $field['tag'] ) && ! in_array( strtoupper( (string) $field['tag'] ), $defaults, true ) ) {
				return true;
			}
		}
		return false;
	}

	private function provider_settings( array $config, string $provider ): array {
		return 'mailchimp' === $provider ? $config : ( isset( $config['providers'][ $provider ] ) && is_array( $config['providers'][ $provider ] ) ? $config['providers'][ $provider ] : array() );
	}

	private function selected_list( array $settings ): string {
		$list = isset( $settings['list'] ) ? $settings['list'] : '';
		if ( is_array( $list ) ) {
			$list = reset( $list );
		}
		return is_scalar( $list ) ? trim( (string) $list ) : '';
	}

	private function has_authentication( int $form_id, string $provider, array $config ): bool {
		if ( 'mailchimp' === $provider ) {
			return ! empty( $config['api'] ) || ( new Cmatic_Lite_Auth_Manager() )->has_oauth( $form_id );
		}
		return Cmatic_Lite_Esp_Credentials::has( $form_id, $provider );
	}

	private static function nonnegative_int( $value ): int {
		return is_scalar( $value ) && is_numeric( $value ) ? max( 0, (int) $value ) : 0;
	}
}
                                                                                                                                                                                                                                                                                        includes/services/signls/class-cmatic-lite-signls-contract.php                                      0000644                 00000050143 15266052620 0020622 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * ChimpMatic Lite rich Signls contract.
 *
 * @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_Signls_Contract {

	private const UINT_MAX = 4294967295;

	private const INTEGRATIONS = array( 'mailchimp', 'brevo', 'mailerlite', 'klaviyo' );

	private const FEATURES = array(
		'advanced_fields',
		'tags_management',
		'groups_segments',
		'gdpr_fields',
		'conditional_logic',
		'double_optin',
		'visitor_geolocation',
		'unsubscribe',
		'multi_group_routing',
		'status_control',
		'consent_metadata',
		'debug_logging',
		'auto_updates',
	);

	public static function get(): array {
		return array(
			'snapshot_schema_version' => 2,
			'observation_profile'     => 'chimpmatic_connector_v1',
			'integrations'            => self::INTEGRATIONS,
			'features'                => self::FEATURES,
			'operations'              => array( 'subscribe', 'connect', 'disconnect', 'refresh_schema' ),
			'companions'              => array( 'chimpmatic' ),
			'observation_schema'      => self::object(
				array(
					'install'           => self::install(),
					'metadata'          => self::metadata(),
					'lifecycle'         => self::lifecycle(),
					'environment'       => self::environment(),
					'api'               => self::api(),
					'submissions'       => self::submissions(),
					'features'          => self::rich_features(),
					'forms'             => self::forms(),
					'performance'       => self::performance(),
					'plugins'           => self::plugins(),
					'competitors'       => self::competitors(),
					'server'            => self::server(),
					'wordpress'         => self::wordpress(),
					'legacy_lifecycle'  => self::legacy_lifecycle(),
					'opportunities'     => self::opportunities(),
					'collection_status' => self::enum( array( 'complete', 'partial', 'failed' ), 'failed' ),
					'missing_sections'  => self::list_of(
						self::enum(
							array( 'install', 'metadata', 'lifecycle', 'environment', 'api', 'submissions', 'features', 'forms', 'performance', 'plugins', 'competitors', 'server', 'wordpress', 'legacy_lifecycle' )
						),
						14
					),
				)
			),
		);
	}

	private static function install(): array {
		return self::object(
			array(
				'plugin_slug' => self::slug( 100 ),
				'quest'       => self::uint(),
				'pro'         => self::object(
					array(
						'installed'       => self::bool(),
						'activated'       => self::bool(),
						'version'         => self::version( true ),
						'licensed'        => self::bool(),
						'license_expires' => self::uint( self::UINT_MAX, true ),
					)
				),
			)
		);
	}

	private static function metadata(): array {
		return self::object(
			array(
				'schedule'             => self::enum( array( 'daily', 'frequent', 'sparse', 'weekly', 'disabled' ), 'daily' ),
				'frequent_started_at'  => self::uint(),
				'is_reactivation'      => self::bool(),
				'disabled_count'       => self::uint(),
				'opt_in_date'          => self::uint(),
				'last_heartbeat'       => self::uint(),
				'failed_heartbeats'    => self::uint(),
				'total_uptime_seconds' => self::uint(),
				'signal_version'       => self::version(),
			)
		);
	}

	private static function lifecycle(): array {
		$properties = array();
		foreach ( array( 'activation_count', 'deactivation_count', 'upgrade_count', 'first_activated', 'last_activated', 'last_deactivated', 'last_upgrade', 'days_since_first_activation', 'days_since_last_upgrade', 'avg_session_length_seconds', 'total_sessions', 'version_history_count', 'days_on_current_version' ) as $field ) {
			$properties[ $field ] = self::uint();
		}
		$properties['previous_version']        = self::version( true );
		$properties['install_method']          = self::enum( array( 'wp_org_search', 'referral', 'migration', 'manual_upload', 'network', 'unknown' ), 'unknown' );
		$properties['activation_timestamps']   = self::accounted_list( self::uint(), 256 );
		$properties['deactivation_timestamps'] = self::accounted_list( self::uint(), 256 );
		$properties['upgrade_timestamps']      = self::accounted_list( self::uint(), 256 );
		$properties['active_session']          = self::bool();
		return self::object( $properties );
	}

	private static function environment(): array {
		$properties = array();
		foreach ( array( 'php_version', 'php_curl_version', 'wp_version', 'mysql_version', 'mysql_client_version', 'theme_version', 'cf7_version', 'plugin_version' ) as $field ) {
			$properties[ $field ] = self::version( true );
		}
		foreach ( array( 'php_sapi', 'php_os', 'php_architecture', 'php_memory_limit', 'php_post_max_size', 'php_upload_max_filesize', 'php_default_timezone', 'php_log_errors', 'php_critical_extensions', 'php_openssl_version', 'wp_memory_limit', 'wp_max_memory_limit', 'wp_auto_update_core', 'db_charset', 'db_collate', 'server_software', 'server_protocol', 'locale', 'timezone', 'site_language', 'site_charset', 'permalink_structure', 'theme', 'theme_author', 'parent_theme' ) as $field ) {
			$properties[ $field ] = self::text();
		}
		foreach ( array( 'php_max_execution_time', 'php_max_input_time', 'php_max_input_vars', 'php_extensions_count', 'wp_db_version', 'db_prefix_length', 'server_port', 'active_plugins_count', 'total_plugins_count', 'must_use_plugins_count', 'network_count' ) as $field ) {
			$properties[ $field ] = self::uint();
		}
		foreach ( array( 'wp_debug', 'wp_debug_log', 'wp_debug_display', 'script_debug', 'wp_cache', 'wp_cron_disabled', 'https', 'is_child_theme', 'theme_supports_html5', 'theme_supports_post_thumbnails', 'is_multisite', 'is_subdomain_install', 'is_main_site', 'cf7_installed' ) as $field ) {
			$properties[ $field ] = self::bool();
		}
		foreach ( array( 'http_host_sha256', 'home_url_sha256', 'site_url_sha256', 'admin_email_sha256' ) as $field ) {
			$properties[ $field ] = self::sha256();
		}
		$properties['user_agent'] = self::text( 512 );
		return self::object( $properties );
	}

	private static function api(): array {
		$properties = array(
			'is_connected'            => self::bool(),
			'api_data_center'         => self::slug( 100 ),
			'success_rate'            => self::decimal( 0, 100, true ),
			'uptime_percentage'       => self::decimal( 0, 100, true ),
			'error_codes'             => self::map_of( self::uint(), 64, 100 ),
			'api_health_score'        => self::decimal( 0, 100, true ),
			'setup_sync_attempted'    => self::bool(),
			'setup_first_success'     => self::bool(),
			'setup_first_failure'     => self::bool(),
			'setup_audience_selected' => self::bool(),
		);
		foreach ( array( 'forms_with_api', 'api_key_length', 'first_connected', 'total_attempts', 'total_successes', 'total_failures', 'last_success', 'last_failure', 'days_since_last_success', 'days_since_last_failure', 'avg_response_time_ms', 'setup_sync_attempts_count', 'setup_failure_count' ) as $field ) {
			$properties[ $field ] = self::uint();
		}
		return self::object( $properties );
	}

	private static function submissions(): array {
		$properties = array();
		foreach ( array( 'total_sent', 'total_failed', 'total_submissions', 'successful_submissions_count', 'failed_count', 'first_submission', 'last_submission', 'last_success', 'last_failure', 'days_since_first', 'days_since_last', 'hours_since_last', 'submissions_busiest_hour', 'submissions_busiest_day', 'this_month', 'last_month', 'peak_month', 'consecutive_successes', 'consecutive_failures', 'longest_success_streak', 'active_forms_count', 'forms_with_submissions' ) as $field ) {
			$properties[ $field ] = self::uint();
		}
		foreach ( array( 'success_rate', 'avg_per_day', 'avg_per_week', 'avg_per_month' ) as $field ) {
			$properties[ $field ] = self::decimal( 0, self::UINT_MAX );
		}
		$properties['success_rate']['nullable'] = true;
		$properties['busiest_hour']             = self::int( 0, 23 );
		$properties['busiest_day']              = self::int( 0, 6 );
		$properties['month_over_month_change']  = self::decimal( -100, self::UINT_MAX, true );
		return self::object( $properties );
	}

	private static function rich_features(): array {
		$properties = array();
		foreach ( array( 'double_optin_count', 'required_consent_count', 'debug_logger_count', 'custom_merge_fields_count', 'interest_groups_count', 'groups_total_mapped', 'tags_enabled_count', 'tags_total_selected', 'arbitrary_tags_count', 'conditional_logic_count', 'total_features_enabled' ) as $field ) {
			$properties[ $field ] = self::uint();
		}
		foreach ( array( 'double_optin', 'required_consent', 'debug_logger', 'custom_merge_fields', 'interest_groups', 'tags_enabled', 'arbitrary_tags', 'conditional_logic', 'auto_update', 'signal_sharing_enabled', 'debug', 'backlink', 'webhook_enabled', 'custom_api_endpoint', 'email_notifications', 'test_modal_used', 'contact_lookup_used' ) as $field ) {
			$properties[ $field ] = self::bool();
		}
		$properties['features_usage_percentage'] = self::decimal( 0, 100 );
		return self::object( $properties );
	}

	private static function forms(): array {
		$audience      = self::object(
			array(
				'audience_id_sha256'    => self::sha256(),
				'member_count'          => self::uint(),
				'merge_field_count'     => self::uint(),
				'double_optin'          => self::bool(),
				'marketing_permissions' => self::bool(),
				'campaign_count'        => self::uint(),
				'is_paired'             => self::bool(),
			)
		);
		$field         = self::object(
			array(
				'name' => self::text(),
				'type' => self::text(),
			)
		);
		$mapping       = self::object(
			array(
				'cf7_field' => self::text(),
				'mc_tag'    => self::text(),
				'mc_type'   => self::text(),
			)
		);
		$form_features = array();
		foreach ( array( 'double_optin', 'required_consent', 'debug_logger', 'tags_enabled', 'interest_groups', 'custom_merge_fields', 'conditional_logic' ) as $name ) {
			$form_features[ $name ] = self::bool();
		}
		$form       = self::object(
			array(
				'form_id_sha256'            => self::sha256(),
				'field_count'               => self::uint(),
				'fields'                    => self::accounted_list( $field, 30 ),
				'paired_audience_id_sha256' => self::sha256( true ),
				'mappings'                  => self::accounted_list( $mapping, 30 ),
				'unmapped_cf7_fields'       => self::uint(),
				'unmapped_mc_fields'        => self::uint(),
				'features'                  => self::object( $form_features ),
			)
		);
		$properties = array();
		foreach ( array( 'total_forms', 'processed_forms', 'active_forms', 'forms_with_api', 'forms_with_lists', 'inactive_forms', 'total_audiences', 'total_contacts', 'max_lists_per_form', 'total_fields_all_forms', 'min_fields_per_form', 'max_fields_per_form', 'oldest_form_created', 'newest_form_created', 'days_since_oldest_form', 'days_since_newest_form', 'forms_with_submissions', 'forms_never_submitted', 'forms_with_double_opt', 'forms_with_consent', 'total_submissions_all_forms' ) as $name ) {
			$properties[ $name ] = self::uint();
		}
		foreach ( array( 'avg_lists_per_form', 'avg_fields_per_form', 'form_utilization_rate' ) as $name ) {
			$properties[ $name ] = self::decimal( 0, self::UINT_MAX, true );
		}
		$properties['audiences']              = self::accounted_list( $audience, 100, 'audience_id_sha256' );
		$properties['forms_detail']           = self::accounted_list( $form, 50, 'form_id_sha256' );
		$properties['forms_truncated']        = self::bool();
		$properties['forms_detail_truncated'] = self::bool();
		$properties['field_types_aggregate']  = self::map_of( self::uint(), 100, 100 );
		$properties['mapping_stats']          = self::object(
			array(
				'total_cf7_fields' => self::uint(),
				'total_mc_fields'  => self::uint(),
				'mapped_fields'    => self::uint(),
				'mapping_rate'     => self::decimal( 0, 100, true ),
			)
		);
		return self::object( $properties );
	}

	private static function performance(): array {
		$properties = array();
		foreach ( array( 'memory_current', 'memory_peak', 'memory_limit_bytes', 'memory_available', 'php_max_execution_time', 'db_queries_count', 'object_cache_hits', 'object_cache_misses' ) as $field ) {
			$properties[ $field ] = self::uint( self::UINT_MAX, true );
		}
		$properties['memory_limit'] = self::text( 32, true );
		foreach ( array( 'memory_usage_percent', 'page_load_time_ms', 'plugin_load_time_ms', 'db_query_time_seconds', 'db_size_mb', 'api_avg_response_ms', 'api_slowest_response_ms', 'api_fastest_response_ms', 'object_cache_hit_rate', 'opcache_hit_rate' ) as $field ) {
			$properties[ $field ] = self::decimal( 0, self::UINT_MAX, true );
		}
		$properties['object_cache_enabled'] = self::bool( true );
		$properties['opcache_enabled']      = self::bool( true );
		return self::object( $properties );
	}

	private static function plugins(): array {
		$row        = self::object(
			array(
				'slug'    => self::slug( 100 ),
				'name'    => self::text(),
				'version' => self::version( true ),
				'author'  => self::text(),
				'status'  => self::enum( array( 'inactive', 'active', 'network-active', 'mu-plugin' ), 'inactive' ),
			)
		);
		$properties = array();
		foreach ( array( 'total_plugins', 'active_plugins', 'inactive_plugins', 'mu_plugins', 'premium_plugins', 'cf7_addons', 'mailchimp_plugins', 'security_plugins', 'cache_plugins', 'seo_plugins' ) as $field ) {
			$properties[ $field ] = self::uint();
		}
		foreach ( array( 'has_woocommerce', 'has_elementor', 'has_jetpack', 'has_wordfence', 'has_yoast_seo' ) as $field ) {
			$properties[ $field ] = self::bool();
		}
		$properties['plugin_list'] = self::accounted_list( $row, 500, 'slug' );
		return self::object( $properties );
	}

	private static function competitors(): array {
		$keys       = array( 'mc4wp', 'mc4wp_premium', 'mailchimp_woo', 'crm_perks', 'easy_forms', 'jetrail', 'cf7_mailchimp_ext', 'newsletter', 'mailpoet', 'fluent_forms', 'wpforms', 'gravity_forms', 'ninja_forms', 'formidable', 'hubspot', 'elementor_pro' );
		$properties = array(
			'has_competitors'       => self::bool(),
			'competitors_installed' => self::uint( 16 ),
			'competitors_active'    => self::uint( 16 ),
			'churn_risk'            => self::enum( array( 'none', 'medium', 'high' ), 'none' ),
			'installed_list'        => self::list_of( self::enum( $keys ), 16 ),
			'active_list'           => self::list_of( self::enum( $keys ), 16 ),
		);
		foreach ( $keys as $key ) {
			$properties[ $key . '_installed' ] = self::bool();
			$properties[ $key . '_active' ]    = self::bool();
		}
		return self::object( $properties );
	}

	private static function server(): array {
		$properties = array();
		foreach ( array( 'load_average_1min', 'load_average_5min', 'load_average_15min', 'disk_usage_percent', 'disk_total_gb' ) as $field ) {
			$properties[ $field ] = self::decimal( 0, self::UINT_MAX, true );
		}
		$properties['server_ip_sha256']       = self::sha256();
		$properties['server_hostname_sha256'] = self::sha256();
		$properties['server_os']              = self::text();
		$properties['server_architecture']    = self::text();
		return self::object( $properties );
	}

	private static function wordpress(): array {
		$properties = array();
		foreach ( array( 'posts_published', 'posts_draft', 'pages_published', 'media_items', 'comments_pending', 'comments_spam', 'users_total', 'users_administrators', 'users_editors', 'users_authors', 'users_subscribers', 'categories_count', 'tags_count', 'revisions_count' ) as $field ) {
			$properties[ $field ] = self::uint();
		}
		$properties['auto_updates_enabled'] = self::bool();
		return self::object( $properties );
	}

	private static function legacy_lifecycle(): array {
		return self::object(
			array(
				'event'         => self::enum( array( 'activated', 'deactivated', 'upgraded', 'heartbeat' ), 'heartbeat' ),
				'install_id'    => self::text( 32 ),
				'version'       => self::version(),
				'site_url'      => self::url(),
				'timestamp'     => self::uint(),
				'wp_version'    => self::version(),
				'php'           => self::version(),
				'mysql_version' => self::version( true ),
				'software'      => self::object( array( 'server' => self::text() ) ),
			)
		);
	}

	private static function opportunities(): array {
		return self::object(
			array(
				'environment_type'   => self::enum( array( 'production', 'staging', 'development', 'local', 'unknown' ), 'unknown' ),
				'setup_stage'        => self::enum( array( 'installed', 'connected', 'audience_selected', 'fields_mapped', 'first_send', 'first_success' ), 'installed' ),
				'setup_percent'      => array(
					'type'    => 'uint',
					'max'     => 100,
					'allowed' => array( 0, 20, 40, 60, 80, 100 ),
				),
				'connector_conflict' => self::bool(),
				'failure_reason'     => self::object(
					array(
						'code'   => self::enum( array( 'revoked_credential', 'deleted_destination', 'merge_mismatch', 'required_field', 'invalid_value', 'rate_limit', 'plan_limit', 'permission', 'duplicate', 'timeout', 'dns', 'tls', 'http_4xx', 'http_5xx', 'configuration', 'validation', 'remote_rejected', 'unknown' ), 'unknown' ),
						'sample' => self::text( 50 ),
					)
				),
				'update_available'   => self::bool(),
				'versions_behind'    => self::uint(),
				'subs_7d'            => self::uint(),
				'subs_30d'           => self::uint(),
				'subs_median'        => self::decimal( 0, self::UINT_MAX ),
				'rest_api_ok'        => self::bool( true ),
				'install_source'     => self::enum( array( 'wp_org_search', 'referral', 'migration', 'manual_upload', 'network', 'unknown' ), 'unknown' ),
				'host_class'         => self::enum( array( 'wpcom', 'wpcom_vip', 'pressable', 'wp_engine', 'other', 'unknown' ), 'unknown' ),
				'cache_dropins'      => self::accounted_list( self::slug( 100 ), 8 ),
			)
		);
	}

	private static function object( array $properties ): array {
		return array(
			'type'       => 'object',
			'properties' => $properties,
		);
	}

	private static function bool( bool $nullable = false ): array {
		return self::scalar( 'bool', $nullable );
	}

	private static function uint( int $maximum = self::UINT_MAX, bool $nullable = false ): array {
		return self::scalar( 'uint', $nullable, array( 'max' => $maximum ) );
	}

	private static function int( int $minimum, int $maximum, bool $nullable = false ): array {
		return self::scalar(
			'int',
			$nullable,
			array(
				'min' => $minimum,
				'max' => $maximum,
			)
		);
	}

	private static function decimal( $minimum, $maximum, bool $nullable = false ): array {
		return self::scalar(
			'decimal',
			$nullable,
			array(
				'min'   => $minimum,
				'max'   => $maximum,
				'scale' => 4,
			)
		);
	}

	private static function enum( array $allowed, string $fallback = '' ): array {
		return array(
			'type'     => 'enum',
			'allowed'  => $allowed,
			'fallback' => $fallback,
		);
	}

	private static function slug( int $maximum = 48 ): array {
		return array(
			'type'       => 'slug',
			'max_length' => $maximum,
		);
	}

	private static function version( bool $nullable = false ): array {
		return self::scalar( 'version', $nullable, array( 'max_length' => 64 ) );
	}

	private static function text( int $maximum = 191, bool $nullable = false ): array {
		return self::scalar( 'text', $nullable, array( 'max_length' => $maximum ) );
	}

	private static function url(): array {
		return array(
			'type'       => 'url',
			'max_length' => 500,
		);
	}

	private static function sha256( bool $nullable = false ): array {
		return self::scalar( 'sha256', $nullable );
	}

	private static function list_of( array $items, int $maximum, string $sort_key = '' ): array {
		$schema = array(
			'type'      => 'list',
			'items'     => $items,
			'max_items' => $maximum,
		);
		if ( '' !== $sort_key ) {
			$schema['sort_key'] = $sort_key;
		}
		return $schema;
	}

	private static function accounted_list( array $items, int $maximum, string $sort_key = '' ): array {
		$schema                    = self::list_of( $items, $maximum, $sort_key );
		$schema['with_accounting'] = true;
		return $schema;
	}

	private static function map_of( array $values, int $maximum, int $key_length ): array {
		return array(
			'type'           => 'map',
			'values'         => $values,
			'max_items'      => $maximum,
			'max_key_length' => $key_length,
		);
	}

	private static function scalar( string $type, bool $nullable, array $extra = array() ): array {
		$schema = array_merge( array( 'type' => $type ), $extra );
		if ( $nullable ) {
			$schema['nullable'] = true;
		}
		return $schema;
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                             includes/services/signls/class-cmatic-lite-signls-failure-reason.php                                0000644                 00000017332 15266052620 0021724 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Deterministic Signls failure classification and scrubbing.
 *
 * @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_Signls_Failure_Reason {

	private const CODES = array(
		'revoked_credential',
		'deleted_destination',
		'merge_mismatch',
		'required_field',
		'invalid_value',
		'rate_limit',
		'plan_limit',
		'permission',
		'duplicate',
		'timeout',
		'dns',
		'tls',
		'http_4xx',
		'http_5xx',
		'configuration',
		'validation',
		'remote_rejected',
		'unknown',
	);

	/**
	 * Convert any provider failure shape into a closed code and safe sample.
	 *
	 * @param mixed  $value    Provider value, result, error or exception.
	 * @param string $fallback Closed fallback code or safe fallback message.
	 * @return array{code:string,sample:string}
	 */
	public static function from_value( $value, string $fallback ): array {
		$text   = self::extract_text( $value );
		$status = self::extract_status( $value );
		$code   = self::classify( $text, $status );

		if ( 'unknown' === $code && in_array( $fallback, self::CODES, true ) ) {
			$code = $fallback;
		}
		if ( '' === $text && ! in_array( $fallback, self::CODES, true ) ) {
			$text = $fallback;
		}

		return array(
			'code'   => $code,
			'sample' => self::scrub( $text ),
		);
	}

	/**
	 * Read only conventional bounded error fields; never serialize a whole payload.
	 *
	 * @param mixed $value Candidate failure value.
	 */
	private static function extract_text( $value ): string {
		if ( is_wp_error( $value ) ) {
			return (string) $value->get_error_message();
		}
		if ( $value instanceof Throwable ) {
			return (string) $value->getMessage();
		}
		if ( is_scalar( $value ) ) {
			return (string) $value;
		}
		if ( ! is_array( $value ) ) {
			return '';
		}

		$parts = array();
		foreach ( array( 'reason', 'code', 'title', 'type', 'error', 'message', 'detail' ) as $key ) {
			if ( isset( $value[ $key ] ) && is_scalar( $value[ $key ] ) ) {
				$parts[] = (string) $value[ $key ];
			}
		}
		if ( isset( $value['errors'] ) && is_array( $value['errors'] ) ) {
			foreach ( array_slice( $value['errors'], 0, 3 ) as $error ) {
				if ( is_scalar( $error ) ) {
					$parts[] = (string) $error;
				} elseif ( is_array( $error ) ) {
					foreach ( array( 'code', 'title', 'message', 'detail' ) as $key ) {
						if ( isset( $error[ $key ] ) && is_scalar( $error[ $key ] ) ) {
							$parts[] = (string) $error[ $key ];
						}
					}
				}
			}
		}
		return implode( ' ', array_slice( $parts, 0, 12 ) );
	}

	/**
	 * Extract a conventional HTTP status without inspecting arbitrary members.
	 *
	 * @param mixed $value Candidate failure value.
	 */
	private static function extract_status( $value ): int {
		if ( ! is_array( $value ) ) {
			return 0;
		}
		foreach ( array( 'status', 'status_code', 'http_code' ) as $key ) {
			if ( isset( $value[ $key ] ) && is_numeric( $value[ $key ] ) ) {
				return (int) $value[ $key ];
			}
		}
		return 0;
	}

	private static function classify( string $text, int $status ): string {
		$value = strtolower( $text );
		$rules = array(
			'revoked_credential'  => '/\b(revoked|invalid api key|invalid access token|expired (api )?(key|token|credential)|unauthori[sz]ed|authentication failed)\b/u',
			'deleted_destination' => '/\b(list|audience|group|destination)\b.{0,32}\b(deleted|does not exist|not found|missing|unknown)\b|\b(deleted|missing|unknown)\b.{0,32}\b(list|audience|group|destination)\b/u',
			'merge_mismatch'      => '/\b(merge[ _-]?(field|tag)|field mapping)\b.{0,32}\b(mismatch|invalid|unknown|missing|not found)\b|\b(mismatch)\b.{0,32}\b(merge|field)\b/u',
			'required_field'      => '/\b(required (field|value)|field .{0,24} is required|missing required)\b/u',
			'invalid_value'       => '/\b(invalid value|value .{0,24} invalid|bad value|malformed value)\b/u',
			'rate_limit'          => '/\b(rate.?limit|too many requests|backoff)\b/u',
			'plan_limit'          => '/\b(plan|quota|account)\b.{0,32}\b(limit|upgrade|exceeded|maximum)\b|\b(limit|quota)\b.{0,32}\b(plan|account)\b/u',
			'permission'          => '/\b(forbidden|permission denied|insufficient permission|access denied|not permitted)\b/u',
			'duplicate'           => '/\b(duplicate|already exists|member exists|already subscribed)\b/u',
			'timeout'             => '/\b(timed? out|timeout|curl error 28)\b/u',
			'dns'                 => '/\b(dns|name resolution|could not resolve|unknown host|curl error 6)\b/u',
			'tls'                 => '/\b(tls|ssl|certificate|handshake|curl error (35|51|58|60|64|66|77|80|82|83|90|91))\b/u',
			'configuration'       => '/\b(configuration|config error|not configured|setup incomplete)\b/u',
			'validation'          => '/\b(validation|failed to validate|schema invalid)\b/u',
			'remote_rejected'     => '/\b(rejected|declined|provider api error|api error)\b/u',
		);
		foreach ( $rules as $code => $pattern ) {
			if ( 1 === preg_match( $pattern, $value ) ) {
				return $code;
			}
		}
		if ( 429 === $status ) {
			return 'rate_limit';
		}
		if ( 401 === $status ) {
			return 'revoked_credential';
		}
		if ( 403 === $status ) {
			return 'permission';
		}
		if ( $status >= 500 && $status <= 599 ) {
			return 'http_5xx';
		}
		if ( $status >= 400 && $status <= 499 ) {
			return 'http_4xx';
		}
		return 'unknown';
	}

	private static function scrub( string $text ): string {
		$text = wp_check_invalid_utf8( $text, true );
		$text = wp_strip_all_tags( $text, true );
		$text = self::replace( '/[\p{Cc}\p{Cf}]+/u', ' ', $text );
		$text = self::replace( '/\b[A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,}\b/iu', '[redacted]', $text );
		$text = self::replace( '~\b(?:https?://|www\.)[^\s<>]+~iu', '[redacted]', $text );
		$text = self::replace( '/\?[^\s<>]+/u', '[redacted]', $text );
		$text = self::replace( '/\b(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])){3}\b/u', '[redacted]', $text );
		$text = self::replace( '/(?<![[:alnum:]])(?:[A-F0-9]{0,4}:){2,7}[A-F0-9]{0,4}(?![[:alnum:]])/iu', '[redacted]', $text );
		$text = self::replace( '/\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/iu', '[redacted]', $text );
		$text = self::replace( '/\b(?:bearer|basic|token|api[_ -]?key)\s*[:=]?\s*[^\s,;]+/iu', '[redacted]', $text );
		$text = self::replace( '/\b[0-9a-f]{12,}\b/iu', '[redacted]', $text );
		$text = self::replace( '/\b(?=[A-Z0-9_+\/=\-]{12,}\b)(?=[A-Z0-9_+\/=\-]*[0-9_+\/=\-])[A-Z0-9_+\/=\-]+\b/iu', '[redacted]', $text );
		$text = trim( self::replace( '/\s+/u', ' ', $text ) );
		$text = self::utf8_prefix( $text, 50 );

		if ( self::contains_forbidden_pattern( $text ) ) {
			return '';
		}
		return $text;
	}

	private static function replace( string $pattern, string $replacement, string $text ): string {
		$result = preg_replace( $pattern, $replacement, $text );
		return is_string( $result ) ? $result : '';
	}

	private static function contains_forbidden_pattern( string $text ): bool {
		$patterns = array(
			'/\b[A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,}\b/iu',
			'~\b(?:https?://|www\.)~iu',
			'/\b(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])){3}\b/u',
			'/\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/iu',
			'/\b(?:bearer|basic|token|api[_ -]?key)\s*[:=]/iu',
		);
		foreach ( $patterns as $pattern ) {
			if ( 1 === preg_match( $pattern, $text ) ) {
				return true;
			}
		}
		return false;
	}

	private static function utf8_prefix( string $value, int $maximum ): string {
		if ( function_exists( 'mb_substr' ) ) {
			return mb_substr( $value, 0, $maximum, 'UTF-8' );
		}
		if ( 1 === preg_match( '/^.{0,' . $maximum . '}/us', $value, $match ) ) {
			return $match[0];
		}
		return substr( $value, 0, $maximum );
	}
}
                                                                                                                                                                                                                                                                                                      includes/services/signls/class-cmatic-lite-signls-snapshot.php                                      0000644                 00000007604 15266052620 0020650 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * ChimpMatic Lite rich Signls snapshot composer.
 *
 * @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_Signls_Snapshot {

	private const SECTIONS = array(
		'install',
		'metadata',
		'lifecycle',
		'environment',
		'api',
		'submissions',
		'features',
		'forms',
		'performance',
		'plugins',
		'competitors',
		'server',
		'wordpress',
		'legacy_lifecycle',
	);

	private $site_collector;
	private $inventory_collector;
	private $performance_collector;
	private $product_collector;
	private $forms_collector;

	public function __construct( $site_collector, $inventory_collector, $performance_collector, $product_collector, $forms_collector ) {
		$this->site_collector        = $site_collector;
		$this->inventory_collector   = $inventory_collector;
		$this->performance_collector = $performance_collector;
		$this->product_collector     = $product_collector;
		$this->forms_collector       = $forms_collector;
	}

	public function collect(): array {
		$observations                  = array_fill_keys( self::SECTIONS, array() );
		$observations['opportunities'] = array();
		$missing                       = array();
		$successes                     = 0;
		$narrow                        = $this->empty_narrow();

		$this->collect_domain(
			$this->product_collector,
			array( 'install', 'metadata', 'lifecycle', 'api', 'submissions', 'features', 'legacy_lifecycle' ),
			$observations,
			$missing,
			$successes,
			$narrow
		);
		$this->collect_domain(
			$this->site_collector,
			array( 'environment', 'server', 'wordpress' ),
			$observations,
			$missing,
			$successes
		);
		$this->collect_domain(
			$this->inventory_collector,
			array( 'plugins', 'competitors' ),
			$observations,
			$missing,
			$successes
		);
		$this->collect_domain(
			$this->performance_collector,
			array( 'performance' ),
			$observations,
			$missing,
			$successes
		);
		$this->collect_domain(
			$this->forms_collector,
			array( 'forms' ),
			$observations,
			$missing,
			$successes
		);

		$missing                           = array_values( array_unique( $missing ) );
		$observations['collection_status'] = empty( $missing ) ? 'complete' : ( 0 === $successes ? 'failed' : 'partial' );
		$observations['missing_sections']  = $missing;
		$narrow['observations']            = $observations;
		return $narrow;
	}

	private function collect_domain( $collector, array $sections, array &$observations, array &$missing, int &$successes, ?array &$narrow = null ): void {
		try {
			$result = is_object( $collector ) && is_callable( array( $collector, 'collect' ) ) ? $collector->collect() : null;
			if ( ! is_array( $result ) ) {
				throw new RuntimeException( 'Invalid collector result.' );
			}
			++$successes;
			if ( null !== $narrow && isset( $result['narrow'] ) && is_array( $result['narrow'] ) ) {
				$narrow = array_replace( $narrow, array_intersect_key( $result['narrow'], $narrow ) );
			}
			foreach ( $sections as $section ) {
				if ( ! isset( $result[ $section ] ) || ! is_array( $result[ $section ] ) ) {
					$missing[] = $section;
					continue;
				}
				$observations[ $section ] = $result[ $section ];
			}
			if ( isset( $result['opportunities'] ) && is_array( $result['opportunities'] ) ) {
				$observations['opportunities'] = array_replace( $observations['opportunities'], $result['opportunities'] );
			}
		} catch ( Throwable $exception ) {
			unset( $exception );
			foreach ( $sections as $section ) {
				$missing[] = $section;
			}
		}
	}

	private function empty_narrow(): array {
		return array(
			'versions'         => array(),
			'is_multisite'     => false,
			'configured_units' => 0,
			'active_units'     => 0,
			'integrations'     => array(),
			'features'         => array(),
			'operation_health' => array(),
			'companions'       => array(),
		);
	}
}
                                                                                                                            includes/services/signls/class-cmatic-lite-signls-performance-collector.php                         0000644                 00000012260 15266052620 0023270 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * ChimpMatic Lite performance Signls collector.
 *
 * @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_Signls_Performance_Collector {

	public function collect(): array {
		$memory_limit       = (string) ini_get( 'memory_limit' );
		$memory_limit_bytes = $this->bytes( $memory_limit );
		$memory_peak        = memory_get_peak_usage( true );
		$cache              = $this->object_cache();
		$opcache            = $this->opcache();
		$request_started    = isset( $_SERVER['REQUEST_TIME_FLOAT'] ) && is_numeric( $_SERVER['REQUEST_TIME_FLOAT'] ) ? (float) $_SERVER['REQUEST_TIME_FLOAT'] : null;

		return array(
			'performance' => array(
				'memory_current'          => memory_get_usage( true ),
				'memory_peak'             => $memory_peak,
				'memory_limit'            => $memory_limit,
				'memory_limit_bytes'      => $memory_limit_bytes > 0 ? $memory_limit_bytes : null,
				'memory_usage_percent'    => $memory_limit_bytes > 0 ? round( ( $memory_peak / $memory_limit_bytes ) * 100, 2 ) : null,
				'memory_available'        => $memory_limit_bytes > 0 ? max( 0, $memory_limit_bytes - $memory_peak ) : null,
				'php_max_execution_time'  => (int) ini_get( 'max_execution_time' ),
				'page_load_time_ms'       => null !== $request_started ? round( ( microtime( true ) - $request_started ) * 1000, 2 ) : null,
				'plugin_load_time_ms'     => $this->option_decimal( 'performance.plugin_load_time' ),
				'db_queries_count'        => function_exists( 'get_num_queries' ) ? get_num_queries() : null,
				'db_query_time_seconds'   => function_exists( 'timer_stop' ) ? (float) timer_stop( 0, 3 ) : null,
				'db_size_mb'              => $this->database_size(),
				'api_avg_response_ms'     => $this->option_decimal( 'performance.api_avg_response' ),
				'api_slowest_response_ms' => $this->option_decimal( 'performance.api_slowest' ),
				'api_fastest_response_ms' => $this->option_decimal( 'performance.api_fastest' ),
				'object_cache_enabled'    => function_exists( 'wp_using_ext_object_cache' ) ? wp_using_ext_object_cache() : null,
				'object_cache_hits'       => $cache['hits'],
				'object_cache_misses'     => $cache['misses'],
				'object_cache_hit_rate'   => $this->hit_rate( $cache['hits'], $cache['misses'] ),
				'opcache_enabled'         => $opcache['enabled'],
				'opcache_hit_rate'        => $opcache['hit_rate'],
			),
		);
	}

	private function bytes( string $value ): int {
		$value = trim( $value );
		if ( '' === $value || '-1' === $value ) {
			return 0;
		}
		$unit   = strtolower( substr( $value, -1 ) );
		$number = (int) $value;
		if ( 'g' === $unit ) {
			return $number * 1073741824;
		}
		if ( 'm' === $unit ) {
			return $number * 1048576;
		}
		if ( 'k' === $unit ) {
			return $number * 1024;
		}
		return max( 0, $number );
	}

	private function database_size(): ?float {
		global $wpdb;

		if ( ! defined( 'DB_NAME' ) ) {
			return null;
		}
		$value = $wpdb->get_var(
			$wpdb->prepare(
				'SELECT SUM(data_length + index_length) / 1024 / 1024 FROM information_schema.TABLES WHERE table_schema = %s',
				DB_NAME
			)
		); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- One aggregate capacity fact; no table content or query text leaves the site.
		return is_numeric( $value ) ? round( (float) $value, 2 ) : null;
	}

	private function object_cache(): array {
		global $wp_object_cache;

		if ( function_exists( 'wp_cache_get_stats' ) ) {
			$stats = wp_cache_get_stats();
			if ( is_array( $stats ) ) {
				return array(
					'hits'   => isset( $stats['hits'] ) && is_numeric( $stats['hits'] ) ? (int) $stats['hits'] : null,
					'misses' => isset( $stats['misses'] ) && is_numeric( $stats['misses'] ) ? (int) $stats['misses'] : null,
				);
			}
		}
		return array(
			'hits'   => is_object( $wp_object_cache ) && isset( $wp_object_cache->cache_hits ) ? (int) $wp_object_cache->cache_hits : null,
			'misses' => is_object( $wp_object_cache ) && isset( $wp_object_cache->cache_misses ) ? (int) $wp_object_cache->cache_misses : null,
		);
	}

	private function opcache(): array {
		if ( ! function_exists( 'opcache_get_status' ) ) {
			return array(
				'enabled'  => null,
				'hit_rate' => null,
			);
		}
		$status = opcache_get_status( false );
		if ( false === $status ) {
			return array(
				'enabled'  => false,
				'hit_rate' => null,
			);
		}
		$stats = isset( $status['opcache_statistics'] ) && is_array( $status['opcache_statistics'] ) ? $status['opcache_statistics'] : array();
		$rate  = isset( $stats['opcache_hit_rate'] ) && is_numeric( $stats['opcache_hit_rate'] ) ? round( (float) $stats['opcache_hit_rate'], 2 ) : null;
		return array(
			'enabled'  => true,
			'hit_rate' => $rate,
		);
	}

	private function hit_rate( $hits, $misses ): ?float {
		if ( null === $hits || null === $misses || ( $hits + $misses ) <= 0 ) {
			return null;
		}
		return round( ( $hits / ( $hits + $misses ) ) * 100, 2 );
	}

	private function option_decimal( string $key ): ?float {
		$value = Cmatic_Options_Repository::get_option( $key, null );
		return is_numeric( $value ) ? round( (float) $value, 2 ) : null;
	}
}
                                                                                                                                                                                                                                                                                                                                                includes/services/class-cmatic-submission-handler.php                                               0000644                 00000016040 15266052621 0017063 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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 {
		add_action( 'plugins_loaded', array( __CLASS__, 'register_submission_router' ), PHP_INT_MAX );
	}

	public static function register_submission_router(): void {
		if ( function_exists( 'cmatic_process_subscription' ) ) {
			remove_action( 'wpcf7_before_send_mail', 'cmatic_process_subscription' );
		}
		add_action( 'wpcf7_before_send_mail', array( __CLASS__, 'process_submission' ) );
	}

	public static function process_submission( $contact_form ): void {
		$form_id  = (int) $contact_form->id();
		$cf7_mch  = get_option( 'cf7_mch_' . $form_id );
		$cf7_mch  = is_array( $cf7_mch ) ? $cf7_mch : array();
		$provider = Cmatic_Lite_Esp_Registry::get_selected( $cf7_mch );
		if ( 'mailchimp' !== $provider ) {
			self::process_provider_submission( $contact_form, $cf7_mch, $provider );
			return;
		}
		if ( function_exists( 'cmatic_process_subscription' ) ) {
			cmatic_process_subscription( $contact_form );
			return;
		}

		$submission = WPCF7_Submission::get_instance();
		if ( ! $submission ) {
			return;
		}

		if ( ! self::is_configured( $cf7_mch, $form_id ) ) {
			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_value = $cf7_mch['list'] ?? '';
		if ( is_array( $list_value ) ) {
			$first_list = reset( $list_value );
			$list_value = (string) ( false === $first_list ? '' : $first_list );
		}
		$list_id = Cmatic_Email_Extractor::replace_tags( $list_value, $posted_data );
		$status  = Cmatic_Status_Resolver::resolve( $cf7_mch, $posted_data, $logger );

		if ( null === $status ) {
			return;
		}

		$merge_vars = Cmatic_Merge_Vars_Builder::build( $cf7_mch, $posted_data );

		$api_key = self::resolve_api_key( $form_id, $cf7_mch );

		Cmatic_Mailchimp_Subscriber::subscribe( $api_key, $list_id, $email, $status, $merge_vars, $form_id, $logger );
	}

	/**
	 * Process a non-Mailchimp provider submission.
	 *
	 * @param WPCF7_ContactForm   $contact_form Contact form being submitted.
	 * @param array<string,mixed> $config       Complete form integration config.
	 * @param string              $slug         Selected provider slug.
	 */
	private static function process_provider_submission( $contact_form, array $config, string $slug ): void {
		$form_id   = (int) $contact_form->id();
		$providers = isset( $config['providers'] ) && is_array( $config['providers'] ) ? $config['providers'] : array();
		$settings  = isset( $providers[ $slug ] ) && is_array( $providers[ $slug ] ) ? $providers[ $slug ] : array();
		if ( 1 !== (int) ( $settings['api-validation'] ?? 0 ) ) {
			return;
		}
		$api_key = Cmatic_Lite_Esp_Credentials::get( $form_id, $slug );
		if ( '' === $api_key ) {
			return;
		}
		$submission = WPCF7_Submission::get_instance();
		if ( ! $submission ) {
			return;
		}
		$posted_data = $submission->get_posted_data();
		$logger      = new Cmatic_File_Logger( 'api-events-' . $slug, (bool) Cmatic_Options_Repository::get_option( 'debug', false ) );
		if ( 'mailerlite' === $slug ) {
			$pipeline = new Cmatic_Mailerlite_Submission_Pipeline( Cmatic_Lite_Esp_Registry::get( $slug ) );
			$pipeline->process(
				$form_id,
				$settings,
				$posted_data,
				Cmatic_Form_Tags::get_tags_with_types( $contact_form ),
				$api_key,
				Cmatic_Lite_Esp_Capabilities::field_limit( $slug, $form_id ),
				$logger
			);
			return;
		}
		$advanced = Cmatic_Lite_Esp_Capabilities::feature_enabled( 'advanced_consent', $slug, $form_id );
		$runtime  = $settings;
		if ( ! $advanced ) {
			foreach ( array( 'consent_gate', 'consent_field', 'subscription_mode', 'doi_template_id', 'doi_redirect_url', 'doi_verified' ) as $premium_key ) {
				unset( $runtime[ $premium_key ] );
			}
		}
		$email = Cmatic_Email_Extractor::extract( $runtime, $posted_data );
		if ( ! is_email( $email ) ) {
			Cmatic_Submission_Feedback::set_result( Cmatic_Submission_Feedback::failure( 'invalid_email', '', $email ) );
			return;
		}
		$list_value = $settings['list'] ?? '';
		if ( is_array( $list_value ) ) {
			$first_list = reset( $list_value );
			$list_value = false === $first_list ? '' : (string) $first_list;
		}
		$list_id = Cmatic_Email_Extractor::replace_tags( (string) $list_value, $posted_data );
		if ( '' === $list_id ) {
			return;
		}
		$status = Cmatic_Status_Resolver::resolve( $runtime, $posted_data, $logger );
		if ( null === $status ) {
			return;
		}
		if (
			$advanced
			&& 'brevo' === $slug
			&& 'double' === ( $runtime['subscription_mode'] ?? '' )
			&& 1 !== (int) ( $runtime['doi_verified'] ?? 0 )
		) {
			return;
		}
		$field_limit = Cmatic_Lite_Esp_Capabilities::field_limit( $slug, $form_id );
		$merge_vars  = Cmatic_Merge_Vars_Builder::build( $runtime, $posted_data, $field_limit );
		$options     = $advanced
			? array(
				'subscription_mode' => sanitize_key( (string) ( $runtime['subscription_mode'] ?? '' ) ),
				'doi_template_id'   => max( 0, (int) ( $runtime['doi_template_id'] ?? 0 ) ),
				'doi_redirect_url'  => esc_url_raw( (string) ( $runtime['doi_redirect_url'] ?? '' ) ),
			)
			: array();
		Cmatic_Lite_Esp_Registry::get( $slug )->subscribe( $api_key, $list_id, $email, $status, $merge_vars, $form_id, $options, $logger );
	}

	private static function is_configured( $cf7_mch, int $form_id = 0 ): bool {
		if ( empty( $cf7_mch ) ) {
			return false;
		}

		if ( ! empty( $cf7_mch['api-validation'] )
			&& 1 === (int) $cf7_mch['api-validation']
			&& ! empty( $cf7_mch['api'] ) ) {
			return true;
		}

		if ( isset( $cf7_mch['auth_type'] ) && 'oauth' === $cf7_mch['auth_type']
			&& ! empty( $cf7_mch['api-validation'] )
			&& 1 === (int) $cf7_mch['api-validation'] ) {
			$auth_manager = Cmatic_Lite_Container::get( 'auth.manager' );
			if ( $auth_manager && $auth_manager->has_oauth( $form_id ) ) {
				return true;
			}
		}

		if ( $form_id && empty( $cf7_mch['api'] ) ) {
			$auth_manager = Cmatic_Lite_Container::get( 'auth.manager' );
			if ( $auth_manager && $auth_manager->has_oauth( $form_id ) ) {
				return true;
			}
		}

		return false;
	}

	private static function resolve_api_key( int $form_id, array $cf7_mch ): string {
		$auth_manager = Cmatic_Lite_Container::get( 'auth.manager' );
		if ( $auth_manager ) {
			return $auth_manager->resolve_api_key( $form_id, '', $cf7_mch );
		}
		return $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() {}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                includes/services/class-cmatic-lite-signls-adapter.php                                              0000644                 00000002710 15266052621 0017124 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * ChimpMatic Lite aggregate Signls adapter.
 *
 * @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_Signls_Adapter implements \Signls\Sdk\V1\ProductAdapterInterface {

	private const PRODUCT = 'contact-form-7-mailchimp-extension';

	public function product_slug(): string {
		return self::PRODUCT;
	}

	public function product_version(): string {
		return defined( 'SPARTAN_MCE_VERSION' ) ? (string) SPARTAN_MCE_VERSION : 'unknown';
	}

	public function signal_sharing_enabled(): bool {
		return 'enabled' === Cmatic_Options_Repository::get_option( 'signls.consent_status', 'unset' );
	}

	public function install_id(): string {
		$install = new Cmatic_Install_Data( Cmatic_Options_Repository::instance() );
		return (string) $install->get_install_id();
	}

	public function contract(): array {
		return Cmatic_Lite_Signls_Contract::get();
	}

	public function snapshot(): array {
		if ( ! $this->signal_sharing_enabled() ) {
			return array();
		}

		$forms    = new Cmatic_Lite_Signls_Forms_Collector();
		$snapshot = new Cmatic_Lite_Signls_Snapshot(
			new Cmatic_Lite_Signls_Site_Collector(),
			new Cmatic_Lite_Signls_Inventory_Collector(),
			new Cmatic_Lite_Signls_Performance_Collector(),
			new Cmatic_Lite_Signls_Product_Collector( $forms ),
			$forms
		);
		return $snapshot->collect();
	}
}
                                                        includes/services/class-cmatic-form-tags.php                                                        0000644                 00000003550 15266052622 0015157 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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+
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

final class Cmatic_Form_Tags {

	public static function get_tags_with_types( $contact_form ): array {
		if ( ! $contact_form ) {
			return array();
		}

		$all_tags = $contact_form->scan_form_tags();
		$result   = array();

		foreach ( $all_tags as $tag ) {
			if ( is_object( $tag ) && ! empty( $tag->name ) ) {
				/** @var WPCF7_FormTag $tag */
				$basetype = sanitize_key( (string) $tag->basetype );
				$item     = array(
					'name'             => sanitize_key( (string) $tag->name ),
					'basetype'         => $basetype,
					'routing_eligible' => false,
					'choices'          => array(),
					'required'         => 'acceptance' === $basetype && ! $tag->has_option( 'optional' ),
					'inverted'         => 'acceptance' === $basetype && $tag->has_option( 'invert' ),
					'content'          => sanitize_text_field( trim( (string) ( $tag->content ? $tag->content : reset( $tag->values ) ) ) ),
				);

				$dynamic = (bool) preg_grep( '/^data:/i', (array) $tag->options );
				if ( in_array( $basetype, array( 'checkbox', 'radio', 'select' ), true ) && ! $tag->has_option( 'free_text' ) && ! $dynamic ) {
					$item['routing_eligible'] = true;
					foreach ( (array) $tag->values as $index => $value ) {
						$canonical         = $tag->pipes instanceof WPCF7_Pipes ? $tag->pipes->do_pipe( (string) $value ) : (string) $value;
						$item['choices'][] = array(
							'label' => sanitize_text_field( (string) ( $tag->labels[ $index ] ?? $value ) ),
							'value' => sanitize_text_field( (string) $canonical ),
						);
					}
				}
				$result[] = $item;
			}
		}

		return $result;
	}

	private function __construct() {}
}
                                                                                                                                                        includes/services/class-cmatic-mailerlite-degradation-reporter.php                                  0000644                 00000003423 15266052622 0021525 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Records visible, PII-free MailerLite feature degradation.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

final class Cmatic_Mailerlite_Degradation_Reporter {
	public const META_KEY = '_cmatic_mailerlite_degraded_features';

	/**
	 * Records the suppressed features observed by a real submission.
	 *
	 * @param int   $form_id  Contact Form 7 form ID.
	 * @param array $features Suppressed feature identifiers.
	 * @param array $rule_ids Suppressed routing-rule identifiers.
	 */
	public static function record_submission( int $form_id, array $features, array $rule_ids ): void {
		$features = self::canonical( $features );
		$rule_ids = self::canonical( $rule_ids );
		if ( empty( $features ) ) {
			return;
		}

		$current = get_post_meta( $form_id, self::META_KEY, true );
		if ( $features !== $current ) {
			update_post_meta( $form_id, self::META_KEY, $features );
		}

		if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Required PII-free operational degradation diagnostic.
			error_log(
				sprintf(
					'[ChimpMatic] MailerLite license degradation: form_id=%d; features=%s; rule_ids=%s',
					$form_id,
					implode( ',', $features ),
					implode( ',', $rule_ids )
				)
			);
		}
	}

	public static function clear( int $form_id ): void {
		delete_post_meta( $form_id, self::META_KEY );
	}

	private static function canonical( array $values ): array {
		$values = array_values(
			array_unique(
				array_filter(
					array_map( 'sanitize_key', $values ),
					static function ( string $value ): bool {
						return '' !== $value;
					}
				)
			)
		);
		sort( $values, SORT_STRING );
		return $values;
	}

	private function __construct() {}
}
                                                                                                                                                                                                                                             includes/services/class-cmatic-redirect.php                                                         0000644                 00000002607 15266052622 0015063 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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;
	}
}
                                                                                                                         includes/ui/class-cmatic-admin-bar-menu.php                                                         0000644                 00000030012 15266052625 0014642 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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(),
				)
			);
		}

		$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( 'help', '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 ) {
		if ( ! class_exists( 'WPCF7_ContactForm' ) ) {
			return;
		}

		$forms = WPCF7_ContactForm::find( array( 'posts_per_page' => -1 ) );

		if ( empty( $forms ) ) {
			return;
		}

		$wp_admin_bar->add_menu(
			array(
				'parent' => self::MENU_IDENTIFIER,
				'id'     => 'chimpmatic-forms-header',
				'title'  => esc_html__( 'Form Settings', 'chimpmatic-lite' ),
				'href'   => false,
			)
		);

		foreach ( $forms as $form ) {
			$form_url = admin_url(
				sprintf(
					'admin.php?page=wpcf7&post=%d&action=edit&active-tab=Chimpmatic',
					$form->id()
				)
			);

			$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() );

		$is_connected = ! empty( $cf7_mch['api-validation'] )
				&& 1 === (int) $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
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      includes/ui/class-cmatic-lite-esp-panel.php                                                         0000644                 00000126015 15266052625 0014676 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Universal ChimpMatic Lite provider panel.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

// phpcs:disable Squiz.Commenting.ClassComment.Missing, Squiz.Commenting.FunctionComment.Missing -- Typed signatures follow the existing Lite provider convention.
final class Cmatic_Lite_Esp_Panel {
	public static function get_api_status( string $slug, array $config, int $form_id ): string {
		if ( 'mailchimp' === $slug ) {
			$has_credentials = ! empty( $config['api'] )
				|| ( isset( $config['auth_type'] ) && 'oauth' === $config['auth_type'] );
			if ( 1 === (int) ( $config['api-validation'] ?? 0 ) ) {
				return 'connected';
			}
			return $has_credentials ? 'disconnected' : 'fresh';
		}

		$settings        = self::provider_settings( $config, $slug );
		$credential      = Cmatic_Lite_Esp_Credentials::get( $form_id, $slug );
		$has_credentials = '' !== $credential;
		unset( $credential );

		if ( $has_credentials && 1 === (int) ( $settings['api-validation'] ?? 0 ) ) {
			return 'connected';
		}
		return $has_credentials ? 'disconnected' : 'fresh';
	}

	public static function render( string $slug, array $config, array $form_tags, int $form_id ): void {
		$manifest       = Cmatic_Lite_Esp_Manifest::all();
		$state          = self::get_public_state( $slug, $config, $form_id, $form_tags );
		$discount       = Cmatic_Pursuit::discount();
		$initial_slug   = in_array( $slug, array( 'brevo', 'mailerlite', 'klaviyo' ), true ) ? $slug : 'brevo';
		$field_limit    = Cmatic_Lite_Esp_Capabilities::field_limit( $initial_slug, $form_id );
		$definition     = $manifest[ $initial_slug ];
		$provider_state = $state['providers'][ $initial_slug ];
		$is_hidden      = '' === $slug || 'mailchimp' === $slug;
		$destination    = strtolower( (string) $definition['destination_singular'] );
		$destinations   = strtolower( (string) $definition['destination_plural'] );
		$data_singular  = strtolower( (string) $definition['data_singular'] );
		$data_plural    = strtolower( (string) $definition['data_plural'] );
		$person         = strtolower( (string) $definition['person_singular'] );
		/* translators: %s: provider destination type, such as list or group. */
		$progress_destination = sprintf( __( 'Choose %s', 'chimpmatic-lite' ), $destination );
		/* translators: %s: provider data name, such as contact attributes or subscriber fields. */
		$progress_mappings = sprintf( __( 'Map %s', 'chimpmatic-lite' ), $data_plural );
		/* translators: %s: provider destination plural, such as lists or groups. */
		$refresh_destinations = sprintf( __( 'Refresh %s', 'chimpmatic-lite' ), $destinations );
		/* translators: %s: email provider name. */
		$connect_to_continue = sprintf( __( 'Connect %s to continue.', 'chimpmatic-lite' ), (string) $definition['label'] );
		/* translators: 1: email provider name, 2: provider data name. */
		$mapping_description = sprintf( __( 'Match each %1$s %2$s to a Contact Form 7 field. Email address mapping is required.', 'chimpmatic-lite' ), (string) $definition['label'], $data_plural );
		/* translators: 1: provider destination type, 2: provider data name. */
		$mapping_locked = sprintf( __( 'Choose a %1$s to load its %2$s.', 'chimpmatic-lite' ), $destination, $data_plural );
		/* translators: %s: provider data name. */
		$unlock_data = sprintf( __( 'Unlock every available %s and advanced features with Chimpmatic Pro', 'chimpmatic-lite' ), $data_singular );
		/* translators: %s: email provider name. */
		$consent_gate_title = sprintf( __( 'Send to %s', 'chimpmatic-lite' ), (string) $definition['label'] );
		/* translators: %s: email provider name. */
		$consent_gate_explanation = sprintf( __( 'Choose whether every valid form submission is sent to %s or only submissions with affirmative consent.', 'chimpmatic-lite' ), (string) $definition['label'] );
		/* translators: %s: email provider name. */
		$provider_optin_title = sprintf( __( 'Confirmation in %s', 'chimpmatic-lite' ), (string) $definition['label'] );
		/* translators: %s: email provider name. */
		$confirmation_settings_link = sprintf( __( 'Open %s confirmation settings', 'chimpmatic-lite' ), (string) $definition['label'] );
		/* translators: %s: email provider name. */
		$provider_optin_explanation = sprintf( __( 'Controls whether %s requires confirmation after the form is submitted.', 'chimpmatic-lite' ), (string) $definition['label'] );
		/* translators: 1: provider person name, 2: email provider name. */
		$testing_notice = sprintf( __( 'Real submission: may create or update a %1$s in %2$s and trigger confirmation emails or automations.', 'chimpmatic-lite' ), $person, (string) $definition['label'] );
		wp_localize_script( 'cmatic-lite-esp', 'chimpmaticLiteEspState', $state );
		?>
		<section
			id="cmatic-provider-onboarding"
			class="cmatic-provider-onboarding"
			aria-labelledby="cmatic-provider-onboarding-title"
			<?php echo '' === $slug ? '' : 'hidden'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Constant HTML attribute. ?>
		>
			<span class="cmatic-provider-onboarding__kicker"><?php esc_html_e( 'Get started', 'chimpmatic-lite' ); ?></span>
			<h2 id="cmatic-provider-onboarding-title"><?php esc_html_e( 'Choose your email provider', 'chimpmatic-lite' ); ?></h2>
			<p><?php esc_html_e( 'Select one provider to begin the guided setup. You can switch providers later.', 'chimpmatic-lite' ); ?></p>
			<div class="cmatic-provider-choices">
				<?php foreach ( $manifest as $provider_slug => $provider_definition ) : ?>
					<?php $description = self::provider_description( (string) $provider_slug ); ?>
					<button type="button" class="cmatic-provider-choice" data-provider-choice="<?php echo esc_attr( (string) $provider_slug ); ?>">
						<img src="<?php echo esc_url( SPARTAN_MCE_PLUGIN_URL . 'assets/images/providers/' . $provider_slug . '.svg' ); ?>" alt="" aria-hidden="true">
						<strong><?php echo esc_html( (string) $provider_definition['label'] ); ?></strong>
						<span><?php echo esc_html( $description ); ?></span>
					</button>
				<?php endforeach; ?>
			</div>
		</section>
		<div
			id="cmatic-provider-data"
			class="cmatic-provider-panel"
			data-cmatic-provider-view="generic"
			data-form-id="<?php echo esc_attr( (string) $form_id ); ?>"
			data-provider="<?php echo esc_attr( $slug ); ?>"
				data-field-limit="<?php echo esc_attr( (string) $field_limit ); ?>"
			<?php echo $is_hidden ? 'hidden' : ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Constant HTML attribute. ?>
		>
			<div id="cmatic-provider-completion" class="cmatic-provider-completion" role="status" aria-live="polite" hidden>
				<span class="cmatic-provider-completion__icon" aria-hidden="true">&#10003;</span>
				<div>
					<strong><?php esc_html_e( 'Setup complete', 'chimpmatic-lite' ); ?></strong>
					<p id="cmatic-provider-completion-outcome"></p>
					<span id="cmatic-provider-completion-meta"></span>
				</div>
			</div>
			<ol id="cmatic-provider-progress" class="cmatic-provider-progress" aria-label="<?php esc_attr_e( 'Setup progress', 'chimpmatic-lite' ); ?>">
				<li><span>1</span><b><?php esc_html_e( 'Connect', 'chimpmatic-lite' ); ?></b></li>
				<li><span>2</span><b id="cmatic-provider-progress-destination"><?php echo esc_html( $progress_destination ); ?></b></li>
				<li><span>3</span><b id="cmatic-provider-progress-mappings"><?php echo esc_html( $progress_mappings ); ?></b></li>
			</ol>
			<p id="cmatic-provider-message" class="cmatic-provider-message" role="status" aria-live="polite"></p>

			<section class="cmatic-provider-section" data-cmatic-stage="credentials">
				<div id="cmatic-provider-credentials-intro">
					<h3 id="cmatic-provider-credentials-heading">
						<?php
						printf(
							/* translators: %s: email provider name */
							esc_html__( 'Connect %s', 'chimpmatic-lite' ),
							esc_html( (string) $definition['label'] )
						);
						?>
					</h3>
					<p id="cmatic-provider-credentials-description" class="description"></p>
				</div>
				<div class="cmatic-provider-auth-row" id="cmatic-provider-auth-row">
					<div id="cmatic-provider-auth-fields">
						<?php self::render_auth_fields( $definition ); ?>
					</div>
					<div class="cmatic-provider-actions">
						<button type="button" class="button button-primary" id="cmatic-provider-connect" disabled>
							<?php esc_html_e( 'Connect', 'chimpmatic-lite' ); ?>
						</button>
						<button type="button" class="button" id="cmatic-provider-cancel-credential" hidden><?php esc_html_e( 'Cancel', 'chimpmatic-lite' ); ?></button>
					</div>
				</div>
				<div id="cmatic-provider-connected-summary" class="cmatic-provider-connection-summary" hidden>
					<span class="cmatic-provider-connection-summary__icon" aria-hidden="true">&#10003;</span>
					<div class="cmatic-provider-connection-summary__text">
						<strong id="cmatic-provider-connected-title"></strong>
						<span><span id="cmatic-provider-credential-storage"><?php esc_html_e( 'Credential stored securely', 'chimpmatic-lite' ); ?></span> &middot; <span id="cmatic-provider-checked-status"><?php esc_html_e( 'Checked just now', 'chimpmatic-lite' ); ?></span></span>
					</div>
					<div class="cmatic-provider-actions">
						<button type="button" class="button" id="cmatic-provider-refresh"><?php echo esc_html( $refresh_destinations ); ?></button>
						<button type="button" class="button" id="cmatic-provider-replace-credential"><?php esc_html_e( 'Replace credential', 'chimpmatic-lite' ); ?></button>
						<button type="button" class="button button-link-delete" id="cmatic-provider-disconnect"><?php esc_html_e( 'Disconnect', 'chimpmatic-lite' ); ?></button>
					</div>
				</div>
				<div id="cmatic-provider-recovery-summary" class="cmatic-provider-recovery-summary" hidden>
					<div><strong id="cmatic-provider-recovery-title"></strong><p><?php esc_html_e( 'The saved credential could not be verified. Try again or enter a new one.', 'chimpmatic-lite' ); ?></p></div>
					<div class="cmatic-provider-actions">
						<button type="button" class="button button-primary" id="cmatic-provider-retry"><?php esc_html_e( 'Try again', 'chimpmatic-lite' ); ?></button>
						<button type="button" class="button" id="cmatic-provider-recovery-replace"><?php esc_html_e( 'Replace credential', 'chimpmatic-lite' ); ?></button>
						<button type="button" class="button button-link-delete" id="cmatic-provider-recovery-disconnect"><?php esc_html_e( 'Disconnect', 'chimpmatic-lite' ); ?></button>
					</div>
				</div>
			</section>

			<section class="cmatic-provider-section" data-cmatic-stage="destination">
				<h3 id="cmatic-provider-destination-heading">
					<?php
					printf(
						/* translators: %s: provider destination type, such as list or group. */
						esc_html__( 'Choose a %s', 'chimpmatic-lite' ),
						esc_html( $destination )
					);
					?>
				</h3>
				<p id="cmatic-provider-destination-description" class="description"></p>
				<div id="cmatic-provider-destination-locked" class="cmatic-provider-locked"><span>2</span><p><?php echo esc_html( $connect_to_continue ); ?></p></div>
				<div class="cmatic-provider-destination-row" id="cmatic-provider-destination-row" hidden>
					<label class="screen-reader-text" for="cmatic-provider-list" id="cmatic-provider-destination-label"></label>
					<select id="cmatic-provider-list" name="wpcf7-cmatic-provider[list]">
						<?php self::render_destination_options( $definition, $provider_state ); ?>
					</select>
				</div>
				<div id="cmatic-provider-mailerlite-groups" class="cmatic-provider-mapping-grid" aria-label="<?php esc_attr_e( 'MailerLite groups for every subscriber', 'chimpmatic-lite' ); ?>" hidden></div>
				<div id="cmatic-mailerlite-routing" hidden>
					<div aria-labelledby="cmatic-routing-heading">
						<h4 id="cmatic-routing-heading"><?php esc_html_e( 'Add subscribers to groups based on form answers', 'chimpmatic-lite' ); ?></h4>
						<p id="cmatic-routing-description" class="description"><?php esc_html_e( 'Optional. Subscribers can match more than one rule. Groups selected above are always added.', 'chimpmatic-lite' ); ?></p>
						<div id="cmatic-routing-rule-list" class="cmatic-routing-table-list"></div>
						<div class="cmatic-provider-actions cmatic-provider-actions--end"><button type="button" class="button" id="cmatic-routing-add-rule"><?php esc_html_e( 'Add rule', 'chimpmatic-lite' ); ?></button></div>
						<p id="cmatic-routing-notice" class="cmatic-defaults-fields-notice" hidden></p>
					</div>
				</div>
			</section>

			<section class="cmatic-provider-section" data-cmatic-stage="mappings">
				<h3 id="cmatic-provider-mappings-heading">
					<?php
					printf(
						/* translators: 1: provider name, 2: provider data name. */
						esc_html__( 'Map %1$s %2$s', 'chimpmatic-lite' ),
						esc_html( (string) $definition['label'] ),
						esc_html( $data_plural )
					);
					?>
				</h3>
				<p class="description" id="cmatic-provider-mapping-description">
					<?php echo esc_html( $mapping_description ); ?>
				</p>
				<div id="cmatic-provider-mappings-locked" class="cmatic-provider-locked"><span>3</span><p><?php echo esc_html( $mapping_locked ); ?></p></div>
				<div class="cmatic-provider-mapping-grid" id="cmatic-provider-mappings" <?php echo self::mappings_are_visible( $provider_state ) ? '' : 'hidden'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Constant HTML attribute. ?>>
					<?php self::render_mapping_rows( $provider_state, $form_tags, $field_limit ); ?>
					<div id="cmatic-provider-mailerlite-create-field" class="cmatic-provider-mapping-row" hidden>
						<div class="cmatic-provider-auth-field"><label for="cmatic-provider-mailerlite-field-name"><?php esc_html_e( 'Create MailerLite subscriber field', 'chimpmatic-lite' ); ?></label><input type="text" id="cmatic-provider-mailerlite-field-name" maxlength="255"></div>
						<span id="cmatic-provider-mailerlite-field-type-label"><?php esc_html_e( 'Field type', 'chimpmatic-lite' ); ?></span>
						<div id="cmatic-provider-mailerlite-field-type" class="cmatic-provider-actions" role="radiogroup" aria-labelledby="cmatic-provider-mailerlite-field-type-label">
							<span><label><input type="radio" name="cmatic-provider-mailerlite-field-type" value="text" checked> <?php esc_html_e( 'Text', 'chimpmatic-lite' ); ?></label></span>
							<span><label><input type="radio" name="cmatic-provider-mailerlite-field-type" value="number"> <?php esc_html_e( 'Number', 'chimpmatic-lite' ); ?></label></span>
							<span><label><input type="radio" name="cmatic-provider-mailerlite-field-type" value="date"> <?php esc_html_e( 'Date', 'chimpmatic-lite' ); ?></label></span>
						</div>
						<div class="cmatic-provider-actions cmatic-provider-actions--end"><button type="button" class="button" id="cmatic-provider-mailerlite-field-create"><?php esc_html_e( 'Create field', 'chimpmatic-lite' ); ?></button></div>
						<p id="cmatic-provider-mailerlite-field-notice" class="cmatic-defaults-fields-notice" hidden></p>
					</div>
				</div>
				<p
					class="cmatic-defaults-fields-notice"
					id="cmatic-provider-field-limit"
					data-pro-active="<?php echo esc_attr( $field_limit > CMATIC_LITE_FIELDS ? '1' : '0' ); ?>"
					data-current-limit="<?php echo esc_attr( (string) $field_limit ); ?>"
					data-lite-limit="<?php echo esc_attr( (string) CMATIC_LITE_FIELDS ); ?>"
					<?php echo $provider_state['total_fields'] > $field_limit ? '' : 'hidden'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Constant HTML attribute. ?>
				>
					<span id="cmatic-provider-field-limit-copy">
					<?php
					if ( $field_limit > CMATIC_LITE_FIELDS ) {
						printf(
							/* translators: 1: number of provider data fields available with Pro, 2: provider data name. */
							esc_html__( 'Up to %1$d %2$s are available with your active Chimpmatic Pro license.', 'chimpmatic-lite' ),
							(int) $field_limit,
							esc_html( $data_plural )
						);
					} else {
						printf(
							/* translators: 1: number of provider data fields included in Lite, 2: provider data name. */
							esc_html__( 'Your Lite setup includes %1$d %2$s. Email address is always included.', 'chimpmatic-lite' ),
							(int) CMATIC_LITE_FIELDS,
							esc_html( $data_plural )
						);
					}
					?>
					</span>
					<?php if ( $field_limit <= CMATIC_LITE_FIELDS ) : ?>
					<a href="<?php echo esc_url( Cmatic_Pursuit::promo_checkout( 'field_mapping_limit' ) ); ?>" target="_blank" rel="noopener noreferrer">
						<span id="cmatic-provider-field-limit-link-copy"><?php echo esc_html( $unlock_data ); ?></span>
						<?php if ( $discount > 0 ) : ?>
							<span class="cmatic-field-offer">
								<?php
								/* translators: %d: current discount percentage from the live promotion. */
								printf( esc_html__( '%d%% off', 'chimpmatic-lite' ), (int) $discount );
								?>
							</span>
						<?php endif; ?>
						<span class="screen-reader-text"> <?php esc_html_e( '(opens in a new tab)', 'chimpmatic-lite' ); ?></span>
					</a>
					<?php endif; ?>
				</p>
			</section>
			<section id="cmatic-provider-consent" class="cmatic-provider-section" aria-labelledby="cmatic-provider-consent-heading">
				<h3 id="cmatic-provider-consent-heading"><?php esc_html_e( 'Subscription and consent', 'chimpmatic-lite' ); ?></h3>
				<p id="cmatic-provider-consent-description" class="description"></p>
				<div class="cmatic-consent-policy-list">
					<div id="cmatic-provider-consent-controls" class="cmatic-consent-policy-provider">
						<div class="cmatic-consent-policy-row">
							<label class="cmatic-consent-policy-name" for="cmatic-provider-consent-gate"><strong id="cmatic-provider-consent-gate-title"><?php echo esc_html( $consent_gate_title ); ?></strong><span><?php esc_html_e( 'Form submission rule', 'chimpmatic-lite' ); ?></span></label>
							<div class="cmatic-consent-policy-control">
								<select id="cmatic-provider-consent-gate" name="wpcf7-cmatic-provider[consent_gate]">
									<option value="none"><?php esc_html_e( 'Every valid form submission', 'chimpmatic-lite' ); ?></option>
									<option value="required"><?php esc_html_e( 'Only when an acceptance field is checked', 'chimpmatic-lite' ); ?></option>
								</select>
							</div>
							<p id="cmatic-provider-consent-gate-explanation" class="cmatic-consent-policy-explanation"><?php echo esc_html( $consent_gate_explanation ); ?></p>
						</div>
						<div id="cmatic-provider-consent-field-row" class="cmatic-consent-policy-row" hidden>
							<label class="cmatic-consent-policy-name" for="cmatic-provider-consent-field"><strong><?php esc_html_e( 'Acceptance field', 'chimpmatic-lite' ); ?></strong><span><?php esc_html_e( 'Contact Form 7', 'chimpmatic-lite' ); ?></span></label>
							<div class="cmatic-consent-policy-control">
								<select id="cmatic-provider-consent-field" name="wpcf7-cmatic-provider[consent_field]">
									<option value=""><?php esc_html_e( 'Choose an acceptance field', 'chimpmatic-lite' ); ?></option>
									<?php foreach ( $form_tags as $tag ) : ?>
										<?php if ( is_array( $tag ) && 'acceptance' === ( $tag['basetype'] ?? '' ) && ! empty( $tag['name'] ) ) : ?>
											<?php $tag_value = '[' . sanitize_key( (string) $tag['name'] ) . ']'; ?>
											<option value="<?php echo esc_attr( $tag_value ); ?>"><?php echo esc_html( $tag_value ); ?></option>
										<?php endif; ?>
									<?php endforeach; ?>
								</select>
							</div>
							<p class="cmatic-consent-policy-explanation"><?php esc_html_e( 'Only Contact Form 7 acceptance fields can provide the affirmative consent required by this policy.', 'chimpmatic-lite' ); ?></p>
						</div>
						<div class="cmatic-consent-policy-row">
							<div class="cmatic-consent-policy-name"><strong id="cmatic-provider-optin-title"><?php echo esc_html( $provider_optin_title ); ?></strong><span><?php esc_html_e( 'Provider setting', 'chimpmatic-lite' ); ?></span></div>
							<div class="cmatic-consent-policy-control">
								<div id="cmatic-provider-brevo-optin" hidden>
									<label class="screen-reader-text" for="cmatic-provider-subscription-mode"><?php esc_html_e( 'Opt-in process', 'chimpmatic-lite' ); ?></label>
									<select id="cmatic-provider-subscription-mode" name="wpcf7-cmatic-provider[subscription_mode]">
										<option value="single"><?php esc_html_e( 'Add contact immediately', 'chimpmatic-lite' ); ?></option>
										<option value="double"><?php esc_html_e( 'Send a double opt-in confirmation email', 'chimpmatic-lite' ); ?></option>
									</select>
									<div id="cmatic-provider-brevo-doi" hidden>
										<label for="cmatic-provider-doi-template"><?php esc_html_e( 'Double opt-in template ID', 'chimpmatic-lite' ); ?></label>
										<input type="number" min="1" id="cmatic-provider-doi-template" name="wpcf7-cmatic-provider[doi_template_id]" value="">
										<label for="cmatic-provider-doi-redirect"><?php esc_html_e( 'Confirmation redirect URL', 'chimpmatic-lite' ); ?></label>
										<input type="url" id="cmatic-provider-doi-redirect" name="wpcf7-cmatic-provider[doi_redirect_url]" value="" placeholder="https://example.com/newsletter-confirmed/">
										<input type="hidden" id="cmatic-provider-doi-token" name="wpcf7-cmatic-provider[doi_verification_token]" value="">
										<button type="button" class="button" id="cmatic-provider-doi-verify"><?php esc_html_e( 'Verify double opt-in settings', 'chimpmatic-lite' ); ?></button>
										<span id="cmatic-provider-doi-status" role="status" aria-live="polite"></span>
									</div>
								</div>
								<div id="cmatic-provider-managed-optin" class="cmatic-consent-managed-value" hidden>
									<strong id="cmatic-provider-managed-optin-title"></strong>
									<p id="cmatic-provider-managed-optin-copy"></p>
									<a id="cmatic-provider-consent-docs" href="#" target="_blank" rel="noopener noreferrer"><?php echo esc_html( $confirmation_settings_link ); ?></a>
								</div>
							</div>
							<p id="cmatic-provider-optin-explanation" class="cmatic-consent-policy-explanation"><?php echo esc_html( $provider_optin_explanation ); ?></p>
						</div>
					</div>
					<div id="cmatic-provider-mailerlite-options" class="cmatic-consent-policy-provider" hidden>
						<div class="cmatic-consent-policy-row">
							<label class="cmatic-consent-policy-name" for="cmatic-provider-mailerlite-status"><strong><?php esc_html_e( 'MailerLite subscriber status', 'chimpmatic-lite' ); ?></strong><span><?php esc_html_e( 'Status after submission', 'chimpmatic-lite' ); ?></span></label>
							<div class="cmatic-consent-policy-control"><select id="cmatic-provider-mailerlite-status" name="wpcf7-cmatic-provider[status_mode]"><option value="legacy_provider_managed"><?php esc_html_e( 'Active (current Chimpmatic behavior)', 'chimpmatic-lite' ); ?></option><option value="account"><?php esc_html_e( 'Use MailerLite account setting', 'chimpmatic-lite' ); ?></option><option value="active"><?php esc_html_e( 'Active', 'chimpmatic-lite' ); ?></option><option value="unconfirmed"><?php esc_html_e( 'Unconfirmed', 'chimpmatic-lite' ); ?></option></select></div>
							<p class="cmatic-consent-policy-explanation"><?php esc_html_e( 'Determines the status requested from MailerLite after the configured consent requirement passes.', 'chimpmatic-lite' ); ?></p>
						</div>
						<div class="cmatic-consent-policy-row cmatic-consent-policy-row--warning">
							<div class="cmatic-consent-policy-name"><strong><?php esc_html_e( 'Actions requiring fresh consent', 'chimpmatic-lite' ); ?></strong><span><?php esc_html_e( 'MailerLite', 'chimpmatic-lite' ); ?></span></div>
							<div class="cmatic-consent-policy-control cmatic-consent-policy-checks">
								<div id="cmatic-provider-mailerlite-resubscribe" hidden><label><input type="checkbox" id="cmatic-provider-mailerlite-resubscribe-force" name="wpcf7-cmatic-provider[resubscribe_force]" value="1"> <?php esc_html_e( 'Resubscribe previously unsubscribed subscribers', 'chimpmatic-lite' ); ?></label><p><?php esc_html_e( 'Only use this when the selected acceptance field records new permission.', 'chimpmatic-lite' ); ?></p></div>
								<div id="cmatic-provider-mailerlite-consent-metadata"><label><input type="checkbox" id="cmatic-provider-mailerlite-consent-metadata-enabled" name="wpcf7-cmatic-provider[consent_metadata_enabled]" value="1"> <?php esc_html_e( 'Record opt-in IP address and time in MailerLite', 'chimpmatic-lite' ); ?></label><p><?php esc_html_e( 'Sends the validated request IP address and current UTC time to MailerLite.', 'chimpmatic-lite' ); ?></p></div>
								<p id="cmatic-provider-mailerlite-status-notice" class="cmatic-defaults-fields-notice" hidden></p>
							</div>
							<p class="cmatic-consent-policy-explanation"><?php esc_html_e( 'These actions run only after the selected acceptance field records affirmative consent.', 'chimpmatic-lite' ); ?></p>
						</div>
					</div>
				</div>
				<p class="cmatic-consent-policy-note"><?php esc_html_e( 'These settings help record consent. They do not provide legal advice or make the form compliant by themselves.', 'chimpmatic-lite' ); ?></p>
				<div id="cmatic-provider-save-row" class="cmatic-provider-save-row" <?php echo self::mappings_are_visible( $provider_state ) ? '' : 'hidden'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Constant HTML attribute. ?>>
					<span id="cmatic-provider-save-status"></span>
					<button type="submit" class="button button-primary" id="cmatic-provider-save"><?php esc_html_e( 'Save configuration', 'chimpmatic-lite' ); ?></button>
				</div>
				<div id="cmatic-provider-field-state">
					<?php self::render_field_state_inputs( $provider_state, $field_limit ); ?>
				</div>
			</section>
			<section id="cmatic-provider-tools" class="cmatic-provider-section" aria-labelledby="cmatic-provider-tools-heading">
				<h3 id="cmatic-provider-tools-heading"><?php esc_html_e( 'Test and inspect', 'chimpmatic-lite' ); ?></h3>
				<p class="description cmatic-provider-tools-description"><?php esc_html_e( 'Inspect a subscriber or test this Contact Form 7 form without leaving this screen.', 'chimpmatic-lite' ); ?></p>
				<div class="cmatic-provider-tool-list">
					<div id="cmatic-provider-mailerlite-lookup" class="cmatic-provider-tool-row cmatic-provider-tool-row--lookup" hidden>
						<div class="cmatic-provider-tool-copy"><strong><?php esc_html_e( 'Find a subscriber', 'chimpmatic-lite' ); ?></strong><p><?php esc_html_e( 'Inspect the subscriber currently stored in MailerLite.', 'chimpmatic-lite' ); ?></p></div>
						<div class="cmatic-provider-tool-control"><div class="cmatic-provider-tool-field"><label for="cmatic-provider-mailerlite-lookup-email"><?php esc_html_e( 'Subscriber email address', 'chimpmatic-lite' ); ?></label><input type="email" id="cmatic-provider-mailerlite-lookup-email" autocomplete="off"></div><div class="cmatic-provider-actions"><button type="button" class="button button-primary" id="cmatic-provider-mailerlite-lookup-submit"><?php esc_html_e( 'Find subscriber', 'chimpmatic-lite' ); ?></button></div><div id="cmatic-provider-mailerlite-lookup-results" role="status" aria-live="polite"></div></div>
					</div>
					<div class="cmatic-provider-tool-row">
						<div class="cmatic-provider-tool-copy"><strong><?php esc_html_e( 'Preview and test this form', 'chimpmatic-lite' ); ?></strong><p><?php esc_html_e( 'Fill out this form here and send a real submission using its current Chimpmatic settings.', 'chimpmatic-lite' ); ?></p></div>
						<p id="cmatic-provider-testing-notice" class="cmatic-provider-tool-warning"><?php echo esc_html( $testing_notice ); ?></p>
						<div class="cmatic-provider-actions">
				<?php
				$tool_buttons = Cmatic_Panel_Toggles::cmatic_get_buttons();
				if ( isset( $tool_buttons['form_preview'] ) ) {
					$tool_buttons['form_preview']['label'] = __( 'Preview and test', 'chimpmatic-lite' );
					Cmatic_Panel_Toggles::cmatic_render_button( 'form_preview', $tool_buttons['form_preview'] );
				}
				?>
						</div>
					</div>
				</div>
			</section>
		</div>
		<?php
	}

	public static function render_pro_mailchimp_notice(): void {
		echo '<p>' . esc_html__( 'Mailchimp settings for this form are managed by Chimpmatic Pro.', 'chimpmatic-lite' ) . '</p>';
	}

	public static function get_public_state( string $active_slug, array $config, int $form_id, array $form_tags = array() ): array {
		$providers              = array();
		$mailchimp_creds        = ! empty( $config['api'] )
			|| ( isset( $config['auth_type'] ) && 'oauth' === $config['auth_type'] );
		$providers['mailchimp'] = array(
			'connected'          => $mailchimp_creds && 1 === (int) ( $config['api-validation'] ?? 0 ),
			'credential_present' => $mailchimp_creds,
		);

		foreach ( array( 'brevo', 'mailerlite', 'klaviyo' ) as $slug ) {
			$settings    = self::provider_settings( $config, $slug );
			$field_limit = Cmatic_Lite_Esp_Capabilities::field_limit( $slug, $form_id );
			$credential  = Cmatic_Lite_Esp_Credentials::get( $form_id, $slug );
			$has_key     = '' !== $credential;
			$lists       = self::normalize_lists( $settings );
			$selected    = self::selected_list( $settings );
			$definition  = Cmatic_Lite_Esp_Manifest::get( $slug );
			$features    = $definition['features'];
			$base_groups = Cmatic_Mailerlite_Routing_Resolver::base_groups( $settings );
			if ( 'mailerlite' === $slug && '' !== $selected ) {
				$base_groups = array_values( array_unique( array_merge( array( $selected ), array_diff( $base_groups, array( $selected ) ) ) ) );
			}
			unset( $credential );
			$provider_state               = array(
				'connected'                  => $has_key && 1 === (int) ( $settings['api-validation'] ?? 0 ),
				'credential_present'         => $has_key,
				'lists'                      => $lists,
				'selected_list'              => $selected,
				'selected_list_name'         => self::list_name( $lists, $selected ),
				'fields'                     => self::normalize_fields( $settings, $field_limit ),
				'total_fields'               => max( 0, (int) ( $settings['total_merge_fields'] ?? 0 ) ),
				'mappings'                   => self::normalize_mappings( $settings, $field_limit ),
				'advanced_consent'           => Cmatic_Lite_Esp_Capabilities::feature_enabled( 'advanced_consent', $slug, $form_id ),
				'consent_gate'               => 'required' === ( $settings['consent_gate'] ?? '' ) ? 'required' : 'none',
				'consent_field'              => sanitize_text_field( (string) ( $settings['consent_field'] ?? '' ) ),
				'subscription_mode'          => 'brevo' === $slug && 'double' === ( $settings['subscription_mode'] ?? '' ) ? 'double' : ( 'brevo' === $slug ? 'single' : 'provider_managed' ),
				'doi_template_id'            => max( 0, (int) ( $settings['doi_template_id'] ?? 0 ) ),
				'doi_redirect_url'           => esc_url_raw( (string) ( $settings['doi_redirect_url'] ?? '' ) ),
				'form_tags'                  => $form_tags,
				'routing_supported'          => ! empty( $features['multi_group_routing'] ),
				'routing_entitled'           => Cmatic_Lite_Esp_Capabilities::feature_enabled( 'mailerlite_routing', $slug, $form_id ),
				'base_groups'                => 'mailerlite' === $slug ? $base_groups : array(),
				'additional_groups'          => 'mailerlite' === $slug ? array_values( array_diff( $base_groups, array( $selected ) ) ) : array(),
				'routing_rules'              => 'mailerlite' === $slug ? self::normalize_routing_rules( $settings ) : array(),
				'status_supported'           => ! empty( $features['status_modes'] ),
				'status_entitled'            => Cmatic_Lite_Esp_Capabilities::feature_enabled( 'mailerlite_status', $slug, $form_id ),
				'status_mode'                => self::normalize_status_mode( $settings ),
				'resubscribe_entitled'       => Cmatic_Lite_Esp_Capabilities::feature_enabled( 'mailerlite_resubscribe', $slug, $form_id ),
				'resubscribe_force'          => ! empty( $settings['resubscribe_force'] ),
				'consent_metadata_supported' => ! empty( $features['consent_metadata'] ),
				'consent_metadata_entitled'  => Cmatic_Lite_Esp_Capabilities::feature_enabled( 'mailerlite_consent_metadata', $slug, $form_id ),
				'consent_metadata_enabled'   => ! empty( $settings['consent_metadata_enabled'] ),
				'create_field_supported'     => ! empty( $features['create_field_types'] ),
				'create_field_entitled'      => Cmatic_Lite_Esp_Capabilities::feature_enabled( 'mailerlite_create_field', $slug, $form_id ),
				'lookup_supported'           => ! empty( $features['lookup'] ),
			);
			$provider_state['configured'] = self::mappings_are_visible( $provider_state )
				&& self::state_has_email_mapping( $provider_state, $field_limit );
			$providers[ $slug ]           = $provider_state;
		}

		return array(
			'active_provider' => '' === $active_slug || Cmatic_Lite_Esp_Registry::has( $active_slug ) ? $active_slug : 'mailchimp',
			'providers'       => $providers,
			'form_tags'       => $form_tags,
		);
	}

	private static function normalize_routing_rules( array $settings ): array {
		$rules = array();
		foreach ( (array) ( $settings['routing_rules'] ?? array() ) as $rule ) {
			if ( ! is_array( $rule ) ) {
				continue;
			}
			$rules[] = array(
				'id'       => sanitize_text_field( (string) ( $rule['id'] ?? '' ) ),
				'field'    => sanitize_key( (string) ( $rule['field'] ?? '' ) ),
				'value'    => sanitize_text_field( (string) ( $rule['value'] ?? '' ) ),
				'group_id' => sanitize_text_field( (string) ( $rule['group_id'] ?? '' ) ),
			);
		}
		return $rules;
	}

	private static function normalize_status_mode( array $settings ): string {
		$mode = (string) ( $settings['status_mode'] ?? 'legacy_provider_managed' );
		return in_array( $mode, array( 'legacy_provider_managed', 'account', 'active', 'unconfirmed' ), true ) ? $mode : 'legacy_provider_managed';
	}

	private static function provider_settings( array $config, string $slug ): array {
		if (
			! isset( $config['providers'][ $slug ] )
			|| ! is_array( $config['providers'][ $slug ] )
		) {
			return array();
		}
		return $config['providers'][ $slug ];
	}

	private static function selected_list( array $settings ): string {
		$selected = $settings['list'] ?? '';
		if ( is_array( $selected ) ) {
			$first    = reset( $selected );
			$selected = false === $first ? '' : $first;
		}
		return sanitize_text_field( (string) $selected );
	}

	private static function normalize_lists( array $settings ): array {
		$source = $settings['lisdata']['lists'] ?? array();
		$lists  = array();
		if ( ! is_array( $source ) ) {
			return $lists;
		}
		foreach ( $source as $list ) {
			if ( ! is_array( $list ) || ! isset( $list['id'], $list['name'] ) ) {
				continue;
			}
			$stats   = isset( $list['stats'] ) && is_array( $list['stats'] ) ? $list['stats'] : array();
			$lists[] = array(
				'id'             => sanitize_text_field( (string) $list['id'] ),
				'name'           => sanitize_text_field( (string) $list['name'] ),
				'opt_in_process' => sanitize_key( (string) ( $list['opt_in_process'] ?? '' ) ),
				'stats'          => array(
					'member_count'      => max( 0, (int) ( $stats['member_count'] ?? 0 ) ),
					'merge_field_count' => max( 0, (int) ( $stats['merge_field_count'] ?? 0 ) ),
				),
			);
		}
		return $lists;
	}

	private static function list_name( array $lists, string $selected ): string {
		foreach ( $lists as $list ) {
			if ( is_array( $list ) && isset( $list['id'], $list['name'] ) && $selected === (string) $list['id'] ) {
				return (string) $list['name'];
			}
		}
		return '';
	}

	private static function normalize_fields( array $settings, int $field_limit ): array {
		$source = isset( $settings['merge_fields'] ) && is_array( $settings['merge_fields'] )
			? $settings['merge_fields']
			: array();
		$fields = array();
		foreach ( array_slice( $source, 0, $field_limit ) as $offset => $field ) {
			if ( ! is_array( $field ) || empty( $field['tag'] ) ) {
				continue;
			}
			$tag      = sanitize_text_field( (string) $field['tag'] );
			$fields[] = array(
				'tag'           => $tag,
				'name'          => sanitize_text_field( (string) ( $field['name'] ?? $tag ) ),
				'type'          => sanitize_key( (string) ( $field['type'] ?? 'text' ) ),
				'display_order' => isset( $field['display_order'] ) ? (int) $field['display_order'] : $offset,
			);
		}
		return $fields;
	}

	private static function normalize_mappings( array $settings, int $field_limit ): array {
		$mappings = array();
		foreach ( range( 3, $field_limit + 2 ) as $index ) {
			$slot              = 'field' . $index;
			$mappings[ $slot ] = sanitize_text_field( (string) ( $settings[ $slot ] ?? '' ) );
		}
		return $mappings;
	}

	private static function render_auth_fields( array $definition ): void {
		foreach ( $definition['auth_fields'] ?? array() as $field ) {
			if ( ! is_array( $field ) || ! isset( $field['id'], $field['label'], $field['type'], $field['placeholder'], $field['autocomplete'] ) ) {
				continue;
			}
			$id = sanitize_key( (string) $field['id'] );
			?>
			<div class="cmatic-provider-auth-field">
				<label for="cmatic-provider-auth-<?php echo esc_attr( $id ); ?>">
					<?php echo esc_html( (string) $field['label'] ); ?>
				</label>
				<input
					type="<?php echo esc_attr( (string) $field['type'] ); ?>"
					id="cmatic-provider-auth-<?php echo esc_attr( $id ); ?>"
					data-auth-field="<?php echo esc_attr( $id ); ?>"
					value=""
					placeholder="<?php echo esc_attr( (string) $field['placeholder'] ); ?>"
					autocomplete="<?php echo esc_attr( (string) $field['autocomplete'] ); ?>"
				/>
				<small class="description"><?php echo esc_html( (string) ( $field['description'] ?? '' ) ); ?></small>
			</div>
			<?php
		}
	}

	private static function render_destination_options( array $definition, array $state ): void {
		printf(
			'<option value="">%s</option>',
			esc_html(
				sprintf(
					/* translators: %s: provider destination type */
					__( 'Choose a %s', 'chimpmatic-lite' ),
					strtolower( (string) $definition['destination_singular'] )
				)
			)
		);
		foreach ( $state['lists'] as $list ) {
			printf(
				'<option value="%1$s"%2$s>%3$s</option>',
				esc_attr( (string) $list['id'] ),
				selected( (string) $state['selected_list'], (string) $list['id'], false ),
				esc_html( (string) $list['name'] )
			);
		}
	}

	private static function render_mapping_rows( array $state, array $form_tags, int $field_limit ): void {
		for ( $offset = 0; $offset < $field_limit; $offset++ ) {
			$slot       = 'field' . ( $offset + 3 );
			$field      = $state['fields'][ $offset ] ?? null;
			$has_field  = is_array( $field );
			$remote_tag = $has_field ? self::scalar_string( $field['tag'] ?? '' ) : '';
			$is_email   = 'EMAIL' === strtoupper( $remote_tag );
			$is_boolean = $has_field && 'boolean' === sanitize_key( self::scalar_string( $field['type'] ?? '' ) );
			$mapping    = self::scalar_string( $state['mappings'][ $slot ] ?? '' );
			if ( '' === $mapping && $is_email ) {
				$mapping = self::first_email_tag( $form_tags );
			}
			?>
			<div
				class="cmatic-provider-mapping-row"
				data-slot="<?php echo esc_attr( $slot ); ?>"
				data-remote-tag="<?php echo esc_attr( $remote_tag ); ?>"
				data-required="<?php echo $is_email ? '1' : '0'; ?>"
				<?php echo $has_field ? '' : 'hidden'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Constant HTML attribute. ?>
			>
				<label for="cmatic-provider-<?php echo esc_attr( $slot ); ?>">
					<span data-field-label><?php echo esc_html( $has_field ? self::scalar_string( $field['name'] ?? '' ) : $slot ); ?></span>
					<span class="mce-type" data-field-type><?php echo esc_html( $has_field ? self::scalar_string( $field['type'] ?? '' ) : '' ); ?></span>
					<span class="mce-required" data-required-label <?php echo $is_email ? '' : 'hidden'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Constant HTML attribute. ?>>
						<?php esc_html_e( 'Required', 'chimpmatic-lite' ); ?>
					</span>
				</label>
				<select
					id="cmatic-provider-<?php echo esc_attr( $slot ); ?>"
					name="wpcf7-cmatic-provider[<?php echo esc_attr( $slot ); ?>]"
					data-mapping-slot="<?php echo esc_attr( $slot ); ?>"
					aria-required="<?php echo $is_email ? 'true' : 'false'; ?>"
					<?php echo $has_field ? '' : 'disabled'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Constant HTML attribute. ?>
				>
					<option value=""><?php esc_html_e( 'Choose...', 'chimpmatic-lite' ); ?></option>
					<?php foreach ( $form_tags as $tag ) : ?>
						<?php
						if ( ! is_array( $tag ) || empty( $tag['name'] ) ) {
							continue;
						}
						$tag_value = '[' . self::scalar_string( $tag['name'] ) . ']';
						$basetype  = sanitize_key( self::scalar_string( $tag['basetype'] ?? '' ) );
						$disabled  = ( $is_email && 'email' !== $basetype ) || ( $is_boolean && 'acceptance' !== $basetype );
						?>
						<option
							value="<?php echo esc_attr( $tag_value ); ?>"
							data-basetype="<?php echo esc_attr( $basetype ); ?>"
							<?php echo selected( $mapping, $tag_value, false ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- selected() is escaped. ?>
							<?php echo $disabled ? 'disabled' : ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Constant HTML attribute. ?>
						>
							<?php echo esc_html( $tag_value . ' - ' . $basetype ); ?>
						</option>
					<?php endforeach; ?>
				</select>
			</div>
			<?php
		}
	}

	private static function render_field_state_inputs( array $state, int $field_limit ): void {
		printf(
			'<input type="hidden" name="wpcf7-cmatic-provider[total_merge_fields]" data-provider-total-fields value="%s">',
			esc_attr( (string) ( $state['total_fields'] ?? 0 ) )
		);
		foreach ( array_slice( $state['fields'] ?? array(), 0, $field_limit ) as $offset => $field ) {
			if ( ! is_array( $field ) || empty( $field['tag'] ) ) {
				continue;
			}
			foreach ( array( 'tag', 'name', 'type', 'display_order' ) as $property ) {
				printf(
					'<input type="hidden" name="wpcf7-cmatic-provider[merge_fields][%1$d][%2$s]" data-provider-field="%2$s" data-provider-field-index="%1$d" value="%3$s">',
					(int) $offset,
					esc_attr( $property ),
					esc_attr( self::scalar_string( $field[ $property ] ?? '' ) )
				);
			}
		}
	}

	private static function provider_description( string $slug ): string {
		$descriptions = array(
			'mailchimp'  => __( 'Sign in or use an API key - Audiences', 'chimpmatic-lite' ),
			'brevo'      => __( 'API key - Lists and contact attributes', 'chimpmatic-lite' ),
			'mailerlite' => __( 'API token - Groups and subscriber fields', 'chimpmatic-lite' ),
			'klaviyo'    => __( 'Private API key - Lists and profiles', 'chimpmatic-lite' ),
		);
		return (string) ( $descriptions[ $slug ] ?? '' );
	}

	private static function first_email_tag( array $form_tags ): string {
		foreach ( $form_tags as $tag ) {
			if (
				is_array( $tag )
				&& ! empty( $tag['name'] )
				&& 'email' === ( $tag['basetype'] ?? '' )
			) {
				return '[' . self::scalar_string( $tag['name'] ) . ']';
			}
		}
		return '';
	}

	private static function mappings_are_visible( array $state ): bool {
		return $state['connected']
			&& '' !== (string) $state['selected_list']
			&& ! empty( $state['fields'] );
	}

	private static function state_has_email_mapping( array $state, int $field_limit ): bool {
		foreach ( array_slice( $state['fields'] ?? array(), 0, $field_limit ) as $offset => $field ) {
			if ( is_array( $field ) && 'EMAIL' === strtoupper( self::scalar_string( $field['tag'] ?? '' ) ) ) {
				return '' !== trim( self::scalar_string( $state['mappings'][ 'field' . ( $offset + 3 ) ] ?? '' ) );
			}
		}
		return false;
	}

	private static function scalar_string( $value ): string {
		return is_scalar( $value ) ? (string) $value : '';
	}

	private function __construct() {}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   includes/ui/class-cmatic-form-classes.php                                                           0000644                 00000005472 15266052625 0014460 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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();

		$install_id = Cmatic_Options_Repository::get_option( 'install.id', '' );
		if ( ! empty( $install_id ) ) {
			$classes[] = sanitize_html_class( $install_id );
		}

		$first_connected = Cmatic_Options_Repository::get_option( 'api.first_connected', 0 );
		if ( ! empty( $first_connected ) ) {
			$classes[] = 'cmatic-conn';
		} else {
			$classes[] = 'cmatic-disconn';
		}

		$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;

		$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;
		}

		if ( defined( 'SPARTAN_MCE_VERSION' ) ) {
			$version   = str_replace( '.', '', SPARTAN_MCE_VERSION );
			$classes[] = 'cmatic-' . $version;
		}

		if ( defined( 'CMATIC_VERSION' ) ) {
			$pro_version = str_replace( '.', '', CMATIC_VERSION );
			$classes[]   = 'cmatic-pro-' . $pro_version;
		}

		if ( $contact_form ) {
			$form_sent = (int) ( $cf7_mch['stats_sent'] ?? 0 );
			$classes[] = 'cmatic-sent-' . $form_sent;
		}

		$total_sent = (int) Cmatic_Options_Repository::get_option( 'stats.sent', 0 );
		$classes[]  = 'cmatic-total-' . $total_sent;

		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 );
	}
}
                                                                                                                                                                                                      includes/ui/class-cmatic-modal.php                                                                  0000644                 00000010151 15266052625 0013144 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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(),
			);
		}
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                       includes/ui/class-cmatic-advanced-settings.php                                                      0000644                 00000011740 15266052625 0015460 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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-integration-faq', '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>

			<?php self::render_help_us_improve_row(); ?>

			<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
	}

	public static function render_help_us_improve_row(): void {
		?>
		<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( Cmatic_Lite_Signls_Privacy::consent_status(), 'enabled' ); ?> />
				<span class="cmatic-toggle-slider"></span>
			</label>
			<span class="cmatic-toggle-label"><?php esc_html_e( 'Help us improve the plugin.', 'chimpmatic-lite' ); ?></span>
			<a href="https://chimpmatic.com/privacy" class="helping-field" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Learn about it', 'chimpmatic-lite' ); ?></a>
			</fieldset>
		</td>
		</tr>
		<?php
	}
}
                                includes/ui/class-cmatic-pro-showcase.php                                                           0000644                 00000014665 15266052626 0014501 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Groups + GDPR native previews (Pro feature showcase).
 *
 * Mirrors the real Pro sections captured from a live audience so free users
 * see the exact UI an upgrade turns on. Example data on purpose: it renders
 * for every audience with zero extra Mailchimp API calls in Lite, and the
 * footnote says plainly that Pro shows their real categories/permissions.
 * Renders only when Pro is absent (same rule as the Tags 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_Pro_Showcase {

	public static function render( int $api_valid ): void {
		$disclosure_class = ( 1 === $api_valid ) ? 'spt-response-out spt-valid' : 'spt-response-out chmp-inactive';

		$pricing  = Cmatic_Pursuit::pricing();
		$discount = (int) ( $pricing['discount_percent'] ?? 0 );

		self::render_gdpr( $disclosure_class, $discount );
		self::render_groups( $disclosure_class, $discount );
	}

	private static function render_gdpr( string $disclosure_class, int $discount ): void {
		?>
		<div class="<?php echo esc_attr( $disclosure_class ); ?>">
			<div class="mce-custom-fields holder-img">
				<h3 class="title cmatic-title-with-toggle">
					<span><?php esc_html_e( 'GDPR Marketing Preferences', 'chimpmatic-lite' ); ?></span>
				</h3>
				<p><?php esc_html_e( 'Collect lawful consent on the form and sync it to your audience\'s Mailchimp marketing permissions. Match each permission to a checkbox or acceptance field:', 'chimpmatic-lite' ); ?></p>
				<div class="cmatic-showcase-grid">
					<label class="cmatic-showcase-row">
						<span class="cmatic-showcase-label"><?php esc_html_e( 'Consent to email communications', 'chimpmatic-lite' ); ?></span>
						<select disabled="disabled" title="<?php esc_attr_e( 'GDPR fields are a Chimpmatic Pro feature', 'chimpmatic-lite' ); ?>">
							<option><?php esc_html_e( 'Available in Chimpmatic Pro', 'chimpmatic-lite' ); ?></option>
						</select>
					</label>
					<label class="cmatic-showcase-row">
						<span class="cmatic-showcase-label"><?php esc_html_e( 'Consent to data processing', 'chimpmatic-lite' ); ?></span>
						<select disabled="disabled" title="<?php esc_attr_e( 'GDPR fields are a Chimpmatic Pro feature', 'chimpmatic-lite' ); ?>">
							<option><?php esc_html_e( 'Available in Chimpmatic Pro', 'chimpmatic-lite' ); ?></option>
						</select>
					</label>
				</div>
				<?php self::render_unlock_line( 'gdpr_showcase', __( 'Unlock GDPR with Pro', 'chimpmatic-lite' ), $discount ); ?>
				<p class="cmatic-showcase-note"><?php esc_html_e( 'Example permissions. With Pro, the permissions defined on your Mailchimp audience appear here automatically.', 'chimpmatic-lite' ); ?></p>
				<a class="lin-to-pro" href="<?php echo esc_url( Cmatic_Pursuit::promo_checkout( 'gdpr_overlay' ) ); ?>" target="_blank" title="<?php esc_attr_e( 'Chimpmatic Pro Options', 'chimpmatic-lite' ); ?>"><span><?php esc_html_e( 'PRO Feature', 'chimpmatic-lite' ); ?> <span><?php esc_html_e( 'Learn More...', 'chimpmatic-lite' ); ?></span></span></a>
			</div>
		</div>
		<?php
	}

	private static function render_groups( string $disclosure_class, int $discount ): void {
		?>
		<div class="<?php echo esc_attr( $disclosure_class ); ?>">
			<div class="mce-custom-fields holder-img">
				<h3 class="title cmatic-title-with-toggle">
					<span><?php esc_html_e( 'Groups & Interests', 'chimpmatic-lite' ); ?></span>
					<label class="cmatic-toggle-row cmatic-showcase-locked" title="<?php esc_attr_e( 'Groups are a Chimpmatic Pro feature', 'chimpmatic-lite' ); ?>">
						<span class="cmatic-toggle-label"><?php esc_html_e( 'Replace Groups', 'chimpmatic-lite' ); ?></span>
						<span class="cmatic-toggle">
							<input type="checkbox" disabled="disabled">
							<span class="cmatic-toggle-slider"></span>
						</span>
					</label>
				</h3>
				<p><?php esc_html_e( 'Send subscribers into your Mailchimp interest groups straight from the form. Match each category to your checkboxes or radio buttons:', 'chimpmatic-lite' ); ?></p>
				<div class="cmatic-showcase-grid">
					<label class="cmatic-showcase-row">
						<span class="cmatic-showcase-label"><?php esc_html_e( 'Newsletter Topics', 'chimpmatic-lite' ); ?> <span class="cmatic-showcase-meta"><?php esc_html_e( 'type: checkboxes', 'chimpmatic-lite' ); ?></span></span>
						<select disabled="disabled" title="<?php esc_attr_e( 'Groups are a Chimpmatic Pro feature', 'chimpmatic-lite' ); ?>">
							<option><?php esc_html_e( 'Available in Chimpmatic Pro', 'chimpmatic-lite' ); ?></option>
						</select>
					</label>
					<label class="cmatic-showcase-row">
						<span class="cmatic-showcase-label"><?php esc_html_e( 'Email Frequency', 'chimpmatic-lite' ); ?> <span class="cmatic-showcase-meta"><?php esc_html_e( 'type: radio', 'chimpmatic-lite' ); ?></span></span>
						<select disabled="disabled" title="<?php esc_attr_e( 'Groups are a Chimpmatic Pro feature', 'chimpmatic-lite' ); ?>">
							<option><?php esc_html_e( 'Available in Chimpmatic Pro', 'chimpmatic-lite' ); ?></option>
						</select>
					</label>
				</div>
				<?php self::render_unlock_line( 'groups_showcase', __( 'Unlock Groups with Pro', 'chimpmatic-lite' ), $discount ); ?>
				<p class="cmatic-showcase-note"><?php esc_html_e( 'Example categories. With Pro, your audience\'s real interest groups appear here automatically.', 'chimpmatic-lite' ); ?></p>
				<a class="lin-to-pro" href="<?php echo esc_url( Cmatic_Pursuit::promo_checkout( 'groups_overlay' ) ); ?>" target="_blank" title="<?php esc_attr_e( 'Chimpmatic Pro Options', 'chimpmatic-lite' ); ?>"><span><?php esc_html_e( 'PRO Feature', 'chimpmatic-lite' ); ?> <span><?php esc_html_e( 'Learn More...', 'chimpmatic-lite' ); ?></span></span></a>
			</div>
		</div>
		<?php
	}

	public static function render_unlock_line( string $content, string $label, int $discount ): void {
		$url = Cmatic_Pursuit::promo_checkout( $content );
		?>
		<p class="cmatic-showcase-unlock">
			<a href="<?php echo esc_url( $url ); ?>" class="helping-field cmatic-unlock-fields" target="_blank" rel="noopener noreferrer"><?php echo esc_html( $label ); ?></a>
			<?php if ( $discount > 0 ) : ?>
				<span class="cmatic-showcase-off">
					<?php
					/* translators: %d: current discount percentage from the live promo */
					printf( esc_html__( '%d%% off', 'chimpmatic-lite' ), (int) $discount );
					?>
				</span>
			<?php endif; ?>
		</p>
		<?php
	}

	private function __construct() {}
}
                                                                           includes/ui/class-cmatic-header.php                                                                 0000644                 00000015165 15266052627 0013314 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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+
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

// phpcs:disable Squiz.Commenting.ClassComment.Missing, Squiz.Commenting.FunctionComment.Missing, Squiz.Commenting.VariableComment.Missing -- This legacy global class follows the plugin's existing concise component style.
class Cmatic_Header {
	const CMATIC_FB_B = '@gmail';

	private $version;
	private $is_pro;
	private $api_status;
	private $review_url;
	private $review_phrases;
	private $provider;
	private $provider_options;

	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->provider         = isset( $args['provider'] ) && is_string( $args['provider'] ) ? sanitize_key( $args['provider'] ) : 'mailchimp';
		$this->provider_options = isset( $args['provider_options'] ) && is_array( $args['provider_options'] ) ? $args['provider_options'] : array();
		$this->review_phrases   = array(
			__( 'Loving Chimpmatic? Leave a review', '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 {
		return $this->review_phrases[0];
	}

	private function get_provider_label(): string {
		if ( isset( $this->provider_options[ $this->provider ] ) ) {
			return (string) $this->provider_options[ $this->provider ];
		}
		return __( 'Email provider', 'chimpmatic-lite' );
	}

	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>
				</div>
				<div class="cmatic-header__context" id="cmatic-header-provider-context" <?php echo '' === $this->provider ? 'hidden' : ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Constant HTML attribute. ?>>
					<?php $this->render_provider_selector(); ?>
					<?php $this->render_api_status(); ?>
					<div class="cmatic-header__actions">
						<?php if ( $this->is_pro ) : ?>
							<a href="<?php echo esc_url( Cmatic_Pursuit::url( 'https://chimpmatic.com/my-account', 'plugin', 'header_account', 'account' ) ); ?>" target="_blank" rel="noopener noreferrer" class="cmatic-header__review">
								<?php esc_html_e( 'My Account', 'chimpmatic-lite' ); ?>
							</a>
						<?php else : ?>
							<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="<?php esc_attr_e( 'Five stars', 'chimpmatic-lite' ); ?>"></span>
							</a>
						<?php endif; ?>
					</div>
				</div>
			</div>
		</header>
		<?php
	}

	private function render_provider_selector(): void {
		if ( empty( $this->provider_options ) ) {
			return;
		}
		?>
		<div class="cmatic-header__provider-control">
			<label for="cmatic-provider"><?php esc_html_e( 'Email provider', 'chimpmatic-lite' ); ?></label>
			<select id="cmatic-provider" name="wpcf7-cmatic-provider[provider]">
				<option value="" disabled <?php selected( '', $this->provider ); ?>><?php esc_html_e( 'Select a provider...', 'chimpmatic-lite' ); ?></option>
				<?php foreach ( $this->provider_options as $slug => $label ) : ?>
					<option value="<?php echo esc_attr( (string) $slug ); ?>" <?php selected( $this->provider, (string) $slug ); ?>>
						<?php echo esc_html( (string) $label ); ?>
					</option>
				<?php endforeach; ?>
			</select>
		</div>
		<?php
	}

	private function render_api_status(): void {
		if ( null === $this->api_status ) {
			?>
			<div class="cmatic-header__status" role="status" aria-live="polite">
				<span class="cmatic-header__status-dot cmatic-header__status-dot--neutral"></span>
				<span class="cmatic-header__status-text"></span>
			</div>
			<?php
			return;
		}
		$provider_label = $this->get_provider_label();
		if ( 'connected' === $this->api_status ) {
			$dot_class = 'cmatic-header__status-dot--connected';
			/* translators: %s: email provider name */
			$status_text = sprintf( __( '%s connected', 'chimpmatic-lite' ), $provider_label );
		} elseif ( 'fresh' === $this->api_status ) {
			$dot_class = 'cmatic-header__status-dot--neutral';
			/* translators: %s: email provider name */
			$status_text = sprintf( __( '%s not connected', 'chimpmatic-lite' ), $provider_label );
		} else {
			$dot_class = 'cmatic-header__status-dot--disconnected';
			/* translators: %s: email provider name */
			$status_text = sprintf( __( '%s connection inactive', 'chimpmatic-lite' ), $provider_label );
		}
		?>
		<div class="cmatic-header__status" role="status" aria-live="polite">
			<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();
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                           includes/ui/class-cmatic-data-container.php                                                         0000644                 00000003663 15266052630 0014747 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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 ),
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built exclusively from escaped data attributes.
			$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 ),
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built exclusively from escaped data attributes.
			$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;
	}
}
                                                                             includes/ui/class-cmatic-test-submission-modal.php                                                  0000644                 00000004564 15266052630 0016321 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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' ),
				)
			);
		}
	}
}
                                                                                                                                            includes/ui/class-cmatic-tags-preview.php                                                           0000644                 00000012224 15266052631 0014465 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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::docs( 'mailchimp-sync-tags-explained', '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="<?php esc_attr_e( 'Available in Chimpmatic Pro', 'chimpmatic-lite' ); ?>" title="<?php esc_attr_e( 'Subscriber tagging is a Chimpmatic Pro feature', 'chimpmatic-lite' ); ?>" disabled="disabled">
						<p class="description">You can type in your tags here. Comma separated text or [mail-tags]</p>
					</label>
				</div>
				<?php
				if ( class_exists( 'Cmatic_Pro_Showcase' ) ) {
					$cmatic_tags_pricing = Cmatic_Pursuit::pricing();
					Cmatic_Pro_Showcase::render_unlock_line( 'tags_showcase', __( 'Unlock Tags with Pro', 'chimpmatic-lite' ), (int) ( $cmatic_tags_pricing['discount_percent'] ?? 0 ) );
				}
				$cmatic_tags_promo_url = Cmatic_Pursuit::promo_checkout( 'tags_overlay' );
				?>
				<a class="lin-to-pro" href="<?php echo esc_url( $cmatic_tags_promo_url ); ?>" 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() {}
}
                                                                                                                                                                                                                                                                                                                                                                            includes/ui/class-cmatic-license-banner.php                                                         0000644                 00000032263 15266052631 0014742 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Admin-wide license state banner.
 *
 * Renders only when ChimpMatic Pro is installed.
 *
 * @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_License_Banner {

	private const AMNESTY_DEFAULT_UNTIL = 1788235199;

	private const CAMPAIGNS = array( 'activate', 'winback', 'renew' );

	private const META_KEY = 'cmatic_banner_dismissals';

	private const SNOOZE = array(
		'unlicensed'        => 7 * DAY_IN_SECONDS,
		'unlicensed_urgent' => DAY_IN_SECONDS,
		'unlicensed_over'   => DAY_IN_SECONDS,
		'expired'           => 30 * DAY_IN_SECONDS,
		'invalid'           => 7 * DAY_IN_SECONDS,
		'expiring'          => 30 * DAY_IN_SECONDS,
	);

	public static function init(): void {
		add_action( 'admin_notices', array( __CLASS__, 'render' ) );
	}

	/** Resolve the banner state, or an empty string when no banner applies. */
	public static function resolve_state(): string {
		if ( ! defined( 'CMATIC_VERSION' ) ) {
			return '';
		}

		$license_state = 'none';
		if ( class_exists( 'Cmatic_License_Validator' ) && method_exists( 'Cmatic_License_Validator', 'get_license_state' ) ) {
			$license_state = (string) Cmatic_License_Validator::get_license_state();
		}

		if ( 'active' === $license_state ) {
			$expires = self::license_expires_at();
			if ( $expires > 0 && ( $expires - time() ) <= 30 * DAY_IN_SECONDS && $expires > time() ) {
				return 'expiring';
			}
			return '';
		}

		if ( 'expired' === $license_state ) {
			return 'expired';
		}

		if ( 'invalid' === $license_state ) {
			return 'invalid';
		}

		$deadline = self::amnesty_until();
		if ( time() > $deadline ) {
			return 'unlicensed_over';
		}
		if ( ( $deadline - time() ) <= 14 * DAY_IN_SECONDS ) {
			return 'unlicensed_urgent';
		}
		return 'unlicensed';
	}

	private static function amnesty_until(): int {
		$cached = (int) get_option( 'chimpmatic_amnesty_until', 0 );
		return $cached > 0 ? $cached : self::AMNESTY_DEFAULT_UNTIL;
	}

	private static function license_expires_at(): int {
		if ( ! function_exists( 'chimpmatic_license' ) ) {
			return 0;
		}
		try {
			$activation = chimpmatic_license()->get_activation();
			if ( $activation && method_exists( $activation, 'get_expires_at' ) ) {
				return (int) $activation->get_expires_at();
			}
		} catch ( \Throwable $e ) {
			return 0;
		}
		return 0;
	}

	/** Resolve the current server-provided promotion. */
	private static function offer( string $kind ): array {
		$percent = 0;
		if ( class_exists( 'Cmatic_Pursuit' ) ) {
			$pricing = Cmatic_Pursuit::pricing();
			$percent = 'activate' === $kind
				? (int) ( $pricing['discount_percent'] ?? 0 )
				: (int) ( $pricing[ $kind . '_discount' ] ?? 0 );
		}
		return array(
			'kind'    => $kind,
			'percent' => $percent,
		);
	}

	public static function render(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
		if ( $screen && ( $screen->is_block_editor() || false !== strpos( (string) $screen->id, 'wpcf7' ) ) ) {
			return;
		}

		$state = self::resolve_state();
		if ( '' === $state ) {
			return;
		}

		$dismissals = get_user_meta( get_current_user_id(), self::META_KEY, true );
		if ( is_array( $dismissals ) && isset( $dismissals[ $state ] ) && (int) $dismissals[ $state ] > time() ) {
			return;
		}

		$view = self::view_for( $state );
		if ( null === $view ) {
			return;
		}
		self::print_banner( $state, $view );
	}

	/** @return array{tone:string,message:string,coupon:?array,cta:string,cta_url:string,secondary:?string,secondary_url:?string,dismissible:bool}|null */
	private static function view_for( string $state ): ?array {
		$days_left = (int) max( 0, ceil( ( self::amnesty_until() - time() ) / DAY_IN_SECONDS ) );

		switch ( $state ) {
			case 'unlicensed':
			case 'unlicensed_urgent':
				$offer = self::offer( 'activate' );
				return array(
					'tone'          => 'unlicensed_urgent' === $state ? 'amber' : 'blue',
					'message'       => sprintf(
						/* translators: %d: days until the activation deadline */
						__( 'You are using Chimpmatic Pro without a license on file. Everything keeps working for %d more days. Make it official today and September 1st is just another day.', 'chimpmatic-lite' ),
						$days_left
					),
					'offer'         => $offer,
					'cta'           => __( 'Make it official', 'chimpmatic-lite' ),
					'cta_url'       => self::offer_url(
						'https://chimpmatic.com/pricing',
						'activate',
						'unlicensed_urgent' === $state ? 'banner_lastcall' : 'banner_hello'
					),
					'secondary'     => __( 'I already have a key', 'chimpmatic-lite' ),
					'secondary_url' => admin_url( 'admin.php?page=wpcf7' ),
					'dismissible'   => 'unlicensed' === $state,
				);

			case 'unlicensed_over':
				return array(
					'tone'          => 'red',
					'message'       => __( 'Chimpmatic Pro is paused on this site. All your settings are saved and nothing was lost. Add a license and everything picks up exactly where it left off.', 'chimpmatic-lite' ),
					'offer'         => self::offer( 'activate' ),
					'cta'           => __( 'Bring Pro back', 'chimpmatic-lite' ),
					'cta_url'       => self::offer_url( 'https://chimpmatic.com/pricing', 'activate', 'banner_freshstart' ),
					'secondary'     => __( 'Enter my license key', 'chimpmatic-lite' ),
					'secondary_url' => admin_url( 'admin.php?page=wpcf7' ),
					'dismissible'   => false,
				);

			case 'expired':
				$offer = self::offer( 'winback' );
				$msg   = $offer['percent'] > 0
					? sprintf(
						/* translators: %d: win-back discount percentage */
						__( 'Good to see you again! Your Chimpmatic Pro license ended a while ago, and we would love to have you back. Renew today for %d%% off, applied automatically at checkout. Everything is just as you left it.', 'chimpmatic-lite' ),
						$offer['percent']
					)
					: __( 'Good to see you again! Your Chimpmatic Pro license ended a while ago, and we would love to have you back. Renew today with a special welcome-back discount, applied automatically at checkout. Everything is just as you left it.', 'chimpmatic-lite' );
				return array(
					'tone'          => 'green',
					'message'       => $msg,
					'offer'         => $offer,
					'cta'           => __( 'Claim my welcome-back offer', 'chimpmatic-lite' ),
					'cta_url'       => self::offer_url( 'https://chimpmatic.com/my-account', 'winback', 'banner_welcomeback' ),
					'secondary'     => null,
					'secondary_url' => null,
					'dismissible'   => true,
				);

			case 'invalid':
				return array(
					'tone'          => 'amber',
					'message'       => __( 'Your Chimpmatic Pro license key is not connecting to your account. It may have been mistyped or used on another site. A quick fix keeps Pro features, updates, and support flowing.', 'chimpmatic-lite' ),
					'offer'         => self::offer( 'activate' ),
					'cta'           => __( 'Fix my license', 'chimpmatic-lite' ),
					'cta_url'       => self::offer_url( 'https://chimpmatic.com/pricing', 'activate', 'banner_keyhelp' ),
					'secondary'     => __( 'Re-enter my key', 'chimpmatic-lite' ),
					'secondary_url' => admin_url( 'admin.php?page=wpcf7' ),
					'dismissible'   => true,
				);

			case 'expiring':
				$offer   = self::offer( 'renew' );
				$expires = self::license_expires_at();
				$date    = esc_html( date_i18n( get_option( 'date_format' ), $expires ) );
				$msg     = $offer['percent'] > 0
					? sprintf(
						/* translators: 1: expiry date, 2: renewal discount percentage */
						__( 'Your Chimpmatic Pro license runs through %1$s. Renew early with %2$d%% off, applied automatically at checkout, and never miss an update.', 'chimpmatic-lite' ),
						$date,
						$offer['percent']
					)
					: sprintf(
						/* translators: %s: expiry date */
						__( 'Your Chimpmatic Pro license runs through %s. Renew early and never miss an update.', 'chimpmatic-lite' ),
						$date
					);
				return array(
					'tone'          => 'blue',
					'message'       => $msg,
					'offer'         => $offer,
					'cta'           => __( 'Lock in my renewal', 'chimpmatic-lite' ),
					'cta_url'       => self::offer_url( 'https://chimpmatic.com/my-account', 'renew', 'banner_earlybird' ),
					'secondary'     => null,
					'secondary_url' => null,
					'dismissible'   => true,
				);
		}

		return null;
	}

	private static function offer_url( string $base, string $campaign, string $content ): string {
		if ( ! class_exists( 'Cmatic_Pursuit' ) ) {
			return $base;
		}
		if ( 'activate' === $campaign ) {
			return add_query_arg(
				array( 'from' => $content ),
				Cmatic_Pursuit::promo_checkout( $content )
			);
		}
		$source = class_exists( 'Cmatic_Options_Repository' )
			? Cmatic_Options_Repository::get_option( 'install.id', '' )
			: '';
		$url    = Cmatic_Pursuit::url( $base, 'banner', $content, 'license_banner' );
		return add_query_arg(
			array(
				'source' => $source ? $source : 'lite-banner',
				'promo'  => $campaign,
			),
			$url
		);
	}

	private static function print_banner( string $state, array $view ): void {
		$tones               = array(
			'blue'  => array( '#2271b1', '#f0f6fb' ),
			'amber' => array( '#996800', '#fcf9e8' ),
			'red'   => array( '#b32d2e', '#fcf0f1' ),
			'green' => array( '#00753e', '#edfaef' ),
		);
		list( $accent, $bg ) = $tones[ $view['tone'] ] ?? $tones['blue'];
		$nonce               = wp_create_nonce( 'wp_rest' );
		?>
		<div class="cmatic-license-banner notice" id="cmatic-license-banner" data-state="<?php echo esc_attr( $state ); ?>"
			style="display:none;position:relative;border:1px solid <?php echo esc_attr( $accent ); ?>33;border-left:4px solid <?php echo esc_attr( $accent ); ?>;background:<?php echo esc_attr( $bg ); ?>;padding:14px 44px 14px 16px;margin:12px 20px 12px 2px;border-radius:4px;display:flex;align-items:center;gap:14px;flex-wrap:wrap;">
			<span style="font-weight:700;color:<?php echo esc_attr( $accent ); ?>;white-space:nowrap;">Chimpmatic</span>
			<span style="flex:1 1 320px;min-width:240px;color:#1d2327;line-height:1.5;">
				<?php echo esc_html( $view['message'] ); ?>
				<?php if ( ! empty( $view['offer']['percent'] ) ) : ?>
					<span style="background:#fff;border:1px dashed <?php echo esc_attr( $accent ); ?>;border-radius:3px;padding:2px 8px;font-weight:700;color:<?php echo esc_attr( $accent ); ?>;margin-left:4px;white-space:nowrap;">
						<?php
						printf(
							/* translators: %d: live discount percentage */
							esc_html__( '%d%% off', 'chimpmatic-lite' ),
							(int) $view['offer']['percent']
						);
						?>
					</span>
				<?php endif; ?>
			</span>
			<span style="display:flex;align-items:center;gap:12px;white-space:nowrap;">
				<a href="<?php echo esc_url( $view['cta_url'] ); ?>" target="_blank" rel="noopener noreferrer" class="button button-primary" style="background:<?php echo esc_attr( $accent ); ?>;border-color:<?php echo esc_attr( $accent ); ?>;">
					<?php echo esc_html( $view['cta'] ); ?>
				</a>
				<?php if ( ! empty( $view['secondary'] ) ) : ?>
					<a href="<?php echo esc_url( $view['secondary_url'] ); ?>" style="color:<?php echo esc_attr( $accent ); ?>;"><?php echo esc_html( $view['secondary'] ); ?></a>
				<?php endif; ?>
			</span>
			<?php if ( $view['dismissible'] ) : ?>
				<button type="button" class="cmatic-license-banner-dismiss" aria-label="<?php esc_attr_e( 'Dismiss this notice', 'chimpmatic-lite' ); ?>"
					style="position:absolute;top:8px;right:10px;background:none;border:none;cursor:pointer;color:#787c82;font-size:16px;line-height:1;padding:4px;">&#10005;</button>
			<?php endif; ?>
		</div>
		<script>
		(function () {
			var banner = document.getElementById('cmatic-license-banner');
			if (!banner) { return; }
			var state = banner.getAttribute('data-state');
			var lsKey = 'cmaticBannerDismiss:' + state;
			// Hide cached markup immediately, then persist the dismissal server-side.
			try {
				var until = parseInt(window.localStorage.getItem(lsKey) || '0', 10);
				if (until > Date.now()) {
					banner.remove();
					return;
				}
			} catch (e) {}
			banner.style.display = 'flex';
			var btn = banner.querySelector('.cmatic-license-banner-dismiss');
			if (!btn) { return; }
			btn.addEventListener('click', function () {
				banner.remove();
				var snoozeMs = <?php echo (int) ( self::SNOOZE[ $state ] ?? WEEK_IN_SECONDS ) * 1000; ?>;
				try { window.localStorage.setItem(lsKey, String(Date.now() + snoozeMs)); } catch (e) {}
				fetch('<?php echo esc_url_raw( rest_url( 'chimpmatic-lite/v1/notices/dismiss' ) ); ?>', {
					method: 'POST',
					headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': '<?php echo esc_js( $nonce ); ?>' },
					body: JSON.stringify({ notice_id: 'license_banner', state: state })
				}).catch(function () {});
			});
		})();
		</script>
		<?php
	}

	/** Snooze a banner state for the current administrator. */
	public static function handle_dismiss( string $state ) {
		if ( ! isset( self::SNOOZE[ $state ] ) ) {
			return false;
		}
		$user_id    = get_current_user_id();
		$dismissals = get_user_meta( $user_id, self::META_KEY, true );
		if ( ! is_array( $dismissals ) ) {
			$dismissals = array();
		}
		$dismissals[ $state ] = time() + self::SNOOZE[ $state ];
		update_user_meta( $user_id, self::META_KEY, $dismissals );
		return $dismissals[ $state ];
	}
}
                                                                                                                                                                                                                                                                                                                                             includes/ui/class-cmatic-dom-classes.php                                                            0000644                 00000001571 15266052631 0014265 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * <body> 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_Dom_Classes {

	public static function init(): void {
		add_filter( 'body_class', array( __CLASS__, 'add_body_class' ) );
	}

	public static function add_body_class( array $classes ): array {
		return array_merge( $classes, self::version_classes() );
	}

	private static function version_classes(): array {
		$classes = array();

		if ( defined( 'SPARTAN_MCE_VERSION' ) ) {
			$classes[] = sanitize_html_class( 'cmatic-' . str_replace( '.', '', SPARTAN_MCE_VERSION ) );
		}

		if ( defined( 'CMATIC_VERSION' ) ) {
			$classes[] = sanitize_html_class( 'cmatic-pro-' . str_replace( '.', '', CMATIC_VERSION ) );
		}

		return $classes;
	}
}
                                                                                                                                       includes/ui/class-cmatic-notification-center.php                                                    0000644                 00000015727 15266052633 0016031 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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;
	}
}
                                         includes/ui/class-cmatic-audiences.php                                                              0000644                 00000007416 15266052633 0014021 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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 {
		$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
				if ( '1' === $apivalid && $count > 0 ) {
					esc_html_e( 'Contacts from this form join the selected audience. Sync Fields refreshes its merge fields from Mailchimp.', 'chimpmatic-lite' );
				} elseif ( '1' === $apivalid ) {
					esc_html_e( 'No audiences found in this account. Create one in Mailchimp, then click Sync Fields.', 'chimpmatic-lite' );
				} else {
					esc_html_e( 'Connect your Mailchimp account above to load your audiences.', '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 audiences', '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;
		}

		foreach ( $listdata['lists'] as $list ) :
			if ( ! is_array( $list ) || ! isset( $list['id'], $list['name'] ) ) {
				continue;
			}
			$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(
					/* translators: 1: audience name, 2: contact count, 3: merge-field count */
					esc_html__( '%1$s (%2$s contacts, %3$d fields)', 'chimpmatic-lite' ),
					esc_html( $list_name ),
					esc_html( number_format_i18n( $member_count ) ),
					(int) $field_count
				);
				?>
			</option>
			<?php
		endforeach;
	}

	public static function output( string $apivalid, ?array $listdata, array $cf7_mch ): void {
		self::render( $apivalid, $listdata, $cf7_mch );
	}
}
                                                                                                                                                                                                                                                  includes/ui/class-cmatic-sidebar-panel.php                                                          0000644                 00000006445 15266052640 0014566 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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 );

		$has_credentials = ( is_array( $cf7_mch ) && ! empty( $cf7_mch['api'] ) )
			|| ( isset( $cf7_mch['auth_type'] ) && 'oauth' === $cf7_mch['auth_type'] );
		if ( 1 === $api_valid ) {
			$status_text = '<span class="chmm valid">API Connected</span>';
		} elseif ( ! $has_credentials ) {
			$status_text = '<span class="chmm neutral">Not Connected</span>';
		} else {
			$status_text = '<span class="chmm invalid">API Inactive</span>';
		}
		?>
		<div class="misc-pub-section chimpmatic-info" id="chimpmatic-version-info">
			<div style="margin-bottom: 3px;">
				<?php if ( defined( 'CMATIC_VERSION' ) ) : ?>
					<strong><?php echo esc_html__( 'Chimpmatic Pro', 'chimpmatic-lite' ) . ' ' . esc_html( CMATIC_VERSION ); ?></strong>
					<div style="color: #646970; font-size: 11px;"><?php echo esc_html__( 'Lite base', 'chimpmatic-lite' ) . ' ' . esc_html( SPARTAN_MCE_VERSION ); ?></div>
				<?php else : ?>
					<strong><?php echo esc_html__( 'Chimpmatic Lite', 'chimpmatic-lite' ) . ' ' . esc_html( SPARTAN_MCE_VERSION ); ?></strong>
				<?php endif; ?>
			</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   = Cmatic_Pursuit::pricing();
		$text      = $pricing['formatted'] ?? '';
		$discount  = (int) ( $pricing['discount_percent'] ?? 0 );
		$promo_url = Cmatic_Pursuit::promo_checkout( 'footer_banner' );
		?>
		<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 function __construct() {}
}
                                                                                                                                                                                                                           includes/ui/class-cmatic-banners.php                                                                0000644                 00000007007 15266052645 0013510 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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 {
		if ( ! Cmatic_Options_Repository::get_option( 'backlink', false ) ) {
			return $output;
		}

		$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;
		}

		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://www.paypal.me/renzojohnson/3.91" target="_blank">buy me a coffee?</a> or <a class="button-primary" href="https://www.paypal.me/renzojohnson/3.91" 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;
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         includes/ui/class-cmatic-field-mapper.php                                                           0000644                 00000026144 15266052650 0014424 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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';
		$upgrade_url  = Cmatic_Pursuit::promo_checkout( 'field_mapping_limit' );
		$discount     = Cmatic_Pursuit::discount();
		?>
		<div class="mce-custom-fields <?php echo esc_attr( $disclosure_class ); ?>" id="cmatic-fields">
			<?php
			$cmatic_cached_fields = is_array( $cf7_mch['merge_fields'] ?? null ) ? count( $cf7_mch['merge_fields'] ) : 0;
			$cmatic_expected      = min( $total_merge, CMATIC_LITE_FIELDS );
			if ( $total_merge > 0 && $cmatic_cached_fields < $cmatic_expected ) :
				?>
				<div class="cmatic-defaults-fields-notice cmatic-visible" id="cmatic-stale-cache-notice">
					<p class="cmatic-notice">
						<?php
						printf(
							/* translators: 1: cached merge-field count, 2: audience merge-field count */
							esc_html__( 'Your audience field list looks out of date (%1$d cached, audience reports %2$d). Click Sync Fields above to refresh your mapping rows.', 'chimpmatic-lite' ),
							(int) $cmatic_cached_fields,
							(int) $total_merge
						);
						?>
					</p>
				</div>
				<?php
			endif;
			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
					printf(
						/* translators: %d: number of fields included in Lite. */
						esc_html__( 'Your Lite setup includes %d merge fields. Contact email address is always included.', 'chimpmatic-lite' ),
						(int) CMATIC_LITE_FIELDS
					);
					?>
					<a href="<?php echo esc_url( $upgrade_url ); ?>" class="helping-field cmatic-unlock-fields" target="_blank" rel="noopener noreferrer">
						<?php esc_html_e( 'Unlock every available field and advanced features with Chimpmatic Pro', 'chimpmatic-lite' ); ?>
						<?php if ( $discount > 0 ) : ?>
							<span class="cmatic-field-offer">
								<?php
								/* translators: %d: current discount percentage from the live promotion. */
								printf( esc_html__( '%d%% off', 'chimpmatic-lite' ), (int) $discount );
								?>
							</span>
						<?php endif; ?>
						<span class="screen-reader-text"> <?php esc_html_e( '(opens in a new tab)', 'chimpmatic-lite' ); ?></span>
					</a>
				</p>
			</div>
		</div>
		<?php
		Cmatic_Tags_Preview::render( $form_tags, $cf7_mch, $api_valid );
		Cmatic_Pro_Showcase::render( $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'] = 'Map the contact email address to a Contact Form 7 email field. <a href="' . esc_url( Cmatic_Pursuit::docs( 'mailchimp-required-email', 'email_field' ) ) . '" class="helping-field" target="_blank" title="Get help with the contact email address mapping">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>
			<?php
			$cmatic_accept_saved = trim( (string) ( $cf7_mch['accept'] ?? '' ) );
			$cmatic_optin_always = __( 'Every valid submission is subscribed. Pick a checkbox field to require consent first.', 'chimpmatic-lite' );
			$cmatic_optin_gated  = __( 'Only subscribe if this checkbox is checked', 'chimpmatic-lite' );
			?>
			<small class="description" id="cmatic-optin-helper" data-always="<?php echo esc_attr( $cmatic_optin_always ); ?>" data-gated="<?php echo esc_attr( $cmatic_optin_gated ); ?>">
				<span class="cmatic-optin-helper-text"><?php echo esc_html( '' === $cmatic_accept_saved ? $cmatic_optin_always : $cmatic_optin_gated ); ?></span>
				<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 audience', '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 function __construct() {}
}
                                                                                                                                                                                                                                                                                                                                                                                                                            includes/ui/class-cmatic-api-panel.php                                                              0000644                 00000024227 15266052650 0013725 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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', int $form_id = 0 ): void {
		$api_key     = isset( $cf7_mch['api'] ) ? $cf7_mch['api'] : '';
		$is_valid    = '1' === $apivalid;
		$has_api_key = ! empty( $api_key );
		$auth_type   = isset( $cf7_mch['auth_type'] ) ? $cf7_mch['auth_type'] : '';
		$is_oauth    = 'oauth' === $auth_type && $is_valid;

		if ( ! $has_api_key && ! $is_oauth && $form_id ) {
			$auth_manager = Cmatic_Lite_Container::get( 'auth.manager' );
			if ( $auth_manager && $auth_manager->has_oauth( $form_id ) ) {
				$is_oauth = true;
				$is_valid = true;
			}
		}

		$help_url = Cmatic_Pursuit::docs( 'how-to-get-your-mailchimp-api-key', 'api_panel_help' );

		if ( $is_oauth ) {
			self::render_oauth_connected( $form_id, $cf7_mch );
		} elseif ( $has_api_key ) {
			$masked_key = self::mask_key( $api_key );
			$is_masked  = strlen( $api_key ) >= 12;

			$btn_value = $is_valid ? 'Connected' : 'Sync Audiences';
			$btn_class = 'button';
			?>
			<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="1"
				/>
			</div>

			<input
				id="chm_activalist"
				type="button"
				value="<?php echo esc_attr( $btn_value ); ?>"
				class="<?php echo esc_attr( $btn_class ); ?>"
			/>

			<small class="description"><?php esc_html_e( 'Stored credentials stay hidden. Paste a new key to replace this one.', 'chimpmatic-lite' ); ?></small>

			<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( $is_valid ? 'chmp-inactive' : 'chmp-active' ); ?>">
				<?php Cmatic_Banners::render_new_user_help(); ?>
			</div>
			</div><!-- .cmatic-field-group -->
			<?php
		} else {
			self::render_fresh_form( $form_id, $help_url );
		}
	}

	private static function render_oauth_connected( int $form_id, array $cf7_mch = array() ): void {
		$account_name   = isset( $cf7_mch['oauth_account_name'] ) ? $cf7_mch['oauth_account_name'] : '';
		$connected_by   = isset( $cf7_mch['oauth_connected_by'] ) ? $cf7_mch['oauth_connected_by'] : '';
		$connected_date = isset( $cf7_mch['oauth_connected_date'] ) ? $cf7_mch['oauth_connected_date'] : '';
		$auth_manager   = Cmatic_Lite_Container::get( 'auth.manager' );
		$credentials    = $auth_manager->get_credentials( $form_id );
		$dc             = $credentials ? $credentials->get_datacenter() : '';

		if ( $account_name ) {
			/* translators: %1$s: Mailchimp account name, %2$s: datacenter ID */
			$status_text = sprintf( __( "Connected to %1\$s's Mailchimp (%2\$s)", 'chimpmatic-lite' ), $account_name, $dc );
		} else {
			/* translators: %s: datacenter ID */
			$status_text = sprintf( __( 'Connected to Mailchimp (%s)', 'chimpmatic-lite' ), $dc );
		}

		$authorized_text = '';
		if ( $connected_by && $connected_date ) {
			$date_formatted = date_i18n( get_option( 'date_format' ), strtotime( $connected_date ) );
			/* translators: %1$s: WordPress user name, %2$s: formatted date */
			$authorized_text = sprintf( __( 'Authorized by %1$s on %2$s', 'chimpmatic-lite' ), $connected_by, $date_formatted );
		}

		$learn_more_url = Cmatic_Pursuit::docs( 'how-to-get-your-mailchimp-api-key', 'oauth_learn_more' );
		?>
		<div class="cmatic-field-group cmatic-oauth-state">
			<label><?php echo esc_html__( 'Mailchimp Connection:', 'chimpmatic-lite' ); ?></label><br />
			<span class="cmatic-oauth-status-text">
				<span class="dashicons dashicons-yes-alt"></span>
				<?php echo esc_html( $status_text ); ?>
			</span>
			<input
				id="chm_activalist"
				type="button"
				value="Connected"
				class="button cmatic-hidden"
				data-form-id="<?php echo esc_attr( $form_id ); ?>"
			/>
			<button type="button" class="button cmatic-oauth-disconnect" data-form-id="<?php echo esc_attr( $form_id ); ?>">
				<?php echo esc_html__( 'Disconnect', 'chimpmatic-lite' ); ?>
			</button>
			<?php if ( $authorized_text ) : ?>
			<br />
			<small class="description">
				<?php echo esc_html( $authorized_text ); ?>
				<a href="<?php echo esc_url( $learn_more_url ); ?>" class="helping-field" target="_blank" rel="noopener noreferrer" title="<?php echo esc_attr__( 'Learn more about Mailchimp OAuth', 'chimpmatic-lite' ); ?>">
					<?php echo esc_html__( 'Learn More', 'chimpmatic-lite' ); ?>
					<span class="red-icon dashicons dashicons-arrow-right"></span>
					<span class="red-icon dashicons dashicons-arrow-right"></span>
				</a>
			</small>
			<?php endif; ?>
		</div>
		<?php
	}

	private static function render_fresh_form( int $form_id, string $help_url ): void {
		?>
		<div class="cmatic-field-group cmatic-oauth-state">
			<div class="cmatic-mailchimp-connect-intro">
				<h2 id="cmatic-mailchimp-connect-heading"><?php echo esc_html__( 'Connect Mailchimp', 'chimpmatic-lite' ); ?></h2>
				<p><?php echo esc_html__( 'Connect Mailchimp to choose an audience and map its merge fields.', 'chimpmatic-lite' ); ?></p>
			</div>
			<div class="cmatic-connect-options" aria-label="<?php echo esc_attr__( 'Mailchimp connection options', 'chimpmatic-lite' ); ?>">
				<div class="cmatic-auth-choice-wrap cmatic-oauth-connect-wrap">
					<button type="button" class="cmatic-auth-choice cmatic-auth-choice--recommended cmatic-oauth-connect" data-form-id="<?php echo esc_attr( $form_id ); ?>">
						<span class="cmatic-auth-choice__top">
							<span class="cmatic-auth-choice__icon dashicons dashicons-yes" aria-hidden="true"></span>
							<span class="cmatic-recommended-badge"><?php echo esc_html__( 'Recommended', 'chimpmatic-lite' ); ?></span>
						</span>
						<strong class="cmatic-auth-choice__title"><?php echo esc_html__( 'Sign in with Mailchimp', 'chimpmatic-lite' ); ?></strong>
						<span class="cmatic-auth-choice__description" data-cmatic-oauth-description><?php echo esc_html__( 'The quickest setup. Sign in securely without copying or managing an API key.', 'chimpmatic-lite' ); ?></span>
						<span class="cmatic-oauth-status" role="status" aria-live="polite" hidden></span>
						<span class="cmatic-auth-choice__action">
							<span data-cmatic-oauth-action-label><?php echo esc_html__( 'Continue with Mailchimp', 'chimpmatic-lite' ); ?></span>
							<span aria-hidden="true">&rarr;</span>
						</span>
					</button>
				</div>
				<div class="cmatic-auth-choice-wrap">
					<button type="button" class="cmatic-auth-choice cmatic-show-api-key" aria-expanded="false" aria-controls="cmatic-manual-api-panel">
						<span class="cmatic-auth-choice__top">
							<span class="cmatic-auth-choice__icon" aria-hidden="true">&bull;&bull;&bull;</span>
						</span>
						<strong class="cmatic-auth-choice__title"><?php echo esc_html__( 'Use an API key', 'chimpmatic-lite' ); ?></strong>
						<span class="cmatic-auth-choice__description"><?php echo esc_html__( 'Choose this when you already have a key or someone else manages the Mailchimp account.', 'chimpmatic-lite' ); ?></span>
						<span class="cmatic-auth-choice__action">
							<span data-cmatic-api-action-label><?php echo esc_html__( 'Enter API key', 'chimpmatic-lite' ); ?></span>
							<span aria-hidden="true">&rarr;</span>
						</span>
					</button>
				</div>
			</div>

			<div id="cmatic-manual-api-panel" class="cmatic-manual-api-panel cmatic-hidden">
				<button type="button" class="cmatic-back-auth-options">&larr; <?php echo esc_html__( 'Connection options', 'chimpmatic-lite' ); ?></button>
				<label for="cmatic-api"><?php echo esc_html__( 'Mailchimp API Key:', 'chimpmatic-lite' ); ?></label><br />
				<div class="cmatic-api-wrap">
					<input
						type="password"
						id="cmatic-api"
						name="wpcf7-mailchimp[api]"
						class="wide"
						placeholder="<?php echo esc_attr__( 'Enter Your Mailchimp API key Here', 'chimpmatic-lite' ); ?>"
						value=""
						data-masked-key=""
						data-is-masked="0"
						data-has-key="0"
					/>
					<button type="button" class="cmatic-eye" title="<?php echo esc_attr__( 'Show/Hide', 'chimpmatic-lite' ); ?>">
						<span class="dashicons dashicons-hidden"></span>
					</button>
				</div>
				<input
					id="chm_activalist"
					type="button"
					value="<?php echo esc_attr__( 'Sync Audiences', 'chimpmatic-lite' ); ?>"
					class="button"
					data-form-id="<?php echo esc_attr( $form_id ); ?>"
				/>
				<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>
		</div>
		<?php
	}

	public static function output( array $cf7_mch, string $apivalid = '0', int $form_id = 0 ): void {
		self::render( $cf7_mch, $apivalid, $form_id );
	}
}
                                                                                                                                                                                                                                                                                                                                                                         includes/ui/class-cmatic-notification.php                                                           0000644                 00000005003 15266052650 0014534 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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 );
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             includes/ui/class-cmatic-panel-toggles.php                                                          0000644                 00000004212 15266052656 0014616 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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', 'chimpmatic-lite' ),
				'aria_controls' => 'cme-container',
				'extra_class'   => '',
				'priority'      => 10,
			),
			'submission_logs'   => array(
				'label'         => __( 'Submission Logs', 'chimpmatic-lite' ),
				'aria_controls' => 'eventlog-sys',
				'extra_class'   => '',
				'priority'      => 40,
			),
			'form_preview'      => array(
				'label'         => __( 'Form Preview and Test', 'chimpmatic-lite' ),
				'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 );

		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>';
	}
}
                                                                                                                                                                                                                                                                                                                                                                                      includes/admin/class-cmatic-lite-signls-privacy.php                                                 0000644                 00000012460 15266052663 0016437 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Signls consent migration and suggested privacy-policy content.
 *
 * @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_Signls_Privacy {
	public const CONSENT_VERSION = 2;

	private const PRODUCT_SLUG = 'contact-form-7-mailchimp-extension';

	private const DEFAULT_SOURCE = 'network_install_default';

	public static function init(): void {
		add_action( 'admin_init', array( self::class, 'add_policy_content' ) );
	}

	public static function consent_status(): string {
		$data   = Cmatic_Options_Repository::get_all_options();
		$status = isset( $data['signls']['consent_status'] ) ? (string) $data['signls']['consent_status'] : '';
		if ( in_array( $status, array( 'enabled', 'disabled' ), true ) ) {
			if ( (int) ( $data['signls']['consent_version'] ?? 0 ) < self::CONSENT_VERSION ) {
				$data['signls']['consent_version'] = self::CONSENT_VERSION;
				Cmatic_Options_Repository::instance()->save( $data );
			}
			return $status;
		}

		$legacy = isset( $data['telemetry']['enabled'] ) ? $data['telemetry']['enabled'] : null;
		$status = 'unset' === $status ? 'enabled' : ( false === $legacy || 0 === $legacy || '0' === $legacy ? 'disabled' : 'enabled' );
		$source = 'disabled' === $status ? 'legacy_disabled' : self::DEFAULT_SOURCE;
		$now    = time();
		if ( ! isset( $data['signls'] ) || ! is_array( $data['signls'] ) ) {
			$data['signls'] = array();
		}
		$data['signls']['consent_status']          = $status;
		$data['signls']['consent_version']         = self::CONSENT_VERSION;
		$data['signls']['consent_last_changed_at'] = $now;
		$data['signls']['consent_source']          = $source;
		$data['signls']['migrated_at']             = $now;
		if ( 'enabled' === $status ) {
			$data['signls']['consent_first_enabled_at'] = $now;
		}
		Cmatic_Options_Repository::instance()->save( $data );
		return $status;
	}

	public static function sync_sdk_consent(): void {
		if ( 'enabled' !== self::consent_status() || ! class_exists( 'Signls\\Sdk\\V1\\Runtime' ) ) {
			return;
		}

		$state = \Signls\Sdk\V1\Runtime::state( self::PRODUCT_SLUG );
		if ( 'unset' !== (string) ( $state['consent_status'] ?? 'unset' ) ) {
			return;
		}

		\Signls\Sdk\V1\Runtime::enable(
			self::PRODUCT_SLUG,
			self::DEFAULT_SOURCE,
			self::CONSENT_VERSION
		);
	}

	public static function add_policy_content(): void {
		if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) {
			return;
		}
		$content  = '<p>' . esc_html__( 'While â€œHelp Us Improveâ€� is enabled, ChimpMatic Lite sends a bounded, signed product snapshot to Signls at https://signls.dev/wp-json/chimpmatic/v1/telemetry so we can measure setup completion, compatibility, feature use, reliability, upgrades and long-term adoption. This build preserves its established initial setting; an administrator can turn sharing off or on repeatedly at any time.', 'chimpmatic-lite' ) . '</p>';
		$content .= '<p>' . esc_html__( 'The snapshot includes random pseudonymous site, installation and rotating device identities; the current site URL and one snapshot user agent; WordPress, PHP, database, server, theme, Contact Form 7 and ChimpMatic versions and configuration facts; a maximum of 500 plugin inventory rows with names, versions, authors and active state; 16 fixed competitor indicators; aggregate provider/authentication, destination, mapping, feature, lifecycle, submission, performance and scrubbed failure facts; and bounded Contact Form 7 configuration labels (up to 100 forms, 50 form-detail rows and 30 field/mapping rows per detailed form). Totals and truncation flags report omitted rows.', 'chimpmatic-lite' ) . '</p>';
		$content .= '<p>' . esc_html__( 'The snapshot excludes contact details, submitted form values, credentials, API or license keys, OAuth secrets, cookies, authorization headers, raw provider responses, raw administrator email, commerce records, raw IP addresses, arbitrary settings, request bodies and file contents. Server address and hostname are exported only as SHA-256 hashes; failure samples are scrubbed and limited to 50 characters.', 'chimpmatic-lite' ) . '</p>';
		$content .= '<p>' . esc_html__( 'Active installations send at most daily and quiet installations at most weekly, with a coalesced refresh after relevant changes. Delivery receipts are retained for 90 days, current typed facts are replaced by newer observations, and daily aggregates plus pseudonymous adoption/lifecycle identity are retained for long-term product analysis. Turning sharing off immediately clears scheduled delivery, pending payloads and the local delivery credential without sending an opt-out event; turning it on later resumes under the same disclosed contract.', 'chimpmatic-lite' ) . '</p>';
		$content .= '<p><a href="https://github.com/signls-dev/signls/blob/main/reference/wordpress-signls-sdk-v2.md">' . esc_html__( 'Complete public field matrix, bounds and retention', 'chimpmatic-lite' ) . '</a> Â· <a href="https://chimpmatic.com/terms-and-conditions">' . esc_html__( 'Terms and Conditions', 'chimpmatic-lite' ) . '</a> Â· <a href="https://chimpmatic.com/privacy">' . esc_html__( 'Privacy Policy', 'chimpmatic-lite' ) . '</a></p>';
		wp_add_privacy_policy_content( 'ChimpMatic Lite', wp_kses_post( $content ) );
	}

	private function __construct() {}
}
                                                                                                                                                                                                                includes/admin/class-cmatic-lite-esp-degrade-notice.php                                             0000644                 00000005133 15266052670 0017121 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * MailerLite license-degradation admin warning.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

final class Cmatic_Lite_Esp_Degrade_Notice {
	public static function init(): void {
		add_action( 'admin_notices', array( __CLASS__, 'render' ) );
	}

	public static function render(): void {
		$forms = get_posts(
			array(
				'post_type'              => 'wpcf7_contact_form',
				'post_status'            => 'any',
				'posts_per_page'         => 20,
				'fields'                 => 'ids',
				'meta_key'               => Cmatic_Mailerlite_Degradation_Reporter::META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Bounded operational query for a dedicated indexed key.
				'no_found_rows'          => true,
				'update_post_term_cache' => false,
			)
		);
		foreach ( $forms as $form_id ) {
			$form_id = (int) $form_id;
			if ( ! current_user_can( 'wpcf7_edit_contact_form', $form_id ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown -- Contact Form 7 meta capability.
				continue;
			}
			$config    = get_option( 'cf7_mch_' . $form_id, array() );
			$config    = is_array( $config ) ? $config : array();
			$providers = isset( $config['providers'] ) && is_array( $config['providers'] ) ? $config['providers'] : array();
			$settings  = isset( $providers['mailerlite'] ) && is_array( $providers['mailerlite'] ) ? $providers['mailerlite'] : array();
			$policy    = Cmatic_Mailerlite_Runtime_Policy::apply( $settings, self::entitlements( $form_id ) );
			if ( ! $policy['degraded'] ) {
				Cmatic_Mailerlite_Degradation_Reporter::clear( $form_id );
				continue;
			}
			$url = add_query_arg(
				array(
					'page'   => 'wpcf7',
					'post'   => $form_id,
					'action' => 'edit',
				),
				admin_url( 'admin.php' )
			);
			printf(
				'<div class="notice notice-warning is-dismissible"><p>%s <a href="%s">%s</a></p></div>',
				/* translators: %d: Contact Form 7 form ID. */
				esc_html( sprintf( __( 'MailerLite Pro features are saved but inactive for form %d. Capture continues using the first group and legacy status.', 'chimpmatic-lite' ), $form_id ) ),
				esc_url( $url ),
				esc_html__( 'Review and renew', 'chimpmatic-lite' )
			);
		}
	}

	private static function entitlements( int $form_id ): array {
		$result = array();
		foreach ( array( 'mailerlite_routing', 'mailerlite_status', 'mailerlite_resubscribe', 'mailerlite_consent_metadata' ) as $feature ) {
			$result[ $feature ] = Cmatic_Lite_Esp_Capabilities::feature_enabled( $feature, 'mailerlite', $form_id );
		}
		return $result;
	}

	private function __construct() {}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                     includes/admin/class-cmatic-deactivation-survey.php                                                 0000644                 00000025334 15266052670 0016541 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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' ),
			);
		}
	}
}
                                                                                                                                                                                                                                                                                                    includes/admin/class-cmatic-asset-loader.php                                                        0000644                 00000055743 15266052670 0015126 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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;

// phpcs:disable Squiz.Commenting.ClassComment.Missing, Squiz.Commenting.FunctionComment.Missing, Squiz.Commenting.VariableComment.Missing -- Preserve the established documentation style in this legacy global class.
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_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();
			self::enqueue_oauth_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(),
				)
			);

			$provider_js_path = SPARTAN_MCE_PLUGIN_DIR . 'assets/js/cmatic-lite-esp.js';
			wp_enqueue_script(
				'cmatic-lite-esp',
				SPARTAN_MCE_PLUGIN_URL . 'assets/js/cmatic-lite-esp.js',
				array( 'chimpmatic-lite-js' ),
				Cmatic_Buster::instance()->get_version( $provider_js_path ),
				true
			);
			wp_localize_script(
				'cmatic-lite-esp',
				'chimpmaticLiteEsp',
				array(
					'restUrl'    => esc_url_raw( rest_url( 'chimpmatic-lite/v1/' ) ),
					'nonce'      => wp_create_nonce( 'wp_rest' ),
					'manifest'   => Cmatic_Lite_Esp_Manifest::all(),
					'fieldLimit' => Cmatic_Lite_Esp_Capabilities::field_limit( '', (int) $form_settings['form_id'] ),
					'i18n'       => array(
						/* translators: %s: email provider name. */
						'connectProvider'            => __( 'Connect %s', 'chimpmatic-lite' ),
						/* translators: %s: email provider name. */
						'connected'                  => __( '%s connected', 'chimpmatic-lite' ),
						/* translators: %s: email provider name. */
						'notConnected'               => __( '%s not connected', 'chimpmatic-lite' ),
						/* translators: %s: email provider name. */
						'inactive'                   => __( 'Reconnect %s', 'chimpmatic-lite' ),
						/* translators: %s: provider-specific destination name. */
						'chooseDestination'          => __( 'Choose a %s', 'chimpmatic-lite' ),
						/* translators: %s: provider-specific destination name. */
						'selectDestination'          => __( 'Select a %s...', 'chimpmatic-lite' ),
						/* translators: %s: email provider name. */
						'updateProvider'             => __( 'Update %s connection', 'chimpmatic-lite' ),
						/* translators: 1: email provider name, 2: destination name, 3: provider data name. */
						'connectDescription'         => __( 'Connect %1$s to choose a %2$s and map its %3$s.', 'chimpmatic-lite' ),
						'updateConnection'           => __( 'Update connection', 'chimpmatic-lite' ),
						'credential'                 => __( 'Credential', 'chimpmatic-lite' ),
						/* translators: %s: email provider name. */
						'checkingConnection'         => __( 'Checking your %s connection...', 'chimpmatic-lite' ),
						/* translators: %s: provider destination plural. */
						'loadingDestinations'        => __( 'Loading %s...', 'chimpmatic-lite' ),
						/* translators: %s: provider destination plural. */
						'refreshDestinations'        => __( 'Refresh %s', 'chimpmatic-lite' ),
						/* translators: %s: credential type. */
						'replaceCredential'          => __( 'Replace %s', 'chimpmatic-lite' ),
						/* translators: %s: email provider name. */
						'reconnectProvider'          => __( 'Reconnect %s', 'chimpmatic-lite' ),
						/* translators: 1: provider destination plural, 2: email provider name. */
						'loadingDestinationsFrom'    => __( 'Loading %1$s from %2$s...', 'chimpmatic-lite' ),
						/* translators: %s: email provider name. */
						'connectToContinue'          => __( 'Connect %s to continue.', 'chimpmatic-lite' ),
						/* translators: 1: selected destination, 2: email provider name, 3: destination type. */
						'onlyDestination'            => __( '%1$s was selected because it is your only %2$s %3$s.', 'chimpmatic-lite' ),
						/* translators: 1: destination count, 2: destination type plural. */
						'destinationsFound'          => __( '%1$d %2$s found. Choose where new submissions from this form should go.', 'chimpmatic-lite' ),
						/* translators: %s: destination type singular. */
						'oneDestinationFound'        => __( '1 %s found. Choose where new submissions from this form should go.', 'chimpmatic-lite' ),
						/* translators: 1: email provider name, 2: provider data name. */
						'mapProviderFields'          => __( 'Map %1$s %2$s', 'chimpmatic-lite' ),
						/* translators: %s: provider data name. */
						'mapData'                    => __( 'Map %s', 'chimpmatic-lite' ),
						/* translators: 1: email provider name, 2: provider data name. */
						'mappedFields'               => __( 'Match each %1$s %2$s to a Contact Form 7 field. Email address mapping is required.', 'chimpmatic-lite' ),
						/* translators: %s: provider destination plural. */
						'waitForDestinations'        => __( 'Wait while %s load.', 'chimpmatic-lite' ),
						/* translators: 1: provider destination type, 2: provider data name. */
						'chooseToLoadFields'         => __( 'Choose a %1$s to load its %2$s.', 'chimpmatic-lite' ),
						'unsavedChanges'             => __( 'Unsaved changes', 'chimpmatic-lite' ),
						'saveChanges'                => __( 'Save changes', 'chimpmatic-lite' ),
						'savedJustNow'               => __( 'Saved', 'chimpmatic-lite' ),
						'saveToActivate'             => __( 'Save to activate this configuration.', 'chimpmatic-lite' ),
						'saveConfiguration'          => __( 'Save configuration', 'chimpmatic-lite' ),
						'saving'                     => __( 'Saving...', 'chimpmatic-lite' ),
						'discardChanges'             => __( 'Discard unsaved changes and switch providers?', 'chimpmatic-lite' ),
						/* translators: 1: email provider name, 2: destination count, 3: provider destination name. */
						'connectedDestinationsReady' => __( '%1$s connected. %2$d %3$s ready.', 'chimpmatic-lite' ),
						/* translators: 1: email provider name, 2: provider data name. */
						'loadingProviderFields'      => __( 'Loading %1$s %2$s...', 'chimpmatic-lite' ),
						/* translators: 1: email provider name, 2: destination name. */
						'onlyDestinationSelected'    => __( '%1$s connected. %2$s was selected automatically.', 'chimpmatic-lite' ),
						/* translators: 1: email provider name, 2: provider data name. */
						'providerFieldsReady'        => __( '%1$s %2$s are ready.', 'chimpmatic-lite' ),
						/* translators: %s: email provider name. */
						'disconnectConfirm'          => __( 'Disconnect %s? Your field mappings will be kept.', 'chimpmatic-lite' ),
						'readyToVerify'              => __( 'Ready to verify.', 'chimpmatic-lite' ),
						/* translators: 1: email provider name, 2: provider destination name. */
						'missingDestination'         => __( 'Connect %1$s and choose a %2$s before saving.', 'chimpmatic-lite' ),
						'missingEmailMapping'        => __( 'Select a Contact Form 7 field for the required email address.', 'chimpmatic-lite' ),
						/* translators: 1: provider person name, 2: selected destination, 3: email provider name. */
						'setupOutcome'               => __( 'New %1$s from this form will be added to %2$s in %3$s.', 'chimpmatic-lite' ),
						/* translators: %d: mapped field count. */
						'mappedCount'                => __( '%d fields mapped - Saved', 'chimpmatic-lite' ),
						/* translators: 1: Contact Form 7 field, 2: form field type, 3: provider field name, 4: provider field type. */
						'mappingTypeWarning'         => __( '%1$s is a %2$s field, but %3$s expects %4$s. Review this mapping.', 'chimpmatic-lite' ),
						/* translators: %s: email provider name. */
						'requestFailed'              => __( '%s could not complete the request. Try again.', 'chimpmatic-lite' ),
						'consentRequiresPro'         => __( 'Advanced consent controls are available with an active Chimpmatic Pro license.', 'chimpmatic-lite' ),
						'doiVerified'                => __( 'DOI settings verified.', 'chimpmatic-lite' ),
						'doiFailed'                  => __( 'DOI settings could not be verified.', 'chimpmatic-lite' ),
						'managedByMailerLite'        => __( 'Managed by MailerLite', 'chimpmatic-lite' ),
						'mailerLiteOptin'            => __( 'MailerLite uses the Double opt-in for API and integrations setting in your account.', 'chimpmatic-lite' ),
						'doubleOptin'                => __( 'Double opt-in', 'chimpmatic-lite' ),
						'singleOptin'                => __( 'Single opt-in', 'chimpmatic-lite' ),
						'optinUnavailable'           => __( 'Opt-in setting unavailable', 'chimpmatic-lite' ),
						'klaviyoOptin'               => __( 'The selected Klaviyo list controls whether confirmation is required.', 'chimpmatic-lite' ),
						'consentIncomplete'          => __( 'Complete the consent and opt-in settings before saving.', 'chimpmatic-lite' ),
						/* translators: %s: provider group name. */
						'useGroupWithoutPro'         => __( 'Use %s when Chimpmatic Pro is inactive', 'chimpmatic-lite' ),
						'groupsForEverySubscriber'   => __( 'Groups for every subscriber', 'chimpmatic-lite' ),
						'mailerLiteGroupsHelp'       => __( 'Every subscriber successfully sent to MailerLite is added to each selected group. Mark one selected group â€œUse when Pro is inactive.â€�', 'chimpmatic-lite' ),
						'useWhenProInactive'         => __( 'Use when Pro is inactive', 'chimpmatic-lite' ),
						'routingRequiresPro'         => __( 'Additional MailerLite groups and answer-based rules require Chimpmatic Pro.', 'chimpmatic-lite' ),
						'routingSavedInactive'       => __( 'MailerLite group rules are saved but inactive. Subscribers are added only to the group marked â€œUse when Pro is inactive.â€� Renew Pro to restore the saved rules.', 'chimpmatic-lite' ),
						'contactFormField'           => __( 'Contact Form 7 field', 'chimpmatic-lite' ),
						'answerIs'                   => __( 'Answer is', 'chimpmatic-lite' ),
						'addSubscriberToGroup'       => __( 'Add subscriber to group', 'chimpmatic-lite' ),
						'whenThisField'              => __( 'When this field', 'chimpmatic-lite' ),
						'isThisValue'                => __( 'is this value', 'chimpmatic-lite' ),
						'when'                       => __( 'When', 'chimpmatic-lite' ),
						'is'                         => __( 'is', 'chimpmatic-lite' ),
						'addSubscriberTo'            => __( 'add subscriber to', 'chimpmatic-lite' ),
						'chooseField'                => __( 'Choose a field', 'chimpmatic-lite' ),
						'chooseValue'                => __( 'Choose a value', 'chimpmatic-lite' ),
						'chooseGroup'                => __( 'Choose a group', 'chimpmatic-lite' ),
						'removeRule'                 => __( 'Remove rule', 'chimpmatic-lite' ),
						/* translators: %d: routing rule number. */
						'removeRuleNumber'           => __( 'Remove rule %d', 'chimpmatic-lite' ),
						/* translators: %d: routing rule number. */
						'ruleNumber'                 => __( 'Rule %d', 'chimpmatic-lite' ),
						'routingIncomplete'          => __( 'Choose a field, an answer, and a destination group.', 'chimpmatic-lite' ),
						'routingInvalid'             => __( 'This rule contains an unavailable field, answer, or group.', 'chimpmatic-lite' ),
						'routingDuplicate'           => __( 'This rule duplicates another rule.', 'chimpmatic-lite' ),
						'routingFixBeforeSave'       => __( 'Complete or remove the highlighted routing rules before saving.', 'chimpmatic-lite' ),
						/* translators: 1: always-used group count, 2: answer-based rule count, 3: mapped subscriber-field count. */
						'mailerLiteSummary'          => __( 'Always-used groups: %1$d Â· Answer-based rules: %2$d Â· Subscriber fields mapped: %3$d Â· Saved', 'chimpmatic-lite' ),
						'proOptionsInactive'         => __( 'Saved MailerLite Pro settings are inactive. Subscribers continue with the current Active behavior; renew Pro to restore the saved settings.', 'chimpmatic-lite' ),
						'fieldCreationRequiresPro'   => __( 'Creating MailerLite subscriber fields requires Chimpmatic Pro.', 'chimpmatic-lite' ),
						'fieldCreated'               => __( 'MailerLite subscriber field created. Choose its Contact Form 7 source and save.', 'chimpmatic-lite' ),
						'fieldCreateUnconfirmed'     => __( 'Field creation was not confirmed. Refresh MailerLite subscriber fields before retrying.', 'chimpmatic-lite' ),
						'fieldRefreshRequired'       => __( 'Subscriber field created; refresh MailerLite subscriber fields before mapping it.', 'chimpmatic-lite' ),
						'creating'                   => __( 'Creating...', 'chimpmatic-lite' ),
						'createField'                => __( 'Create field', 'chimpmatic-lite' ),
						'findSubscriber'             => __( 'Find subscriber', 'chimpmatic-lite' ),
						'findingSubscriber'          => __( 'Finding subscriber...', 'chimpmatic-lite' ),
						/* translators: %s: MailerLite subscriber status. */
						'subscriberFound'            => __( 'Subscriber found Â· %s', 'chimpmatic-lite' ),
						'noSubscriberFound'          => __( 'No subscriber found.', 'chimpmatic-lite' ),
						'statusUnavailable'          => __( 'status unavailable', 'chimpmatic-lite' ),
						/* translators: %s: comma-separated MailerLite group names. */
						'groupsLabel'                => __( 'Groups: %s', 'chimpmatic-lite' ),
						'lookupFailed'               => __( 'MailerLite could not find the subscriber. Try again.', 'chimpmatic-lite' ),
						/* translators: 1: provider person name, 2: email provider name. */
						'testingWarning'             => __( 'Real submission: may create or update a %1$s in %2$s and trigger confirmation emails or automations.', 'chimpmatic-lite' ),
						/* translators: %s: provider credential label. */
						'credentialStored'           => __( '%s stored securely', 'chimpmatic-lite' ),
						/* translators: %s: email provider name. */
						'sendToProvider'             => __( 'Send to %s', 'chimpmatic-lite' ),
						/* translators: %s: email provider name. */
						'consentGateExplanation'     => __( 'Choose whether every valid form submission is sent to %s or only submissions with affirmative consent.', 'chimpmatic-lite' ),
						/* translators: %s: email provider name. */
						'confirmationInProvider'     => __( 'Confirmation in %s', 'chimpmatic-lite' ),
						/* translators: %s: email provider name. */
						'confirmationExplanation'    => __( 'Controls whether %s requires confirmation after the form is submitted.', 'chimpmatic-lite' ),
						/* translators: %s: email provider name. */
						'openConfirmationSettings'   => __( 'Open %s confirmation settings', 'chimpmatic-lite' ),
						/* translators: 1: number of provider data fields available with Pro, 2: provider data name. */
						'proDataLimit'               => __( 'Up to %1$d %2$s are available with your active Chimpmatic Pro license.', 'chimpmatic-lite' ),
						/* translators: 1: number of provider data fields included in Lite, 2: provider data name. */
						'liteDataLimit'              => __( 'Your Lite setup includes %1$d %2$s. Email address is always included.', 'chimpmatic-lite' ),
						/* translators: %s: provider data name. */
						'unlockData'                 => __( 'Unlock every available %s and advanced features with Chimpmatic Pro', 'chimpmatic-lite' ),
					),
				)
			);
			self::$scripts['cmatic-lite-esp'] = $provider_js_path;

			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;
		}

		private static function enqueue_oauth_js(): void {
			$oauth_js_path = SPARTAN_MCE_PLUGIN_DIR . 'assets/js/cmatic-oauth-admin.js';
			if ( ! file_exists( $oauth_js_path ) ) {
				return;
			}

			wp_enqueue_script(
				'cmatic-oauth-admin',
				SPARTAN_MCE_PLUGIN_URL . 'assets/js/cmatic-oauth-admin.js',
				array( 'chimpmatic-lite-js' ),
				Cmatic_Buster::instance()->get_version( $oauth_js_path ),
				true
			);

			$form_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only asset context; authorization is enforced by WordPress admin.

			wp_localize_script(
				'cmatic-oauth-admin',
				'chimpmaticOAuth',
				array(
					'restUrl' => esc_url_raw( rest_url( 'chimpmatic-lite/v1/' ) ),
					'nonce'   => wp_create_nonce( 'wp_rest' ),
					'formId'  => $form_id,
				)
			);

			self::$scripts['cmatic-oauth-admin'] = $oauth_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; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only asset context; authorization is enforced by WordPress admin.
			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; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only asset context; authorization is enforced by WordPress admin.
			$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' ),
				'findContact'        => __( 'Find contact', 'chimpmatic-lite' ),
				'findingContact'     => __( 'Finding contact...', 'chimpmatic-lite' ),
				'findAnotherContact' => __( 'Find another contact', 'chimpmatic-lite' ),
			);
		}

		public static function get_registered_scripts(): array {
			return self::$scripts;
		}

		public static function get_registered_styles(): array {
			return self::$styles;
		}
	}
}
                             includes/admin/class-cmatic-plugin-links.php                                                        0000644                 00000005053 15266052670 0015144 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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;
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     includes/admin/class-cmatic-admin-panel.php                                                         0000644                 00000071372 15266052670 0014724 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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;

// phpcs:disable Squiz.Commenting.ClassComment.Missing, Squiz.Commenting.FunctionComment.Missing, Squiz.Commenting.FunctionComment.MissingParamTag -- Preserve the established documentation style in this legacy global class.
final class Cmatic_Admin_Panel {

	private const PANEL_KEY = 'Chimpmatic';

	/**
	 * Settings captured before another saver runs.
	 *
	 * @var array<int, array<string, mixed>>
	 */
	private static $settings_before_save = array();

	public static function init(): void {
		add_filter( 'wpcf7_editor_panels', array( __CLASS__, 'register_panel' ) );
		add_filter( 'wpcf7_editor_panels', array( __CLASS__, 'append_lite_settings_to_pro_panel' ), 20 );
		add_action( 'wpcf7_after_save', array( __CLASS__, 'capture_settings' ), 1 );
		add_action( 'wpcf7_after_save', array( __CLASS__, 'save_settings' ), 10 );
		add_action( 'wpcf7_after_save', array( __CLASS__, 'save_provider_settings' ), 12 );
		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 {
		$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 ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown -- Contact Form 7 registers and maps this capability.
			return $panels;
		}
		if (
			class_exists( 'Cmatic_Pro_Esp_Bridge' )
			&& Cmatic_Pro_Esp_Bridge::is_compatible()
		) {
			return $panels;
		}

		$panel_key            = defined( 'CMATIC_VERSION' ) ? 'Chimpmatic Providers' : self::PANEL_KEY;
		$panels[ $panel_key ] = array(
			'title'    => defined( 'CMATIC_VERSION' ) ? __( 'Email Providers', 'chimpmatic-lite' ) : __( 'Chimpmatic', 'chimpmatic-lite' ),
			'callback' => array( __CLASS__, 'render_panel' ),
		);

		return $panels;
	}

	public static function append_lite_settings_to_pro_panel( array $panels ): array {
		if (
			! class_exists( 'Cmatic_Pro_Esp_Bridge' )
			|| ! Cmatic_Pro_Esp_Bridge::is_compatible()
			|| ! isset( $panels[ self::PANEL_KEY ]['callback'] )
			|| 'wpcf7_chimp_add_mailchimp' !== $panels[ self::PANEL_KEY ]['callback']
		) {
			return $panels;
		}

		$panels[ self::PANEL_KEY ]['callback'] = array( __CLASS__, 'render_pro_panel_with_lite_settings' );

		return $panels;
	}

	public static function render_pro_panel_with_lite_settings( $contact_form ): void {
		if ( ! function_exists( 'wpcf7_chimp_add_mailchimp' ) ) {
			return;
		}

		ob_start();
		wpcf7_chimp_add_mailchimp( $contact_form );
		$panel = (string) ob_get_clean();

		echo self::inject_help_us_improve_row( $panel ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The trusted Pro callback already escaped its panel; Lite only inserts its own escaped row.
	}

	private static function inject_help_us_improve_row( string $panel ): string {
		if ( false !== strpos( $panel, 'id="cmatic-telemetry-enabled"' ) ) {
			return $panel;
		}

		$advanced_start = strpos( $panel, 'id="cme-container"' );
		$tbody_start    = false === $advanced_start ? false : strpos( $panel, '<tbody>', $advanced_start );
		$tbody_end      = false === $tbody_start ? false : strpos( $panel, '</tbody>', $tbody_start );
		if ( false === $advanced_start || false === $tbody_start || false === $tbody_end ) {
			return $panel;
		}

		ob_start();
		Cmatic_Advanced_Settings::render_help_us_improve_row();
		$row = (string) ob_get_clean();

		$advanced_body = substr( $panel, $tbody_start, $tbody_end - $tbody_start );
		$license       = strpos( $advanced_body, '<th scope="row">License Reset</th>' );
		$insert_at     = false === $license ? $tbody_end : strrpos( substr( $panel, 0, $tbody_start + $license ), '<tr>' );
		if ( false === $insert_at ) {
			$insert_at = $tbody_end;
		}

		return substr( $panel, 0, $insert_at ) . $row . substr( $panel, $insert_at );
	}

	public static function render_panel( $contact_form ): void {
		$form_id   = (int) ( $contact_form->id() ?? 0 );
		$cf7_mch   = get_option( 'cf7_mch_' . $form_id, array() );
		$cf7_mch   = is_array( $cf7_mch ) ? $cf7_mch : array();
		$provider  = empty( $cf7_mch ) ? '' : Cmatic_Lite_Esp_Registry::get_selected( $cf7_mch );
		$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;

		if ( class_exists( 'Cmatic_Data_Container' ) ) {
			$extra_data = array();
			if ( isset( $cf7_mch['auth_type'] ) && 'oauth' === $cf7_mch['auth_type'] ) {
				$extra_data['auth_type'] = 'oauth';
			}
			Cmatic_Data_Container::render_open( $form_id, (string) $api_valid, $extra_data );
		} else {
			echo '<div class="cmatic-inner">';
		}

		if ( class_exists( 'Cmatic_Header' ) ) {
			Cmatic_Header::output(
				array(
					'api_status'       => '' === $provider ? null : Cmatic_Lite_Esp_Panel::get_api_status( $provider, $cf7_mch, $form_id ),
					'provider'         => $provider,
					'provider_options' => Cmatic_Lite_Esp_Manifest::selector_options(),
				)
			);
		}

		echo '<div class="cmatic-content">';
		printf(
			'<div id="cmatic-mailchimp-settings" data-cmatic-provider-view="mailchimp"%s>',
			'mailchimp' === $provider ? '' : ' hidden'
		);

		if ( defined( 'CMATIC_VERSION' ) ) {
			Cmatic_Lite_Esp_Panel::render_pro_mailchimp_notice();
		} else {
			Cmatic_Api_Panel::render( $cf7_mch, (string) $api_valid, $form_id );
			if ( class_exists( 'Cmatic_Audiences' ) ) {
				Cmatic_Audiences::render( (string) $api_valid, $list_data, $cf7_mch );
			}
			Cmatic_Field_Mapper_UI::render( $api_valid, $list_data, $cf7_mch, $form_tags, $form_id );
			Cmatic_Panel_Toggles::cmatic_render();
			if ( class_exists( 'Cmatic_Contact_Lookup' ) ) {
				Cmatic_Contact_Lookup::cmatic_render( array( 'form_id' => $form_id ) );
			}
			Cmatic_Log_Viewer::render();
			echo '<div id="cme-container" class="mce-custom-fields vc-advanced-settings">';
			Cmatic_Advanced_Settings::render();
			echo '</div>';
			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_Lite_Esp_Panel::render( $provider, $cf7_mch, $form_tags, $form_id );
		echo '</div>';

		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'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- The nonce is verified by can_save() before form data is read.
			return;
		}

		$form_id = (int) $contact_form->id();
		if ( ! self::can_save( $form_id ) ) {
			return;
		}

		$option_name  = 'cf7_mch_' . $form_id;
		$old_settings = get_option( $option_name, array() );
		$old_settings = is_array( $old_settings ) ? $old_settings : array();
		$posted_data  = wp_unslash( $_POST['wpcf7-mailchimp'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing -- Verified by can_save(); sanitized below.
		if ( ! is_array( $posted_data ) ) {
			return;
		}
		$sanitized = self::sanitize_settings( $posted_data, $old_settings );
		if ( array_key_exists( 'api', $sanitized ) && (string) ( $old_settings['api'] ?? '' ) !== $sanitized['api'] ) {
			$sanitized['api-validation'] = 0;
			$sanitized['lisdata']        = array();
			$sanitized['list']           = '';
		}

		if ( ! empty( $sanitized['api'] ) && isset( $old_settings['auth_type'] ) && 'oauth' === $old_settings['auth_type'] ) {
			$auth_manager = Cmatic_Lite_Container::get( 'auth.manager' );
			if ( $auth_manager ) {
				$auth_manager->disconnect( $form_id );
			}
			unset( $sanitized['auth_type'] );
			unset( $sanitized['api_key_backup'] );
			$sanitized['lisdata']        = array();
			$sanitized['list']           = '';
			$sanitized['api-validation'] = 0;
			$old_settings                = get_option( $option_name, array() );
			$old_settings                = is_array( $old_settings ) ? $old_settings : array();
		}

		$updated_settings = array_merge( $old_settings, $sanitized );

		update_option( $option_name, self::mirror_legacy_mappings( $updated_settings ) );

		try {
			\Signls\Sdk\V1\Runtime::relevant_change( 'contact-form-7-mailchimp-extension' );
		} catch ( \Throwable $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Signal bookkeeping must never alter the Contact Form 7 save result.
		}
	}


	private static function mirror_legacy_mappings( array $settings ): array {
		if ( empty( $settings['merge_fields'] ) || ! is_array( $settings['merge_fields'] ) ) {
			return $settings;
		}

		$row         = 1;
		$field_index = 3;
		foreach ( $settings['merge_fields'] as $merge_field ) {
			$tag       = isset( $merge_field['tag'] ) ? (string) $merge_field['tag'] : '';
			$field_key = 'field' . $field_index;
			$mapped    = isset( $settings[ $field_key ] ) ? (string) $settings[ $field_key ] : '';
			++$field_index;
			if ( '' === $tag || '' === $mapped ) {
				continue;
			}
			$field_name = preg_match( '/\[\s*([a-zA-Z_][0-9a-zA-Z:._-]*)\s*\]/', $mapped, $m ) ? $m[1] : trim( $mapped );
			if ( '' === $field_name ) {
				continue;
			}
			$settings[ "CustomKey{$row}" ]     = $tag;
			$settings[ "CustomValue{$row}" ]   = $field_name;
			$settings[ "CustomKeyType{$row}" ] = isset( $merge_field['type'] ) ? (string) $merge_field['type'] : 'text';
			if ( 'EMAIL' === $tag ) {
				$settings['email'] = $field_name;
			}
			++$row;
		}

		for ( $i = $row; $i <= 50; $i++ ) {
			if ( ! isset( $settings[ "CustomKey{$i}" ] ) && ! isset( $settings[ "CustomValue{$i}" ] ) ) {
				break;
			}
			unset( $settings[ "CustomKey{$i}" ], $settings[ "CustomValue{$i}" ], $settings[ "CustomKeyType{$i}" ] );
		}

		return $settings;
	}

	private static function sanitize_settings( array $posted, array $old ): array {
		$sanitized   = array();
		$text_fields = array( 'api', 'list', 'accept' );

		$max_index = CMATIC_LITE_FIELDS + 2;
		for ( $i = 3; $i <= $max_index; $i++ ) {
			$text_fields[] = 'field' . $i;
		}

		for ( $i = 1; $i <= 10; $i++ ) {
			$text_fields[] = 'CustomValue' . $i;
			$text_fields[] = 'CustomKey' . $i;
		}

		foreach ( $text_fields as $field ) {
			if ( isset( $posted[ $field ] ) ) {
				$value               = trim( sanitize_text_field( $posted[ $field ] ) );
				$sanitized[ $field ] = $value;
			}
		}

		if ( isset( $sanitized['api'] ) && strpos( $sanitized['api'], 'â€¢' ) !== false ) {
			if ( ! empty( $old['api'] ) && strpos( $old['api'], 'â€¢' ) === false ) {
				$sanitized['api'] = $old['api'];
			}
		}

		$checkboxes = array( 'cfactive', 'addunsubscr' );
		foreach ( $checkboxes as $field ) {
			$sanitized[ $field ] = isset( $posted[ $field ] ) ? '1' : '0';
		}

		$sanitized['confsubs'] = isset( $posted['confsubs'] ) && '1' === $posted['confsubs'] ? '1' : '0';

		return $sanitized;
	}

	/**
	 * Capture the complete option before Lite or Pro saves it.
	 *
	 * @param WPCF7_ContactForm $contact_form Contact form being saved.
	 */
	public static function capture_settings( $contact_form ): void {
		$form_id                                = (int) $contact_form->id();
		$settings                               = get_option( 'cf7_mch_' . $form_id, array() );
		self::$settings_before_save[ $form_id ] = is_array( $settings ) ? $settings : array();
	}

	/**
	 * Restore omitted state and save the provider selector/mappings.
	 *
	 * @param WPCF7_ContactForm $contact_form Contact form being saved.
	 */
	public static function save_provider_settings( $contact_form ): void {
		if ( ! isset( $_POST['wpcf7-cmatic-provider'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- The nonce is verified by can_save() before form data is read.
			return;
		}
		$form_id = (int) $contact_form->id();
		if ( ! self::can_save( $form_id ) ) {
			return;
		}
		$posted = wp_unslash( $_POST['wpcf7-cmatic-provider'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing -- Verified by can_save(); each accepted field is sanitized below.
		if ( ! is_array( $posted ) ) {
			return;
		}
		$slug = isset( $posted['provider'] ) ? sanitize_key( (string) $posted['provider'] ) : '';
		if ( '' === $slug || ! Cmatic_Lite_Esp_Registry::has( $slug ) ) {
			return;
		}
			$field_limit = Cmatic_Lite_Esp_Capabilities::field_limit( $slug, $form_id );
		$option          = 'cf7_mch_' . $form_id;
		$current         = get_option( $option, array() );
		$current         = is_array( $current ) ? $current : array();
		$before          = self::$settings_before_save[ $form_id ] ?? array();
		if ( defined( 'CMATIC_VERSION' ) ) {
			$current = array_replace( $before, $current );
		}
		$current['provider'] = $slug;
		if ( 'mailchimp' !== $slug ) {
			$current['providers'] = isset( $current['providers'] ) && is_array( $current['providers'] ) ? $current['providers'] : array();
			$settings             = isset( $current['providers'][ $slug ] ) && is_array( $current['providers'][ $slug ] ) ? $current['providers'][ $slug ] : array();
			$list                 = 'mailerlite' === $slug
				? sanitize_text_field( (string) ( $posted['primary_group'] ?? '' ) )
				: sanitize_text_field( (string) ( $posted['list'] ?? '' ) );
			if ( 'mailerlite' === $slug && ! Cmatic_Lite_Esp_Capabilities::feature_enabled( 'mailerlite_routing', $slug, $form_id ) && Cmatic_Mailerlite_Routing_Resolver::is_premium_configured( $settings ) ) {
				$list = self::stored_primary_group( $settings );
			}
				$merge_fields = self::sanitize_provider_fields( $posted['merge_fields'] ?? array(), $field_limit );
			if ( '' === $list || empty( $merge_fields ) || ! self::provider_list_exists( $settings, $list ) ) {
				return;
			}
			$settings['list'] = $list;
			if ( 'mailerlite' === $slug ) {
				$routing_entitled = Cmatic_Lite_Esp_Capabilities::feature_enabled( 'mailerlite_routing', $slug, $form_id );
				if ( ! $routing_entitled && Cmatic_Mailerlite_Routing_Resolver::is_premium_configured( $settings ) ) {
					$list             = self::stored_primary_group( $settings );
					$settings['list'] = $list;
				} else {
					$routing = self::sanitize_mailerlite_routing( $list, $posted, $settings, $contact_form );
					if ( null === $routing ) {
						return;
					}
					$settings['routing_schema'] = 1;
					$settings['base_groups']    = $routing['base_groups'];
					$settings['routing_rules']  = $routing['routing_rules'];
				}
			}
			$settings['merge_fields']       = $merge_fields;
			$settings['total_merge_fields'] = isset( $posted['total_merge_fields'] ) ? max( count( $merge_fields ), absint( $posted['total_merge_fields'] ) ) : count( $merge_fields );
			foreach ( range( 3, $field_limit + 2 ) as $index ) {
				$key = 'field' . $index;
				if ( isset( $posted[ $key ] ) ) {
					$settings[ $key ] = sanitize_text_field( (string) $posted[ $key ] );
				}
			}
			if ( ! self::has_required_email_mapping( $settings, $field_limit ) ) {
				return;
			}
			if ( 'mailerlite' === $slug && ! self::mailerlite_boolean_mappings_valid( $settings, $contact_form, $field_limit ) ) {
				return;
			}
			if ( Cmatic_Lite_Esp_Capabilities::feature_enabled( 'advanced_consent', $slug, $form_id ) ) {
				$consent = self::sanitize_provider_consent( $posted, $slug, $contact_form );
				if ( null === $consent ) {
					return;
				}
				if ( 'brevo' === $slug && 'double' === $consent['subscription_mode'] ) {
					$token       = isset( $posted['doi_verification_token'] ) ? sanitize_text_field( (string) $posted['doi_verification_token'] ) : '';
					$expected    = array(
						'form_id'       => $form_id,
						'provider'      => $slug,
						'list_id'       => $list,
						'template_id'   => $consent['doi_template_id'],
						'redirect_hash' => hash( 'sha256', $consent['doi_redirect_url'] ),
					);
					$current_key = Cmatic_Lite_Esp_Credentials::get( $form_id, $slug );
					if ( '' === $current_key ) {
						return;
					}
					$expected['credential_fingerprint'] = Cmatic_Lite_Esp_Rest_Controller::credential_fingerprint( $current_key );
					unset( $current_key );
					if ( ! Cmatic_Lite_Esp_Rest_Controller::verify_consent_token( $token, $expected ) ) {
						return;
					}
					$consent['doi_verified'] = 1;
				}
				$settings = array_merge( $settings, $consent );
			}
			if ( 'mailerlite' === $slug ) {
				$settings = self::save_mailerlite_options( $settings, $posted, $contact_form, $form_id );
				if ( empty( $settings ) ) {
					return;
				}
				if ( ! Cmatic_Mailerlite_Runtime_Policy::apply( $settings, self::mailerlite_entitlements( $form_id ) )['degraded'] ) {
					Cmatic_Mailerlite_Degradation_Reporter::clear( $form_id );
				}
			}
				$current['providers'][ $slug ] = $settings;
		}
		update_option( $option, $current );
		unset( self::$settings_before_save[ $form_id ] );
	}

	private static function sanitize_provider_fields( $fields, int $field_limit ): array {
		if ( ! is_array( $fields ) ) {
			return array();
		}
		$sanitized = array();
		foreach ( array_slice( $fields, 0, $field_limit ) as $offset => $field ) {
			if ( ! is_array( $field ) || empty( $field['tag'] ) ) {
				continue;
			}
			$tag         = sanitize_text_field( (string) $field['tag'] );
			$sanitized[] = array(
				'tag'           => $tag,
				'name'          => sanitize_text_field( (string) ( $field['name'] ?? $tag ) ),
				'type'          => sanitize_key( (string) ( $field['type'] ?? 'text' ) ),
				'display_order' => isset( $field['display_order'] ) ? (int) $field['display_order'] : $offset,
			);
		}
		return $sanitized;
	}

	private static function provider_list_exists( array $settings, string $list_id ): bool {
		$lists = isset( $settings['lisdata']['lists'] ) && is_array( $settings['lisdata']['lists'] )
			? $settings['lisdata']['lists']
			: array();
		foreach ( $lists as $list ) {
			if ( is_array( $list ) && isset( $list['id'] ) && $list_id === (string) $list['id'] ) {
				return true;
			}
		}
		return false;
	}

	private static function stored_primary_group( array $settings ): string {
		$list = $settings['list'] ?? '';
		if ( is_array( $list ) ) {
			$list = reset( $list );
		}
		return sanitize_text_field( (string) $list );
	}

	private static function sanitize_mailerlite_routing( string $primary, array $posted, array $settings, $contact_form ): ?array {
		if ( '' === $primary || ! self::provider_list_exists( $settings, $primary ) ) {
			return null;
		}
		$additional = array();
		if ( isset( $posted['base_groups'] ) && is_array( $posted['base_groups'] ) ) {
			$additional = $posted['base_groups'];
		} elseif ( isset( $posted['additional_groups'] ) && is_array( $posted['additional_groups'] ) ) {
			$additional = $posted['additional_groups'];
		}
		$groups = array( $primary );
		foreach ( $additional as $group_id ) {
			$group_id = sanitize_text_field( (string) $group_id );
			if ( '' !== $group_id && $primary !== $group_id && self::provider_list_exists( $settings, $group_id ) ) {
				$groups[] = $group_id;
			}
		}
		$groups = array_slice( array_values( array_unique( $groups ) ), 0, 20 );

		$choice_index = array();
		foreach ( Cmatic_Form_Tags::get_tags_with_types( $contact_form ) as $tag ) {
			if ( ! is_array( $tag ) || empty( $tag['routing_eligible'] ) || ! isset( $tag['name'] ) || ! is_scalar( $tag['name'] ) ) {
				continue;
			}
			$choices = array();
			foreach ( isset( $tag['choices'] ) && is_array( $tag['choices'] ) ? $tag['choices'] : array() as $choice ) {
				if ( is_array( $choice ) && isset( $choice['value'] ) && is_scalar( $choice['value'] ) ) {
					$choices[] = (string) $choice['value'];
				}
			}
			$choice_index[ (string) $tag['name'] ] = $choices;
		}

		$rules     = array();
		$seen      = array();
		$seen_rule = array();
		foreach ( array_slice( (array) ( $posted['routing_rules'] ?? array() ), 0, 50 ) as $rule ) {
			if ( ! is_array( $rule ) ) {
				return null;
			}
			$id       = sanitize_text_field( self::scalar_string( $rule['id'] ?? '' ) );
			$field    = sanitize_key( self::scalar_string( $rule['field'] ?? '' ) );
			$value    = sanitize_text_field( self::scalar_string( $rule['value'] ?? '' ) );
			$group_id = sanitize_text_field( self::scalar_string( $rule['group_id'] ?? '' ) );
			if ( 1 !== preg_match( '/^[a-f0-9]{8}-[a-f0-9]{4}-[1-5a-f0-9][a-f0-9]{3}-[89ab0-9][a-f0-9]{3}-[a-f0-9]{12}$/i', $id ) || isset( $seen[ $id ] ) ) {
				return null;
			}
			if ( ! isset( $choice_index[ $field ] ) || ! in_array( $value, $choice_index[ $field ], true ) || ! self::provider_list_exists( $settings, $group_id ) ) {
				return null;
			}
			$rule_key = hash( 'sha256', $field . "\0" . $value . "\0" . $group_id );
			if ( isset( $seen_rule[ $rule_key ] ) ) {
				return null;
			}
			$seen[ $id ]            = true;
			$seen_rule[ $rule_key ] = true;
			$rules[]                = compact( 'id', 'field', 'value', 'group_id' );
		}

		return array(
			'base_groups'   => $groups,
			'routing_rules' => $rules,
		);
	}

	/**
	 * Ensures MailerLite Boolean fields only map to Contact Form 7 acceptance tags.
	 *
	 * @param array $settings     Provider settings being saved.
	 * @param mixed $contact_form Contact Form 7 form.
	 * @param int   $field_limit  Effective mapping limit.
	 */
	private static function mailerlite_boolean_mappings_valid( array $settings, $contact_form, int $field_limit ): bool {
		$types = array();
		foreach ( Cmatic_Form_Tags::get_tags_with_types( $contact_form ) as $tag ) {
			if ( is_array( $tag ) && isset( $tag['name'] ) && is_scalar( $tag['name'] ) ) {
				$types[ (string) $tag['name'] ] = self::scalar_string( $tag['basetype'] ?? '' );
			}
		}

		foreach ( array_slice( (array) ( $settings['merge_fields'] ?? array() ), 0, $field_limit ) as $offset => $definition ) {
			if ( ! is_array( $definition ) || 'boolean' !== sanitize_key( self::scalar_string( $definition['type'] ?? '' ) ) ) {
				continue;
			}
			$mapping = self::scalar_string( $settings[ 'field' . ( $offset + 3 ) ] ?? '' );
			if ( '' === $mapping ) {
				continue;
			}
			if ( 1 !== preg_match( '/^\[([A-Za-z0-9_-]+)\]$/', $mapping, $matches ) || 'acceptance' !== ( $types[ $matches[1] ] ?? '' ) ) {
				return false;
			}
		}

		return true;
	}

	private static function save_mailerlite_options( array $settings, array $posted, $contact_form, int $form_id ): array {
		$status_entitled = Cmatic_Lite_Esp_Capabilities::feature_enabled( 'mailerlite_status', 'mailerlite', $form_id );
		$resub_entitled  = Cmatic_Lite_Esp_Capabilities::feature_enabled( 'mailerlite_resubscribe', 'mailerlite', $form_id );
		$meta_entitled   = Cmatic_Lite_Esp_Capabilities::feature_enabled( 'mailerlite_consent_metadata', 'mailerlite', $form_id );
		if ( $status_entitled ) {
			$mode = sanitize_key( (string) ( $posted['status_mode'] ?? 'legacy_provider_managed' ) );
			if ( ! in_array( $mode, array( 'legacy_provider_managed', 'account', 'active', 'unconfirmed' ), true ) ) {
				return array();
			}
			$settings['status_mode'] = $mode;
			if ( $resub_entitled ) {
				$force = ! empty( $posted['resubscribe_force'] );
				if ( $force && ( 'active' !== $mode || ! self::eligible_acceptance_field( self::scalar_string( $settings['consent_field'] ?? '' ), $contact_form ) ) ) {
					return array();
				}
				$settings['resubscribe_force'] = $force ? 1 : 0;
			}
		}

		if ( $meta_entitled ) {
			$enabled = ! empty( $posted['consent_metadata_enabled'] );
			if ( $enabled && ! self::eligible_acceptance_field( self::scalar_string( $settings['consent_field'] ?? '' ), $contact_form ) ) {
				return array();
			}
			$settings['consent_metadata_enabled'] = $enabled ? 1 : 0;
			if ( $enabled ) {
				$field                        = self::scalar_string( $settings['consent_field'] ?? '' );
				$text                         = self::acceptance_text( $field, $contact_form );
				$settings['consent_snapshot'] = array(
					'field'       => $field,
					'text_hash'   => hash( 'sha256', $text ),
					'captured_at' => gmdate( 'Y-m-d H:i:s' ),
				);
			}
		}
		return $settings;
	}

	private static function eligible_acceptance_field( string $field, $contact_form ): bool {
		$field = trim( $field, '[]' );
		foreach ( Cmatic_Form_Tags::get_tags_with_types( $contact_form ) as $tag ) {
			if ( is_array( $tag ) && self::scalar_string( $tag['name'] ?? '' ) === $field && 'acceptance' === ( $tag['basetype'] ?? '' ) && ! empty( $tag['required'] ) && empty( $tag['inverted'] ) ) {
				return true;
			}
		}
		return false;
	}

	private static function acceptance_text( string $field, $contact_form ): string {
		$field = trim( $field, '[]' );
		foreach ( Cmatic_Form_Tags::get_tags_with_types( $contact_form ) as $tag ) {
			if ( is_array( $tag ) && self::scalar_string( $tag['name'] ?? '' ) === $field ) {
				return self::scalar_string( $tag['content'] ?? '' );
			}
		}
		return '';
	}

	private static function mailerlite_entitlements( int $form_id ): array {
		$result = array();
		foreach ( array( 'mailerlite_routing', 'mailerlite_status', 'mailerlite_resubscribe', 'mailerlite_consent_metadata' ) as $feature ) {
			$result[ $feature ] = Cmatic_Lite_Esp_Capabilities::feature_enabled( $feature, 'mailerlite', $form_id );
		}
		return $result;
	}

	private static function has_required_email_mapping( array $settings, int $field_limit ): bool {
		$fields = isset( $settings['merge_fields'] ) && is_array( $settings['merge_fields'] )
			? $settings['merge_fields']
			: array();
		foreach ( array_slice( $fields, 0, $field_limit ) as $offset => $field ) {
			if ( is_array( $field ) && 'EMAIL' === strtoupper( self::scalar_string( $field['tag'] ?? '' ) ) ) {
				return '' !== trim( self::scalar_string( $settings[ 'field' . ( $offset + 3 ) ] ?? '' ) );
			}
		}
		return false;
	}

	private static function sanitize_provider_consent( array $posted, string $slug, $contact_form ): ?array {
		$gate  = isset( $posted['consent_gate'] ) ? sanitize_key( (string) $posted['consent_gate'] ) : 'none';
		$gate  = in_array( $gate, array( 'none', 'required' ), true ) ? $gate : '';
		$field = isset( $posted['consent_field'] ) ? sanitize_text_field( (string) $posted['consent_field'] ) : '';
		if ( '' === $gate || ( 'required' === $gate && ! self::is_acceptance_field( $field, $contact_form ) ) ) {
			return null;
		}

		$consent = array(
			'consent_gate'      => $gate,
			'consent_field'     => 'required' === $gate ? $field : '',
			'subscription_mode' => 'provider_managed',
		);
		if ( 'brevo' !== $slug ) {
			return $consent;
		}

		$mode = isset( $posted['subscription_mode'] ) ? sanitize_key( (string) $posted['subscription_mode'] ) : 'single';
		if ( ! in_array( $mode, array( 'single', 'double' ), true ) ) {
			return null;
		}
		$consent['subscription_mode'] = $mode;
		if ( 'single' === $mode ) {
			$consent['doi_template_id']  = 0;
			$consent['doi_redirect_url'] = '';
			$consent['doi_verified']     = 0;
			return $consent;
		}

		$template_id = isset( $posted['doi_template_id'] ) ? absint( $posted['doi_template_id'] ) : 0;
		$redirect    = isset( $posted['doi_redirect_url'] ) ? esc_url_raw( (string) $posted['doi_redirect_url'] ) : '';
		if ( $template_id < 1 || 'https' !== wp_parse_url( $redirect, PHP_URL_SCHEME ) ) {
			return null;
		}
		$consent['doi_template_id']  = $template_id;
		$consent['doi_redirect_url'] = $redirect;
		$consent['doi_verified']     = 0;
		return $consent;
	}

	private static function is_acceptance_field( string $field, $contact_form ): bool {
		if ( 1 !== preg_match( '/^\[([a-zA-Z_][0-9a-zA-Z:._-]*)\]$/', $field, $match ) ) {
			return false;
		}
		foreach ( Cmatic_Form_Tags::get_tags_with_types( $contact_form ) as $tag ) {
			if (
				is_array( $tag )
				&& 'acceptance' === ( $tag['basetype'] ?? '' )
				&& self::scalar_string( $tag['name'] ?? '' ) === $match[1]
			) {
				return true;
			}
		}
		return false;
	}

	private static function scalar_string( $value ): string {
		return is_scalar( $value ) ? (string) $value : '';
	}

	/**
	 * Check the Contact Form 7 save nonce and capability.
	 *
	 * @param int $form_id Contact form ID.
	 * @return bool Whether the current request may save the form.
	 */
	private static function can_save( int $form_id ): bool {
		$nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '';
		// phpcs:ignore WordPress.WP.Capabilities.Unknown -- Contact Form 7 registers this meta capability.
		return wp_verify_nonce( $nonce, sprintf( 'wpcf7-save-contact-form_%s', $form_id ) ) && current_user_can( 'wpcf7_edit_contact_form', $form_id );
	}

	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() {}
}
                                                                                                                                                                                                                                                                      includes/api/class-cmatic-log-viewer.php                                                            0000644                 00000026642 15266052670 0014300 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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.', 'chimpmatic-lite' ),
					'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.', 'chimpmatic-lite' ),
					$prefix,
					self::$max_lines
				)
				: __( 'Debug log is empty.', 'chimpmatic-lite' );

			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.', 'chimpmatic-lite' ),
				),
				200
			);
		}

		if ( ! wp_is_writable( $log_path ) ) {
			return new WP_REST_Response(
				array(
					'success' => false,
					'cleared' => false,
					'message' => __( 'Debug log file is not writable.', 'chimpmatic-lite' ),
				),
				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.', 'chimpmatic-lite' ),
				),
				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.', 'chimpmatic-lite' ),
			),
			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 ) { // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found -- Result growth is the loop termination condition.
			$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}',

		getRestRoot: function() {
			if (typeof wpApiSettings !== 'undefined' && wpApiSettings.root) {
				return wpApiSettings.root;
			}
			if (typeof chimpmaticLite !== 'undefined' && chimpmaticLite.restUrl) {
				return chimpmaticLite.restUrl.replace(/chimpmatic-lite\/v1\/$/, '');
			}
			return window.location.origin + '/wp-json/';
		},

		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();
	});

	window.CmaticLogViewer = CmaticLogViewer;
})(jQuery);
JS;
	}

	public static function render( $args = array() ) {
		$defaults = array(
			'title'       => __( 'Submission Logs', 'chimpmatic-lite' ),
			'clear_text'  => __( 'Clear Logs', 'chimpmatic-lite' ),
			'placeholder' => __( 'Click "View Debug Logs" to fetch the log content.', 'chimpmatic-lite' ),
			'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
	}
}
                                                                                              includes/api/class-cmatic-contact-lookup.php                                                        0000644                 00000025455 15266052671 0015164 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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( hash_hmac( 'sha256', (string) wp_json_encode( $value ), wp_salt( 'auth' ) ), 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 ) ) {
			$auth_manager = Cmatic_Lite_Container::get( 'auth.manager' );
			if ( $auth_manager ) {
				$api_key = $auth_manager->resolve_api_key( $form_id, '', $cf7_mch );
			}
		}

		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( strtolower( $email ) ); // nosemgrep
		$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( 'Find a Mailchimp contact', 'chimpmatic-lite' ); ?></h3>
				<p><?php esc_html_e( 'Check whether a contact exists in Mailchimp and view their status in each audience.', '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( 'Find contact', 'chimpmatic-lite' ); ?>
					</button>
				</div>

				<div id="cmatic-lookup-results" class="cmatic-hidden"></div>
			</div>
		</div>
		<?php
	}
}
                                                                                                                                                                                                                   includes/api/class-cmatic-rest-lists.php                                                            0000644                 00000035026 15266052671 0014326 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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( self::class, 'register_routes' ) );
		self::$initialized = true;
	}

	public static function register_routes() {
		register_rest_route(
			self::$namespace,
			'/lists',
			array(
				'methods'             => 'POST',
				'callback'            => array( self::class, 'get_lists' ),
				'permission_callback' => array( self::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'          => false,
						'type'              => 'string',
						'default'           => '',
						'sanitize_callback' => 'sanitize_text_field',
						'validate_callback' => function ( $param ) {
							if ( empty( $param ) ) {
								return true;
							}
							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( self::class, 'get_merge_fields' ),
				'permission_callback' => array( self::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( self::class, 'get_api_key' ),
				'permission_callback' => array( self::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' );

		$key_from_oauth = false;

		if ( empty( $api_key ) ) {
			$auth_manager = Cmatic_Lite_Container::get( 'auth.manager' );
			if ( $auth_manager ) {
				$credentials = $auth_manager->get_credentials( $form_id );
				if ( $credentials ) {
					$api_key        = $credentials->get_api_key();
					$key_from_oauth = true;
				} else {
					$api_key = $auth_manager->resolve_api_key( $form_id );
				}
			}
		}

		if ( empty( $api_key ) ) {
			self::record_transition( 'connect', false, 'configuration', 'API key is not configured.', 'configuration' );
			return new WP_Error(
				'missing_api_key',
				esc_html__( 'API key not found. Please connect to Mailchimp first.', 'chimpmatic-lite' ),
				array( 'status' => 400 )
			);
		}

		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' => 'Contact email address',
				'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;
				}
			}

			$extra = array(
				'api'          => $key_from_oauth ? '' : $api_key,
				'merge_fields' => $merge_fields,
			);

			if ( ! empty( $validation_result['account_name'] ) ) {
				$extra['oauth_account_name'] = $validation_result['account_name'];
			}

			$settings_to_save = array_merge( $cf7_mch, $validation_result, $lists_result, $extra );
			update_option( $option_name, $settings_to_save );

			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() );
			}
			self::record_transition(
				'connect',
				1 === (int) $api_valid,
				1 === (int) $api_valid ? 'unknown' : 'auth',
				1 === (int) $api_valid ? '' : $validation_result,
				1 === (int) $api_valid ? 'unknown' : 'revoked_credential'
			);

			return rest_ensure_response(
				array(
					'success'      => true,
					'api_valid'    => $api_valid === 1,
					'lists'        => $lists,
					'total'        => count( $lists ),
					'merge_fields' => $merge_fields,
				)
			);

		} catch ( Exception $e ) {
			self::record_transition( 'connect', false, 'remote_rejected', $e, 'remote_rejected' );
			$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 audiences. 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 ) ) {
			$auth_manager = Cmatic_Lite_Container::get( 'auth.manager' );
			if ( $auth_manager ) {
				$api_key = $auth_manager->resolve_api_key( $form_id, '', $cf7_mch );
			}
		}

		if ( empty( $api_key ) ) {
			self::record_transition( 'refresh_schema', false, 'configuration', 'API key is not configured.', 'configuration' );
			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' => 'Contact email address',
				'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() );
			}

			self::record_transition( 'refresh_schema', true, 'unknown', '', 'unknown' );

			$audience_name = '';
			if ( isset( $cf7_mch['lisdata']['lists'] ) && is_array( $cf7_mch['lisdata']['lists'] ) ) {
				foreach ( $cf7_mch['lisdata']['lists'] as $cmatic_list ) {
					if ( isset( $cmatic_list['id'], $cmatic_list['name'] ) && $cmatic_list['id'] === $list_id ) {
						$audience_name = (string) $cmatic_list['name'];
						break;
					}
				}
			}

			return rest_ensure_response(
				array(
					'success'            => true,
					'merge_fields'       => $merge_fields,
					'audience_name'      => $audience_name,
					'total_merge_fields' => (int) $raw_field_count,
					'lite_limit'         => CMATIC_LITE_FIELDS,
				)
			);

		} catch ( Exception $e ) {
			self::record_transition( 'refresh_schema', false, 'remote_rejected', $e, 'remote_rejected' );
			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();
		}

		$auth_type          = isset( $cf7_mch['auth_type'] ) ? sanitize_key( (string) $cf7_mch['auth_type'] ) : '';
		$auth_manager       = Cmatic_Lite_Container::get( 'auth.manager' );
		$credential_present = 'oauth' === $auth_type
			? ( $auth_manager && (bool) $auth_manager->get_credentials( $form_id ) )
			: ! empty( $cf7_mch['api'] );

		return rest_ensure_response(
			array(
				'success'            => true,
				'api_key'            => '',
				'auth_type'          => $auth_type,
				'credential_present' => $credential_present,
			)
		);
	}

	private static function record_transition( string $operation, bool $success, string $failure_class, $value, string $fallback ): void {
		try {
			$reason        = class_exists( 'Cmatic_Lite_Signls_Failure_Reason' )
				? Cmatic_Lite_Signls_Failure_Reason::from_value( $value, $fallback )
				: array(
					'code'   => $fallback,
					'sample' => '',
				);
			$failure_class = $success ? 'unknown' : self::failure_class_for_reason( $reason['code'], $failure_class );
			$recorded      = \Signls\Sdk\V1\CounterStore::record_outcome(
				'contact-form-7-mailchimp-extension',
				'mailchimp',
				$operation,
				$success,
				$failure_class,
				$success ? '' : $reason['code'],
				$success ? '' : $reason['sample']
			);
			if ( $recorded ) {
				\Signls\Sdk\V1\Runtime::relevant_change( 'contact-form-7-mailchimp-extension' );
			}
		} catch ( Throwable $error ) {
			// Signals must never change a REST result.
			return;
		}
	}

	private static function failure_class_for_reason( string $reason_code, string $fallback ): string {
		$map = array(
			'dns'                => 'transport_dns',
			'tls'                => 'transport_tls',
			'timeout'            => 'transport_timeout',
			'rate_limit'         => 'http_429',
			'http_4xx'           => 'http_4xx',
			'http_5xx'           => 'http_5xx',
			'revoked_credential' => 'auth',
			'permission'         => 'auth',
			'configuration'      => 'configuration',
			'validation'         => 'validation',
		);
		return isset( $map[ $reason_code ] ) ? $map[ $reason_code ] : $fallback;
	}

	private function __construct() {}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          includes/api/class-cmatic-rest-settings.php                                                         0000644                 00000017720 15266052671 0015031 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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' => 'signls',
			'path' => 'signls.consent_status',
		),
	);

	/** @var array Field labels for user messages. */
	protected static $field_labels = array(
		'debug'       => 'Debug Logger',
		'backlink'    => 'Developer Backlink',
		'auto_update' => 'Auto Update',
		'telemetry'   => 'Help Us Improve',
	);

	public static function init() {
		if ( self::$initialized ) {
			return;
		}
		add_action( 'rest_api_init', array( self::class, 'register_routes' ) );
		self::$initialized = true;
	}

	public static function register_routes() {
		register_rest_route(
			self::$namespace,
			'/settings/toggle',
			array(
				'methods'             => 'POST',
				'callback'            => array( self::class, 'toggle_setting' ),
				'permission_callback' => array( self::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( self::class, 'dismiss_notice' ),
				'permission_callback' => array( self::class, 'check_admin_permission' ),
				'args'                => array(
					'notice_id' => array(
						'required'          => false,
						'type'              => 'string',
						'default'           => 'news',
						'sanitize_callback' => 'sanitize_text_field',
						'enum'              => array( 'news', 'upgrade', 'license_banner', 'signls_consent' ),
					),
					'state'     => array(
						'required'          => false,
						'type'              => 'string',
						'default'           => '',
						'sanitize_callback' => 'sanitize_key',
					),
				),
			)
		);
	}

	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 ) {
			if ( ! self::handle_signls_toggle( (bool) $enabled ) ) {
				return new WP_Error( 'setting_not_saved', esc_html__( 'The sharing preference could not be saved.', 'chimpmatic-lite' ), array( 'status' => 500 ) );
			}
		} else {
			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
					// translators: %s: setting label.
					? sprintf( __( '%s enabled.', 'chimpmatic-lite' ), $label )
					// translators: %s: setting label.
					: sprintf( __( '%s disabled.', 'chimpmatic-lite' ), $label ),
			)
		);
	}

	private static function handle_signls_toggle( bool $enabled ): bool {
		$data = Cmatic_Options_Repository::get_all_options();
		if ( ! isset( $data['signls'] ) || ! is_array( $data['signls'] ) ) {
			$data['signls'] = array();
		}
		if ( ! isset( $data['telemetry'] ) || ! is_array( $data['telemetry'] ) ) {
			$data['telemetry'] = array();
		}
		$now                                       = time();
		$data['telemetry']['enabled']              = $enabled;
		$data['signls']['consent_status']          = $enabled ? 'enabled' : 'disabled';
		$data['signls']['consent_version']         = Cmatic_Lite_Signls_Privacy::CONSENT_VERSION;
		$data['signls']['consent_last_changed_at'] = $now;
		$data['signls']['consent_source']          = 'admin_settings_rest';
		if ( $enabled && empty( $data['signls']['consent_first_enabled_at'] ) ) {
			$data['signls']['consent_first_enabled_at'] = $now;
		}
		$saved = Cmatic_Options_Repository::instance()->save( $data );
		if ( ! $saved && get_option( 'cmatic', array() ) !== $data ) {
			return false;
		}
		if ( $enabled ) {
			\Signls\Sdk\V1\Runtime::enable( 'contact-form-7-mailchimp-extension', 'admin_settings_rest', Cmatic_Lite_Signls_Privacy::CONSENT_VERSION );
		} else {
			\Signls\Sdk\V1\Runtime::disable( 'contact-form-7-mailchimp-extension', 'admin_settings_rest', Cmatic_Lite_Signls_Privacy::CONSENT_VERSION );
		}
		return true;
	}

	protected static function handle_telemetry_toggle( $enabled ) {
		return self::handle_signls_toggle( (bool) $enabled );
	}

	public static function toggle_telemetry( $request ) {
		$enabled = $request->get_param( 'enabled' );

		$saved = self::handle_telemetry_toggle( $enabled );
		if ( ! $saved ) {
			return new WP_Error( 'setting_not_saved', esc_html__( 'The sharing preference could not be saved.', 'chimpmatic-lite' ), array( 'status' => 500 ) );
		}

		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 'signls_consent':
				// Compatibility for an already-open pre-removal admin page. The retired notice
				// disappears on navigation; its stale dismissal request intentionally has no side effects.
				$message = __( 'Sharing notice retired.', 'chimpmatic-lite' );
				break;

			case 'license_banner':
				$result = class_exists( 'Cmatic_License_Banner' )
					? Cmatic_License_Banner::handle_dismiss( (string) $request->get_param( 'state' ) )
					: false;
				if ( false === $result ) {
					return new WP_Error( 'bad_state', esc_html__( 'Unknown banner state.', 'chimpmatic-lite' ), array( 'status' => 400 ) );
				}
				$message = __( 'License banner snoozed.', 'chimpmatic-lite' );
				break;

			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() {}
}
                                                includes/api/class-cmatic-lite-esp-rest-controller.php                                              0000644                 00000074011 15266052671 0017070 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * ChimpMatic Lite provider REST transitions.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

// phpcs:disable Squiz.Commenting.ClassComment.Missing, Squiz.Commenting.FunctionComment.Missing -- Typed signatures follow the existing Lite provider convention.
final class Cmatic_Lite_Esp_Rest_Controller {
	private const REST_NAMESPACE = 'chimpmatic-lite/v1';

	public static function init(): void {
		add_action( 'rest_api_init', array( __CLASS__, 'register_routes' ) );
	}

	public static function register_routes(): void {
		foreach ( array( 'connect', 'fields', 'disconnect' ) as $action ) {
			register_rest_route(
				self::REST_NAMESPACE,
				'/providers/' . $action,
				array(
					'methods'             => 'POST',
					'callback'            => array( __CLASS__, $action ),
					'permission_callback' => array( __CLASS__, 'permission' ),
					'args'                => array(
						'form_id'  => array(
							'required'          => true,
							'type'              => 'integer',
							'sanitize_callback' => 'absint',
						),
						'provider' => array(
							'required'          => true,
							'type'              => 'string',
							'sanitize_callback' => 'sanitize_key',
							'validate_callback' => array( __CLASS__, 'validate_provider' ),
						),
						'api_key'  => array(
							'required'          => false,
							'type'              => 'string',
							'sanitize_callback' => 'sanitize_text_field',
						),
						'list_id'  => array(
							'required'          => false,
							'type'              => 'string',
							'sanitize_callback' => 'sanitize_text_field',
						),
					),
				)
			);
		}
		register_rest_route(
			self::REST_NAMESPACE,
			'/providers/consent/validate',
			array(
				'methods'             => 'POST',
				'callback'            => array( __CLASS__, 'validate_consent' ),
				'permission_callback' => array( __CLASS__, 'permission' ),
				'args'                => array(
					'form_id'           => array(
						'required'          => true,
						'type'              => 'integer',
						'sanitize_callback' => 'absint',
					),
					'provider'          => array(
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_key',
					),
					'list_id'           => array(
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					),
					'subscription_mode' => array(
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_key',
					),
					'doi_template_id'   => array(
						'required'          => true,
						'type'              => 'integer',
						'sanitize_callback' => 'absint',
					),
					'doi_redirect_url'  => array(
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'esc_url_raw',
					),
				),
			)
		);
		register_rest_route(
			self::REST_NAMESPACE,
			'/providers/fields/create',
			array(
				'methods'             => 'POST',
				'callback'            => array( __CLASS__, 'create_field' ),
				'permission_callback' => array( __CLASS__, 'permission' ),
				'args'                => self::tool_args(),
			)
		);
		register_rest_route(
			self::REST_NAMESPACE,
			'/providers/lookup',
			array(
				'methods'             => 'POST',
				'callback'            => array( __CLASS__, 'lookup' ),
				'permission_callback' => array( __CLASS__, 'permission' ),
				'args'                => self::tool_args(),
			)
		);
	}

	private static function tool_args(): array {
		return array(
			'form_id'  => array(
				'required'          => true,
				'type'              => 'integer',
				'sanitize_callback' => 'absint',
			),
			'provider' => array(
				'required'          => true,
				'type'              => 'string',
				'sanitize_callback' => 'sanitize_key',
			),
			'name'     => array(
				'required'          => false,
				'type'              => 'string',
				'sanitize_callback' => 'sanitize_text_field',
			),
			'type'     => array(
				'required'          => false,
				'type'              => 'string',
				'sanitize_callback' => 'sanitize_key',
			),
			'email'    => array(
				'required'          => false,
				'type'              => 'string',
				'sanitize_callback' => 'sanitize_email',
			),
		);
	}

	public static function validate_provider( string $provider ): bool {
		return 'mailchimp' !== $provider && Cmatic_Lite_Esp_Registry::has( $provider );
	}

	public static function permission( $request ) {
		$form_id = (int) $request->get_param( 'form_id' );
        // phpcs:ignore WordPress.WP.Capabilities.Unknown -- Contact Form 7 registers this meta capability.
		if ( ! current_user_can( 'wpcf7_edit_contact_form', $form_id ) ) {
			return new WP_Error(
				'rest_forbidden',
				esc_html__( 'You cannot edit this form.', 'chimpmatic-lite' ),
				array( 'status' => 403 )
			);
		}
		if ( ! wp_verify_nonce( (string) $request->get_header( 'X-WP-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 connect( $request ) {
		$form_id      = (int) $request->get_param( 'form_id' );
		$slug         = (string) $request->get_param( 'provider' );
		$field_limit  = Cmatic_Lite_Esp_Capabilities::field_limit( $slug, $form_id );
		$submitted    = trim( (string) $request->get_param( 'api_key' ) );
		$previous_key = Cmatic_Lite_Esp_Credentials::get( $form_id, $slug );
		$key          = '' !== $submitted ? $submitted : $previous_key;

		if ( '' === $key ) {
			self::record_transition( $slug, 'connect', false, 'configuration', 'Provider credential is not configured.', 'configuration' );
			return new WP_Error(
				'missing_provider_key',
				esc_html__( 'Enter a provider credential first.', 'chimpmatic-lite' ),
				array( 'status' => 400 )
			);
		}

		$provider   = Cmatic_Lite_Esp_Registry::get( $slug );
		$logging    = (bool) Cmatic_Options_Repository::get_option( 'debug', false );
		$validation = $provider->validate_key( $key, $logging );
		if ( 1 !== (int) ( $validation['api-validation'] ?? 0 ) ) {
			self::record_transition( $slug, 'connect', false, 'auth', $validation, 'revoked_credential' );
			return new WP_Error(
				'invalid_provider_key',
				esc_html__( 'The provider rejected this credential.', 'chimpmatic-lite' ),
				array( 'status' => 400 )
			);
		}

		$lists = $provider->get_lists( $key, $logging );
		if ( ! empty( $lists['_cmatic_error'] ) ) {
			self::record_transition( $slug, 'connect', false, 'remote_rejected', $lists['_cmatic_error'], 'remote_rejected' );
			return new WP_Error(
				'provider_lists_failed',
				esc_html__( 'The credential is valid, but provider destinations could not be loaded.', 'chimpmatic-lite' ),
				array( 'status' => 502 )
			);
		}

		$list_items  = isset( $lists['lisdata']['lists'] ) && is_array( $lists['lisdata']['lists'] )
			? $lists['lisdata']['lists']
			: array();
		$key_changed = '' !== $submitted
			&& ( '' === $previous_key || ! hash_equals( $previous_key, $submitted ) );
		$settings    = self::provider_settings( $form_id, $slug );
		$selected    = self::selected_list( $settings );
		$changes     = array_merge( $validation, $lists );

		if (
			$key_changed
			|| (
				'' !== $selected
				&& ! self::list_exists( $list_items, $selected )
			)
		) {
			$changes = array_merge( $changes, self::empty_account_state( $field_limit ) );
		}

		$credential_replaced = false;
		if ( $key_changed ) {
			if ( ! Cmatic_Lite_Esp_Credentials::save( $form_id, $slug, $submitted ) ) {
				self::record_transition( $slug, 'connect', false, 'configuration', 'Credential persistence failed.', 'configuration' );
				return new WP_Error(
					'credential_save_failed',
					esc_html__( 'Could not encrypt provider credentials.', 'chimpmatic-lite' ),
					array( 'status' => 500 )
				);
			}
			$credential_replaced = true;
		}

		if ( ! self::merge_provider_settings( $form_id, $slug, $changes ) ) {
			if ( $credential_replaced ) {
				if ( '' === $previous_key ) {
					Cmatic_Lite_Esp_Credentials::delete( $form_id, $slug );
				} else {
					Cmatic_Lite_Esp_Credentials::save( $form_id, $slug, $previous_key );
				}
			}
			self::record_transition( $slug, 'connect', false, 'configuration', 'Provider settings persistence failed.', 'configuration' );
			return new WP_Error(
				'provider_settings_save_failed',
				esc_html__( 'The provider connected, but its settings could not be saved.', 'chimpmatic-lite' ),
				array( 'status' => 500 )
			);
		}
		if ( $key_changed && 'mailerlite' === $slug ) {
			Cmatic_Mailerlite_Degradation_Reporter::clear( $form_id );
		}
		if ( ! Cmatic_Options_Repository::get_option( 'api.first_connected' ) ) {
			Cmatic_Options_Repository::set_option( 'api.first_connected', time() );
		}
		self::record_transition( $slug, 'connect', true, 'unknown', '', 'unknown' );

		return rest_ensure_response(
			array(
				'success'            => true,
				'provider'           => $slug,
				'connected'          => true,
				'credential_present' => true,
				'key_changed'        => $key_changed,
				'lists'              => $list_items,
			)
		);
	}

	public static function fields( $request ) {
		$form_id     = (int) $request->get_param( 'form_id' );
		$slug        = (string) $request->get_param( 'provider' );
		$list_id     = trim( (string) $request->get_param( 'list_id' ) );
		$field_limit = Cmatic_Lite_Esp_Capabilities::field_limit( $slug, $form_id );

		if ( '' === $list_id ) {
			self::record_transition( $slug, 'refresh_schema', true, 'unknown', '', 'unknown' );
			return rest_ensure_response(
				array(
					'success'            => true,
					'provider'           => $slug,
					'list_id'            => '',
					'merge_fields'       => array(),
					'total_merge_fields' => 0,
					'mappings'           => self::empty_mappings( $field_limit ),
				)
			);
		}

		if ( ! self::is_known_list( $form_id, $slug, $list_id ) ) {
			self::record_transition( $slug, 'refresh_schema', false, 'validation', 'Destination is missing or no longer known.', 'deleted_destination' );
			return new WP_Error(
				'invalid_provider_list',
				esc_html__( 'Select a destination returned by the connected provider.', 'chimpmatic-lite' ),
				array( 'status' => 400 )
			);
		}

		$key = Cmatic_Lite_Esp_Credentials::get( $form_id, $slug );
		if ( '' === $key ) {
			self::record_transition( $slug, 'refresh_schema', false, 'configuration', 'Provider credential is not configured.', 'configuration' );
			return new WP_Error(
				'missing_provider_key',
				esc_html__( 'Connect this provider first.', 'chimpmatic-lite' ),
				array( 'status' => 400 )
			);
		}

		$settings = self::provider_settings( $form_id, $slug );
		$result   = Cmatic_Lite_Esp_Registry::get( $slug )->get_merge_fields(
			$key,
			$list_id,
			(bool) Cmatic_Options_Repository::get_option( 'debug', false )
		);
		if ( ! empty( $result['_cmatic_error'] ) ) {
			self::record_transition( $slug, 'refresh_schema', false, 'remote_rejected', $result['_cmatic_error'], 'remote_rejected' );
			return new WP_Error(
				'provider_fields_failed',
				esc_html__( 'Provider fields could not be loaded; existing mappings were preserved.', 'chimpmatic-lite' ),
				array( 'status' => 502 )
			);
		}

		$fields   = $result['merge_fields']['merge_fields'] ?? array();
		$fields   = is_array( $fields ) ? $fields : array();
		$capped   = array_slice( $fields, 0, $field_limit );
		$mappings = self::reconcile_mappings( $settings, $capped, $field_limit );
		if ( ! Cmatic_Options_Repository::get_option( 'api.audience_selected' ) ) {
			Cmatic_Options_Repository::set_option( 'api.audience_selected', time() );
		}
		self::record_transition( $slug, 'refresh_schema', true, 'unknown', '', 'unknown' );
		return rest_ensure_response(
			array(
				'success'            => true,
				'provider'           => $slug,
				'list_id'            => $list_id,
				'merge_fields'       => $capped,
				'total_merge_fields' => count( $fields ),
				'mappings'           => $mappings,
			)
		);
	}

	public static function disconnect( $request ) {
		$form_id      = (int) $request->get_param( 'form_id' );
		$slug         = (string) $request->get_param( 'provider' );
		$previous_key = Cmatic_Lite_Esp_Credentials::get( $form_id, $slug );
		Cmatic_Lite_Esp_Credentials::delete( $form_id, $slug );

		if ( ! self::merge_provider_settings( $form_id, $slug, array( 'api-validation' => 0 ) ) ) {
			if ( '' !== $previous_key ) {
				Cmatic_Lite_Esp_Credentials::save( $form_id, $slug, $previous_key );
			}
			self::record_transition( $slug, 'disconnect', false, 'configuration', 'Provider settings persistence failed.', 'configuration' );
			return new WP_Error(
				'provider_settings_save_failed',
				esc_html__( 'The provider could not be disconnected.', 'chimpmatic-lite' ),
				array( 'status' => 500 )
			);
		}
		self::record_transition( $slug, 'disconnect', true, 'unknown', '', 'unknown' );

		return rest_ensure_response(
			array(
				'success'            => true,
				'provider'           => $slug,
				'connected'          => false,
				'credential_present' => false,
			)
		);
	}

	public static function create_field( $request ) {
		$form_id  = (int) $request->get_param( 'form_id' );
		$slug     = (string) $request->get_param( 'provider' );
		$name     = sanitize_text_field( (string) $request->get_param( 'name' ) );
		$type     = sanitize_key( (string) $request->get_param( 'type' ) );
		$provider = Cmatic_Lite_Esp_Registry::get( $slug );
		if ( 'mailerlite' !== $slug || ! $provider instanceof Cmatic_Lite_Esp_Field_Creator_Interface || ! Cmatic_Lite_Esp_Capabilities::feature_enabled( 'mailerlite_create_field', $slug, $form_id ) ) {
			return new WP_Error( 'provider_field_create_forbidden', esc_html__( 'Field creation is unavailable.', 'chimpmatic-lite' ), array( 'status' => 403 ) );
		}
		if ( '' === $name || strlen( $name ) > 255 || ! in_array( $type, array( 'text', 'number', 'date' ), true ) ) {
			return new WP_Error( 'invalid_provider_field', esc_html__( 'Enter a valid field name and type.', 'chimpmatic-lite' ), array( 'status' => 400 ) );
		}
		$key = Cmatic_Lite_Esp_Credentials::get( $form_id, $slug );
		if ( '' === $key ) {
			return new WP_Error( 'missing_provider_key', esc_html__( 'Connect MailerLite first.', 'chimpmatic-lite' ), array( 'status' => 400 ) );
		}
		$lock = 'cmatic_ml_field_' . substr( hash_hmac( 'sha256', get_current_user_id() . '|' . $form_id . '|' . strtolower( $name ) . '|' . $type, wp_salt( 'nonce' ) ), 0, 40 );
		if ( ! self::acquire_lock( $lock, 60 ) ) {
			return new WP_Error( 'provider_field_create_locked', esc_html__( 'An identical field creation is already running.', 'chimpmatic-lite' ), array( 'status' => 409 ) );
		}
		try {
			$result = $provider->create_field(
				$key,
				array(
					'name' => $name,
					'type' => $type,
				),
				(bool) Cmatic_Options_Repository::get_option( 'debug', false )
			);
		} finally {
			delete_option( $lock );
		}
		if ( empty( $result['success'] ) ) {
			return new WP_Error( 'provider_field_create_failed', esc_html__( 'Creation was not confirmed. Refresh fields before trying again.', 'chimpmatic-lite' ), array( 'status' => 502 ) );
		}
		$data = isset( $result['body']['data'] ) && is_array( $result['body']['data'] ) ? $result['body']['data'] : (array) ( $result['body'] ?? array() );
		return rest_ensure_response(
			array(
				'success' => true,
				'field'   => array(
					'id'   => sanitize_text_field( (string) ( $data['id'] ?? '' ) ),
					'key'  => sanitize_text_field( (string) ( $data['key'] ?? $data['name'] ?? '' ) ),
					'name' => sanitize_text_field( (string) ( $data['name'] ?? $name ) ),
					'type' => sanitize_key( (string) ( $data['type'] ?? $type ) ),
				),
			)
		);
	}

	public static function lookup( $request ) {
		$form_id  = (int) $request->get_param( 'form_id' );
		$slug     = (string) $request->get_param( 'provider' );
		$email    = sanitize_email( (string) $request->get_param( 'email' ) );
		$provider = Cmatic_Lite_Esp_Registry::get( $slug );
		if ( ! current_user_can( 'manage_options' ) || 'mailerlite' !== $slug || ! $provider instanceof Cmatic_Lite_Esp_Lookup_Interface ) {
			return new WP_Error( 'provider_lookup_forbidden', esc_html__( 'Subscriber lookup is unavailable.', 'chimpmatic-lite' ), array( 'status' => 403 ) );
		}
		if ( ! is_email( $email ) ) {
			return new WP_Error( 'invalid_lookup_email', esc_html__( 'Enter a valid email address.', 'chimpmatic-lite' ), array( 'status' => 400 ) );
		}
		if ( ! self::consume_lookup_limit( $form_id, $email ) ) {
			return new WP_Error( 'provider_lookup_limited', esc_html__( 'Too many lookups. Try again later.', 'chimpmatic-lite' ), array( 'status' => 429 ) );
		}
		$key = Cmatic_Lite_Esp_Credentials::get( $form_id, $slug );
		if ( '' === $key ) {
			return new WP_Error( 'missing_provider_key', esc_html__( 'Connect MailerLite first.', 'chimpmatic-lite' ), array( 'status' => 400 ) );
		}
		$result = $provider->lookup( $key, $email );
		if ( empty( $result['success'] ) ) {
			return new WP_Error( 'provider_lookup_failed', esc_html__( 'MailerLite lookup failed.', 'chimpmatic-lite' ), array( 'status' => 502 ) );
		}
		$data     = isset( $result['data'] ) && is_array( $result['data'] ) ? $result['data'] : array();
		$settings = self::provider_settings( $form_id, $slug );
		$allowed  = self::mapped_field_keys( $settings );
		$fields   = array_intersect_key( is_array( $data['fields'] ?? null ) ? $data['fields'] : array(), array_flip( $allowed ) );
		$groups   = array();
		foreach ( (array) ( $data['groups'] ?? array() ) as $group ) {
			if ( is_array( $group ) ) {
				$groups[] = array(
					'id'   => sanitize_text_field( self::scalar_string( $group['id'] ?? '' ) ),
					'name' => sanitize_text_field( self::scalar_string( $group['name'] ?? '' ) ),
				);
			}
		}
		$sanitized_fields = array();
		foreach ( $fields as $field_key => $field_value ) {
			if ( is_scalar( $field_value ) ) {
				$sanitized_fields[ $field_key ] = sanitize_text_field( (string) $field_value );
			}
		}
		$response = rest_ensure_response(
			array(
				'found'  => ! empty( $result['found'] ),
				'status' => sanitize_key( (string) ( $data['status'] ?? '' ) ),
				'groups' => $groups,
				'fields' => $sanitized_fields,
			)
		);
		$response->header( 'Cache-Control', 'no-store, private' );
		return $response;
	}

	public static function validate_consent( $request ) {
		$form_id     = (int) $request->get_param( 'form_id' );
		$slug        = (string) $request->get_param( 'provider' );
		$list_id     = trim( (string) $request->get_param( 'list_id' ) );
		$template_id = (int) $request->get_param( 'doi_template_id' );
		$redirect    = esc_url_raw( (string) $request->get_param( 'doi_redirect_url' ) );
		$mode        = (string) $request->get_param( 'subscription_mode' );
		if (
			'brevo' !== $slug
			|| 'double' !== $mode
			|| ! Cmatic_Lite_Esp_Capabilities::feature_enabled( 'advanced_consent', $slug, $form_id )
		) {
			return new WP_Error( 'provider_consent_forbidden', esc_html__( 'Advanced provider consent is unavailable.', 'chimpmatic-lite' ), array( 'status' => 403 ) );
		}
		if ( $template_id < 1 || 'https' !== wp_parse_url( $redirect, PHP_URL_SCHEME ) || ! self::is_known_list( $form_id, $slug, $list_id ) ) {
			return new WP_Error( 'invalid_provider_consent', esc_html__( 'Check the selected list and double opt-in settings.', 'chimpmatic-lite' ), array( 'status' => 400 ) );
		}

		$key = Cmatic_Lite_Esp_Credentials::get( $form_id, $slug );
		if ( '' === $key ) {
			return new WP_Error( 'missing_provider_key', esc_html__( 'Connect Brevo before verifying double opt-in.', 'chimpmatic-lite' ), array( 'status' => 400 ) );
		}
		$options                = array(
			'subscription_mode' => $mode,
			'doi_template_id'   => $template_id,
			'doi_redirect_url'  => $redirect,
		);
		$result                 = Cmatic_Lite_Esp_Registry::get( $slug )->validate_subscription_options( $key, $list_id, $options );
		$credential_fingerprint = self::credential_fingerprint( $key );
		unset( $key );
		if ( ! $result['success'] ) {
			return new WP_Error( 'provider_consent_invalid', esc_html__( 'Brevo did not accept this double opt-in template.', 'chimpmatic-lite' ), array( 'status' => 409 ) );
		}
		$payload                           = self::consent_token_payload( $form_id, $slug, $list_id, $template_id, $redirect );
		$payload['credential_fingerprint'] = $credential_fingerprint;
		return rest_ensure_response(
			array(
				'success'            => true,
				'provider'           => $slug,
				'verified'           => true,
				'template'           => array(
					'id'   => (int) ( $result['data']['template_id'] ?? $template_id ),
					'name' => sanitize_text_field( (string) ( $result['data']['template_name'] ?? '' ) ),
				),
				'verification_token' => self::sign_consent_payload( $payload ),
			)
		);
	}

	public static function verify_consent_token( string $token, array $expected ): bool {
		$parts = explode( '.', $token, 2 );
		if ( 2 !== count( $parts ) ) {
			return false;
		}
		$payload_json = self::base64url_decode( $parts[0] );
		$signature    = self::base64url_decode( $parts[1] );
		if ( false === $payload_json || false === $signature ) {
			return false;
		}
		$expected_signature = hash_hmac( 'sha256', $parts[0], wp_salt( 'nonce' ), true );
		if ( ! hash_equals( $expected_signature, $signature ) ) {
			return false;
		}
		$payload = json_decode( $payload_json, true );
		if ( ! is_array( $payload ) || (int) ( $payload['expires'] ?? 0 ) < time() ) {
			return false;
		}
		foreach ( $expected as $key => $value ) {
			if ( ! array_key_exists( $key, $payload ) || (string) $payload[ $key ] !== (string) $value ) {
				return false;
			}
		}
		return (int) ( $payload['user_id'] ?? 0 ) === get_current_user_id();
	}

	private static function consent_token_payload( int $form_id, string $slug, string $list_id, int $template_id, string $redirect ): array {
		return array(
			'form_id'       => $form_id,
			'provider'      => $slug,
			'list_id'       => $list_id,
			'template_id'   => $template_id,
			'redirect_hash' => hash( 'sha256', $redirect ),
			'user_id'       => get_current_user_id(),
			'expires'       => time() + ( 5 * MINUTE_IN_SECONDS ),
		);
	}

	public static function credential_fingerprint( string $credential ): string {
		return hash_hmac( 'sha256', $credential, wp_salt( 'auth' ) );
	}

	private static function sign_consent_payload( array $payload ): string {
		$json      = wp_json_encode( $payload );
		$encoded   = self::base64url_encode( false === $json ? '{}' : $json );
		$signature = hash_hmac( 'sha256', $encoded, wp_salt( 'nonce' ), true );
		return $encoded . '.' . self::base64url_encode( $signature );
	}

	private static function base64url_encode( string $value ): string {
		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- URL-safe signed token encoding.
		return rtrim( strtr( base64_encode( $value ), '+/', '-_' ), '=' );
	}

	private static function base64url_decode( string $value ) {
		$padding = strlen( $value ) % 4;
		if ( $padding ) {
			$value .= str_repeat( '=', 4 - $padding );
		}
		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- URL-safe signed token decoding.
		return base64_decode( strtr( $value, '-_', '+/' ), true );
	}

	private static function empty_account_state( int $field_limit ): array {
		return array_merge(
			array(
				'list'               => '',
				'base_groups'        => array(),
				'routing_rules'      => array(),
				'routing_schema'     => 1,
				'merge_fields'       => array(),
				'total_merge_fields' => 0,
			),
			self::empty_mappings( $field_limit )
		);
	}

	private static function acquire_lock( string $name, int $ttl ): bool {
		$expires = time() + $ttl;
		if ( add_option( $name, $expires, '', false ) ) {
			return true;
		}
		$stored  = get_option( $name, 0 );
		$current = is_numeric( $stored ) ? (int) $stored : 0;
		if ( $current >= time() ) {
			return false;
		}
		delete_option( $name );
		return add_option( $name, $expires, '', false );
	}

	private static function consume_lookup_limit( int $form_id, string $email ): bool {
		$user_id     = get_current_user_id();
		$base        = 'cmatic_ml_lookup_' . $user_id . '_' . $form_id;
		$target      = $base . '_' . substr( hash_hmac( 'sha256', strtolower( $email ), wp_salt( 'nonce' ) ), 0, 24 );
		$total_value = get_transient( $base );
		$same_value  = get_transient( $target );
		$total       = is_numeric( $total_value ) ? (int) $total_value : 0;
		$same        = is_numeric( $same_value ) ? (int) $same_value : 0;
		if ( $total >= 10 || $same >= 3 ) {
			return false;
		}
		set_transient( $base, $total + 1, 10 * MINUTE_IN_SECONDS );
		set_transient( $target, $same + 1, 10 * MINUTE_IN_SECONDS );
		return true;
	}

	private static function mapped_field_keys( array $settings ): array {
		$keys = array();
		foreach ( (array) ( $settings['merge_fields'] ?? array() ) as $offset => $field ) {
			if ( is_array( $field ) && isset( $field['tag'] ) && is_scalar( $field['tag'] ) && ! empty( $settings[ 'field' . ( (int) $offset + 3 ) ] ) ) {
				$keys[] = (string) $field['tag'];
			}
		}
		return $keys;
	}

	private static function empty_mappings( int $field_limit ): array {
		$mappings = array();
		foreach ( range( 3, $field_limit + 2 ) as $index ) {
			$mappings[ 'field' . $index ] = '';
		}
		return $mappings;
	}

	private static function reconcile_mappings( array $settings, array $new_fields, int $field_limit ): array {
		$by_tag     = array();
		$old_fields = isset( $settings['merge_fields'] ) && is_array( $settings['merge_fields'] )
			? $settings['merge_fields']
			: array();

		foreach ( array_slice( $old_fields, 0, $field_limit ) as $offset => $field ) {
			if ( ! is_array( $field ) || ! isset( $field['tag'] ) || ! is_scalar( $field['tag'] ) || '' === (string) $field['tag'] ) {
				continue;
			}
			$slot                             = 'field' . ( $offset + 3 );
			$by_tag[ (string) $field['tag'] ] = sanitize_text_field(
				self::scalar_string( $settings[ $slot ] ?? '' )
			);
		}

		$mappings = self::empty_mappings( $field_limit );
		foreach ( array_slice( $new_fields, 0, $field_limit ) as $offset => $field ) {
			if ( ! is_array( $field ) || ! isset( $field['tag'] ) || ! is_scalar( $field['tag'] ) || '' === (string) $field['tag'] ) {
				continue;
			}
			$slot              = 'field' . ( $offset + 3 );
			$mappings[ $slot ] = $by_tag[ (string) $field['tag'] ] ?? '';
		}
		return $mappings;
	}

	private static function provider_settings( int $form_id, string $slug ): array {
		$config = get_option( 'cf7_mch_' . $form_id, array() );
		$config = is_array( $config ) ? $config : array();
		if (
			! isset( $config['providers'][ $slug ] )
			|| ! is_array( $config['providers'][ $slug ] )
		) {
			return array();
		}
		return $config['providers'][ $slug ];
	}

	private static function selected_list( array $settings ): string {
		$selected = $settings['list'] ?? '';
		if ( is_array( $selected ) ) {
			$first    = reset( $selected );
			$selected = false === $first ? '' : $first;
		}
		return sanitize_text_field( (string) $selected );
	}

	private static function list_exists( array $lists, string $list_id ): bool {
		foreach ( $lists as $list ) {
			if (
				is_array( $list )
				&& isset( $list['id'] )
				&& is_scalar( $list['id'] )
				&& $list_id === (string) $list['id']
			) {
				return true;
			}
		}
		return false;
	}

	private static function scalar_string( $value ): string {
		return is_scalar( $value ) ? (string) $value : '';
	}

	private static function merge_provider_settings( int $form_id, string $slug, array $changes ): bool {
		$option                       = 'cf7_mch_' . $form_id;
		$config                       = get_option( $option, array() );
		$config                       = is_array( $config ) ? $config : array();
		$config['providers']          = isset( $config['providers'] ) && is_array( $config['providers'] )
			? $config['providers']
			: array();
		$current                      = isset( $config['providers'][ $slug ] ) && is_array( $config['providers'][ $slug ] )
			? $config['providers'][ $slug ]
			: array();
		$config['providers'][ $slug ] = array_merge( $current, $changes );
		if ( update_option( $option, $config ) ) {
			return true;
		}

		$stored = self::provider_settings( $form_id, $slug );
		foreach ( $changes as $key => $value ) {
			if ( ! array_key_exists( $key, $stored ) || maybe_serialize( $stored[ $key ] ) !== maybe_serialize( $value ) ) {
				return false;
			}
		}
		return true;
	}

	private static function is_known_list( int $form_id, string $slug, string $list_id ): bool {
		$settings = self::provider_settings( $form_id, $slug );
		$lists    = isset( $settings['lisdata']['lists'] ) && is_array( $settings['lisdata']['lists'] )
			? $settings['lisdata']['lists']
			: array();
		return self::list_exists( $lists, $list_id );
	}

	private static function record_transition( string $provider, string $operation, bool $success, string $failure_class, $value, string $fallback ): void {
		try {
			$reason        = class_exists( 'Cmatic_Lite_Signls_Failure_Reason' )
				? Cmatic_Lite_Signls_Failure_Reason::from_value( $value, $fallback )
				: array(
					'code'   => $fallback,
					'sample' => '',
				);
			$failure_class = $success ? 'unknown' : self::failure_class_for_reason( $reason['code'], $failure_class );
			$recorded      = \Signls\Sdk\V1\CounterStore::record_outcome(
				'contact-form-7-mailchimp-extension',
				$provider,
				$operation,
				$success,
				$failure_class,
				$success ? '' : $reason['code'],
				$success ? '' : $reason['sample']
			);
			if ( $recorded ) {
				\Signls\Sdk\V1\Runtime::relevant_change( 'contact-form-7-mailchimp-extension' );
			}
		} catch ( Throwable $error ) {
			// Signals must never change a provider REST result.
			return;
		}
	}

	private static function failure_class_for_reason( string $reason_code, string $fallback ): string {
		$map = array(
			'dns'                => 'transport_dns',
			'tls'                => 'transport_tls',
			'timeout'            => 'transport_timeout',
			'rate_limit'         => 'http_429',
			'http_4xx'           => 'http_4xx',
			'http_5xx'           => 'http_5xx',
			'revoked_credential' => 'auth',
			'permission'         => 'auth',
			'configuration'      => 'configuration',
			'validation'         => 'validation',
		);
		return isset( $map[ $reason_code ] ) ? $map[ $reason_code ] : $fallback;
	}

	private function __construct() {}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       includes/api/class-cmatic-submission-feedback.php                                                   0000644                 00000016525 15266052671 0016135 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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' ),
			'provider_api_error'           => __( 'The email provider rejected the subscription request.', 'chimpmatic-lite' ),
			'provider_network_error'       => __( 'A network error occurred while connecting to the email provider.', 'chimpmatic-lite' ),
			'provider_configuration_error' => __(
				'The selected provider cannot honor this form\'s opt-in status without additional configuration.',
				'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 );
	}
}
                                                                                                                                                                           includes/api/class-cmatic-rest-form.php                                                             0000644                 00000024765 15266052671 0014143 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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( self::class, 'register_routes' ) );
		self::$initialized = true;
	}

	public static function register_routes() {
		register_rest_route(
			self::$namespace,
			'/tags/toggle',
			array(
				'methods'             => 'POST',
				'callback'            => array( self::class, 'toggle_tag' ),
				'permission_callback' => array( self::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( self::class, 'save_field' ),
				'permission_callback' => array( self::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( self::class, 'save_setting' ),
				'permission_callback' => array( self::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
					// translators: %s: tag name.
					? sprintf( __( 'Tag [%s] enabled.', 'chimpmatic-lite' ), $tag )
					// translators: %s: tag name.
					: 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',
				// translators: %s: field name.
				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() {}
}
           includes/api/class-cmatic-rest-oauth.php                                                            0000644                 00000027406 15266052671 0014313 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * OAuth REST API controller.
 *
 * @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_OAuth {

	protected static $namespace   = 'chimpmatic-lite/v1';
	protected static $initialized = false;

	public static function init() {
		if ( self::$initialized ) {
			return;
		}
		add_action( 'rest_api_init', array( self::class, 'register_routes' ) );
		self::$initialized = true;
	}

	public static function register_routes() {
		register_rest_route(
			self::$namespace,
			'/oauth/start',
			array(
				'methods'             => 'POST',
				'callback'            => array( self::class, 'oauth_start' ),
				'permission_callback' => array( self::class, 'check_admin_permissions' ),
				'args'                => array(
					'form_id' => array(
						'required'          => true,
						'type'              => 'integer',
						'sanitize_callback' => 'absint',
					),
				),
			)
		);

		register_rest_route(
			self::$namespace,
			'/oauth/status',
			array(
				'methods'             => 'POST',
				'callback'            => array( self::class, 'oauth_check_status' ),
				'permission_callback' => array( self::class, 'check_admin_permissions' ),
				'args'                => array(
					'url' => array(
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'esc_url_raw',
					),
				),
			)
		);

		register_rest_route(
			self::$namespace,
			'/oauth/finish',
			array(
				'methods'             => 'POST',
				'callback'            => array( self::class, 'oauth_finish' ),
				'permission_callback' => array( self::class, 'check_admin_permissions' ),
				'args'                => array(
					'token'   => array(
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					),
					'form_id' => array(
						'required'          => true,
						'type'              => 'integer',
						'sanitize_callback' => 'absint',
					),
				),
			)
		);

		register_rest_route(
			self::$namespace,
			'/oauth/disconnect',
			array(
				'methods'             => 'POST',
				'callback'            => array( self::class, 'disconnect_oauth' ),
				'permission_callback' => array( self::class, 'check_admin_permissions' ),
				'args'                => array(
					'form_id' => array(
						'required'          => true,
						'type'              => 'integer',
						'sanitize_callback' => 'absint',
					),
				),
			)
		);

		register_rest_route(
			self::$namespace,
			'/oauth/event',
			array(
				'methods'             => 'POST',
				'callback'            => array( self::class, 'oauth_event' ),
				'permission_callback' => array( self::class, 'check_admin_permissions' ),
				'args'                => array(
					'event'  => array(
						'required'          => true,
						'type'              => 'string',
						'enum'              => array( 'popup_blocked', 'popup_opened', 'timeout', 'finish_failed' ),
						'sanitize_callback' => 'sanitize_text_field',
					),
					'token'  => array(
						'required'          => false,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					),
					'reason' => array(
						'required'          => false,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
			)
		);
	}

	public static function check_admin_permissions() {
		return current_user_can( 'manage_options' );
	}

	public static function oauth_start( $request ) {
		$form_id = $request->get_param( 'form_id' );
		if ( ! $form_id ) {
			return new WP_Error( 'missing_form_id', 'Form ID is required', array( 'status' => 400 ) );
		}

		$secret = wp_generate_password( 32, false );
		set_transient( 'cmatic_oauth_secret_' . $form_id, $secret, HOUR_IN_SECONDS );
		$start_body = array(
			'domain' => site_url(),
			'secret' => $secret,
		);
		$context    = Cmatic_Lite_Service_Context::payload( 'oauth_admin_connect' );
		if ( ! empty( $context ) ) {
			$start_body['context'] = $context;
		}

		$response = wp_safe_remote_post(
			Cmatic_Lite_Auth_Manager::OAUTH_GATEWAY . '/api/start',
			array(
				'headers' => array( 'Content-Type' => 'application/json' ),
				'body'    => wp_json_encode( $start_body ),
				'timeout' => 15,
			)
		);

		if ( is_wp_error( $response ) ) {
			self::logger()->log(
				'error',
				'oauth start unreachable',
				array(
					'form_id' => $form_id,
					'reason'  => $response->get_error_code(),
				)
			);
			return $response;
		}

		$body = json_decode( wp_remote_retrieve_body( $response ), true );
		$code = wp_remote_retrieve_response_code( $response );

		if ( 201 !== $code || empty( $body['token'] ) ) {
			self::logger()->log(
				'error',
				'oauth start failed',
				array(
					'form_id' => $form_id,
					'code'    => $code,
				)
			);
			return new WP_Error( 'oauth_start_failed', 'Could not start OAuth session', array( 'status' => 502 ) );
		}

		self::logger()->log(
			'info',
			'oauth start ok',
			array(
				'form_id' => $form_id,
				'token'   => substr( $body['token'], 0, 8 ),
			)
		);
		return rest_ensure_response(
			array(
				'token'    => $body['token'],
				'auth_url' => Cmatic_Lite_Auth_Manager::OAUTH_GATEWAY . '/auth/start/' . $body['token'],
			)
		);
	}

	public static function oauth_check_status( $request ) {
		$url = $request->get_param( 'url' );
		if ( empty( $url ) ) {
			return new WP_Error( 'missing_url', 'Status URL is required', array( 'status' => 400 ) );
		}

		if ( 0 !== strpos( $url, Cmatic_Lite_Auth_Manager::OAUTH_GATEWAY . '/' ) ) {
			return new WP_Error( 'invalid_url', 'URL must point to the OAuth gateway', array( 'status' => 400 ) );
		}

		$response = wp_safe_remote_post(
			$url,
			array(
				'headers' => array(),
				'timeout' => 10,
			)
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$body = json_decode( wp_remote_retrieve_body( $response ), true );

		$safe_response = array(
			'status' => isset( $body['status'] ) ? sanitize_text_field( $body['status'] ) : 'unknown',
		);

		return rest_ensure_response( $safe_response );
	}

	public static function oauth_finish( $request ) {
		$token   = $request->get_param( 'token' );
		$form_id = $request->get_param( 'form_id' );

		if ( ! $token || ! $form_id ) {
			return new WP_Error( 'missing_params', 'Token and form_id are required', array( 'status' => 400 ) );
		}

		$secret = get_transient( 'cmatic_oauth_secret_' . $form_id );
		if ( empty( $secret ) ) {
			self::logger()->log(
				'error',
				'oauth finish expired',
				array(
					'form_id' => $form_id,
					'token'   => substr( $token, 0, 8 ),
				)
			);
			self::relay( 'wp_expired_secret', $token, 'transient_expired' );
			return new WP_Error( 'oauth_expired', 'OAuth session expired', array( 'status' => 410 ) );
		}

		$response = wp_safe_remote_post(
			Cmatic_Lite_Auth_Manager::OAUTH_GATEWAY . '/api/finish',
			array(
				'headers' => array( 'Content-Type' => 'application/json' ),
				'body'    => wp_json_encode(
					array(
						'domain' => site_url(),
						'secret' => $secret,
						'token'  => $token,
					)
				),
				'timeout' => 15,
			)
		);

		delete_transient( 'cmatic_oauth_secret_' . $form_id );

		if ( is_wp_error( $response ) ) {
			self::logger()->log(
				'error',
				'oauth finish unreachable',
				array(
					'form_id' => $form_id,
					'token'   => substr( $token, 0, 8 ),
					'reason'  => $response->get_error_code(),
				)
			);
			self::relay( 'wp_gateway_error', $token, $response->get_error_code() );
			return $response;
		}

		$body = json_decode( wp_remote_retrieve_body( $response ), true );
		$code = wp_remote_retrieve_response_code( $response );

		if ( 200 !== $code || empty( $body['access_token'] ) || empty( $body['data_center'] ) ) {
			self::logger()->log(
				'error',
				'oauth finish failed',
				array(
					'form_id' => $form_id,
					'code'    => $code,
					'token'   => substr( $token, 0, 8 ),
				)
			);
			self::relay( 'wp_gateway_error', $token, 'http_' . $code );
			return new WP_Error( 'oauth_finish_failed', 'Could not retrieve credentials', array( 'status' => 502 ) );
		}

		if ( ! preg_match( '/^[a-z]{2}\d{1,3}$/', $body['data_center'] ) ) {
			self::logger()->log(
				'error',
				'oauth finish bad dc',
				array(
					'form_id' => $form_id,
					'token'   => substr( $token, 0, 8 ),
				)
			);
			self::relay( 'wp_invalid_dc', $token, 'bad_datacenter' );
			return new WP_Error( 'invalid_datacenter', 'Invalid datacenter format from gateway', array( 'status' => 502 ) );
		}

		$api_key = $body['access_token'] . '-' . $body['data_center'];

		$creds = Cmatic_Lite_Credentials::from_oauth( $api_key );
		if ( ! $creds ) {
			self::logger()->log(
				'error',
				'oauth finish bad key',
				array(
					'form_id' => $form_id,
					'token'   => substr( $token, 0, 8 ),
				)
			);
			self::relay( 'wp_invalid_key', $token, 'bad_key_format' );
			return new WP_Error( 'invalid_key', 'Invalid API key format from gateway', array( 'status' => 502 ) );
		}

		$auth_manager = Cmatic_Lite_Container::get( 'auth.manager' );
		$result       = $auth_manager->save_oauth_credentials( $form_id, $api_key );

		if ( is_wp_error( $result ) ) {
			self::logger()->log(
				'error',
				'oauth finish save failed',
				array(
					'form_id' => $form_id,
					'token'   => substr( $token, 0, 8 ),
				)
			);
			self::relay( 'wp_save_failed', $token, $result->get_error_code() );
			return $result;
		}

		$option_name = 'cf7_mch_' . $form_id;
		$config      = get_option( $option_name, array() );
		if ( is_array( $config ) ) {
			$current_user                   = wp_get_current_user();
			$config['oauth_connected_by']   = sanitize_text_field( $current_user->display_name );
			$config['oauth_connected_date'] = current_time( 'Y-m-d' );
			update_option( $option_name, $config );
		}

		self::logger()->log(
			'info',
			'oauth finish ok',
			array(
				'form_id' => $form_id,
				'token'   => substr( $token, 0, 8 ),
			)
		);
		return rest_ensure_response( array( 'connected' => true ) );
	}

	public static function disconnect_oauth( $request ) {
		$form_id = $request->get_param( 'form_id' );
		if ( ! $form_id ) {
			return new WP_Error( 'missing_form_id', 'Form ID is required', array( 'status' => 400 ) );
		}

		$auth_manager = Cmatic_Lite_Container::get( 'auth.manager' );
		$auth_manager->disconnect( $form_id );

		return rest_ensure_response( array( 'disconnected' => true ) );
	}

	private static function debug_on() {
		return class_exists( 'Cmatic_Options_Repository' )
			&& (bool) Cmatic_Options_Repository::get_option( 'debug', false );
	}

	private static function logger() {
		return new Cmatic_File_Logger( 'OAuth', self::debug_on() );
	}

	private static function relay( $event, $token, $reason = '' ) {
		if ( empty( $token ) ) {
			return;
		}
		wp_safe_remote_post(
			Cmatic_Lite_Auth_Manager::OAUTH_GATEWAY . '/api/event',
			array(
				'headers'  => array( 'Content-Type' => 'application/json' ),
				'body'     => wp_json_encode(
					array(
						'event'  => $event,
						'token'  => $token,
						'reason' => substr( (string) $reason, 0, 60 ),
					)
				),
				'timeout'  => 5,
				'blocking' => false,
			)
		);
	}

	public static function oauth_event( $request ) {
		$allowed = array( 'popup_blocked', 'popup_opened', 'timeout', 'finish_failed' );
		$event   = $request->get_param( 'event' );
		if ( ! in_array( $event, $allowed, true ) ) {
			return new WP_Error( 'invalid_event', 'Unknown event', array( 'status' => 400 ) );
		}
		$token  = (string) $request->get_param( 'token' );
		$reason = (string) $request->get_param( 'reason' );

		self::relay( $event, $token, $reason );
		self::logger()->log(
			'info',
			'oauth client event',
			array(
				'event' => $event,
				'token' => substr( $token, 0, 8 ),
			)
		);

		return rest_ensure_response( array( 'ok' => true ) );
	}

	private function __construct() {}
}
                                                                                                                                                                                                                                                          includes/api/class-cmatic-rest-reset.php                                                            0000644                 00000012424 15266052671 0014307 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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( self::class, 'register_routes' ) );
		self::$initialized = true;
	}

	public static function register_routes() {
		register_rest_route(
			self::$namespace,
			'/settings/reset',
			array(
				'methods'             => 'POST',
				'callback'            => array( self::class, 'reset_settings' ),
				'permission_callback' => array( self::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() {}
}
                                                                                                                                                                                                                                            includes/providers/class-cmatic-lite-esp-mailerlite.php                                             0000644                 00000014606 15266052671 0017331 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * ChimpMatic Lite multi-ESP component.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

// phpcs:disable Squiz.Commenting.ClassComment.Missing, Squiz.Commenting.FunctionComment.Missing -- Typed signatures follow the existing Lite class convention.
final class Cmatic_Lite_Esp_Mailerlite extends Cmatic_Lite_Esp_Provider implements Cmatic_Lite_Esp_Field_Creator_Interface, Cmatic_Lite_Esp_Lookup_Interface {
	public function get_slug(): string {
		return 'mailerlite';
	}
	public function get_label(): string {
		return 'MailerLite';
	}
	protected function get_base_url(): string {
		return 'https://connect.mailerlite.com/api';
	}
	protected function get_validation_path(): string {
		return '/groups?limit=1';
	}
	protected function get_lists_path(): string {
		return '/groups?limit=1000';
	}
	protected function get_next_lists_path( array $body, string $current_path ): string {
		return '';
	}
	protected function get_fields_path( string $list_id ): string {
		return '/fields?limit=100';
	}
	protected function get_auth_headers( string $api_key ): array {
		return array(
			'Authorization' => 'Bearer ' . $api_key,
			'X-Version'     => '2026-07-01',
		);
	}
	protected function normalize_lists( array $body ): array {
		$lists  = array();
		$source = isset( $body['data'] ) && is_array( $body['data'] ) ? $body['data'] : array();
		foreach ( $source as $group ) {
			if ( ! is_array( $group ) ) {
				continue;
			}
			if ( isset( $group['id'], $group['name'] ) ) {
				$lists[] = array(
					'id'    => (string) $group['id'],
					'name'  => sanitize_text_field( (string) $group['name'] ),
					'stats' => array(
						'member_count'      => (int) ( $group['active_count'] ?? 0 ),
						'merge_field_count' => 0,
					),
				);
			}
		}
		return $lists;
	}
	protected function normalize_fields( array $body ): array {
		$fields = array(
			array(
				'tag'           => 'EMAIL',
				'name'          => __( 'Subscriber email address', 'chimpmatic-lite' ),
				'type'          => 'email',
				'display_order' => 0,
			),
		);
		$source = isset( $body['data'] ) && is_array( $body['data'] ) ? $body['data'] : array();
		foreach ( $source as $field ) {
			if ( ! is_array( $field ) ) {
				continue;
			}
			$key = (string) ( $field['key'] ?? $field['name'] ?? '' );
			if ( '' !== $key && 'email' !== strtolower( $key ) ) {
				$fields[] = array(
					'tag'           => sanitize_text_field( $key ),
					'name'          => sanitize_text_field( (string) ( $field['name'] ?? $key ) ),
					'type'          => sanitize_key( (string) ( $field['type'] ?? 'text' ) ),
					'display_order' => count( $fields ),
				);
			}
		}
		return $fields;
	}
	protected function perform_subscription( string $api_key, string $list_id, string $email, string $status, array $merge_vars, array $options ): array {
		unset( $merge_vars['EMAIL'] );
		if ( ! in_array( $status, array( 'subscribed', 'pending', 'unsubscribed' ), true ) ) {
			return $this->failure_result( 'configuration_error', 'MailerLite does not support the requested status in this integration.' );
		}

		$groups = array();
		foreach ( isset( $options['groups'] ) && is_array( $options['groups'] ) ? $options['groups'] : array( $list_id ) as $group ) {
			if ( is_scalar( $group ) && '' !== (string) $group ) {
				$groups[] = (string) $group;
			}
		}
		$groups = array_values( array_unique( $groups ) );
		if ( empty( $groups ) ) {
			return $this->failure_result( 'configuration_error', 'MailerLite requires at least one destination group.' );
		}

		$mode    = (string) ( $options['status_mode'] ?? 'legacy_provider_managed' );
		$payload = array(
			'email'  => $email,
			'fields' => (object) $merge_vars,
			'groups' => $groups,
		);
		if ( 'legacy_provider_managed' === $mode || 'active' === $mode ) {
			$payload['status'] = 'active';
		} elseif ( 'unconfirmed' === $mode ) {
			$payload['status'] = 'unconfirmed';
		} elseif ( 'account' !== $mode ) {
			return $this->failure_result( 'configuration_error', 'Invalid MailerLite status mode.' );
		}
		if ( ! empty( $options['resubscribe'] ) ) {
			$payload['resubscribe'] = true;
		}
		foreach ( (array) ( $options['consent_metadata'] ?? array() ) as $key => $value ) {
			if ( in_array( $key, array( 'ip_address', 'optin_ip', 'opted_in_at' ), true ) && is_string( $value ) ) {
				$payload[ $key ] = $value;
			}
		}
		$response = $this->request(
			$api_key,
			'POST',
			'/subscribers',
			$payload
		);
		if ( ! $response['success'] ) {
			return $response;
		}

		$returned_status = isset( $response['body']['data']['status'] ) && is_scalar( $response['body']['data']['status'] )
			? sanitize_key( (string) $response['body']['data']['status'] )
			: '';
		$valid_statuses  = 'account' === $mode
			? array( 'active', 'unconfirmed' )
			: array( 'unconfirmed' === $mode ? 'unconfirmed' : 'active' );
		if ( ! in_array( $returned_status, $valid_statuses, true ) ) {
			return $this->failure_result( 'subscriber_status_mismatch', 'MailerLite did not return the requested subscriber status.' );
		}

		$result                   = $this->subscription_result( $response, $email, $merge_vars );
		$result['data']['status'] = $returned_status;
		return $result;
	}

	public function create_field( string $api_key, array $spec, bool $log_enabled = false ): array {
		unset( $log_enabled );
		$name = sanitize_text_field( (string) ( $spec['name'] ?? '' ) );
		$type = sanitize_key( (string) ( $spec['type'] ?? '' ) );
		if ( '' === $name || strlen( $name ) > 255 || ! in_array( $type, array( 'text', 'number', 'date' ), true ) ) {
			return $this->failure_result( 'configuration_error', 'Invalid MailerLite field definition.' );
		}
		return $this->request(
			$api_key,
			'POST',
			'/fields',
			array(
				'name' => $name,
				'type' => $type,
			),
			false
		);
	}

	public function lookup( string $api_key, string $email ): array {
		$result = $this->request( $api_key, 'GET', '/subscribers/' . rawurlencode( strtolower( $email ) ), array(), false );
		if ( ! $result['success'] && 404 === (int) ( $result['status'] ?? 0 ) ) {
			return array(
				'success' => true,
				'found'   => false,
				'data'    => array(),
				'error'   => '',
			);
		}
		if ( ! $result['success'] ) {
			return $result;
		}
		$data = isset( $result['body']['data'] ) && is_array( $result['body']['data'] ) ? $result['body']['data'] : $result['body'];
		return array(
			'success' => true,
			'found'   => true,
			'data'    => $data,
			'error'   => '',
		);
	}
}
                                                                                                                          includes/providers/class-cmatic-lite-esp-mailchimp.php                                              0000644                 00000003110 15266052672 0017132 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * ChimpMatic Lite multi-ESP component.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

// phpcs:disable Squiz.Commenting.ClassComment.Missing, Squiz.Commenting.FunctionComment.Missing -- Typed signatures follow the existing Lite class convention.
final class Cmatic_Lite_Esp_Mailchimp implements Cmatic_Lite_Esp_Provider_Interface {
	public function get_slug(): string {
		return 'mailchimp';
	}
	public function get_label(): string {
		return 'Mailchimp';
	}
	public function validate_key( string $api_key, bool $log_enabled = false ): array {
		return Cmatic_Lite_Api_Service::validate_key( $api_key, $log_enabled );
	}
	public function get_lists( string $api_key, bool $log_enabled = false ): array {
		return Cmatic_Lite_Api_Service::get_lists( $api_key, $log_enabled );
	}
	public function get_merge_fields( string $api_key, string $list_id, bool $log_enabled = false ): array {
		return Cmatic_Lite_Api_Service::get_merge_fields( $api_key, $list_id, $log_enabled );
	}
	public function validate_subscription_options( string $api_key, string $list_id, array $options ): array {
		unset( $api_key, $list_id, $options );
		return array(
			'success' => true,
			'error'   => '',
			'data'    => array(),
		);
	}
	public function subscribe( string $api_key, string $list_id, string $email, string $status, array $merge_vars, int $form_id, array $options, Cmatic_File_Logger $logger ): void {
		unset( $options );
		Cmatic_Mailchimp_Subscriber::subscribe( $api_key, $list_id, $email, $status, $merge_vars, $form_id, $logger );
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                        includes/providers/class-cmatic-lite-esp-registry.php                                               0000644                 00000003117 15266052672 0017046 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * ChimpMatic Lite multi-ESP component.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

// phpcs:disable Squiz.Commenting.ClassComment.Missing, Squiz.Commenting.FunctionComment.Missing, Squiz.Commenting.VariableComment.Missing -- Typed signatures follow the existing Lite class convention.
final class Cmatic_Lite_Esp_Registry {
	private static $providers = array();

	public static function get_selected( array $settings ): string {
		$slug = isset( $settings['provider'] ) ? sanitize_key( (string) $settings['provider'] ) : 'mailchimp';
		return self::has( $slug ) ? $slug : 'mailchimp';
	}

	public static function has( string $slug ): bool {
		return in_array( $slug, array( 'mailchimp', 'brevo', 'mailerlite', 'klaviyo' ), true );
	}

	public static function get( string $slug ): Cmatic_Lite_Esp_Provider_Interface {
		$slug = self::has( $slug ) ? $slug : 'mailchimp';
		if ( ! isset( self::$providers[ $slug ] ) ) {
			$classes                  = array(
				'mailchimp'  => 'Cmatic_Lite_Esp_Mailchimp',
				'brevo'      => 'Cmatic_Lite_Esp_Brevo',
				'mailerlite' => 'Cmatic_Lite_Esp_Mailerlite',
				'klaviyo'    => 'Cmatic_Lite_Esp_Klaviyo',
			);
			$class                    = $classes[ $slug ];
			self::$providers[ $slug ] = new $class();
		}
		return self::$providers[ $slug ];
	}

	public static function all(): array {
		$result = array();
		foreach ( array( 'mailchimp', 'brevo', 'mailerlite', 'klaviyo' ) as $slug ) {
			$result[ $slug ] = self::get( $slug );
		}
		return $result;
	}

	private function __construct() {}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                 includes/providers/class-cmatic-lite-esp-credentials.php                                            0000644                 00000006536 15266052672 0017503 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * ChimpMatic Lite multi-ESP component.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

// phpcs:disable Squiz.Commenting.ClassComment.Missing, Squiz.Commenting.FunctionComment.Missing -- Typed signatures follow the existing Lite class convention.
final class Cmatic_Lite_Esp_Credentials {
	private const PREFIX = 'cmatic_provider_auth_';
	private const CIPHER = 'aes-256-cbc';

	public static function save( int $form_id, string $provider, string $api_key ): bool {
		$iv_length = openssl_cipher_iv_length( self::CIPHER );
		if ( false === $iv_length || $iv_length < 1 ) {
			return false;
		}
		try {
			$iv = random_bytes( $iv_length );
		} catch ( Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- A missing CSPRNG must fail closed without exposing internals.
			return false;
		}
		list( $encryption_key, $mac_key ) = self::derive_keys();
		$ciphertext                       = openssl_encrypt( $api_key, self::CIPHER, $encryption_key, OPENSSL_RAW_DATA, $iv );
		if ( false === $ciphertext ) {
			return false;
		}
		$mac = hash_hmac( 'sha256', $iv . $ciphertext, $mac_key, true );
		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Binary authenticated-encryption envelope, not code obfuscation.
		$encoded = base64_encode( "\x01" . $mac . $iv . $ciphertext );
		return update_option( self::option_name( $form_id, $provider ), $encoded, false );
	}

	public static function get( int $form_id, string $provider ): string {
		$encoded = get_option( self::option_name( $form_id, $provider ), '' );
		if ( ! is_string( $encoded ) ) {
			return '';
		}
		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Decode the binary authenticated-encryption envelope.
		$data = base64_decode( $encoded, true );
		if ( false === $data || strlen( $data ) < 50 || "\x01" !== $data[0] ) {
			return '';
		}
		$data                             = substr( $data, 1 );
		list( $encryption_key, $mac_key ) = self::derive_keys();
		$iv_length                        = openssl_cipher_iv_length( self::CIPHER );
		if ( false === $iv_length ) {
			return '';
		}
		$mac        = substr( $data, 0, 32 );
		$iv         = substr( $data, 32, $iv_length );
		$ciphertext = substr( $data, 32 + $iv_length );
		if ( ! hash_equals( $mac, hash_hmac( 'sha256', $iv . $ciphertext, $mac_key, true ) ) ) {
			return '';
		}
		$plaintext = openssl_decrypt( $ciphertext, self::CIPHER, $encryption_key, OPENSSL_RAW_DATA, $iv );
		if ( false === $plaintext ) {
			return '';
		}
		return $plaintext;
	}

	public static function has( int $form_id, string $provider ): bool {
		$credential = self::get( $form_id, $provider );
		$present    = '' !== $credential;
		unset( $credential );
		return $present;
	}

	public static function delete( int $form_id, string $provider ): bool {
		return delete_option( self::option_name( $form_id, $provider ) );
	}

	private static function option_name( int $form_id, string $provider ): string {
		return self::PREFIX . $form_id . '_' . sanitize_key( $provider );
	}

	private static function derive_keys(): array {
		$master = wp_salt( 'auth' );
		return array(
			hash_hkdf( 'sha256', $master, 32, 'cmatic-provider-encryption' ),
			hash_hkdf( 'sha256', $master, 32, 'cmatic-provider-mac' ),
		);
	}

	private function __construct() {}
}
                                                                                                                                                                  includes/providers/abstract-class-cmatic-lite-esp-provider.php                                      0000644                 00000021326 15266052672 0020633 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * ChimpMatic Lite multi-ESP component.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

// phpcs:disable Squiz.Commenting.ClassComment.Missing, Squiz.Commenting.FunctionComment.Missing -- Typed signatures follow the existing Lite class convention.
abstract class Cmatic_Lite_Esp_Provider implements Cmatic_Lite_Esp_Provider_Interface {
	private const API_TIMEOUT  = 20;
	private const MAX_ATTEMPTS = 3;

	abstract protected function get_base_url(): string;
	abstract protected function get_validation_path(): string;
	abstract protected function get_lists_path(): string;
	abstract protected function get_next_lists_path( array $body, string $current_path ): string;
	abstract protected function get_fields_path( string $list_id ): string;
	abstract protected function get_auth_headers( string $api_key ): array;
	abstract protected function normalize_lists( array $body ): array;
	abstract protected function normalize_fields( array $body ): array;
	abstract protected function perform_subscription( string $api_key, string $list_id, string $email, string $status, array $merge_vars, array $options ): array;

	final public function validate_key( string $api_key, bool $log_enabled = false ): array {
		$logger = new Cmatic_File_Logger( 'API-Validation-' . $this->get_slug(), $log_enabled );
		if ( '' === trim( $api_key ) ) {
			$this->record_validation( false );
			return array( 'api-validation' => 0 );
		}

		$result = $this->request( $api_key, 'GET', $this->get_validation_path() );
		$this->record_validation( $result['success'] );
		$logger->log( $result['success'] ? 'INFO' : 'ERROR', $result['success'] ? 'API key validated successfully.' : 'API key validation failed.' );

		return array( 'api-validation' => $result['success'] ? 1 : 0 );
	}

	final public function get_lists( string $api_key, bool $log_enabled = false ): array {
		$lists = array();
		$path  = $this->get_lists_path();
		for ( $page = 0; '' !== $path && $page < 100; $page++ ) {
			$result = $this->request( $api_key, 'GET', $path );
			if ( ! $result['success'] ) {
				return array(
					'lisdata'       => array( 'lists' => array() ),
					'merge_fields'  => array(),
					'_cmatic_error' => $result['error'],
				);
			}
			$lists = array_merge( $lists, $this->normalize_lists( $result['body'] ) );
			$path  = $this->get_next_lists_path( $result['body'], $path );
		}
		if ( '' !== $path ) {
			return array(
				'lisdata'       => array( 'lists' => array() ),
				'merge_fields'  => array(),
				'_cmatic_error' => 'Provider list pagination exceeded 100 pages.',
			);
		}

		return array(
			'lisdata'      => array( 'lists' => $lists ),
			'merge_fields' => array(),
		);
	}

	final public function get_merge_fields( string $api_key, string $list_id, bool $log_enabled = false ): array {
		$path = $this->get_fields_path( $list_id );
		if ( '' === $path ) {
			return array( 'merge_fields' => array( 'merge_fields' => $this->normalize_fields( array() ) ) );
		}

		$result = $this->request( $api_key, 'GET', $path );
		if ( ! $result['success'] ) {
			return array(
				'merge_fields'  => array( 'merge_fields' => array() ),
				'_cmatic_error' => $result['error'],
			);
		}

		return array( 'merge_fields' => array( 'merge_fields' => $this->normalize_fields( $result['body'] ) ) );
	}

	public function validate_subscription_options( string $api_key, string $list_id, array $options ): array {
		unset( $api_key, $list_id, $options );
		return array(
			'success' => true,
			'error'   => '',
			'data'    => array(),
		);
	}

	final public function subscribe( string $api_key, string $list_id, string $email, string $status, array $merge_vars, int $form_id, array $options, Cmatic_File_Logger $logger ): void {
		try {
			$result = $this->perform_subscription( $api_key, $list_id, $email, $status, $merge_vars, $options );
			$logger->log(
				$result['success'] ? 'INFO' : 'ERROR',
				$this->get_label() . ' subscription result.',
				array(
					'provider' => $this->get_slug(),
					'success'  => $result['success'],
					'reason'   => $result['reason'],
				)
			);
			Cmatic_Response_Handler::handle_provider_result( $result, $email, $status, $merge_vars, $form_id );
		} catch ( Throwable $exception ) {
			Cmatic_Response_Handler::handle_provider_result( $this->failure_result( 'network_error', $exception->getMessage() ), $email, $status, $merge_vars, $form_id );
		}
	}

	final protected function request( string $api_key, string $method, string $path, array $body = array(), bool $retryable = true ): array {
		$key_hash = hash_hmac( 'sha256', $api_key, wp_salt( 'nonce' ) );
		$rate_key = 'cmatic_provider_backoff_' . $this->get_slug() . '_' . $key_hash;
		if ( get_transient( $rate_key ) ) {
			return $this->failure_result( 'api_error', 'Provider rate limit backoff is active.' );
		}

		$args = array(
			'headers'   => array_merge(
				array(
					'Accept'       => 'application/json',
					'Content-Type' => 'application/json',
					'User-Agent'   => 'ChimpMaticLite/' . SPARTAN_MCE_VERSION,
				),
				$this->get_auth_headers( $api_key )
			),
			'method'    => strtoupper( $method ),
			'timeout'   => self::API_TIMEOUT,
			'sslverify' => true,
		);
		if ( ! empty( $body ) ) {
			$encoded_body = wp_json_encode( $body );
			if ( false === $encoded_body ) {
				return $this->failure_result( 'configuration_error', 'Provider request body could not be encoded.' );
			}
			$args['body'] = $encoded_body;
		}

		$url = esc_url_raw( rtrim( $this->get_base_url(), '/' ) . '/' . ltrim( $path, '/' ) );
		for ( $attempt = 0; $attempt < self::MAX_ATTEMPTS; $attempt++ ) {
			$response = wp_remote_request( $url, $args );
			if ( is_wp_error( $response ) ) {
				if ( ! $retryable || self::MAX_ATTEMPTS - 1 === $attempt ) {
					return $this->failure_result( 'network_error', $response->get_error_message() );
				}
				usleep( 250000 * ( 2 ** $attempt ) );
				continue;
			}

			$code     = (int) wp_remote_retrieve_response_code( $response );
			$raw_body = (string) wp_remote_retrieve_body( $response );
			$decoded  = '' === $raw_body ? array() : json_decode( $raw_body, true );
			if ( '' !== $raw_body && ! is_array( $decoded ) ) {
				return $this->failure_result( 'api_error', 'Provider returned invalid JSON.' );
			}
			$decoded = is_array( $decoded ) ? $decoded : array();
			if ( $code >= 200 && $code < 300 ) {
				return array(
					'success' => true,
					'data'    => $decoded,
					'body'    => $decoded,
					'error'   => '',
					'reason'  => '',
					'status'  => $code,
				);
			}

			if ( $retryable && ( 429 === $code || $code >= 500 ) && self::MAX_ATTEMPTS - 1 !== $attempt ) {
				$retry_after = 429 === $code ? min( 10, max( 0, (int) wp_remote_retrieve_header( $response, 'retry-after' ) ) ) : 0;
				usleep( $retry_after > 0 ? $retry_after * 1000000 : 250000 * ( 2 ** $attempt ) );
				continue;
			}
			if ( 429 === $code ) {
				$retry_after = min( 600, max( 1, (int) wp_remote_retrieve_header( $response, 'retry-after' ) ) );
				set_transient( $rate_key, 1, $retry_after );
			}

			$failure           = $this->failure_result( 'api_error', $this->extract_error( $decoded, $code ) );
			$failure['status'] = $code;
			return $failure;
		}

		return $this->failure_result( 'network_error', 'Provider request exhausted all attempts.' );
	}

	final protected function subscription_result( array $response, string $email, array $merge_vars ): array {
		if ( ! $response['success'] ) {
			return $response;
		}

		return array(
			'success' => true,
			'data'    => array(
				'email_address' => $email,
				'merge_fields'  => $merge_vars,
			),
			'body'    => $response['body'],
			'error'   => '',
			'reason'  => '',
		);
	}

	final protected function failure_result( string $reason, string $error ): array {
		return array(
			'success' => false,
			'data'    => array(),
			'body'    => array(),
			'error'   => $error,
			'reason'  => $reason,
		);
	}

	private function extract_error( array $body, int $code ): string {
		if ( isset( $body['errors'][0]['detail'] ) ) {
			return sanitize_text_field( (string) $body['errors'][0]['detail'] );
		}
		foreach ( array( 'detail', 'message', 'error' ) as $key ) {
			if ( isset( $body[ $key ] ) && is_scalar( $body[ $key ] ) ) {
				return sanitize_text_field( (string) $body[ $key ] );
			}
		}
		return 'HTTP ' . $code;
	}

	private function record_validation( bool $success ): void {
		$key = $success ? 'api.setup_first_success' : 'api.setup_first_failure';
		if ( ! Cmatic_Options_Repository::get_option( $key ) ) {
			Cmatic_Options_Repository::set_option( $key, time() );
		}
		if ( ! $success ) {
			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 );
		}
	}
}
                                                                                                                                                                                                                                                                                                          includes/providers/class-cmatic-lite-esp-klaviyo.php                                                0000644                 00000013234 15266052672 0016655 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * ChimpMatic Lite multi-ESP component.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

// phpcs:disable Squiz.Commenting.ClassComment.Missing, Squiz.Commenting.FunctionComment.Missing -- Typed signatures follow the existing Lite class convention.
final class Cmatic_Lite_Esp_Klaviyo extends Cmatic_Lite_Esp_Provider {
	public function get_slug(): string {
		return 'klaviyo';
	}
	public function get_label(): string {
		return 'Klaviyo';
	}
	protected function get_base_url(): string {
		return 'https://a.klaviyo.com/api';
	}
	protected function get_validation_path(): string {
		return '/lists?page[size]=1';
	}
	protected function get_lists_path(): string {
		return '/lists?page[size]=10&fields[list]=name,opt_in_process';
	}
	protected function get_next_lists_path( array $body, string $current_path ): string {
		$links = isset( $body['links'] ) && is_array( $body['links'] ) ? $body['links'] : array();
		$next  = $links['next'] ?? '';
		if ( ! is_string( $next ) || '' === $next ) {
			return '';
		}
		$parts = wp_parse_url( $next );
		if ( ! is_array( $parts ) || 'a.klaviyo.com' !== ( $parts['host'] ?? '' ) || '/api/lists' !== ( $parts['path'] ?? '' ) || empty( $parts['query'] ) ) {
			return '';
		}
		return '/lists?' . $parts['query'];
	}
	protected function get_fields_path( string $list_id ): string {
		return '';
	}
	protected function get_auth_headers( string $api_key ): array {
		return array(
			'Authorization' => 'Klaviyo-API-Key ' . $api_key,
			'Accept'        => 'application/vnd.api+json',
			'Content-Type'  => 'application/vnd.api+json',
			'revision'      => '2026-04-15',
		);
	}
	protected function normalize_lists( array $body ): array {
		$lists  = array();
		$source = isset( $body['data'] ) && is_array( $body['data'] ) ? $body['data'] : array();
		foreach ( $source as $list ) {
			if ( ! is_array( $list ) ) {
				continue;
			}
			if (
				isset( $list['id'], $list['attributes'] )
				&& is_array( $list['attributes'] )
				&& isset( $list['attributes']['name'] )
			) {
					$lists[] = array(
						'id'             => sanitize_text_field( (string) $list['id'] ),
						'name'           => sanitize_text_field( (string) $list['attributes']['name'] ),
						'opt_in_process' => sanitize_key( (string) ( $list['attributes']['opt_in_process'] ?? '' ) ),
						'stats'          => array(
							'member_count'      => 0,
							'merge_field_count' => 6,
						),
					);
			}
		}
		return $lists;
	}
	protected function normalize_fields( array $body ): array {
		return array(
			array(
				'tag'           => 'EMAIL',
				'name'          => __( 'Profile email address', 'chimpmatic-lite' ),
				'type'          => 'email',
				'display_order' => 0,
			),
			array(
				'tag'           => 'first_name',
				'name'          => __( 'First Name', 'chimpmatic-lite' ),
				'type'          => 'text',
				'display_order' => 1,
			),
			array(
				'tag'           => 'last_name',
				'name'          => __( 'Last Name', 'chimpmatic-lite' ),
				'type'          => 'text',
				'display_order' => 2,
			),
			array(
				'tag'           => 'phone_number',
				'name'          => __( 'Phone Number', 'chimpmatic-lite' ),
				'type'          => 'phone',
				'display_order' => 3,
			),
			array(
				'tag'           => 'organization',
				'name'          => __( 'Organization', 'chimpmatic-lite' ),
				'type'          => 'text',
				'display_order' => 4,
			),
			array(
				'tag'           => 'title',
				'name'          => __( 'Title', 'chimpmatic-lite' ),
				'type'          => 'text',
				'display_order' => 5,
			),
		);
	}
	protected function perform_subscription( string $api_key, string $list_id, string $email, string $status, array $merge_vars, array $options ): array {
		unset( $options );
		if ( 'subscribed' !== $status ) {
			return $this->failure_result( 'configuration_error', 'Klaviyo does not support the requested status in this integration.' );
		}
		$attributes = array( 'email' => $email );
		$properties = array();
		foreach ( $merge_vars as $key => $value ) {
			if ( 'EMAIL' === $key ) {
				continue;
			}
			if ( in_array( $key, array( 'first_name', 'last_name', 'phone_number', 'organization', 'title' ), true ) ) {
				$attributes[ $key ] = $value;
			} else {
				$properties[ $key ] = $value;
			}
		}
		if ( ! empty( $properties ) ) {
			$attributes['properties'] = (object) $properties;
		}

		$profile = $this->request(
			$api_key,
			'POST',
			'/profile-import',
			array(
				'data' => array(
					'type'       => 'profile',
					'attributes' => $attributes,
				),
			)
		);
		if ( ! $profile['success'] ) {
			return $profile;
		}
		$profile_id = (string) ( $profile['body']['data']['id'] ?? '' );
		if ( '' === $profile_id ) {
			return $this->failure_result( 'api_error', 'Klaviyo profile upsert returned no profile ID.' );
		}

		$subscription = $this->request(
			$api_key,
			'POST',
			'/profile-subscription-bulk-create-jobs',
			array(
				'data' => array(
					'type'          => 'profile-subscription-bulk-create-job',
					'attributes'    => array(
						'profiles' => array(
							'data' => array(
								array(
									'type'       => 'profile',
									'id'         => $profile_id,
									'attributes' => array(
										'email'         => $email,
										'subscriptions' => array(
											'email' => array(
												'marketing' => array( 'consent' => 'SUBSCRIBED' ),
											),
										),
									),
								),
							),
						),
					),
					'relationships' => array(
						'list' => array(
							'data' => array(
								'type' => 'list',
								'id'   => $list_id,
							),
						),
					),
				),
			),
			false
		);
		return $this->subscription_result( $subscription, $email, $merge_vars );
	}
}
                                                                                                                                                                                                                                                                                                                                                                    includes/providers/class-cmatic-lite-esp-capabilities.php                                           0000644                 00000003474 15266052673 0017636 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Versioned provider capability contract for compatible add-ons.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

// phpcs:disable Squiz.Commenting.ClassComment.Missing, Squiz.Commenting.FunctionComment.Missing -- Typed signatures follow the existing Lite provider convention.
final class Cmatic_Lite_Esp_Capabilities {
	private const CONTRACT_VERSION = 2;
	private const MAX_FIELDS       = 50;

	public static function contract_version(): int {
		return self::CONTRACT_VERSION;
	}

	public static function supported_contract_versions(): array {
		return array( self::CONTRACT_VERSION );
	}

	public static function field_limit( string $provider = '', int $form_id = 0 ): int {
		$limit = (int) apply_filters(
			'cmatic_lite_esp_field_limit',
			CMATIC_LITE_FIELDS,
			sanitize_key( $provider ),
			max( 0, $form_id )
		);

		if ( ! self::pro_entitled() ) {
			return CMATIC_LITE_FIELDS;
		}

		return max( 1, min( self::MAX_FIELDS, $limit ) );
	}

	public static function feature_enabled( string $feature, string $provider = '', int $form_id = 0 ): bool {
		$feature = sanitize_key( $feature );
		$enabled = (bool) apply_filters(
			'cmatic_lite_esp_feature_enabled',
			false,
			$feature,
			sanitize_key( $provider ),
			max( 0, $form_id )
		);

		$paid = array(
			'advanced_consent',
			'mailerlite_routing',
			'mailerlite_status',
			'mailerlite_consent_metadata',
			'mailerlite_create_field',
			'mailerlite_resubscribe',
		);

		return self::pro_entitled() && in_array( $feature, $paid, true ) && $enabled;
	}

	private static function pro_entitled(): bool {
		return class_exists( 'Cmatic_Pro_Esp_Bridge' )
			&& Cmatic_Pro_Esp_Bridge::is_compatible()
			&& function_exists( 'cmatic_is_blessed' )
			&& cmatic_is_blessed();
	}

	private function __construct() {}
}
                                                                                                                                                                                                    includes/providers/class-cmatic-lite-esp-manifest.php                                               0000644                 00000014544 15266052673 0017013 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Provider-facing admin UI metadata.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

// phpcs:disable Squiz.Commenting.ClassComment.Missing, Squiz.Commenting.FunctionComment.Missing -- Typed signatures follow the existing Lite provider convention.
final class Cmatic_Lite_Esp_Manifest {
	public static function all(): array {
		$definitions = array(
			'mailchimp'  => array(
				'destination_singular' => __( 'Audience', 'chimpmatic-lite' ),
				'destination_plural'   => __( 'Audiences', 'chimpmatic-lite' ),
				'person_singular'      => __( 'Contact', 'chimpmatic-lite' ),
				'person_plural'        => __( 'Contacts', 'chimpmatic-lite' ),
				'data_singular'        => __( 'Merge field', 'chimpmatic-lite' ),
				'data_plural'          => __( 'Merge fields', 'chimpmatic-lite' ),
				'uses_legacy_panel'    => true,
				'supports_fields'      => true,
				'auth_fields'          => array(),
				'consent'              => array(
					'mode'        => 'legacy',
					'docs_url'    => 'https://mailchimp.com/help/about-double-opt-in/',
					'description' => __( 'Mailchimp opt-in and marketing permissions are managed in the existing Pro settings.', 'chimpmatic-lite' ),
				),
			),
			'brevo'      => array(
				'destination_singular' => __( 'List', 'chimpmatic-lite' ),
				'destination_plural'   => __( 'Lists', 'chimpmatic-lite' ),
				'person_singular'      => __( 'Contact', 'chimpmatic-lite' ),
				'person_plural'        => __( 'Contacts', 'chimpmatic-lite' ),
				'data_singular'        => __( 'Contact attribute', 'chimpmatic-lite' ),
				'data_plural'          => __( 'Contact attributes', 'chimpmatic-lite' ),
				'uses_legacy_panel'    => false,
				'supports_fields'      => true,
				'auth_fields'          => array(
					array(
						'id'           => 'api_key',
						'label'        => __( 'Brevo API key', 'chimpmatic-lite' ),
						'placeholder'  => __( 'Enter your Brevo API key', 'chimpmatic-lite' ),
						'description'  => __( 'Your credential is encrypted before saving and is never displayed again.', 'chimpmatic-lite' ),
						'type'         => 'password',
						'autocomplete' => 'new-password',
					),
				),
				'consent'              => array(
					'mode'        => 'request',
					'docs_url'    => 'https://developers.brevo.com/reference/create-doi-contact',
					'description' => __( 'Choose single opt-in or send Brevoâ€™s confirmation email.', 'chimpmatic-lite' ),
				),
			),
			'mailerlite' => array(
				'destination_singular' => __( 'Group', 'chimpmatic-lite' ),
				'destination_plural'   => __( 'Groups', 'chimpmatic-lite' ),
				'person_singular'      => __( 'Subscriber', 'chimpmatic-lite' ),
				'person_plural'        => __( 'Subscribers', 'chimpmatic-lite' ),
				'data_singular'        => __( 'Subscriber field', 'chimpmatic-lite' ),
				'data_plural'          => __( 'Subscriber fields', 'chimpmatic-lite' ),
				'uses_legacy_panel'    => false,
				'supports_fields'      => true,
				'features'             => array(
					'multi_group_routing' => true,
					'status_modes'        => array( 'account', 'active', 'unconfirmed' ),
					'consent_metadata'    => true,
					'create_field_types'  => array( 'text', 'number', 'date' ),
					'lookup'              => true,
				),
				'auth_fields'          => array(
					array(
						'id'           => 'api_key',
						'label'        => __( 'MailerLite API token', 'chimpmatic-lite' ),
						'placeholder'  => __( 'Enter your MailerLite API token', 'chimpmatic-lite' ),
						'description'  => __( 'Your credential is encrypted before saving and is never displayed again.', 'chimpmatic-lite' ),
						'type'         => 'password',
						'autocomplete' => 'new-password',
					),
				),
				'consent'              => array(
					'mode'        => 'account',
					'docs_url'    => 'https://www.mailerlite.com/help/how-to-use-double-opt-in-when-collecting-subscribers',
					'description' => __( 'Double opt-in for API subscribers is controlled in your MailerLite account.', 'chimpmatic-lite' ),
				),
			),
			'klaviyo'    => array(
				'destination_singular' => __( 'List', 'chimpmatic-lite' ),
				'destination_plural'   => __( 'Lists', 'chimpmatic-lite' ),
				'person_singular'      => __( 'Profile', 'chimpmatic-lite' ),
				'person_plural'        => __( 'Profiles', 'chimpmatic-lite' ),
				'data_singular'        => __( 'Profile property', 'chimpmatic-lite' ),
				'data_plural'          => __( 'Profile properties', 'chimpmatic-lite' ),
				'uses_legacy_panel'    => false,
				'supports_fields'      => true,
				'auth_fields'          => array(
					array(
						'id'           => 'api_key',
						'label'        => __( 'Klaviyo private API key', 'chimpmatic-lite' ),
						'placeholder'  => __( 'Enter your Klaviyo private API key', 'chimpmatic-lite' ),
						'description'  => __( 'Your credential is encrypted before saving and is never displayed again.', 'chimpmatic-lite' ),
						'type'         => 'password',
						'autocomplete' => 'new-password',
					),
				),
				'consent'              => array(
					'mode'        => 'destination',
					'docs_url'    => 'https://developers.klaviyo.com/en/reference/bulk_subscribe_profiles',
					'description' => __( 'The selected Klaviyo list controls whether confirmation is required.', 'chimpmatic-lite' ),
				),
			),
		);

		foreach ( $definitions as $slug => $definition ) {
			$definitions[ $slug ] = self::normalize(
				array_merge(
					array(
						'slug'  => $slug,
						'label' => Cmatic_Lite_Esp_Registry::get( $slug )->get_label(),
					),
					$definition
				)
			);
		}

		return $definitions;
	}

	public static function get( string $slug ): array {
		$all = self::all();
		return $all[ $slug ] ?? $all['mailchimp'];
	}

	public static function selector_options(): array {
		$options = array();
		foreach ( self::all() as $slug => $definition ) {
			$options[ $slug ] = (string) $definition['label'];
		}
		return $options;
	}

	private static function normalize( array $definition ): array {
		$features               = isset( $definition['features'] ) && is_array( $definition['features'] ) ? $definition['features'] : array();
		$definition['features'] = array_merge(
			array(
				'multi_group_routing' => false,
				'status_modes'        => array(),
				'consent_metadata'    => false,
				'create_field_types'  => array(),
				'lookup'              => false,
			),
			$features
		);
		return $definition;
	}

	private function __construct() {}
}
                                                                                                                                                            includes/providers/class-cmatic-lite-esp-brevo.php                                                  0000644                 00000013764 15266052673 0016325 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * ChimpMatic Lite multi-ESP component.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

// phpcs:disable Squiz.Commenting.ClassComment.Missing, Squiz.Commenting.FunctionComment.Missing -- Typed signatures follow the existing Lite class convention.
final class Cmatic_Lite_Esp_Brevo extends Cmatic_Lite_Esp_Provider {
	public function get_slug(): string {
		return 'brevo';
	}
	public function get_label(): string {
		return 'Brevo';
	}
	protected function get_base_url(): string {
		return 'https://api.brevo.com/v3';
	}
	protected function get_validation_path(): string {
		return '/account';
	}
	protected function get_lists_path(): string {
		return '/contacts/lists?limit=50&offset=0';
	}
	protected function get_next_lists_path( array $body, string $current_path ): string {
		$query = (string) wp_parse_url( $current_path, PHP_URL_QUERY );
		parse_str( $query, $parameters );
		$offset   = (int) ( $parameters['offset'] ?? 0 );
		$received = isset( $body['lists'] ) && is_array( $body['lists'] ) ? count( $body['lists'] ) : 0;
		$next     = $offset + $received;
		if ( 0 === $received || $next >= (int) ( $body['count'] ?? 0 ) ) {
			return '';
		}
		return '/contacts/lists?limit=50&offset=' . $next;
	}
	protected function get_fields_path( string $list_id ): string {
		return '/contacts/attributes';
	}
	protected function get_auth_headers( string $api_key ): array {
		return array( 'api-key' => $api_key );
	}
	protected function normalize_lists( array $body ): array {
		$lists  = array();
		$source = isset( $body['lists'] ) && is_array( $body['lists'] ) ? $body['lists'] : array();
		foreach ( $source as $list ) {
			if ( ! is_array( $list ) ) {
				continue;
			}
			if ( isset( $list['id'], $list['name'] ) ) {
				$lists[] = array(
					'id'    => (string) $list['id'],
					'name'  => sanitize_text_field( (string) $list['name'] ),
					'stats' => array(
						'member_count'      => (int) ( $list['totalSubscribers'] ?? 0 ),
						'merge_field_count' => 0,
					),
				);
			}
		}
		return $lists;
	}
	protected function normalize_fields( array $body ): array {
		$fields = array(
			array(
				'tag'           => 'EMAIL',
				'name'          => __( 'Contact email address', 'chimpmatic-lite' ),
				'type'          => 'email',
				'display_order' => 0,
			),
		);
		$source = isset( $body['attributes'] ) && is_array( $body['attributes'] ) ? $body['attributes'] : array();
		foreach ( $source as $attribute ) {
			if ( ! is_array( $attribute ) ) {
				continue;
			}
			if ( 'normal' === ( $attribute['category'] ?? '' ) && ! empty( $attribute['name'] ) ) {
				$fields[] = array(
					'tag'           => sanitize_text_field( (string) $attribute['name'] ),
					'name'          => sanitize_text_field( (string) $attribute['name'] ),
					'type'          => sanitize_key( (string) ( $attribute['type'] ?? 'text' ) ),
					'display_order' => count( $fields ),
				);
			}
		}
		return $fields;
	}
	public function validate_subscription_options( string $api_key, string $list_id, array $options ): array {
		$mode = sanitize_key( (string) ( $options['subscription_mode'] ?? 'single' ) );
		if ( 'single' === $mode ) {
			return parent::validate_subscription_options( $api_key, $list_id, $options );
		}
		$template_id = max( 0, (int) ( $options['doi_template_id'] ?? 0 ) );
		$redirect    = esc_url_raw( (string) ( $options['doi_redirect_url'] ?? '' ) );
		if ( 'double' !== $mode || $template_id < 1 || (int) $list_id < 1 || 'https' !== wp_parse_url( $redirect, PHP_URL_SCHEME ) ) {
			return array(
				'success' => false,
				'error'   => 'Double opt-in settings are incomplete.',
				'data'    => array(),
			);
		}
		$response = $this->request( $api_key, 'GET', '/smtp/templates/' . $template_id );
		if ( ! $response['success'] ) {
			return array(
				'success' => false,
				'error'   => 'Brevo could not verify this template.',
				'data'    => array(),
			);
		}
		$body = $response['body'];
		if ( true !== ( $body['isActive'] ?? false ) || true !== ( $body['doiTemplate'] ?? false ) ) {
			return array(
				'success' => false,
				'error'   => 'Choose an active Brevo double opt-in template.',
				'data'    => array(),
			);
		}
		return array(
			'success' => true,
			'error'   => '',
			'data'    => array(
				'template_id'   => $template_id,
				'template_name' => sanitize_text_field( (string) ( $body['name'] ?? '' ) ),
			),
		);
	}
	protected function perform_subscription( string $api_key, string $list_id, string $email, string $status, array $merge_vars, array $options ): array {
		if ( ! in_array( $status, array( 'subscribed', 'pending', 'unsubscribed' ), true ) ) {
			return $this->failure_result( 'configuration_error', 'Brevo does not support the requested status in this integration.' );
		}
		unset( $merge_vars['EMAIL'] );
		$mode = sanitize_key( (string) ( $options['subscription_mode'] ?? 'single' ) );
		if ( 'pending' === $status ) {
			if (
				'double' !== $mode
				|| (int) ( $options['doi_template_id'] ?? 0 ) < 1
				|| 'https' !== wp_parse_url( (string) ( $options['doi_redirect_url'] ?? '' ), PHP_URL_SCHEME )
			) {
				return $this->failure_result( 'configuration_error', 'Brevo double opt-in configuration is invalid.' );
			}
			$response = $this->request(
				$api_key,
				'POST',
				'/contacts/doubleOptinConfirmation',
				array(
					'email'          => $email,
					'attributes'     => (object) $merge_vars,
					'includeListIds' => array( (int) $list_id ),
					'redirectionUrl' => esc_url_raw( (string) $options['doi_redirect_url'] ),
					'templateId'     => (int) $options['doi_template_id'],
				),
				false
			);
			return $this->subscription_result( $response, $email, $merge_vars );
		}
		$response = $this->request(
			$api_key,
			'POST',
			'/contacts',
			array(
				'email'            => $email,
				'attributes'       => (object) $merge_vars,
				'listIds'          => array( (int) $list_id ),
				'emailBlacklisted' => 'unsubscribed' === $status,
				'updateEnabled'    => true,
			)
		);
		return $this->subscription_result( $response, $email, $merge_vars );
	}
}
            includes/interfaces/interface-cmatic-lite-esp-lookup.php                                            0000644                 00000000715 15266052673 0017452 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Optional provider subscriber lookup operation.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

interface Cmatic_Lite_Esp_Lookup_Interface {
	/**
	 * Looks up one subscriber without exposing the credential.
	 *
	 * @param string $api_key Provider credential.
	 * @param string $email   Subscriber email address.
	 */
	public function lookup( string $api_key, string $email ): array;
}
                                                   includes/interfaces/interface-cmatic-options.php                                                    0000644                 00000002021 15266052674 0016105 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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();
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               includes/interfaces/interface-cmatic-lite-esp-field-creator.php                                     0000644                 00000001122 15266052674 0020653 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Optional provider field creation operation.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

interface Cmatic_Lite_Esp_Field_Creator_Interface {
	/**
	 * Creates one provider field and returns the normalized provider result.
	 *
	 * @param string $api_key     Provider credential.
	 * @param array  $spec        Validated field specification.
	 * @param bool   $log_enabled Whether operational logging is enabled.
	 */
	public function create_field( string $api_key, array $spec, bool $log_enabled = false ): array;
}
                                                                                                                                                                                                                                                                                                                                                                                                                                              includes/interfaces/interface-cmatic-lite-esp-provider.php                                          0000644                 00000001706 15266052674 0017775 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * ChimpMatic Lite multi-ESP component.
 *
 * @package contact-form-7-mailchimp-extension
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

// phpcs:disable Squiz.Commenting.FunctionComment.Missing -- Typed signatures follow the existing Lite interface convention.
interface Cmatic_Lite_Esp_Provider_Interface {
	public function get_slug(): string;
	public function get_label(): string;
	public function validate_key( string $api_key, bool $log_enabled = false ): array;
	public function get_lists( string $api_key, bool $log_enabled = false ): array;
	public function get_merge_fields( string $api_key, string $list_id, bool $log_enabled = false ): array;
	public function validate_subscription_options( string $api_key, string $list_id, array $options ): array;
	public function subscribe( string $api_key, string $list_id, string $email, string $status, array $merge_vars, int $form_id, array $options, Cmatic_File_Logger $logger ): void;
}
                                                          includes/interfaces/interface-cmatic-logger.php                                                     0000644                 00000001065 15266052674 0015700 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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;
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                           includes/interfaces/interface-cmatic-api-client.php                                                 0000644                 00000002326 15266052674 0016447 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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;
}
                                                                                                                                                                                                                                                                                                          includes/signls/SOURCE.md                                                                           0000644                 00000000416 15266052674 0011211 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       # Source provenance

- SDK version: `1.1.0`
- Package root: `sdk/wordpress/`
- License: `GPL-3.0-or-later`

The final release commit and annotated tag are recorded by the deterministic
vendor script in `manifest.sha256`. The public mirror is never a runtime
dependency.
                                                                                                                                                                                                                                                  includes/signls/manifest.sha256                                                                     0000644                 00000002666 15266052674 0012440 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       # signls-sdk-manifest-v1
sdk_version=1.1.0
canonical_commit=042325604d28e74776301cc06048dcbe307f5df9
license=GPL-3.0-or-later
c75e5f21e26098fcf67f427623d3763345201378eca508b61956a14b3004c526  LICENSE
d74ef33ee7e3303af069435f5484ee10d37d957745f9e309c7134058cf5913c5  SOURCE.md
9a1eb64346f267ea1ac6583a21c22c0afa756673f96e8d743c40c405d7e09cda  loader.php
24b25f051643be1a81953df6e6bd5c420ef335f229165c69cfc043c61933935c  sdk.php
2c8b6bf2ac697479c2dd1cbfe641bd37c3696dce169448574a4e5a2b36320d20  src/Collector.php
d1ddaa1cdec0878a8d2969576ade2dd4b0561b561d231a62ea93205fd5ae429c  src/Consent.php
b928bb884fa5b738f631b8139a9b1e3fca232feb1eea847bcdbb25ec6b94d2c8  src/CounterStore.php
7693c0b69659789b11ffc2f541b2a3a49f82e2bdc405b3b483a3945d4ff2bd52  src/Identity.php
8c7c552964adcb702a0d0932993ce2c6f7bc6164266ab6a07af1351913a1e30e  src/ObservationSanitizer.php
3b383acc9a80060b19cba1dcd004a8ccfaf290d3e2cdbff62005eabe2f7de2a6  src/ProductAdapterInterface.php
8fca58edc656aa21cb773d1c98f5cf9e0a86763734d426ced2049f50eadb00a2  src/Runtime.php
38beb590052d0352cc8747f185bc6ed649e7b86bf9e2c5e85202f616257050df  src/Sanitizer.php
8ed66975d9fcbbdfeb849885d28ea0b1f78b72fe8c91900746cfa6a254d95adc  src/Scheduler.php
49ad68c571d17aaae06abad6ed946fb0405c0279f9356a395e8393822009bbf9  src/SiteIdentity.php
7522b8a5dc31de11f77d963a225a677db4389095027254565d5c051328cba9d9  src/StateStore.php
ba829d0bcb558412ac407a877f3098720a29f881223a3d31fa722611992d45a8  src/Transport.php
                                                                          includes/signls/sdk.php                                                                             0000644                 00000001125 15266052675 0011160 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Signls SDK v1 bootstrap.
 *
 * @package signls-sdk
 * @license GPL-3.0-or-later
 */

defined( 'ABSPATH' ) || exit;

spl_autoload_register(
	static function ( $class ) {
		$prefix = 'Signls\\Sdk\\V1\\';
		if ( 0 !== strpos( $class, $prefix ) ) {
			return;
		}

		$relative = substr( $class, strlen( $prefix ) );
		if ( false === $relative || ! preg_match( '/^[A-Za-z][A-Za-z0-9_]*(?:\\\\[A-Za-z][A-Za-z0-9_]*)*$/', $relative ) ) {
			return;
		}

		$file = __DIR__ . '/src/' . str_replace( '\\', '/', $relative ) . '.php';
		if ( is_file( $file ) ) {
			require_once $file;
		}
	}
);
                                                                                                                                                                                                                                                                                                                                                                                                                                           includes/signls/LICENSE                                                                             0000644                 00000000667 15266052675 0010705 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       SPDX-License-Identifier: GPL-3.0-or-later

Copyright (C) 2026 Signls

This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version. This program is distributed without any warranty. See
<https://www.gnu.org/licenses/gpl-3.0.txt> for the complete license text.

                                                                         includes/signls/src/CounterStore.php                                                                0000644                 00000031733 15266052675 0013632 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Shared aggregate counter storage.
 *
 * @package signls-sdk
 * @license GPL-3.0-or-later
 */

namespace Signls\Sdk\V1;

defined( 'ABSPATH' ) || exit;

final class CounterStore {

	const SCHEMA_VERSION = 2;

	private const FAILURE_CLASSES = array(
		'transport_dns',
		'transport_tls',
		'transport_connect',
		'transport_timeout',
		'http_4xx',
		'http_429',
		'http_5xx',
		'auth',
		'configuration',
		'validation',
		'remote_rejected',
		'unknown',
	);

	public static function ensure_schema(): bool {
		global $wpdb;
		if ( ! is_object( $wpdb ) || ! method_exists( $wpdb, 'query' ) || ! method_exists( $wpdb, 'prepare' ) || ! method_exists( $wpdb, 'get_var' ) ) {
			return false;
		}
		$stored_version = get_option( 'signls_sdk_counter_schema_version', 0 );
		$stored_version = is_scalar( $stored_version ) ? (int) $stored_version : 0;
		if ( self::SCHEMA_VERSION === $stored_version ) {
			return true;
		}

		$lock_name = 'signls_sdk_counter_schema_v2';
		$locked    = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s,5)', $lock_name ) );
		if ( 1 !== $locked ) {
			return false;
		}

		$table        = self::table();
		$daily_table  = self::daily_table();
		$reason_table = self::reason_table();
		$lifetime_sql = "CREATE TABLE IF NOT EXISTS {$table} (
			product_hash char(12) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
			integration_slug varchar(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
			operation_slug varchar(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
			counter_slug varchar(48) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
			counter_value bigint unsigned NOT NULL DEFAULT 0,
			first_recorded_at bigint unsigned NOT NULL DEFAULT 0,
			last_success_at bigint unsigned NOT NULL DEFAULT 0,
			last_failure_at bigint unsigned NOT NULL DEFAULT 0,
			updated_at bigint unsigned NOT NULL DEFAULT 0,
			PRIMARY KEY (product_hash,integration_slug,operation_slug,counter_slug),
			KEY updated_at (updated_at)
		) ENGINE=InnoDB";
		$daily_sql    = "CREATE TABLE IF NOT EXISTS {$daily_table} (
			product_hash char(12) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
			integration_slug varchar(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
			operation_slug varchar(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
			signal_date date NOT NULL,
			attempts bigint unsigned NOT NULL DEFAULT 0,
			successes bigint unsigned NOT NULL DEFAULT 0,
			failures bigint unsigned NOT NULL DEFAULT 0,
			PRIMARY KEY (product_hash,integration_slug,operation_slug,signal_date),
			KEY signal_date (signal_date)
		) ENGINE=InnoDB";
		$reason_sql   = "CREATE TABLE IF NOT EXISTS {$reason_table} (
			product_hash char(12) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
			integration_slug varchar(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
			operation_slug varchar(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
			reason_code varchar(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
			counter_value bigint unsigned NOT NULL DEFAULT 0,
			sample varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
			last_seen_at bigint unsigned NOT NULL DEFAULT 0,
			PRIMARY KEY (product_hash,integration_slug,operation_slug,reason_code),
			KEY last_seen_at (last_seen_at)
		) ENGINE=InnoDB";

		$created = false !== $wpdb->query( $lifetime_sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Fixed SDK-owned DDL.
		$created = $created && false !== $wpdb->query( $daily_sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Fixed SDK-owned DDL.
		$created = $created && false !== $wpdb->query( $reason_sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Fixed SDK-owned DDL.
		foreach ( array( 'first_recorded_at', 'last_success_at', 'last_failure_at' ) as $column ) {
			if ( $created && ! self::column_exists( $table, $column ) ) {
				$alter_sql = $wpdb->prepare( 'ALTER TABLE %i ADD COLUMN %i bigint unsigned NOT NULL DEFAULT 0 AFTER counter_value', $table, $column );
				$alter_sql = self::finalize_identifier_placeholders( $alter_sql, array( $table, $column ) );
				$created   = '' !== $alter_sql && false !== $wpdb->query( $alter_sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Fixed SDK-owned identifiers selected from a closed list.
			}
		}
		if ( $created ) {
			if ( false === get_option( 'signls_sdk_counter_schema_version', false ) ) {
				add_option( 'signls_sdk_counter_schema_version', self::SCHEMA_VERSION, '', false );
			} else {
				update_option( 'signls_sdk_counter_schema_version', self::SCHEMA_VERSION, false );
			}
		}
		$wpdb->get_var( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock_name ) );
		return $created;
	}

	public static function record_outcome( string $product, string $integration, string $operation, bool $success, string $failure_class = 'unknown', string $reason_code = '', string $reason_sample = '' ): bool {
		global $wpdb;
		if ( ! self::ensure_schema() ) {
			return false;
		}

		$integration = Sanitizer::slug( $integration, 32 );
		$operation   = Sanitizer::slug( $operation, 32 );
		$failure     = Sanitizer::slug( $failure_class, 32, 'unknown' );
		if ( '' === $integration || '' === $operation || ! in_array( $failure, self::FAILURE_CLASSES, true ) ) {
			return false;
		}

		$counters = array( 'attempts', $success ? 'successes' : 'failures', $success ? 'outcome_success' : 'failure_' . $failure );
		$values   = array();
		$args     = array();
		$hash     = self::product_hash( $product );
		$now      = time();
		foreach ( $counters as $counter ) {
			$values[] = '(%s,%s,%s,%s,1,%d,%d,%d,%d)';
			array_push( $args, $hash, $integration, $operation, $counter, $now, $success ? $now : 0, $success ? 0 : $now, $now );
		}

		$lifetime_sql  = 'INSERT INTO ' . self::table() . ' (product_hash,integration_slug,operation_slug,counter_slug,counter_value,first_recorded_at,last_success_at,last_failure_at,updated_at) VALUES ' . implode( ',', $values ) . ' ON DUPLICATE KEY UPDATE counter_value=counter_value+1,first_recorded_at=IF(first_recorded_at=0,VALUES(first_recorded_at),LEAST(first_recorded_at,VALUES(first_recorded_at))),last_success_at=GREATEST(last_success_at,VALUES(last_success_at)),last_failure_at=GREATEST(last_failure_at,VALUES(last_failure_at)),updated_at=VALUES(updated_at)';
		$daily_table   = self::daily_table();
		$daily_sql     = $wpdb->prepare(
			'INSERT INTO %i (product_hash,integration_slug,operation_slug,signal_date,attempts,successes,failures) VALUES (%s,%s,%s,%s,1,%d,%d) ON DUPLICATE KEY UPDATE attempts=attempts+1,successes=successes+VALUES(successes),failures=failures+VALUES(failures)',
			$daily_table,
			$hash,
			$integration,
			$operation,
			gmdate( 'Y-m-d', $now ),
			$success ? 1 : 0,
			$success ? 0 : 1
		);
		$daily_sql     = self::finalize_identifier_placeholders( $daily_sql, array( $daily_table ) );
		$reason_code   = Sanitizer::slug( $reason_code, 32 );
		$reason_sample = Sanitizer::text( $reason_sample, 50 );

		if ( false === $wpdb->query( 'START TRANSACTION' ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- SDK-owned atomic counter transaction.
			return false;
		}
		$ok = false !== $wpdb->query( $wpdb->prepare( $lifetime_sql, $args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Dynamic placeholders only; identifiers are SDK-owned.
		$ok = $ok && false !== $wpdb->query( $daily_sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared above.
		if ( $ok && ! $success && '' !== $reason_code ) {
			$reason_table = self::reason_table();
			$reason_sql   = $wpdb->prepare(
				'INSERT INTO %i (product_hash,integration_slug,operation_slug,reason_code,counter_value,sample,last_seen_at) VALUES (%s,%s,%s,%s,1,%s,%d) ON DUPLICATE KEY UPDATE counter_value=counter_value+1,sample=IF(VALUES(sample)<>\'\',VALUES(sample),sample),last_seen_at=VALUES(last_seen_at)',
				$reason_table,
				$hash,
				$integration,
				$operation,
				$reason_code,
				$reason_sample,
				$now
			);
			$reason_sql   = self::finalize_identifier_placeholders( $reason_sql, array( $reason_table ) );
			$ok           = false !== $wpdb->query( $reason_sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared above.
		}
		if ( ! $ok ) {
			$wpdb->query( 'ROLLBACK' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- SDK-owned atomic counter transaction.
			return false;
		}
		if ( false === $wpdb->query( 'COMMIT' ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- SDK-owned atomic counter transaction.
			$wpdb->query( 'ROLLBACK' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- SDK-owned atomic counter transaction.
			return false;
		}
		return true;
	}

	public static function read_daily( string $product, int $days ): array {
		global $wpdb;
		if ( ! self::ensure_schema() ) {
			return array();
		}
		$days = max( 1, min( 365, $days ) );
		$from = gmdate( 'Y-m-d', time() - ( $days - 1 ) * DAY_IN_SECONDS );
		$sql  = $wpdb->prepare( 'SELECT integration_slug,operation_slug,signal_date,attempts,successes,failures FROM ' . self::daily_table() . ' WHERE product_hash=%s AND signal_date>=%s ORDER BY signal_date ASC,integration_slug ASC,operation_slug ASC', self::product_hash( $product ), $from ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Fixed SDK-owned table and prepared values.
		$rows = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared above.
		return is_array( $rows ) ? $rows : array();
	}

	public static function read_reasons( string $product ): array {
		global $wpdb;
		if ( ! self::ensure_schema() ) {
			return array();
		}
		$sql  = $wpdb->prepare( 'SELECT integration_slug,operation_slug,reason_code,counter_value,sample,last_seen_at FROM ' . self::reason_table() . ' WHERE product_hash=%s ORDER BY counter_value DESC,reason_code ASC', self::product_hash( $product ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Fixed SDK-owned table and prepared value.
		$rows = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared above.
		return is_array( $rows ) ? $rows : array();
	}

	public static function read_product( string $product ): array {
		global $wpdb;
		if ( ! self::ensure_schema() ) {
			return array();
		}
		// phpcs:disable Generic.Formatting.MultipleStatementAlignment -- The prepared-query annotation would otherwise distort local assignment indentation.
		$hash = self::product_hash( $product );
		$sql  = $wpdb->prepare( 'SELECT integration_slug,operation_slug,counter_slug,counter_value,first_recorded_at,last_success_at,last_failure_at,updated_at FROM ' . self::table() . ' WHERE product_hash=%s', $hash ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The SDK owns the fixed table name; the value remains prepared.
		$rows = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared above.
		// phpcs:enable Generic.Formatting.MultipleStatementAlignment
		return is_array( $rows ) ? $rows : array();
	}

	public static function delete_product( string $product ): bool {
		global $wpdb;
		$hash = self::product_hash( $product );
		if ( false === $wpdb->query( 'START TRANSACTION' ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- SDK-owned product cleanup transaction.
			return false;
		}
		foreach ( array( self::reason_table(), self::daily_table(), self::table() ) as $table ) {
			if ( false === $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $table . ' WHERE product_hash=%s', $hash ) ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Closed SDK-owned table list and prepared value.
				$wpdb->query( 'ROLLBACK' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- SDK-owned product cleanup transaction.
				return false;
			}
		}
		return false !== $wpdb->query( 'COMMIT' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- SDK-owned product cleanup transaction.
	}

	public static function table(): string {
		global $wpdb;
		return $wpdb->prefix . 'signls_signal_counters';
	}

	public static function daily_table(): string {
		global $wpdb;
		return $wpdb->prefix . 'signls_signal_daily_counters';
	}

	public static function reason_table(): string {
		global $wpdb;
		return $wpdb->prefix . 'signls_signal_reason_counters';
	}

	private static function product_hash( string $product ): string {
		return substr( hash( 'sha256', $product ), 0, 12 );
	}

	private static function finalize_identifier_placeholders( string $sql, array $identifiers ): string {
		foreach ( $identifiers as $identifier ) {
			if ( false === strpos( $sql, '%i' ) ) {
				break;
			}
			if ( ! is_string( $identifier ) || 1 !== preg_match( '/^[A-Za-z0-9_]+$/', $identifier ) ) {
				return '';
			}
			$sql = (string) preg_replace( '/%i/', '`' . $identifier . '`', $sql, 1 );
		}
		return false === strpos( $sql, '%i' ) ? $sql : '';
	}

	private static function column_exists( string $table, string $column ): bool {
		global $wpdb;
		$count = $wpdb->get_var(
			$wpdb->prepare(
				'SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=%s AND COLUMN_NAME=%s',
				$table,
				$column
			)
		);
		return (int) $count > 0;
	}
}
                                     includes/signls/src/StateStore.php                                                                  0000644                 00000003125 15266052675 0013265 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Product-scoped non-autoloaded state.
 *
 * @package signls-sdk
 * @license GPL-3.0-or-later
 */

namespace Signls\Sdk\V1;

defined( 'ABSPATH' ) || exit;

final class StateStore {

	private $product;

	private $option_name;

	public function __construct( string $product ) {
		$this->product     = $product;
		$this->option_name = 'signls_sdk_v1_' . substr( hash( 'sha256', $product ), 0, 12 );
	}

	public function product(): string {
		return $this->product;
	}

	public function option_name(): string {
		return $this->option_name;
	}

	public function all(): array {
		$value = get_option( $this->option_name, array() );
		return is_array( $value ) ? $value : array();
	}

	public function get( string $key, $default = null ) {
		$state = $this->all();
		return array_key_exists( $key, $state ) ? $state[ $key ] : $default;
	}

	public function set( string $key, $value ): bool {
		$state         = $this->all();
		$state[ $key ] = $value;
		return $this->replace( $state );
	}

	public function set_many( array $values ): bool {
		return $this->replace( array_merge( $this->all(), $values ) );
	}

	public function delete_keys( array $keys ): bool {
		$state = $this->all();
		foreach ( $keys as $key ) {
			unset( $state[ $key ] );
		}
		return $this->replace( $state );
	}

	public function delete(): bool {
		return delete_option( $this->option_name );
	}

	private function replace( array $state ): bool {
		if ( false === get_option( $this->option_name, false ) ) {
			return add_option( $this->option_name, $state, '', false );
		}
		return update_option( $this->option_name, $state, false );
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                           includes/signls/src/Sanitizer.php                                                                   0000644                 00000006113 15266052675 0013140 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Closed aggregate value sanitizer.
 *
 * @package signls-sdk
 * @license GPL-3.0-or-later
 */

namespace Signls\Sdk\V1;

defined( 'ABSPATH' ) || exit;

final class Sanitizer {

	public static function slug( $value, int $max = 48, string $fallback = '' ): string {
		$value = is_string( $value ) ? strtolower( $value ) : '';
		return 1 === preg_match( '/^[a-z0-9][a-z0-9_-]{0,' . ( max( 1, $max ) - 1 ) . '}$/', $value ) ? $value : $fallback;
	}

	public static function version( $value, int $maximum = 20 ): string {
		$maximum = max( 1, min( 64, $maximum ) );
		$value   = is_string( $value ) ? $value : '';
		return 1 === preg_match( '/^[A-Za-z0-9][A-Za-z0-9._+-]{0,' . ( $maximum - 1 ) . '}$/', $value ) ? $value : 'unknown';
	}

	public static function counter( $value ): int {
		if ( ! is_int( $value ) && ! ctype_digit( (string) $value ) ) {
			return 0;
		}
		$value = (int) $value;
		return max( 0, min( 4294967295, $value ) );
	}

	public static function age( $value ) {
		if ( null === $value ) {
			return null;
		}
		return self::counter( $value );
	}

	public static function bool( $value ): bool {
		return true === $value || 1 === $value || '1' === $value;
	}

	public static function int( $value, int $minimum, int $maximum ): int {
		if ( ! is_int( $value ) && ! ( is_string( $value ) && 1 === preg_match( '/^-?[0-9]+$/', $value ) ) ) {
			return $minimum;
		}
		return max( $minimum, min( $maximum, (int) $value ) );
	}

	public static function decimal( $value, float $minimum, float $maximum, int $scale = 4 ): float {
		if ( ! is_numeric( $value ) ) {
			return $minimum;
		}
		$value = (float) $value;
		if ( is_nan( $value ) || is_infinite( $value ) ) {
			return $minimum;
		}
		return round( max( $minimum, min( $maximum, $value ) ), max( 0, min( 8, $scale ) ) );
	}

	public static function enum( $value, array $allowed, string $fallback = '' ): string {
		$value = is_string( $value ) ? $value : '';
		return in_array( $value, $allowed, true ) ? $value : $fallback;
	}

	public static function text( $value, int $maximum ): string {
		if ( ! is_string( $value ) || $maximum < 1 || 1 !== preg_match( '//u', $value ) || 1 === preg_match( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', $value ) ) {
			return '';
		}
		if ( function_exists( 'mb_substr' ) ) {
			return mb_substr( $value, 0, $maximum, 'UTF-8' );
		}
		if ( false !== preg_match_all( '/./us', $value, $characters ) ) {
			return implode( '', array_slice( $characters[0], 0, $maximum ) );
		}
		return '';
	}

	public static function url( $value, int $maximum = 500 ): string {
		$value = self::text( $value, $maximum );
		if ( '' === $value ) {
			return '';
		}
		$parts = wp_parse_url( $value );
		if ( ! is_array( $parts ) || ! isset( $parts['scheme'], $parts['host'] ) || isset( $parts['user'] ) || isset( $parts['pass'] ) ) {
			return '';
		}
		$scheme = strtolower( (string) $parts['scheme'] );
		return in_array( $scheme, array( 'http', 'https' ), true ) ? $value : '';
	}

	public static function sha256( $value ): string {
		$value = is_string( $value ) ? $value : '';
		return 1 === preg_match( '/^[a-f0-9]{64}$/', $value ) ? $value : '';
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                     includes/signls/src/Scheduler.php                                                                   0000644                 00000007267 15266052675 0013121 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Product-scoped durable scheduling.
 *
 * @package signls-sdk
 * @license GPL-3.0-or-later
 */

namespace Signls\Sdk\V1;

defined( 'ABSPATH' ) || exit;

final class Scheduler {

	private $product;

	private $state;

	private $routine_hook;

	private $refresh_hook;

	public function __construct( string $product, StateStore $state ) {
		$hash               = substr( hash( 'sha256', $product ), 0, 12 );
		$this->product      = $product;
		$this->state        = $state;
		$this->routine_hook = 'signls_sdk_v1_' . $hash . '_routine';
		$this->refresh_hook = 'signls_sdk_v1_' . $hash . '_refresh';
	}

	public function register(): void {
		add_action( $this->routine_hook, array( $this, 'run' ) );
		add_action( $this->refresh_hook, array( $this, 'run' ) );
		add_action( 'admin_init', array( $this, 'failsafe' ), 999 );
	}

	public function activate( string $install_id, string $device_id ): void {
		if ( 'enabled' !== $this->state->get( 'consent_status', 'unset' ) ) {
			return;
		}
		if ( wp_next_scheduled( $this->routine_hook ) ) {
			return;
		}
		$delay = 300 + self::jitter( $this->product . '|' . $install_id . '|' . $device_id, 21300 );
		wp_schedule_single_event( time() + $delay, $this->routine_hook );
	}

	public function relevant_change(): void {
		$this->state->set_many(
			array(
				'last_relevant_activity' => time(),
				'cadence_mode'           => 'daily',
			)
		);
		if ( ! wp_next_scheduled( $this->refresh_hook ) ) {
			wp_schedule_single_event( time() + 300 + self::jitter( $this->product . '|refresh', 600 ), $this->refresh_hook );
		}
	}

	public function run(): void {
		if ( 'enabled' !== $this->state->get( 'consent_status', 'unset' ) ) {
			return;
		}
		Runtime::run( $this->product );
		$this->schedule_next();
	}

	public function schedule_next(): void {
		if ( 'enabled' !== $this->state->get( 'consent_status', 'unset' ) ) {
			return;
		}
		if ( wp_next_scheduled( $this->routine_hook ) ) {
			wp_clear_scheduled_hook( $this->routine_hook );
		}
		$last_activity = (int) $this->state->get( 'last_relevant_activity', time() );
		$sparse        = time() - $last_activity >= 30 * DAY_IN_SECONDS;
		$interval      = $sparse ? WEEK_IN_SECONDS : DAY_IN_SECONDS;
		$mode          = $sparse ? 'sparse' : 'daily';
		$this->state->set( 'cadence_mode', $mode );
		wp_schedule_single_event( time() + $interval + self::jitter( $this->product . '|' . gmdate( 'Y-m-d' ), 3600 ), $this->routine_hook );
	}

	public function schedule_retry( int $when ): void {
		if ( 'enabled' !== $this->state->get( 'consent_status', 'unset' ) ) {
			return;
		}
		if ( ! wp_next_scheduled( $this->refresh_hook ) ) {
			wp_schedule_single_event( max( time() + 60, $when ), $this->refresh_hook );
		}
	}

	public function clear(): void {
		wp_clear_scheduled_hook( $this->routine_hook );
		wp_clear_scheduled_hook( $this->refresh_hook );
	}

	public function failsafe(): void {
		if ( 'enabled' !== $this->state->get( 'consent_status', 'unset' ) ) {
			return;
		}
		$last = (int) $this->state->get( 'last_acknowledged_at', 0 );
		$mode = (string) $this->state->get( 'cadence_mode', 'daily' );
		$age  = 'sparse' === $mode ? 9 * DAY_IN_SECONDS : 36 * HOUR_IN_SECONDS;
		if ( $last > 0 && time() - $last < $age ) {
			return;
		}
		if ( ! wp_next_scheduled( $this->refresh_hook ) ) {
			wp_schedule_single_event( time() + 300 + self::jitter( $this->product . '|failsafe', 600 ), $this->refresh_hook );
		}
	}

	public function routine_hook(): string {
		return $this->routine_hook;
	}

	public function refresh_hook(): string {
		return $this->refresh_hook;
	}

	private static function jitter( string $seed, int $maximum ): int {
		$hex = substr( hash( 'sha256', $seed ), 0, 8 );
		return (int) ( hexdec( $hex ) % ( max( 1, $maximum ) + 1 ) );
	}
}
                                                                                                                                                                                                                                                                                                                                         includes/signls/src/ProductAdapterInterface.php                                                     0000644                 00000000675 15266052675 0015741 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Product adapter contract.
 *
 * @package signls-sdk
 * @license GPL-3.0-or-later
 */

namespace Signls\Sdk\V1;

defined( 'ABSPATH' ) || exit;

interface ProductAdapterInterface {

	public function product_slug(): string;

	public function product_version(): string;

	public function signal_sharing_enabled(): bool;

	public function install_id(): string;

	public function contract(): array;

	public function snapshot(): array;
}
                                                                   includes/signls/src/Transport.php                                                                   0000644                 00000025736 15266052675 0013200 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Acknowledged Signls delivery transport.
 *
 * @package signls-sdk
 * @license GPL-3.0-or-later
 */

namespace Signls\Sdk\V1;

defined( 'ABSPATH' ) || exit;

final class Transport {

	private const DEFAULT_ENDPOINT = 'https://signls.dev';

	private const SDK_VERSION = '1.1.0';

	private $state;

	private $identity;

	private $site_identity;

	public function __construct( StateStore $state, Identity $identity, SiteIdentity $site_identity ) {
		$this->state         = $state;
		$this->identity      = $identity;
		$this->site_identity = $site_identity;
	}

	public function deliver( ProductAdapterInterface $adapter, array $payload ): array {
		$started = time();
		$this->state->set( 'last_attempt_started_at', $started );

		if ( '' === (string) $this->state->get( 'credential_id', '' ) ) {
			$enrollment = $this->enroll( $adapter );
			if ( ! $enrollment['ok'] ) {
				return $this->finish( $enrollment );
			}
		}

		$result = $this->snapshot( $adapter, $payload, false );
		if ( 'stale_timestamp' === $result['class'] && isset( $result['server_time'] ) ) {
			$this->state->set( 'clock_offset', (int) $result['server_time'] - time() );
			$result = $this->snapshot( $adapter, $payload, true );
		}
		return $this->finish( $result );
	}

	public static function retry_delay( int $attempt ): int {
		$delays = array( 900, 3600, 14400, 43200, 86400 );
		return $delays[ min( max( 0, $attempt ), count( $delays ) - 1 ) ];
	}

	private function enroll( ProductAdapterInterface $adapter ): array {
		$body    = array(
			'schema_version' => 1,
			'sdk_version'    => self::SDK_VERSION,
			'product'        => $adapter->product_slug(),
			'install_id'     => $adapter->install_id(),
			'device_id'      => $this->identity->device_id(),
			'timestamp'      => time(),
		);
		$encoded = wp_json_encode( $body );
		if ( ! is_string( $encoded ) ) {
			return array(
				'ok'        => false,
				'class'     => 'invalid_payload',
				'status'    => 0,
				'permanent' => true,
			);
		}
		$response = $this->post( '/signals/v2/enroll', $encoded, array() );
		if ( ! $response['ok'] ) {
			return $response;
		}

		$data = $response['data'];
		if ( ! isset( $data['credential_id'], $data['credential_secret'], $data['key_id'], $data['server_time'] ) || ! is_string( $data['credential_id'] ) || ! is_string( $data['credential_secret'] ) || ! is_string( $data['key_id'] ) ) {
			return array(
				'ok'        => false,
				'class'     => 'invalid_response',
				'status'    => $response['status'],
				'permanent' => true,
			);
		}

		$this->state->set_many(
			array(
				'credential_id'     => substr( $data['credential_id'], 0, 64 ),
				'credential_secret' => substr( $data['credential_secret'], 0, 128 ),
				'credential_key_id' => substr( $data['key_id'], 0, 48 ),
				'clock_offset'      => (int) $data['server_time'] - time(),
			)
		);
		return array(
			'ok'        => true,
			'class'     => 'enrolled',
			'status'    => $response['status'],
			'permanent' => false,
		);
	}

	private function snapshot( ProductAdapterInterface $adapter, array $payload, bool $clock_retry ): array {
		$contract       = $adapter->contract();
		$schema_version = isset( $contract['snapshot_schema_version'] ) ? (int) $contract['snapshot_schema_version'] : 1;
		$schema_version = 2 === $schema_version ? 2 : 1;
		$profile        = 2 === $schema_version && isset( $contract['observation_profile'] ) ? Sanitizer::slug( $contract['observation_profile'], 48 ) : '';
		$pending        = (string) $this->state->get( 'pending_body', '' );
		if ( '' !== $pending && ( self::SDK_VERSION !== (string) $this->state->get( 'pending_sdk_version', '' ) || $adapter->product_version() !== (string) $this->state->get( 'pending_product_version', '' ) ) ) {
			$this->state->delete_keys( array( 'pending_sequence', 'pending_body', 'pending_body_hash', 'pending_sdk_version', 'pending_product_version' ) );
			$pending = '';
		}
		if ( '' === $pending ) {
			$sequence = max( 1, (int) $this->state->get( 'last_acknowledged_sequence', 0 ) + 1 );
			$body     = array(
				'schema_version' => $schema_version,
				'sdk_version'    => self::SDK_VERSION,
				'product'        => $adapter->product_slug(),
				'device_id'      => $this->identity->device_id(),
				'install_id'     => $adapter->install_id(),
			);
			if ( 2 === $schema_version ) {
				$body['site_id']             = $this->site_identity->site_id();
				$body['observation_profile'] = $profile;
			}
			$body['sequence']    = $sequence;
			$body['observed_at'] = time();
			$body['payload']     = $payload;
			$pending             = (string) wp_json_encode( $body );
			$this->state->set_many(
				array(
					'pending_sequence'        => $sequence,
					'pending_body'            => $pending,
					'pending_body_hash'       => hash( 'sha256', $pending ),
					'pending_sdk_version'     => self::SDK_VERSION,
					'pending_product_version' => $adapter->product_version(),
				)
			);
		}

		$timestamp = time() + (int) $this->state->get( 'clock_offset', 0 );
		$secret    = self::base64url_decode( (string) $this->state->get( 'credential_secret', '' ) );
		if ( false === $secret || 32 !== strlen( $secret ) ) {
			return array(
				'ok'        => false,
				'class'     => 'invalid_credential',
				'status'    => 0,
				'permanent' => false,
			);
		}
		$signature = self::base64url( hash_hmac( 'sha256', $timestamp . "\n" . hash( 'sha256', $pending ), $secret, true ) );
		$response  = $this->post(
			'/signals/v2/snapshot',
			$pending,
			array(
				'X-Signls-Credential' => (string) $this->state->get( 'credential_id', '' ),
				'X-Signls-Timestamp'  => (string) $timestamp,
				'X-Signls-Signature'  => 'v1=' . $signature,
			)
		);

		if ( $response['ok'] ) {
			$data     = $response['data'];
			$sequence = (int) $this->state->get( 'pending_sequence', 0 );
			if ( empty( $data['accepted'] ) || (int) ( isset( $data['sequence'] ) ? $data['sequence'] : 0 ) !== $sequence ) {
				return array(
					'ok'        => false,
					'class'     => 'invalid_response',
					'status'    => $response['status'],
					'permanent' => true,
				);
			}
			$this->state->set_many(
				array(
					'last_acknowledged_sequence' => $sequence,
					'last_acknowledged_at'       => (int) ( isset( $data['received_at'] ) ? $data['received_at'] : time() ),
					'last_success_at'            => time(),
					'retry_attempt'              => 0,
				)
			);
			$this->state->delete_keys( array( 'pending_sequence', 'pending_body', 'pending_body_hash', 'pending_sdk_version', 'pending_product_version', 'failure_class', 'next_retry_at' ) );
			return array(
				'ok'        => true,
				'class'     => ! empty( $data['duplicate'] ) ? 'duplicate' : 'accepted',
				'status'    => $response['status'],
				'permanent' => false,
			);
		}

		if ( 'stale_timestamp' === $response['class'] && ! $clock_retry && isset( $response['data']['server_time'] ) ) {
			$response['server_time'] = (int) $response['data']['server_time'];
		}
		return $response;
	}

	private function post( string $path, string $body, array $headers ): array {
		$endpoint = $this->endpoint();
		if ( '' === $endpoint ) {
			return array(
				'ok'        => false,
				'class'     => 'invalid_endpoint',
				'status'    => 0,
				'permanent' => true,
			);
		}

		$headers['Content-Type'] = 'application/json';
		$response                = wp_remote_post(
			$endpoint . $path,
			array(
				'body'        => $body,
				'headers'     => $headers,
				'timeout'     => 10,
				'redirection' => 0,
				'blocking'    => true,
			)
		);

		if ( is_wp_error( $response ) ) {
			$error_code = $response->get_error_code();
			$error_code = is_scalar( $error_code ) ? substr( (string) $error_code, 0, 191 ) : '';
			return array(
				'ok'        => false,
				'class'     => self::transport_class( $error_code ),
				'status'    => 0,
				'permanent' => false,
			);
		}

		$status = (int) wp_remote_retrieve_response_code( $response );
		$raw    = (string) wp_remote_retrieve_body( $response );
		$data   = json_decode( $raw, true );
		$data   = is_array( $data ) ? $data : array();
		if ( in_array( $status, array( 200, 201, 202 ), true ) ) {
			return array(
				'ok'        => true,
				'class'     => 'success',
				'status'    => $status,
				'data'      => $data,
				'permanent' => false,
			);
		}

		$code      = Sanitizer::slug( isset( $data['code'] ) ? $data['code'] : '', 48, 'http_' . $status );
		$transient = in_array( $status, array( 408, 425, 429 ), true ) || $status >= 500;
		return array(
			'ok'        => false,
			'class'     => $code,
			'status'    => $status,
			'data'      => $data,
			'permanent' => ! $transient,
		);
	}

	private function endpoint(): string {
		$endpoint = self::DEFAULT_ENDPOINT;
		if ( function_exists( 'apply_filters' ) ) {
			$endpoint = (string) apply_filters( 'signls_sdk_v1_endpoint', $endpoint, $this->state->product() );
		}
		$endpoint = untrailingslashit( $endpoint );
		$parts    = wp_parse_url( $endpoint );
		$host     = isset( $parts['host'] ) ? strtolower( $parts['host'] ) : '';
		$scheme   = isset( $parts['scheme'] ) ? strtolower( $parts['scheme'] ) : '';
		$local    = 'localhost' === $host || '.test' === substr( $host, -5 );
		return ( 'https' === $scheme || ( 'http' === $scheme && $local ) ) ? $endpoint : '';
	}

	private function finish( array $result ): array {
		$values = array(
			'last_attempt_finished_at' => time(),
			'last_http_status'         => (int) ( isset( $result['status'] ) ? $result['status'] : 0 ),
			'failure_class'            => $result['ok'] ? '' : Sanitizer::slug( isset( $result['class'] ) ? $result['class'] : '', 48, 'unknown' ),
		);
		if ( ! $result['ok'] ) {
			$attempt                 = (int) $this->state->get( 'retry_attempt', 0 );
			$values['retry_attempt'] = $attempt + 1;
			if ( in_array( $values['failure_class'], array( 'invalid_credential', 'device_already_enrolled' ), true ) ) {
				$last_rotation = (int) $this->state->get( 'last_device_rotation_at', 0 );
				if ( time() - $last_rotation >= DAY_IN_SECONDS ) {
					$this->identity->rotate_device();
					$values['last_device_rotation_at'] = time();
				}
				$result['permanent'] = false;
			}
			if ( empty( $result['permanent'] ) ) {
				$values['next_retry_at'] = time() + self::retry_delay( $attempt ) + random_int( 0, 300 );
			}
		}
		$this->state->set_many( $values );
		return $result;
	}

	private static function transport_class( string $code ): string {
		$code = strtolower( $code );
		if ( false !== strpos( $code, 'resolve' ) || false !== strpos( $code, 'dns' ) ) {
			return 'transport_dns';
		}
		if ( false !== strpos( $code, 'ssl' ) || false !== strpos( $code, 'tls' ) ) {
			return 'transport_tls';
		}
		if ( false !== strpos( $code, 'timeout' ) ) {
			return 'transport_timeout';
		}
		return 'transport_connect';
	}

	private static function base64url( string $value ): string {
		return rtrim( strtr( base64_encode( $value ), '+/', '-_' ), '=' );
	}

	private static function base64url_decode( string $value ) {
		if ( '' === $value || 1 !== preg_match( '/^[A-Za-z0-9_-]+$/', $value ) ) {
			return false;
		}
		$padding = ( 4 - strlen( $value ) % 4 ) % 4;
		return base64_decode( strtr( $value, '-_', '+/' ) . str_repeat( '=', $padding ), true );
	}
}
                                  includes/signls/src/SiteIdentity.php                                                                0000644                 00000004442 15266052675 0013611 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Shared random WordPress-origin identity.
 *
 * @package signls-sdk
 * @license GPL-3.0-or-later
 */

namespace Signls\Sdk\V1;

defined( 'ABSPATH' ) || exit;

final class SiteIdentity {

	private const ID_OPTION = 'signls_sdk_site_id';

	private const ORIGIN_OPTION = 'signls_sdk_site_origin';

	public function site_id(): string {
		$current_origin          = $this->origin_fingerprint();
		$stored_origin_candidate = get_option( self::ORIGIN_OPTION, '' );
		$site_id_candidate       = get_option( self::ID_OPTION, '' );
		$stored_origin           = is_scalar( $stored_origin_candidate ) ? (string) $stored_origin_candidate : '';
		$site_id                 = is_scalar( $site_id_candidate ) ? (string) $site_id_candidate : '';

		if ( ! self::valid( $site_id ) || ( '' !== $stored_origin && ! hash_equals( $stored_origin, $current_origin ) ) ) {
			$site_id = bin2hex( random_bytes( 16 ) );
			self::replace_option( self::ID_OPTION, $site_id );
			self::replace_option( self::ORIGIN_OPTION, $current_origin );
		} elseif ( '' === $stored_origin ) {
			self::replace_option( self::ORIGIN_OPTION, $current_origin );
		}

		return $site_id;
	}

	private function origin_fingerprint(): string {
		$origin = function_exists( 'home_url' ) ? (string) home_url( '/' ) : 'http://local/';
		$parts  = wp_parse_url( $origin );
		$parts  = is_array( $parts ) ? $parts : array();
		$scheme = isset( $parts['scheme'] ) ? strtolower( (string) $parts['scheme'] ) : 'http';
		$host   = isset( $parts['host'] ) ? strtolower( rtrim( (string) $parts['host'], '.' ) ) : 'local';
		$port   = isset( $parts['port'] ) ? (int) $parts['port'] : ( 'https' === $scheme ? 443 : 80 );
		$path   = isset( $parts['path'] ) ? '/' . trim( (string) $parts['path'], '/' ) : '/';
		$path   = '/' === $path ? $path : $path . '/';
		$salt   = function_exists( 'wp_salt' ) ? (string) wp_salt( 'auth' ) : 'signls-test-salt';
		return hash_hmac( 'sha256', $scheme . '://' . $host . ':' . $port . $path, $salt );
	}

	private static function replace_option( string $name, string $value ): bool {
		if ( false === get_option( $name, false ) ) {
			return add_option( $name, $value, '', false );
		}
		return update_option( $name, $value, false );
	}

	private static function valid( string $value ): bool {
		return 1 === preg_match( '/^[a-f0-9]{32}$/', $value );
	}
}
                                                                                                                                                                                                                              includes/signls/src/Runtime.php                                                                     0000644                 00000010204 15266052675 0012607 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Product runtime coordinator.
 *
 * @package signls-sdk
 * @license GPL-3.0-or-later
 */

namespace Signls\Sdk\V1;

defined( 'ABSPATH' ) || exit;

final class Runtime {

	private static $products = array();

	private static $site_identity;

	public static function boot( array $descriptor ): bool {
		try {
			$adapter = self::adapter( $descriptor );
			if ( ! $adapter instanceof ProductAdapterInterface ) {
				return false;
			}
			$product = $adapter->product_slug();
			if ( isset( self::$products[ $product ] ) ) {
				return true;
			}

			$state    = new StateStore( $product );
			$consent  = new Consent( $state );
			$identity = new Identity( $state );
			if ( ! self::$site_identity instanceof SiteIdentity ) {
				self::$site_identity = new SiteIdentity();
			}
			$scheduler = new Scheduler( $product, $state );
			$transport = new Transport( $state, $identity, self::$site_identity );
			$scheduler->register();

			self::$products[ $product ] = array(
				'adapter'   => $adapter,
				'state'     => $state,
				'consent'   => $consent,
				'identity'  => $identity,
				'site'      => self::$site_identity,
				'scheduler' => $scheduler,
				'transport' => $transport,
			);

			if ( $adapter->signal_sharing_enabled() && $consent->enabled() ) {
				CounterStore::ensure_schema();
				$scheduler->activate( $adapter->install_id(), $identity->device_id() );
			}
			return true;
		} catch ( \Throwable $error ) {
			return false;
		}
	}

	public static function enable( string $product, string $source, int $notice_version ): bool {
		$runtime = self::product( $product );
		if ( null === $runtime || ! $runtime['consent']->enable( $source, $notice_version ) ) {
			return false;
		}
		CounterStore::ensure_schema();
		$runtime['scheduler']->activate( $runtime['adapter']->install_id(), $runtime['identity']->device_id() );
		return true;
	}

	public static function disable( string $product, string $source, int $notice_version ): bool {
		$runtime = self::product( $product );
		if ( null === $runtime || ! $runtime['consent']->disable( $source, $notice_version ) ) {
			return false;
		}
		$runtime['scheduler']->clear();
		$runtime['identity']->forget_device();
		return true;
	}

	public static function relevant_change( string $product ): void {
		$runtime = self::product( $product );
		if ( null !== $runtime && $runtime['consent']->enabled() ) {
			$runtime['scheduler']->relevant_change();
		}
	}

	public static function run( string $product ): array {
		$runtime = self::product( $product );
		if ( null === $runtime || ! $runtime['consent']->enabled() || ! $runtime['adapter']->signal_sharing_enabled() ) {
			return array(
				'ok'        => false,
				'class'     => 'consent_required',
				'status'    => 0,
				'permanent' => true,
			);
		}

		try {
			$payload = Collector::collect( $runtime['adapter'] );
			$result  = $runtime['transport']->deliver( $runtime['adapter'], $payload );
			if ( ! $result['ok'] && empty( $result['permanent'] ) ) {
				$runtime['scheduler']->schedule_retry( (int) $runtime['state']->get( 'next_retry_at', time() + 900 ) );
			}
			return $result;
		} catch ( \Throwable $error ) {
			return array(
				'ok'        => false,
				'class'     => 'runtime_failure',
				'status'    => 0,
				'permanent' => false,
			);
		}
	}

	public static function state( string $product ): array {
		$runtime = self::product( $product );
		return null === $runtime ? array() : $runtime['state']->all();
	}

	public static function cleanup( string $product ): void {
		$runtime = self::product( $product );
		if ( null !== $runtime ) {
			$runtime['scheduler']->clear();
			$runtime['state']->delete();
		}
		CounterStore::delete_product( $product );
	}

	private static function adapter( array $descriptor ) {
		if ( isset( $descriptor['adapter'] ) && $descriptor['adapter'] instanceof ProductAdapterInterface ) {
			return $descriptor['adapter'];
		}
		$class = isset( $descriptor['adapter_class'] ) ? (string) $descriptor['adapter_class'] : '';
		return '' !== $class && class_exists( $class ) ? new $class() : null;
	}

	private static function product( string $product ) {
		return isset( self::$products[ $product ] ) ? self::$products[ $product ] : null;
	}
}
                                                                                                                                                                                                                                                                                                                                                                                            includes/signls/src/ObservationSanitizer.php                                                        0000644                 00000014476 15266052675 0015367 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Declarative rich-observation sanitizer.
 *
 * @package signls-sdk
 * @license GPL-3.0-or-later
 */

namespace Signls\Sdk\V1;

defined( 'ABSPATH' ) || exit;

final class ObservationSanitizer {

	private const TYPES = array(
		'object',
		'list',
		'map',
		'bool',
		'uint',
		'int',
		'decimal',
		'enum',
		'slug',
		'version',
		'text',
		'url',
		'sha256',
	);

	public static function sanitize( array $source, array $schema ): array {
		if ( 'object' !== self::type( $schema ) ) {
			return array();
		}
		$value = self::node( $source, $schema );
		return is_array( $value ) ? $value : array();
	}

	private static function node( $value, array $schema ) {
		$type     = self::type( $schema );
		$nullable = ! empty( $schema['nullable'] );
		if ( null === $value && $nullable ) {
			return null;
		}

		switch ( $type ) {
			case 'object':
				return self::object( $value, $schema );
			case 'list':
				return self::list_value( $value, $schema );
			case 'map':
				return self::map_value( $value, $schema );
			case 'bool':
				return Sanitizer::bool( $value );
			case 'uint':
				return Sanitizer::int( $value, 0, self::integer_bound( $schema, 'max', 4294967295 ) );
			case 'int':
				return Sanitizer::int(
					$value,
					self::integer_bound( $schema, 'min', -2147483648 ),
					self::integer_bound( $schema, 'max', 2147483647 )
				);
			case 'decimal':
				return Sanitizer::decimal(
					$value,
					self::float_bound( $schema, 'min', 0.0 ),
					self::float_bound( $schema, 'max', 4294967295.0 ),
					self::integer_bound( $schema, 'scale', 4 )
				);
			case 'enum':
				$allowed  = self::allowed( $schema );
				$fallback = isset( $schema['fallback'] ) && is_string( $schema['fallback'] ) ? $schema['fallback'] : '';
				return Sanitizer::enum( $value, $allowed, $fallback );
			case 'slug':
				return Sanitizer::slug( $value, self::length( $schema, 48 ), self::fallback( $schema ) );
			case 'version':
				return Sanitizer::version( $value, self::length( $schema, 20 ) );
			case 'text':
				return Sanitizer::text( $value, self::length( $schema, 191 ) );
			case 'url':
				return Sanitizer::url( $value, self::length( $schema, 500 ) );
			case 'sha256':
				return Sanitizer::sha256( $value );
			default:
				return $nullable ? null : '';
		}
	}

	private static function object( $value, array $schema ): array {
		$source     = is_array( $value ) ? $value : array();
		$properties = isset( $schema['properties'] ) && is_array( $schema['properties'] ) ? $schema['properties'] : array();
		$result     = array();
		foreach ( $properties as $key => $property ) {
			if ( ! is_string( $key ) || ! is_array( $property ) || ! array_key_exists( $key, $source ) ) {
				continue;
			}
			$result[ $key ] = self::node( $source[ $key ], $property );
		}
		return $result;
	}

	private static function list_value( $value, array $schema ) {
		$accounted = ! empty( $schema['with_accounting'] );
		$source    = is_array( $value ) ? $value : array();
		$rows      = $accounted && isset( $source['items'] ) && is_array( $source['items'] ) ? $source['items'] : $source;
		$item      = isset( $schema['items'] ) && is_array( $schema['items'] ) ? $schema['items'] : array();
		$maximum   = max( 1, min( 1000, self::integer_bound( $schema, 'max_items', 1 ) ) );
		$clean     = array();
		foreach ( array_slice( $rows, 0, $maximum ) as $row ) {
			$clean[] = self::node( $row, $item );
		}

		$sort_key = isset( $schema['sort_key'] ) && is_string( $schema['sort_key'] ) ? $schema['sort_key'] : '';
		if ( '' !== $sort_key ) {
			usort(
				$clean,
				static function ( $left, $right ) use ( $sort_key ) {
					$left_candidate  = is_array( $left ) && isset( $left[ $sort_key ] ) ? $left[ $sort_key ] : '';
					$right_candidate = is_array( $right ) && isset( $right[ $sort_key ] ) ? $right[ $sort_key ] : '';
					$left_value      = is_scalar( $left_candidate ) ? (string) $left_candidate : '';
					$right_value     = is_scalar( $right_candidate ) ? (string) $right_candidate : '';
					return strcmp( $left_value, $right_value );
				}
			);
		}

		if ( ! $accounted ) {
			return $clean;
		}
		$reported = isset( $source['reported_total'] ) ? Sanitizer::counter( $source['reported_total'] ) : count( $rows );
		$reported = max( $reported, count( $rows ) );
		return array(
			'items'          => $clean,
			'reported_total' => $reported,
			'truncated'      => ! empty( $source['truncated'] ) || $reported > count( $clean ),
		);
	}

	private static function map_value( $value, array $schema ): array {
		$source     = is_array( $value ) ? $value : array();
		$value_rule = isset( $schema['values'] ) && is_array( $schema['values'] ) ? $schema['values'] : array();
		$maximum    = max( 1, min( 1000, self::integer_bound( $schema, 'max_items', 1 ) ) );
		$key_length = max( 1, min( 100, self::integer_bound( $schema, 'max_key_length', 48 ) ) );
		$result     = array();
		ksort( $source, SORT_STRING );
		foreach ( $source as $key => $item ) {
			$key = Sanitizer::slug( $key, $key_length );
			if ( '' === $key || isset( $result[ $key ] ) ) {
				continue;
			}
			$result[ $key ] = self::node( $item, $value_rule );
			if ( count( $result ) >= $maximum ) {
				break;
			}
		}
		return $result;
	}

	private static function type( array $schema ): string {
		$type = isset( $schema['type'] ) && is_string( $schema['type'] ) ? $schema['type'] : '';
		return in_array( $type, self::TYPES, true ) ? $type : '';
	}

	private static function allowed( array $schema ): array {
		$values = isset( $schema['allowed'] ) && is_array( $schema['allowed'] ) ? $schema['allowed'] : array();
		return array_values(
			array_filter(
				$values,
				static function ( $value ) {
					return is_string( $value );
				}
			)
		);
	}

	private static function fallback( array $schema ): string {
		return isset( $schema['fallback'] ) && is_string( $schema['fallback'] ) ? $schema['fallback'] : '';
	}

	private static function length( array $schema, int $fallback ): int {
		return max( 1, min( 4096, self::integer_bound( $schema, 'max_length', $fallback ) ) );
	}

	private static function integer_bound( array $schema, string $key, int $fallback ): int {
		return isset( $schema[ $key ] ) && is_int( $schema[ $key ] ) ? $schema[ $key ] : $fallback;
	}

	private static function float_bound( array $schema, string $key, float $fallback ): float {
		return isset( $schema[ $key ] ) && is_numeric( $schema[ $key ] ) ? (float) $schema[ $key ] : $fallback;
	}
}
                                                                                                                                                                                                  includes/signls/src/Consent.php                                                                     0000644                 00000003106 15266052676 0012601 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Explicit product consent state.
 *
 * @package signls-sdk
 * @license GPL-3.0-or-later
 */

namespace Signls\Sdk\V1;

defined( 'ABSPATH' ) || exit;

final class Consent {

	const VERSION = 1;

	private $state;

	public function __construct( StateStore $state ) {
		$this->state = $state;
	}

	public function status(): string {
		$status = (string) $this->state->get( 'consent_status', 'unset' );
		return in_array( $status, array( 'unset', 'enabled', 'disabled' ), true ) ? $status : 'unset';
	}

	public function enabled(): bool {
		return 'enabled' === $this->status();
	}

	public function enable( string $source, int $notice_version ): bool {
		$now           = time();
		$first_enabled = (int) $this->state->get( 'consent_first_enabled_at', 0 );
		if ( $first_enabled <= 0 ) {
			$first_enabled = $now;
		}

		return $this->state->set_many(
			array(
				'consent_status'           => 'enabled',
				'consent_version'          => self::VERSION,
				'consent_first_enabled_at' => $first_enabled,
				'consent_last_changed_at'  => $now,
				'consent_source'           => Sanitizer::slug( $source, 48, 'unknown' ),
				'consent_notice_version'   => max( 1, $notice_version ),
			)
		);
	}

	public function disable( string $source, int $notice_version ): bool {
		return $this->state->set_many(
			array(
				'consent_status'          => 'disabled',
				'consent_version'         => self::VERSION,
				'consent_last_changed_at' => time(),
				'consent_source'          => Sanitizer::slug( $source, 48, 'unknown' ),
				'consent_notice_version'  => max( 1, $notice_version ),
			)
		);
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                          includes/signls/src/Identity.php                                                                    0000644                 00000005547 15266052676 0012774 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Stable installation and rotating device identity.
 *
 * @package signls-sdk
 * @license GPL-3.0-or-later
 */

namespace Signls\Sdk\V1;

defined( 'ABSPATH' ) || exit;

final class Identity {

	private $state;

	public function __construct( StateStore $state ) {
		$this->state = $state;
	}

	public function device_id(): string {
		$current_origin = $this->origin_fingerprint();
		$stored_origin  = (string) $this->state->get( 'origin_fingerprint', '' );
		$device_id      = (string) $this->state->get( 'device_id', '' );

		if ( ! self::valid_uuid( $device_id ) || ( '' !== $stored_origin && ! hash_equals( $stored_origin, $current_origin ) ) ) {
			$device_id = self::uuid4();
			$this->state->set_many(
				array(
					'device_id'          => $device_id,
					'origin_fingerprint' => $current_origin,
				)
			);
			$this->clear_credential();
		} elseif ( '' === $stored_origin ) {
			$this->state->set( 'origin_fingerprint', $current_origin );
		}

		return $device_id;
	}

	public function rotate_device(): string {
		$device_id = self::uuid4();
		$this->state->set_many(
			array(
				'device_id'          => $device_id,
				'origin_fingerprint' => $this->origin_fingerprint(),
			)
		);
		$this->clear_credential();
		return $device_id;
	}

	public function clear_credential(): void {
		$this->state->delete_keys(
			array(
				'credential_id',
				'credential_secret',
				'credential_key_id',
				'pending_sequence',
				'pending_body',
				'pending_body_hash',
				'pending_sdk_version',
				'pending_product_version',
				'last_acknowledged_sequence',
				'last_acknowledged_at',
				'last_success_at',
				'last_attempt_started_at',
				'last_attempt_finished_at',
				'last_http_status',
				'failure_class',
				'retry_attempt',
				'next_retry_at',
				'clock_offset',
			)
		);
	}

	public function forget_device(): void {
		$this->state->delete_keys( array( 'device_id', 'origin_fingerprint' ) );
		$this->clear_credential();
	}

	private function origin_fingerprint(): string {
		$host = function_exists( 'home_url' ) ? (string) wp_parse_url( home_url(), PHP_URL_HOST ) : 'local';
		$salt = function_exists( 'wp_salt' ) ? (string) wp_salt( 'auth' ) : 'signls-test-salt';
		global $wpdb;
		$prefix = isset( $wpdb->prefix ) ? (string) $wpdb->prefix : 'wp_';
		return hash_hmac( 'sha256', strtolower( $host ) . '|' . $prefix, $salt );
	}

	private static function uuid4(): string {
		$bytes    = random_bytes( 16 );
		$bytes[6] = chr( ( ord( $bytes[6] ) & 0x0f ) | 0x40 );
		$bytes[8] = chr( ( ord( $bytes[8] ) & 0x3f ) | 0x80 );
		$hex      = bin2hex( $bytes );
		return substr( $hex, 0, 8 ) . '-' . substr( $hex, 8, 4 ) . '-' . substr( $hex, 12, 4 ) . '-' . substr( $hex, 16, 4 ) . '-' . substr( $hex, 20 );
	}

	private static function valid_uuid( string $value ): bool {
		return 1 === preg_match( '/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/', $value );
	}
}
                                                                                                                                                         includes/signls/src/Collector.php                                                                   0000644                 00000016516 15266052676 0013127 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Closed product snapshot collector.
 *
 * @package signls-sdk
 * @license GPL-3.0-or-later
 */

namespace Signls\Sdk\V1;

defined( 'ABSPATH' ) || exit;

final class Collector {

	private const FAILURE_CLASSES = array(
		'transport_dns',
		'transport_tls',
		'transport_connect',
		'transport_timeout',
		'http_4xx',
		'http_429',
		'http_5xx',
		'auth',
		'configuration',
		'validation',
		'remote_rejected',
		'unknown',
	);

	public static function collect( ProductAdapterInterface $adapter ): array {
		$contract = $adapter->contract();
		$source   = $adapter->snapshot();
		$allowed  = self::allowlists( $contract );

		$payload        = array(
			'versions'         => self::versions( isset( $source['versions'] ) ? $source['versions'] : array(), $adapter ),
			'is_multisite'     => Sanitizer::bool( isset( $source['is_multisite'] ) ? $source['is_multisite'] : false ),
			'configured_units' => Sanitizer::counter( isset( $source['configured_units'] ) ? $source['configured_units'] : 0 ),
			'active_units'     => Sanitizer::counter( isset( $source['active_units'] ) ? $source['active_units'] : 0 ),
			'integrations'     => self::integrations( isset( $source['integrations'] ) ? $source['integrations'] : array(), $allowed['integrations'] ),
			'features'         => self::features( isset( $source['features'] ) ? $source['features'] : array(), $allowed['features'] ),
			'operation_health' => self::operations( isset( $source['operation_health'] ) ? $source['operation_health'] : array(), $allowed ),
			'companions'       => self::companions( isset( $source['companions'] ) ? $source['companions'] : array(), $allowed['companions'] ),
		);
		$schema_version = isset( $contract['snapshot_schema_version'] ) ? (int) $contract['snapshot_schema_version'] : 1;
		if ( 2 === $schema_version ) {
			$observation_source      = isset( $source['observations'] ) && is_array( $source['observations'] ) ? $source['observations'] : array();
			$observation_schema      = isset( $contract['observation_schema'] ) && is_array( $contract['observation_schema'] ) ? $contract['observation_schema'] : array();
			$payload['observations'] = ObservationSanitizer::sanitize( $observation_source, $observation_schema );
		}

		$payload['active_units'] = min( $payload['active_units'], $payload['configured_units'] );
		return $payload;
	}

	private static function allowlists( array $contract ): array {
		$result = array();
		foreach ( array( 'integrations', 'features', 'operations', 'companions' ) as $key ) {
			$result[ $key ] = array();
			$values         = isset( $contract[ $key ] ) && is_array( $contract[ $key ] ) ? $contract[ $key ] : array();
			foreach ( $values as $value ) {
				$slug = Sanitizer::slug( $value );
				if ( '' !== $slug ) {
					$result[ $key ][ $slug ] = true;
				}
			}
		}
		return $result;
	}

	private static function versions( $source, ProductAdapterInterface $adapter ): array {
		$source = is_array( $source ) ? $source : array();
		return array(
			'product'   => Sanitizer::version( $adapter->product_version() ),
			'wordpress' => Sanitizer::version( isset( $source['wordpress'] ) ? $source['wordpress'] : get_bloginfo( 'version' ) ),
			'php'       => Sanitizer::version( PHP_VERSION ),
			'cf7'       => Sanitizer::version( isset( $source['cf7'] ) ? $source['cf7'] : 'unknown' ),
		);
	}

	private static function integrations( $rows, array $allowlist ): array {
		$result = array();
		$rows   = is_array( $rows ) ? $rows : array();
		foreach ( $rows as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}
			$slug = Sanitizer::slug( isset( $row['slug'] ) ? $row['slug'] : '' );
			if ( '' === $slug || ! isset( $allowlist[ $slug ] ) || isset( $result[ $slug ] ) ) {
				continue;
			}
			$clean = array( 'slug' => $slug );
			foreach ( array( 'configured_units', 'credential_units', 'oauth_units', 'api_key_units', 'destination_units', 'mapping_count', 'attempts', 'successes', 'failures' ) as $key ) {
				$clean[ $key ] = Sanitizer::counter( isset( $row[ $key ] ) ? $row[ $key ] : 0 );
			}
			$result[ $slug ] = $clean;
		}
		return array_values( $result );
	}

	private static function features( $rows, array $allowlist ): array {
		$result = array();
		foreach ( is_array( $rows ) ? $rows : array() as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}
			$slug = Sanitizer::slug( isset( $row['slug'] ) ? $row['slug'] : '' );
			if ( '' === $slug || ! isset( $allowlist[ $slug ] ) || isset( $result[ $slug ] ) ) {
				continue;
			}
			$result[ $slug ] = array(
				'slug'             => $slug,
				'configured_units' => Sanitizer::counter( isset( $row['configured_units'] ) ? $row['configured_units'] : 0 ),
				'source'           => Sanitizer::slug( isset( $row['source'] ) ? $row['source'] : '', 48, 'unknown' ),
			);
		}
		return array_values( $result );
	}

	private static function operations( $rows, array $allowlists ): array {
		$result = array();
		foreach ( is_array( $rows ) ? $rows : array() as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}
			$integration = Sanitizer::slug( isset( $row['integration'] ) ? $row['integration'] : '' );
			$operation   = Sanitizer::slug( isset( $row['operation'] ) ? $row['operation'] : '' );
			$key         = $integration . '|' . $operation;
			if ( ! isset( $allowlists['integrations'][ $integration ], $allowlists['operations'][ $operation ] ) || isset( $result[ $key ] ) ) {
				continue;
			}
			$classes = array();
			$source  = isset( $row['failure_classes'] ) && is_array( $row['failure_classes'] ) ? $row['failure_classes'] : array();
			foreach ( self::FAILURE_CLASSES as $class ) {
				$classes[ $class ] = Sanitizer::counter( isset( $source[ $class ] ) ? $source[ $class ] : 0 );
			}
			$result[ $key ] = array(
				'integration'      => $integration,
				'operation'        => $operation,
				'last_success_age' => Sanitizer::age( isset( $row['last_success_age'] ) ? $row['last_success_age'] : null ),
				'last_failure_age' => Sanitizer::age( isset( $row['last_failure_age'] ) ? $row['last_failure_age'] : null ),
				'failure_classes'  => $classes,
			);
		}
		return array_values( $result );
	}

	private static function companions( $rows, array $allowlist ): array {
		$result = array();
		$states = array( 'active', 'expired', 'inactive', 'legacy_activated', 'not_present', 'unknown' );
		foreach ( is_array( $rows ) ? $rows : array() as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}
			$slug  = Sanitizer::slug( isset( $row['slug'] ) ? $row['slug'] : '' );
			$state = Sanitizer::slug( isset( $row['license_state'] ) ? $row['license_state'] : '', 48, 'unknown' );
			if ( '' === $slug || ! isset( $allowlist[ $slug ] ) || isset( $result[ $slug ] ) ) {
				continue;
			}
			$result[ $slug ] = array(
				'slug'                   => $slug,
				'installed'              => Sanitizer::bool( isset( $row['installed'] ) ? $row['installed'] : false ),
				'active'                 => Sanitizer::bool( isset( $row['active'] ) ? $row['active'] : false ),
				'version'                => Sanitizer::version( isset( $row['version'] ) ? $row['version'] : 'unknown' ),
				'license_state'          => in_array( $state, $states, true ) ? $state : 'unknown',
				'source'                 => Sanitizer::slug( isset( $row['source'] ) ? $row['source'] : '', 48, 'unknown' ),
				'observation_started_at' => Sanitizer::counter( isset( $row['observation_started_at'] ) ? $row['observation_started_at'] : time() ),
			);
		}
		return array_values( $result );
	}
}
                                                                                                                                                                                  includes/signls/loader.php                                                                          0000644                 00000005567 15266052677 0011665 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Collision-safe loader for vendored Signls SDK copies.
 *
 * @package signls-sdk
 * @license GPL-3.0-or-later
 */

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'Signls_Sdk_Loader', false ) ) {
	final class Signls_Sdk_Loader {

		private static $candidates = array();

		private static $descriptors = array();

		private static $booted = false;

		private static $hooked = false;

		public static function add_candidate( $version, $bootstrap, $minimum_php = '7.4.0' ) {
			if ( ! is_string( $version ) || ! is_string( $bootstrap ) || ! is_string( $minimum_php ) ) {
				return;
			}

			if ( ! is_file( $bootstrap ) || version_compare( PHP_VERSION, $minimum_php, '<' ) ) {
				return;
			}

			self::$candidates[ $version . '|' . $bootstrap ] = array(
				'version'   => $version,
				'bootstrap' => $bootstrap,
			);
		}

		public static function register( array $descriptor ) {
			$product = isset( $descriptor['product_slug'] ) ? (string) $descriptor['product_slug'] : '';
			if ( ! preg_match( '/^[a-z0-9][a-z0-9-]{1,63}$/', $product ) ) {
				return false;
			}

			self::$descriptors[ $product ] = $descriptor;
			self::schedule_boot();
			return true;
		}

		public static function boot() {
			if ( self::$booted ) {
				return;
			}

			self::$booted = true;
			$candidate    = self::best_candidate();
			if ( null === $candidate ) {
				return;
			}

			require_once $candidate['bootstrap'];
			if ( ! class_exists( 'Signls\\Sdk\\V1\\Runtime' ) ) {
				return;
			}

			foreach ( self::$descriptors as $descriptor ) {
				try {
					$adapter_file = isset( $descriptor['adapter_file'] ) ? (string) $descriptor['adapter_file'] : '';
					if ( '' !== $adapter_file ) {
						if ( ! is_file( $adapter_file ) ) {
							continue;
						}
						require_once $adapter_file;
					}
					\Signls\Sdk\V1\Runtime::boot( $descriptor );
				} catch ( \Throwable $error ) {
					// A product's observations must never break the host plugin.
					continue;
				}
			}
		}

		public static function selected_version() {
			$candidate = self::best_candidate();
			return null === $candidate ? null : $candidate['version'];
		}

		private static function schedule_boot() {
			if ( self::$booted ) {
				return;
			}

			if ( function_exists( 'did_action' ) && did_action( 'plugins_loaded' ) ) {
				self::boot();
				return;
			}

			if ( ! self::$hooked && function_exists( 'add_action' ) ) {
				self::$hooked = true;
				add_action( 'plugins_loaded', array( __CLASS__, 'boot' ), PHP_INT_MAX );
			}
		}

		private static function best_candidate() {
			if ( empty( self::$candidates ) ) {
				return null;
			}

			$candidates = array_values( self::$candidates );
			usort(
				$candidates,
				static function ( $left, $right ) {
					return version_compare( $right['version'], $left['version'] );
				}
			);

			return $candidates[0];
		}
	}
}

Signls_Sdk_Loader::add_candidate( '1.1.0', __DIR__ . '/sdk.php', '7.4.0' );
                                                                                                                                         includes/utils/class-cmatic-lite-get-fields.php                                                     0000644                 00000001362 15266052677 0015564 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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;
	}
}
                                                                                                                                                                                                                                                                              includes/utils/class-cmatic-remote-fetcher.php                                                      0000644                 00000016750 15266052677 0015526 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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,
		'headers'         => array(),
		'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;
		}

		$args    = array(
			'timeout'    => $this->config['timeout'],
			'user-agent' => 'WordPress/' . get_bloginfo( 'version' ) . '; ' . home_url(),
		);
		$headers = $this->request_headers();
		if ( ! empty( $headers ) ) {
			$args['headers'] = $headers;
		}

		$response = wp_remote_get( $this->config['url'], $args );

		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 );
	}

	private function request_headers() {
		$headers = $this->config['headers'];
		if ( ! is_array( $headers ) || count( $headers ) > 16 ) {
			return array();
		}

		$sanitized = array();
		foreach ( $headers as $name => $value ) {
			if ( ! is_string( $name )
				|| 1 !== preg_match( '/^[A-Za-z0-9-]{1,64}$/', $name )
				|| ( ! is_string( $value ) && ! is_int( $value ) ) ) {
				return array();
			}
			$value = (string) $value;
			if ( strlen( $value ) > 128 || false !== strpos( $value, "\r" ) || false !== strpos( $value, "\n" ) ) {
				return array();
			}
			$sanitized[ $name ] = $value;
		}

		return $sanitized;
	}

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

		foreach ( array( 'winback_discount', 'renew_discount' ) as $campaign_key ) {
			if ( isset( $json[ $campaign_key ] ) && (int) $json[ $campaign_key ] > 0 ) {
				$pricing_data[ $campaign_key ] = (int) $json[ $campaign_key ];
			}
		}

		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();
	}
}
                        includes/utils/class-cmatic-buster.php                                                              0000644                 00000002201 15266052677 0014103 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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( (string) hash_file( 'sha256', $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;
	}
}
                                                                                                                                                                                                                                                                                                                                                                                               includes/utils/class-cmatic-file-logger.php                                                         0000644                 00000005032 15266052677 0015000 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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';
		}
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      includes/utils/class-cmatic-pursuit.php                                                             0000644                 00000010032 15266052700 0014276 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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',
		'help'    => 'https://chimpmatic.com/help',
		'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 pricing(): array {
		$fetcher = new CMatic_Remote_Fetcher(
			array(
				'url'            => 'https://api.chimpmatic.com/promo',
				'cache_key'      => 'cmatic_pricing_data',
				'cache_duration' => DAY_IN_SECONDS,
				'headers'        => Cmatic_Lite_Service_Context::headers( 'promo_pricing_refresh' ),
				'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();
	}

	public static function discount(): int {
		$pricing = self::pricing();
		return max( 0, (int) ( $pricing['discount_percent'] ?? 0 ) );
	}

	public static function promo_checkout( string $content = '' ): string {
		$discount = self::discount();
		$source   = class_exists( 'Cmatic_Options_Repository' )
		? Cmatic_Options_Repository::get_option( 'install.id', '' )
		: '';
		return add_query_arg(
			array(
				'source' => $source,
				'promo'  => 'pro' . $discount,
			),
			self::promo( $content, $discount )
		);
	}

	public static function promo( string $content = '', ?int $discount = null ): string {
		if ( null === $discount ) { // Default follows the live promo, not a constant.
			$pricing  = self::pricing();
			$discount = (int) ( $pricing['discount_percent'] ?? 0 );
		}
		$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 ) ) ) );
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      includes/utils/class-cmatic-utils.php                                                               0000644                 00000003542 15266052700 0013733 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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' );
	}
}
                                                                                                                                                              includes/bootstrap.php                                                                              0000644                 00000000646 15266052700 0011111 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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;

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();
                                                                                          includes/core/class-cmatic-container.php                                                            0000644                 00000002766 15266052700 0014354 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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 ) {
		if ( isset( self::$services[ $id ] ) ) {
			return self::$services[ $id ];
		}

		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 {
		self::factory(
			Cmatic_Options_Interface::class,
			function () {
				return Cmatic_Options_Repository::instance();
			}
		);

		self::factory(
			'options',
			function () {
				return self::get( Cmatic_Options_Interface::class );
			}
		);

		self::factory(
			'auth.manager',
			function () {
				return new Cmatic_Lite_Auth_Manager();
			}
		);
	}
}
          includes/core/class-cmatic-migration.php                                                            0000644                 00000012143 15266052700 0014351 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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 ) {
		if ( ! empty( $data['api']['first_connected'] ) ) {
			return;
		}

		$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;
		}

		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'] ) {
				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 );
		}
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                             includes/core/class-cmatic-deactivator.php                                                          0000644                 00000003367 15266052700 0014675 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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;

	public function __construct() {
		$this->options = Cmatic_Options_Repository::instance();
	}

	public function deactivate() {
		$this->options->set( 'lifecycle.is_active', false );

		$this->record_deactivation();

		do_action( 'cmatic_deactivated' );
	}

	private function record_deactivation() {
		$source   = $this->options->get( 'lifecycle.deactivations', array() );
		$items    = is_array( $source ) && isset( $source['items'] ) && is_array( $source['items'] ) ? $source['items'] : ( is_array( $source ) ? $source : array() );
		$reported = is_array( $source ) && isset( $source['reported_total'] ) && is_scalar( $source['reported_total'] ) ? (int) $source['reported_total'] : count( $items );
		$total    = max( count( $items ), $reported );
		$items[]  = time();
		$items    = array_values(
			array_filter(
				array_map(
					static function ( $value ): int {
						return is_scalar( $value ) ? (int) $value : 0;
					},
					$items
				)
			)
		);
		sort( $items, SORT_NUMERIC );
		++$total;
		$items = array_slice( $items, -256 );
		$this->options->set(
			'lifecycle.deactivations',
			array(
				'items'          => $items,
				'reported_total' => $total,
				'truncated'      => $total > count( $items ),
			)
		);
		try {
			if ( class_exists( '\\Signls\\Sdk\\V1\\Runtime' ) ) {
				\Signls\Sdk\V1\Runtime::relevant_change( 'contact-form-7-mailchimp-extension' );
			}
		} catch ( Throwable $error ) {
			// Signals must never change the deactivation result.
			return;
		}
	}
}
                                                                                                                                                                                                                                                                         includes/core/class-cmatic-install-data.php                                                         0000644                 00000011655 15266052700 0014744 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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 const INSTALL_ID_OPTION = 'cmatic_install_id';

	private $options;

	public function __construct( Cmatic_Options_Repository $options ) {
		$this->options = $options;
	}

	public function ensure() {
		$data        = $this->options->get_all();
		$changed     = false;
		$regenerated = false;

		if ( ! isset( $data['install'] ) || ! is_array( $data['install'] ) ) {
			$data['install'] = array();
			$changed         = true;
		}

		if ( ! self::valid_install_id( isset( $data['install']['id'] ) ? $data['install']['id'] : '' ) ) {
			list( $data['install']['id'], $regenerated ) = $this->recover_or_generate_id();
			$changed                                     = true;
		}

		if ( $regenerated ) {
			$regen_count                       = isset( $data['install']['id_regen_count'] ) ? (int) $data['install']['id_regen_count'] : 0;
			$data['install']['id_regen_count'] = $regen_count + 1;
			$data['install']['id_regen_at']    = time();
		}

		$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 ( ! empty( $data['install']['id'] ) && get_option( self::INSTALL_ID_OPTION, '' ) === '' ) {
			update_option( self::INSTALL_ID_OPTION, $data['install']['id'], 'no' );
		}

		if ( $changed ) {
			$this->options->save( $data );
			update_option( self::INSTALL_ID_OPTION, $data['install']['id'], 'no' );
		}
	}

	public function get_install_id() {
		$install_id = $this->options->get( 'install.id', '' );
		if ( self::valid_install_id( $install_id ) ) {
			if ( get_option( self::INSTALL_ID_OPTION, '' ) === '' ) {
				update_option( self::INSTALL_ID_OPTION, $install_id, 'no' );
			}
			return $install_id;
		}

		list( $install_id, $regenerated ) = $this->recover_or_generate_id();

		$this->options->set( 'install.id', $install_id );
		update_option( self::INSTALL_ID_OPTION, $install_id, 'no' );

		if ( $regenerated ) {
			$regen_count = (int) $this->options->get( 'install.id_regen_count', 0 );
			$this->options->set( 'install.id_regen_count', $regen_count + 1 );
			$this->options->set( 'install.id_regen_at', time() );
		}

		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;
	}

	/**
	 * Three-tier install_id recovery with generation as last resort.
	 *
	 * @return array{0: string, 1: bool} The install_id and whether it was newly generated.
	 */
	private function recover_or_generate_id(): array {
		$standalone = get_option( self::INSTALL_ID_OPTION, '' );
		if ( self::valid_install_id( $standalone ) ) {
			return array( $standalone, false );
		}

		$raw     = get_option( 'cmatic', array() );
		$install = is_array( $raw ) && isset( $raw['install'] ) && is_array( $raw['install'] ) ? $raw['install'] : array();
		$legacy  = isset( $install['id'] ) && is_string( $install['id'] ) ? $install['id'] : '';
		if ( self::valid_install_id( $legacy ) ) {
			update_option( self::INSTALL_ID_OPTION, $legacy, 'no' );
			return array( $legacy, false );
		}

		return array( $this->generate_install_id(), true );
	}

	private function generate_install_id(): string {
		return bin2hex( random_bytes( 16 ) );
	}

	private static function valid_install_id( $install_id ): bool {
		return is_string( $install_id ) && 1 === preg_match( '/^(?:[a-f0-9]{12}|[a-f0-9]{32})$/', $install_id );
	}

	private function determine_quest( $data ) {
		$candidates = array();

		$loyalty = $this->options->get_legacy( 'mce_loyalty' );
		if ( is_array( $loyalty ) && ! empty( $loyalty[0] ) ) {
			$candidates[] = (int) $loyalty[0];
		}

		$activations = isset( $data['lifecycle']['activations'] ) ? $data['lifecycle']['activations'] : array();
		if ( ! empty( $activations ) && is_array( $activations ) ) {
			$candidates[] = (int) min( $activations );
		}

		$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;
		}

		$api_first = isset( $data['api']['first_connected'] ) ? (int) $data['api']['first_connected'] : 0;
		if ( $api_first >= self::MIN_VALID_TIMESTAMP ) {
			$candidates[] = $api_first;
		}

		$sub_first = isset( $data['submissions']['first'] ) ? (int) $data['submissions']['first'] : 0;
		if ( $sub_first >= self::MIN_VALID_TIMESTAMP ) {
			$candidates[] = $sub_first;
		}

		return ! empty( $candidates ) ? min( $candidates ) : time();
	}
}
                                                                                   includes/core/class-cmatic-activator.php                                                            0000644                 00000014161 15266052701 0014357 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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 $version;

	public function __construct( $version ) {
		$this->version      = $version;
		$this->options      = Cmatic_Options_Repository::instance();
		$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 );
	}

	public function activate() {
		$this->do_activation( true );
	}

	public function ensure_initialized() {
		$this->record_upgrade();

		if ( get_option( self::INITIALIZED_FLAG ) ) {
			$install_id = $this->options->get( 'install.id' );
			if ( ! empty( $install_id ) ) {
				return;
			}
			delete_option( self::INITIALIZED_FLAG );
		}

		$this->do_activation( false );
	}

	private function do_activation( $is_normal_activation ) {
		$this->install_data->ensure();

		$this->migration->run();

		$this->pro_status->update();

		$this->record_upgrade();

		$this->record_activation();

		if ( $is_normal_activation ) {
			$this->redirect->schedule();
		}

		$this->options->set( 'lifecycle.is_active', true );

		add_option( self::INITIALIZED_FLAG, true );

		do_action( 'cmatic_activated', $is_normal_activation );
	}

	private function record_activation() {
		$activations = $this->append_timestamp( 'lifecycle.activations', time() );

		$this->options->set( 'lifecycle.is_reactivation', $activations['reported_total'] > 1 );
		$this->relevant_change();
	}

	private function record_upgrade(): void {
		$current  = (string) $this->version;
		$previous = (string) $this->options->get( 'lifecycle.current_version', '' );
		if ( '' === $previous ) {
			$this->options->set( 'lifecycle.current_version', $current );
			return;
		}
		if ( $current === $previous ) {
			return;
		}

		$this->append_timestamp( 'lifecycle.upgrades', time() );
		$history   = $this->options->get( 'lifecycle.version_history', array() );
		$history   = is_array( $history ) ? $history : array();
		$history[] = array(
			'from'      => $previous,
			'to'        => $current,
			'timestamp' => time(),
		);
		$this->options->set( 'lifecycle.version_history', array_slice( $history, -256 ) );
		$this->options->set( 'lifecycle.previous_version', $previous );
		$this->options->set( 'lifecycle.current_version', $current );
		$this->relevant_change();
	}

	private function append_timestamp( string $key, int $timestamp ): array {
		$source   = $this->options->get( $key, array() );
		$items    = is_array( $source ) && isset( $source['items'] ) && is_array( $source['items'] ) ? $source['items'] : ( is_array( $source ) ? $source : array() );
		$reported = is_array( $source ) && isset( $source['reported_total'] ) && is_scalar( $source['reported_total'] ) ? (int) $source['reported_total'] : count( $items );
		$total    = max( count( $items ), $reported );
		$items[]  = $timestamp;
		$items    = array_values(
			array_filter(
				array_map(
					static function ( $value ): int {
						return is_scalar( $value ) ? (int) $value : 0;
					},
					$items
				)
			)
		);
		sort( $items, SORT_NUMERIC );
		++$total;
		$items = array_slice( $items, -256 );
		$value = array(
			'items'          => $items,
			'reported_total' => $total,
			'truncated'      => $total > count( $items ),
		);
		$this->options->set( $key, $value );
		return $value;
	}

	private function relevant_change(): void {
		try {
			if ( class_exists( '\\Signls\\Sdk\\V1\\Runtime' ) ) {
				\Signls\Sdk\V1\Runtime::relevant_change( 'contact-form-7-mailchimp-extension' );
			}
		} catch ( Throwable $error ) {
			// Signals must never change the activation result.
			return;
		}
	}

	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 ( ! $thinks_active ) {
			return;
		}

		if ( ! function_exists( 'is_plugin_active' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}

		$actually_active = is_plugin_active( SPARTAN_MCE_PLUGIN_BASENAME );

		if ( $actually_active ) {
			return;
		}

		$this->handle_missed_deactivation();
	}

	private function handle_missed_deactivation(): void {
		$this->options->set( 'lifecycle.is_active', false );

		$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 );

		Cmatic_Cron::unschedule();

		$this->relevant_change();

		delete_option( self::INITIALIZED_FLAG );

		do_action( 'cmatic_missed_deactivation_handled' );
	}

	public static function register_hooks( string $plugin_file, string $version ): void {
		register_activation_hook(
			$plugin_file,
			function () use ( $version ) {
				$activator = new self( $version );
				$activator->activate();
			}
		);

		register_deactivation_hook(
			$plugin_file,
			function () {
				$deactivator = new Cmatic_Deactivator();
				$deactivator->deactivate();
			}
		);

		add_action(
			'admin_init',
			function () use ( $version ) {
				$activator = new self( $version );

				$activator->verify_lifecycle_state();

				$activator->ensure_initialized();
				$activator->get_redirect()->maybe_redirect();
			},
			5
		);

		add_action(
			'plugins_loaded',
			function () use ( $version ) {
				$activator = new self( $version );
				$activator->get_pro_status()->update();
			},
			99
		);
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                               includes/core/class-cmatic-plugin.php                                                               0000644                 00000022406 15266052701 0013662 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?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;

// phpcs:disable Squiz.Commenting.ClassComment.Missing, Squiz.Commenting.FunctionComment.Missing, Squiz.Commenting.VariableComment.Missing -- Preserve the established documentation style in this legacy bootstrap class.
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 {
		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';
		require_once $this->dir . 'includes/interfaces/interface-cmatic-lite-esp-provider.php';
		require_once $this->dir . 'includes/interfaces/interface-cmatic-lite-esp-field-creator.php';
		require_once $this->dir . 'includes/interfaces/interface-cmatic-lite-esp-lookup.php';

		require_once $this->dir . 'includes/core/class-cmatic-container.php';

		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/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';

		require_once $this->dir . 'includes/auth/class-cmatic-lite-credentials.php';
		require_once $this->dir . 'includes/auth/class-cmatic-lite-auth-manager.php';
	}

	private function register_lifecycle_hooks(): void {
		Cmatic_Activator::register_hooks( $this->file, $this->version );
		register_deactivation_hook( $this->file, array( 'Cmatic_Lite_Auth_Manager', 'restore_all_api_keys' ) );
	}

	private function load_module_dependencies(): void {
		$modules = array(
			'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/class-cmatic-cf7-tags.php',
			'services/class-cmatic-cron.php',
			'services/class-cmatic-api-service.php',
			'services/class-cmatic-form-tags.php',
			'providers/class-cmatic-lite-esp-capabilities.php',
			'providers/abstract-class-cmatic-lite-esp-provider.php',
			'providers/class-cmatic-lite-esp-mailchimp.php',
			'providers/class-cmatic-lite-esp-brevo.php',
			'providers/class-cmatic-lite-esp-mailerlite.php',
			'providers/class-cmatic-lite-esp-klaviyo.php',
			'providers/class-cmatic-lite-esp-credentials.php',
			'providers/class-cmatic-lite-esp-registry.php',
			'providers/class-cmatic-lite-esp-manifest.php',
			'services/submission/class-cmatic-email-extractor.php',
			'services/submission/class-cmatic-mailerlite-routing-resolver.php',
			'services/submission/class-cmatic-mailerlite-runtime-policy.php',
			'services/submission/class-cmatic-consent-decision.php',
			'services/submission/class-cmatic-request-ip.php',
			'services/submission/class-cmatic-mailerlite-field-normalizer.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-mailerlite-degradation-reporter.php',
			'services/submission/class-cmatic-mailerlite-submission-pipeline.php',
			'services/class-cmatic-submission-handler.php',
			'api/class-cmatic-rest-lists.php',
			'api/class-cmatic-rest-settings.php',
			'api/class-cmatic-rest-form.php',
			'api/class-cmatic-lite-esp-rest-controller.php',
			'api/class-cmatic-rest-reset.php',
			'admin/class-cmatic-plugin-links.php',
			'admin/class-cmatic-deactivation-survey.php',
			'admin/class-cmatic-asset-loader.php',
			'admin/class-cmatic-admin-panel.php',
			'admin/class-cmatic-lite-esp-degrade-notice.php',
			'api/class-cmatic-log-viewer.php',
			'api/class-cmatic-contact-lookup.php',
			'api/class-cmatic-rest-oauth.php',
			'api/class-cmatic-submission-feedback.php',
			'ui/class-cmatic-header.php',
			'ui/class-cmatic-api-panel.php',
			'ui/class-cmatic-lite-esp-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-pro-showcase.php',
			'ui/class-cmatic-banners.php',
			'ui/class-cmatic-form-classes.php',
			'ui/class-cmatic-dom-classes.php',
			'ui/class-cmatic-field-mapper.php',
			'ui/class-cmatic-sidebar-panel.php',
			'ui/class-cmatic-license-banner.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 {
		Cmatic_Lite_Container::boot();

		Cmatic_CF7_Dependency::init();
		Cmatic_License_Banner::init();
		Cmatic_Pro_Syncer::init();

		Cmatic_Log_Viewer::init( 'chimpmatic-lite', '[Chimpmatic Lite]', 'chimpmatic-lite' );

		Cmatic_Rest_Lists::init();
		Cmatic_Rest_Settings::init();
		Cmatic_Rest_Form::init();
		Cmatic_Lite_Esp_Rest_Controller::init();
		Cmatic_Rest_Reset::init();

		Cmatic_Contact_Lookup::init();
		Cmatic_Rest_OAuth::init();
		Cmatic_Submission_Feedback::init();

		Cmatic_Deactivation_Survey::init_lite();
		Cmatic_Asset_Loader::init();

		Cmatic_CF7_Tags::init();
		Cmatic_Admin_Panel::init();
		Cmatic_Lite_Esp_Degrade_Notice::init();
		Cmatic_Submission_Handler::init();
		Cmatic_Banners::init();
		Cmatic_Form_Classes::init();
		Cmatic_Dom_Classes::init();

		Cmatic_Cron::init( $this->file );
		Cmatic_Plugin_Links::init( $this->basename );
	}

	private function load_late_dependencies(): void {
		require_once $this->dir . 'includes/ui/class-cmatic-modal.php';
		require_once $this->dir . 'includes/ui/class-cmatic-test-submission-modal.php';

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

		require_once $this->dir . 'includes/admin/class-cmatic-lite-signls-privacy.php';
		require_once $this->dir . 'includes/services/signls/class-cmatic-lite-signls-contract.php';
		require_once $this->dir . 'includes/services/signls/class-cmatic-lite-signls-snapshot.php';
		require_once $this->dir . 'includes/services/signls/class-cmatic-lite-signls-failure-reason.php';
		require_once $this->dir . 'includes/services/signls/class-cmatic-lite-signls-site-collector.php';
		require_once $this->dir . 'includes/services/signls/class-cmatic-lite-signls-inventory-collector.php';
		require_once $this->dir . 'includes/services/signls/class-cmatic-lite-signls-performance-collector.php';
		require_once $this->dir . 'includes/services/signls/class-cmatic-lite-signls-product-collector.php';
		require_once $this->dir . 'includes/services/signls/class-cmatic-lite-signls-forms-collector.php';
		require_once $this->dir . 'includes/signls/loader.php';
		require_once $this->dir . 'includes/services/class-cmatic-lite-service-context.php';
	}

	private function initialize_late_components(): void {
		$test_submission_modal = new Cmatic_Test_Submission_Modal();
		$test_submission_modal->init();

		Cmatic_Notification_Center::get();
		Cmatic_Admin_Bar_Menu::instance();

		Cmatic_Lite_Signls_Privacy::init();
		$signls_registered = Signls_Sdk_Loader::register(
			array(
				'product_slug'      => 'contact-form-7-mailchimp-extension',
				'main_file'         => $this->file,
				'sdk_path'          => $this->dir . 'includes/signls',
				'sdk_version'       => '1.1.0',
				'adapter_file'      => $this->dir . 'includes/services/class-cmatic-lite-signls-adapter.php',
				'adapter_class'     => 'Cmatic_Lite_Signls_Adapter',
				'consent_reader'    => array( 'Cmatic_Lite_Signls_Privacy', 'consent_status' ),
				'install_id_reader' => static function (): string {
					return ( new Cmatic_Lite_Signls_Adapter() )->install_id();
				},
			)
		);
		if ( $signls_registered ) {
			if ( did_action( 'plugins_loaded' ) ) {
				Cmatic_Lite_Signls_Privacy::sync_sdk_consent();
			} else {
				add_action( 'plugins_loaded', array( 'Cmatic_Lite_Signls_Privacy', 'sync_sdk_consent' ), PHP_INT_MAX );
			}
		}
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          