__( 'Insufficient permissions.', 'advanced-form-integration' ),
), 403 );
}
}
/**
* Redirects the user to a given URL via inline JavaScript.
*
* All in-tree callers pass admin-built URLs, but we double-escape on the way
* out anyway: esc_url_raw() canonicalizes the URL string, esc_js() neutralizes
* any quote/newline characters that would otherwise break out of the string
* literal in the inline ';
echo $string;
}
/**
* Site-local "now" as a Unix-style timestamp.
*
* Behaviour-identical, WPCS-clean drop-in for the no-longer-recommended
* current_time( 'timestamp' ): real epoch time shifted by the site's
* configured UTC offset. Use only where a *local* wall-clock value is
* needed — for a true UTC timestamp use time().
*
* @return int
*/
function adfoin_local_timestamp() {
return time() + (int) (get_option( 'gmt_offset' ) * HOUR_IN_SECONDS);
}
/**
* Retrieves an array of supported form providers for the plugin.
* @return array An array containing the names and display labels of supported form providers.
*/
function adfoin_get_form_providers() {
$providers = array(
'academylms' => 'Academy LMS',
'acf' => 'Advanced Custom Fields (ACF)',
'advancedcoupons' => 'Advanced Coupons for WooCommerce',
'affiliatewp' => 'AffiliateWP',
'aiowpsecurity' => 'All-In-One Security (AIOS)',
'amelia' => 'Amelia',
'anspress' => 'AnsPress',
'appointmenthourbooking' => 'Appointment Hour Booking',
'arforms' => 'ARForms',
'armember' => 'ARMember',
'asgarosforum' => 'Asgaros Forum',
'avadaforms' => 'Avada Forms',
'awesomesupport' => 'Awesome Support',
'bbpress' => 'bbPress',
'beaver' => 'Beaver Form',
'bitform' => 'BitForm',
'bookly' => 'Bookly',
'breakdance' => 'Breakdance Form',
'bricks' => 'Bricks Form',
'buddyboss' => 'BuddyBoss',
'buddypress' => 'BuddyPress',
'businessdirectory' => 'Business Directory Plugin',
'calderaforms' => 'Caldera Forms',
'cartflows' => 'CartFlows',
'cf7' => 'Contact Form 7',
'charitable' => 'Charitable',
'civicrms' => 'CiviCRM',
'convertpro' => 'ConvertPro Forms',
'coolformkit' => 'Cool FormKit',
'crowdsignalforms' => 'Crowdsignal Forms',
'customerreviewswoocommerce' => 'Customer Reviews for WooCommerce',
'defendersecurity' => 'Defender Security',
'depicter' => 'Depicter',
'digimember' => 'DigiMember',
'diviform' => 'Divi Form',
'dokan' => 'Dokan',
'downloadmanager' => 'Download Manager',
'downloadmonitor' => 'Download Monitor',
'easyaffiliate' => 'Easy Affiliate',
'easyappointments' => 'Easy Appointments',
'edd' => 'Easy Digital Downloads',
'eform' => 'eForm',
'elementorpro' => 'Elementor Pro Form',
'emailsubscribers' => 'Email Subscribers & Newsletters',
'eventespressodecaf' => 'Event Espresso Decaf',
'eventin' => 'Eventin',
'eventsmanager' => 'Events Manager',
'eventtickets' => 'Event Tickets',
'everestforms' => 'Everest Forms',
'fluentaffiliate' => 'Fluent Affiliate',
'fluentboards' => 'Fluent Boards',
'fluentbooking' => 'Fluent Booking',
'fluentcart' => 'FluentCart',
'fluentcommunity' => 'Fluent Community',
'fluentcrm' => 'FluentCRM',
'fluentforms' => 'Fluent Forms',
'fluentsecurity' => 'FluentAuth',
'fluentsmtp' => 'FluentSMTP',
'fluentsupport' => 'Fluent Support',
'fooevents' => 'FooEvents',
'formcraft' => 'FormCraft 3',
'formcraftb' => 'FormCraft Basic',
'formidable' => 'Formidable Forms',
'forminator' => 'Forminator (forms only)',
'formlayer' => 'FormLayer',
'formmaker' => 'Form Maker by 10Web',
'fundraisingforwoocommerce' => 'Fundraising for WooCommerce',
'funnelbuilder' => 'FunnelKit Funnel Builder',
'funnelkit' => 'FunnelKit Automations',
'gamipress' => 'GamiPress',
'givewp' => 'GiveWP',
'gravityforms' => 'Gravity Forms',
'groundhogg' => 'Groundhogg',
'gutenaforms' => 'Gutena Forms',
'gutenverseform' => 'Gutenverse Form',
'happyforms' => 'Happy Forms',
'hashform' => 'Hash Form',
'htcontactform' => 'HT Contact Form',
'htmlforms' => 'HTML Forms',
'hustle' => 'Hustle',
'jetformbuilder' => 'JetFormBuilder',
'jetpackcrm' => 'Jetpack CRM',
'jetpackforms' => 'Jetpack Forms',
'kadence' => 'Kadence Blocks Form',
'kaliforms' => 'Kali Forms',
'kkstarratings' => 'kk Star Ratings',
'latepoint' => 'LatePoint',
'learndash' => 'LearnDash',
'learnpress' => 'LearnPress',
'lifterlms' => 'LifterLMS',
'liveforms' => 'Live Forms',
'mailpoet' => 'MailPoet',
'masterstudy' => 'MasterStudy LMS',
'memberpress' => 'MemberPress',
'metform' => 'Metform',
'mptimetable' => 'Timetable and Event Schedule',
'mwwpform' => 'MW WP Form',
'mycred' => 'myCred',
'mystickyelements' => 'My Sticky Elements',
'newsletter' => 'Newsletter',
'nexforms' => 'NEX-Forms',
'nextendsociallogin' => 'Nextend Social Login and Register',
'ninjaforms' => 'Ninja Forms',
'ninjatables' => 'Ninja Tables',
'otterforms' => 'Otter Blocks Form',
'paidmembershippro' => 'Paid Memberships Pro',
'peepso' => 'PeepSo',
'prettylinks' => 'PrettyLinks',
'profilepress' => 'ProfilePress',
'qsm' => 'Quiz And Survey Master (QSM)',
'quform' => 'Quform',
'quillforms' => 'QuillForms',
'rafflepress' => 'RafflePress',
'registrationmagic' => 'RegistrationMagic',
'restrictcontentpro' => 'Restrict Content Pro',
'restropress' => 'RestroPress',
'romethemeform' => 'RomeTheme Form',
'senseilms' => 'Sensei LMS',
'simplemembership' => 'Simple Membership',
'simplyscheduleappointments' => 'Simply Schedule Appointments',
'siteorigincontact' => 'SiteOrigin Contact Form',
'sitereviews' => 'Site Reviews',
'slicewp' => 'SliceWP',
'smartforms' => 'Smart Forms',
'snowmonkeyforms' => 'Snow Monkey Forms',
'solidsecurity' => 'Kadence Security (formerly Solid Security / iThemes Security)',
'spectraproforms' => 'Spectra Pro Forms',
'strongtestimonials' => 'Strong Testimonials',
'surecart' => 'SureCart',
'sureforms' => 'SureForms',
'suremembers' => 'SureMembers',
'tawkto' => 'Tawk.to',
'temporarylogin' => 'Temporary Login Without Password',
'theeventscalendar' => 'The Events Calendar',
'thrivequizbuilder' => 'Thrive Quiz Builder',
'thriveapprentice' => 'Thrive Apprentice',
'thriveleads' => 'Thrive Leads',
'tickera' => 'Tickera',
'tutorlms' => 'Tutor LMS',
'twofactor' => 'Two Factor',
'ultimatemember' => 'Ultimate Member',
'updraftplus' => 'UpdraftPlus',
'userfeedback' => 'UserFeedback',
'userregistration' => 'User Registration',
'verysimplecontactform' => 'VS Contact Form',
'wcboostwishlist' => 'WCBoost Wishlist',
'webbabookinglite' => 'Webba Booking',
'weforms' => 'weForms',
'wooabandonedcart' => 'Cart Abandonment Recovery for WooCommerce',
'woocommerce' => 'WooCommerce',
'woocommerceanalytics' => 'WooCommerce Analytics',
'woocommercebookings' => 'WooCommerce Bookings',
'woocommercememberships' => 'WooCommerce Memberships',
'woocommercesubscriptions' => 'WooCommerce Subscriptions',
'wordpress' => 'WordPress',
'wpbookingcalendar' => 'WP Booking Calendar',
'wpcsmartwishlist' => 'WPC Smart Wishlist for WooCommerce',
'wpdiscuz' => 'wpDiscuz',
'wpforms' => 'WPForms',
'wpforo' => 'wpForo',
'wpjobmanager' => 'WP Job Manager',
'wplms' => 'WPLMS',
'wpmembers' => 'WP-Members',
'wppizza' => 'WP Pizza',
'wppolls' => 'WP-Polls',
'wppostratings' => 'WP Post Ratings',
'wprecipemaker' => 'WP Recipe Maker',
'wpsimplepay' => 'WP Simple Pay',
'wptravelengine' => 'WP Travel Engine',
'wpuf' => 'WP User Frontend',
'wpulike' => 'WP ULike',
'wpusermanager' => 'WP User Manager',
'wpzoomforms' => 'WPZOOM Forms',
'wsform' => 'WS Form',
'yithwishlist' => 'YITH WooCommerce Wishlist',
);
return apply_filters( 'adfoin_form_providers', $providers );
}
function adfoin_get_trigger_keys() {
$form_providers = adfoin_get_form_providers();
$provider_keys = array_keys( $form_providers );
return $provider_keys;
}
/**
* Retrieves the available form integration action providers.
*
* @return array The available form integration action providers.
*/
function adfoin_get_actions() {
$actions = array();
return apply_filters( 'adfoin_action_providers', $actions );
}
/**
* Retrieves the available form integration action providers as an associative array with provider key as key and provider title as value.
*
* @return array The available form integration action providers as an associative array with provider key as key and provider title as value.
*/
function adfoin_get_action_providers() {
$actions = adfoin_get_actions();
$providers = array();
foreach ( $actions as $key => $value ) {
$providers[$key] = $value['title'];
}
return $providers;
}
/**
* Returns form providers as an alphabetically sorted (by visible label,
* case-insensitive, locale aware) list of { value, label } items suitable
* for the Vue component.
*
* @since 1.128.1
* @return array
*/
function adfoin_get_form_providers_array() {
$providers = adfoin_get_form_providers();
if ( !is_array( $providers ) ) {
return array();
}
natcasesort( $providers );
$list = array();
foreach ( $providers as $key => $label ) {
$list[] = array(
'value' => (string) $key,
'label' => (string) $label,
);
}
return $list;
}
/**
* Returns action providers as an alphabetically sorted (by visible label,
* case-insensitive, locale aware) list of { value, label } items suitable
* for the Vue component.
*
* @since 1.128.1
* @return array
*/
function adfoin_get_action_providers_array() {
$providers = adfoin_get_action_providers();
if ( !is_array( $providers ) ) {
return array();
}
natcasesort( $providers );
$list = array();
foreach ( $providers as $key => $label ) {
$list[] = array(
'value' => (string) $key,
'label' => (string) $label,
);
}
return $list;
}
/**
* Compute health stats for a single integration over the last $days days.
*
* Returns:
* total int submissions counted in window
* success int 2xx responses in window
* failure int non-2xx responses in window
* success_rate int|null percentage 0–100 (null if total==0)
* series int[] per-day counts oldest→newest, length = $days
* last_run_time string|null MySQL datetime of most recent log
* last_run_ok bool true if most recent log was 2xx
* last_log_id int|null id of most recent log row (for Resend)
* last_request string|null raw request_data JSON for Resend
*
* @since 1.128.1
* @param int $integration_id
* @param int $days Window length in days (default 30).
* @return array|null
*/
function adfoin_get_integration_stats( $integration_id, $days = 30 ) {
global $wpdb;
$integration_id = (int) $integration_id;
$days = max( 1, (int) $days );
if ( $integration_id <= 0 ) {
return null;
}
$log_table = $wpdb->prefix . 'adfoin_log';
// Total + success counts in window.
$row = $wpdb->get_row( $wpdb->prepare( "SELECT COUNT(*) AS total,\n SUM( CASE WHEN SUBSTRING( response_code, 1, 1 ) = '2' THEN 1 ELSE 0 END ) AS success\n FROM {$log_table}\n WHERE integration_id = %d\n AND time >= DATE_SUB( NOW(), INTERVAL %d DAY )", $integration_id, $days ), ARRAY_A );
$total = ( $row ? (int) $row['total'] : 0 );
$success = ( $row ? (int) $row['success'] : 0 );
$failure = max( 0, $total - $success );
$success_rate = ( $total > 0 ? (int) round( $success / $total * 100 ) : null );
// Per-day counts.
$daily = $wpdb->get_results( $wpdb->prepare( "SELECT DATE( time ) AS day, COUNT(*) AS cnt\n FROM {$log_table}\n WHERE integration_id = %d\n AND time >= DATE_SUB( NOW(), INTERVAL %d DAY )\n GROUP BY DATE( time )\n ORDER BY DATE( time ) ASC", $integration_id, $days ), ARRAY_A );
$by_day = array();
if ( is_array( $daily ) ) {
foreach ( $daily as $d ) {
$by_day[$d['day']] = (int) $d['cnt'];
}
}
$series = array();
for ($i = $days - 1; $i >= 0; $i--) {
$key = date( 'Y-m-d', strtotime( "-{$i} days", adfoin_local_timestamp() ) );
$series[] = ( isset( $by_day[$key] ) ? $by_day[$key] : 0 );
}
// Last run + raw request data (for Resend).
$last = $wpdb->get_row( $wpdb->prepare( "SELECT id, response_code, time, request_data\n FROM {$log_table}\n WHERE integration_id = %d\n ORDER BY id DESC\n LIMIT 1", $integration_id ), ARRAY_A );
$last_run_time = ( $last ? $last['time'] : null );
$last_run_ok = $last && !empty( $last['response_code'] ) && '2' === substr( (string) $last['response_code'], 0, 1 );
$last_log_id = ( $last ? (int) $last['id'] : null );
$last_request = ( $last ? $last['request_data'] : null );
return array(
'total' => $total,
'success' => $success,
'failure' => $failure,
'success_rate' => $success_rate,
'series' => $series,
'last_run_time' => $last_run_time,
'last_run_ok' => $last_run_ok,
'last_log_id' => $last_log_id,
'last_request' => $last_request,
'window_days' => $days,
);
}
/**
* Return integration IDs that recorded at least one non-2xx response
* in the last N days. Used by the "Failing" tab on the integrations
* list table so the user can jump straight to the integrations that
* need attention.
*
* Definition of "failing" here is intentionally broad: any non-2xx
* (or empty) response within the window. We don't restrict to "last
* run failed" because a single transient blip would mask earlier
* failures the user still wants to investigate.
*
* @since 1.128.3
* @param int $days Window size, default 7. Clamped to >= 1.
* @return int[] Distinct integration IDs.
*/
function adfoin_get_failing_integration_ids( $days = 7 ) {
global $wpdb;
$days = max( 1, (int) $days );
// Memoized per request — the integrations list table reads the same window
// twice per pageload (the "Failing" tab count and the per-row badges), and
// each call is a date-range scan + DISTINCT.
static $cache = array();
if ( isset( $cache[$days] ) ) {
return $cache[$days];
}
$log_table = $wpdb->prefix . 'adfoin_log';
$ids = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT integration_id\n FROM {$log_table}\n WHERE time >= DATE_SUB( NOW(), INTERVAL %d DAY )\n AND ( response_code IS NULL\n OR response_code = ''\n OR SUBSTRING( response_code, 1, 1 ) != '2' )", $days ) );
$result = ( empty( $ids ) ? array() : array_values( array_filter( array_map( 'intval', $ids ) ) ) );
$cache[$days] = $result;
return $result;
}
/**
* Bulk version of the health portion of adfoin_get_integration_stats.
* Used by the integrations list table to render the per-row Health
* column without firing N+1 queries — one aggregate query for the
* window totals and one for each integration's most recent log row.
*
* Returned shape (keyed by integration_id):
* array(
* 12 => array(
* 'total' => 7,
* 'success' => 6,
* 'failure' => 1,
* 'success_rate' => 86, // null when total=0
* 'last_run_time' => '2026-04-29 04:01:33', // null when never run
* 'last_run_ok' => true, // null when never run
* 'window_days' => 7,
* ),
* ...
* )
*
* Integrations with no log activity in the window appear in the
* result with zero totals + null last_run_*; callers can render the
* "Never run" state without separate lookups.
*
* @since 1.128.3
* @param int[] $ids Integration IDs.
* @param int $days Window size for totals/success rate (default 7).
* @return array
*/
function adfoin_get_integration_health_bulk( $ids, $days = 7 ) {
global $wpdb;
$ids = array_values( array_filter( array_map( 'intval', (array) $ids ) ) );
$days = max( 1, (int) $days );
// Pre-seed every requested ID so callers can rely on isset()
// without checking. Empty rows render as "Never run" downstream.
$out = array();
foreach ( $ids as $id ) {
$out[$id] = array(
'total' => 0,
'success' => 0,
'failure' => 0,
'success_rate' => null,
'last_run_time' => null,
'last_run_ok' => null,
'window_days' => $days,
);
}
if ( empty( $ids ) ) {
return $out;
}
$log_table = $wpdb->prefix . 'adfoin_log';
$placeholders = implode( ', ', array_fill( 0, count( $ids ), '%d' ) );
// ---- Window totals + successes per integration ----
$totals_sql = $wpdb->prepare( "SELECT integration_id,\n COUNT(*) AS total,\n SUM( CASE WHEN SUBSTRING( response_code, 1, 1 ) = '2' THEN 1 ELSE 0 END ) AS success\n FROM {$log_table}\n WHERE integration_id IN ({$placeholders})\n AND time >= DATE_SUB( NOW(), INTERVAL %d DAY )\n GROUP BY integration_id", array_merge( $ids, array($days) ) );
$totals = $wpdb->get_results( $totals_sql, ARRAY_A );
if ( is_array( $totals ) ) {
foreach ( $totals as $row ) {
$iid = (int) $row['integration_id'];
if ( !isset( $out[$iid] ) ) {
continue;
}
$total = (int) $row['total'];
$success = (int) $row['success'];
$out[$iid]['total'] = $total;
$out[$iid]['success'] = $success;
$out[$iid]['failure'] = max( 0, $total - $success );
$out[$iid]['success_rate'] = ( $total > 0 ? (int) round( $success / $total * 100 ) : null );
}
}
// ---- Most recent log row per integration ----
// Latest log id per integration via correlated subquery; works on
// older MySQLs that don't support window functions.
$last_sql = $wpdb->prepare( "SELECT l.integration_id, l.response_code, l.time\n FROM {$log_table} l\n INNER JOIN (\n SELECT integration_id, MAX(id) AS max_id\n FROM {$log_table}\n WHERE integration_id IN ({$placeholders})\n GROUP BY integration_id\n ) m ON m.max_id = l.id", $ids );
$last_rows = $wpdb->get_results( $last_sql, ARRAY_A );
if ( is_array( $last_rows ) ) {
foreach ( $last_rows as $row ) {
$iid = (int) $row['integration_id'];
if ( !isset( $out[$iid] ) ) {
continue;
}
$out[$iid]['last_run_time'] = $row['time'];
$out[$iid]['last_run_ok'] = !empty( $row['response_code'] ) && '2' === substr( (string) $row['response_code'], 0, 1 );
}
}
return $out;
}
/**
* Render an inline SVG sparkline for a numeric series.
* Uses currentColor so the host can theme it via CSS.
*
* @since 1.128.1
* @param int[] $series
* @param int $width
* @param int $height
* @return string HTML
*/
function adfoin_render_sparkline( $series, $width = 120, $height = 28 ) {
if ( empty( $series ) || !is_array( $series ) ) {
return '';
}
$count = count( $series );
$max = max( $series );
$width = max( 1, (int) $width );
$height = max( 8, (int) $height );
$points = array();
if ( $max <= 0 ) {
// All zero — flat baseline.
$y = $height - 2;
$points[] = '0,' . $y;
$points[] = $width . ',' . $y;
} else {
for ($i = 0; $i < $count; $i++) {
$x = ( $count > 1 ? round( $i / ($count - 1) * $width, 2 ) : 0 );
$y = $height - round( $series[$i] / $max * ($height - 4) ) - 2;
$points[] = $x . ',' . $y;
}
}
return sprintf(
'',
$width,
$height,
esc_attr( implode( ' ', $points ) )
);
}
/**
* Retrieves the available form integration action tasks for the specified provider.
*
* @param string $provider The provider key for which the action tasks should be retrieved.
*
* @return array The available form integration action tasks for the specified provider.
*/
function adfoin_get_action_tasks( $provider = '' ) {
$actions = adfoin_get_actions();
$tasks = array();
if ( $provider ) {
foreach ( $actions as $key => $value ) {
if ( $provider == $key ) {
$tasks = $value['tasks'];
}
}
}
return $tasks;
}
/**
* Returns an array of available settings tabs for the Advanced Form Integration plugin.
*
* @return array An array of available settings tabs.
*/
function adfoin_get_settings_tabs() {
$tabs = array(
'general' => __( 'General', 'advanced-form-integration' ),
);
return apply_filters( 'adfoin_settings_tabs', $tabs );
}
/**
* Returns an array of supported integrations and their titles.
* @return array An array of integrations and their titles.
*/
function adfoin_get_action_platform_list() {
return array(
'academylms' => 'Academy LMS',
'acculynx' => 'AccuLynx',
'acelle' => 'Acelle Mail',
'acfac' => 'Advanced Custom Fields',
'activecampaign' => 'ActiveCampaign',
'acuity' => 'Acuity Scheduling',
'acumbamail' => 'Acumbamail',
'addcal' => 'AddCal',
'adobecampaign' => 'Adobe Campaign / Journey Optimizer',
'adobeconnect' => 'Adobe Connect',
'affiliatewp' => 'AffiliateWP',
'agilecrm' => 'Agile CRM',
'aircall' => 'Aircall',
'airmeet' => 'Airmeet',
'airtable' => 'Airtable',
'apollo' => 'Apollo.io',
'appointmenthourbooking' => 'Appointment Hour Booking',
'apptivo' => 'Apptivo',
'asana' => 'Asana',
'attentive' => 'Attentive',
'attio' => 'Attio CRM',
'audienceful' => 'Audienceful',
'autopilot' => 'Autopilot',
'autopilotnew' => 'Ortto',
'aweber' => 'Aweber',
'awesomesupportac' => 'Awesome Support',
'bbpress' => 'bbPress',
'beehiiv' => 'beehiiv',
'benchmark' => 'Benchmark',
'bigin' => 'Bigin',
'bigmarker' => 'BigMarker',
'bigpurpledot' => 'Big Purple Dot',
'bloomerang' => 'Bloomerang',
'bombbomb' => 'BombBomb',
'braze' => 'Braze',
'breezechms' => 'Breeze ChMS',
'brevo' => 'Brevo',
'brokermint' => 'Brokermint',
'buddyboss' => 'BuddyBoss',
'buddypressac' => 'BuddyPress',
'cakemail' => 'Cakemail',
'calcom' => 'Cal.com',
'calendly' => 'Calendly',
'campaigner' => 'Campaigner',
'campaignmonitor' => 'Campaign Monitor',
'campayn' => 'Campayn',
'capsulecrm' => 'Capsule CRM',
'charitable' => 'Charitable',
'cinc' => 'CINC',
'civicrm' => 'CiviCRM',
'cleverreach' => 'CleverReach',
'clickup' => 'Clickup',
'clinchpad' => 'ClinchPad',
'clio' => 'Clio',
'close' => 'Close',
'companyhub' => 'CompanyHub',
'constantcontact' => 'Constant Contact',
'convertkit' => 'ConvertKit',
'copernica' => 'Copernica',
'copper' => 'Copper',
'courrielleur' => 'Courrielleur',
'crisp' => 'Crisp',
'curated' => 'Curated',
'customerio' => 'Customer.io',
'dataverse' => 'Dataverse (Generic)',
'dealmachine' => 'DealMachine',
'demio' => 'Demio',
'deputy' => 'Deputy',
'directiq' => 'DirectIQ',
'discord' => 'Discord',
'docusign' => 'DocuSign',
'donorbox' => 'Donorbox',
'donorperfect' => 'DonorPerfect',
'doppler' => 'Doppler',
'dotdigital' => 'Dotdigital',
'dotloop' => 'Dotloop',
'drchrono' => 'DrChrono',
'drip' => 'Drip',
'dropbox' => 'Dropbox',
'dynamics365' => 'Dynamics 365 CRM',
'dynamics365customerservice' => 'Dynamics 365 Customer Service',
'dynamics365fieldservice' => 'Dynamics 365 Field Service',
'dynamics365marketing' => 'Dynamics 365 Marketing',
'dynamics365sales' => 'Dynamics 365 Sales',
'easysendy' => 'EasySendy',
'economic' => 'e-conomic',
'eddac' => 'Easy Digital Downloads',
'elasticemail' => 'Elastic Email',
'emailchef' => 'Emailchef',
'emailfig' => 'Emailfig',
'emailit' => 'Emailit',
'emailoctopus' => 'EmailOctopus',
'emma' => 'Emma',
'employmenthero' => 'Employment Hero',
'encharge' => 'Encharge',
'encompass' => 'Encompass (ICE Mortgage)',
'engagebay' => 'EngageBay',
'enormail' => 'Enormail',
'erp' => 'WP ERP',
'eventbrite' => 'Eventbrite',
'eventinac' => 'Eventin',
'eventsmanager' => 'Events Manager',
'everwebinar' => 'EverWebinar',
'ewebinar' => 'eWebinar',
'ezlynx' => 'EZLynx',
'eztexting' => 'EZ Texting',
'fieldnation' => 'Field Nation',
'filevine' => 'Filevine',
'flamingo' => 'Flamingo',
'flodesk' => 'Flodesk',
'flowlu' => 'Flowlu',
'fluentaffiliate' => 'Fluent Affiliate',
'fluentboards' => 'Fluent Boards',
'fluentbooking' => 'Fluent Booking',
'fluentcartac' => 'Fluent Cart',
'fluentcommunity' => 'Fluent Community',
'fluentcrm' => 'Fluent CRM',
'fluentsupport' => 'Fluent Support',
'followupboss' => 'FollowUpBoss',
'fortnox' => 'Fortnox',
'freeagent' => 'FreeAgent',
'freshbooks' => 'FreshBooks',
'freshdesk' => 'Freshdesk',
'freshsales' => 'Freshworks CRM',
'front' => 'Front',
'ga4' => 'Google Analytics 4',
'gamipress' => 'GamiPress',
'getresponse' => 'GetResponse',
'gist' => 'Gist',
'givebutter' => 'Givebutter',
'givewp' => 'GiveWP',
'googlecalendar' => 'Google Calendar',
'googledrive' => 'Google Drive',
'googlesheets' => 'Google Sheets',
'googletasks' => 'Google Tasks',
'gotowebinar' => 'GoToWebinar',
'gravityformsac' => 'Gravity Forms',
'greenhouse' => 'Greenhouse',
'groundhoggac' => 'Groundhogg',
'helpscout' => 'Help Scout',
'highlevel' => 'HighLevel',
'housecallpro' => 'Housecall Pro',
'hubspot' => 'Hubspot',
'icontact' => 'iContact',
'insellerate' => 'Insellerate',
'insightly' => 'Insightly CRM',
'instantly' => 'Instantly',
'intercom' => 'Intercom',
'iterable' => 'Iterable',
'ixactcontact' => 'IXACT Contact',
'jetpackcrmac' => 'Jetpack CRM',
'jobber' => 'Jobber',
'jobnimbus' => 'JobNimbus',
'jungo' => 'Jungo',
'justcall' => 'JustCall',
'kartra' => 'Kartra',
'keap' => 'Keap',
'keila' => 'Keila',
'kintone' => 'Kintone',
'kirimemail' => 'KIRIM.EMAIL',
'kit' => 'Kit',
'klaviyo' => 'Klaviyo',
'knack' => 'Knack',
'lacrm' => 'Less Annoying CRM',
'laposta' => 'Laposta',
'latepoint' => 'LatePoint',
'learndashac' => 'LearnDash',
'lemlist' => 'lemlist',
'lexoffice' => 'Lexoffice',
'lifterlmsac' => 'LifterLMS',
'liondesk' => 'LionDesk',
'listmonk' => 'Listmonk',
'livechat' => 'LiveChat',
'livestorm' => 'Livestorm',
'localiq' => 'LocaliQ',
'lofty' => 'Lofty (formerly Chime)',
'loops' => 'Loops',
'mailbluster' => 'MailBluster',
'mailchimp' => 'Mailchimp',
'mailcoach' => 'Mailcoach',
'maileon' => 'Maileon',
'mailercloud' => 'Mailercloud',
'mailerlite' => 'MailerLite Classic',
'mailerlite2' => 'MailerLite',
'mailersend' => 'MailerSend',
'mailguntransactional' => 'Mailgun Transactional',
'mailify' => 'Mailify',
'mailingboss' => 'MailingBoss',
'mailjet' => 'Mailjet',
'mailmint' => 'Mail Mint',
'mailmodo' => 'Mailmodo',
'mailpoet' => 'MailPoet',
'mailrelay' => 'MailRelay',
'mailshake' => 'Mailshake',
'mailster' => 'Mailster',
'mailup' => 'MailUp',
'mailwizz' => 'MailWizz',
'marketo' => 'Marketo',
'maropost' => 'Maropost',
'masterstudyac' => 'MasterStudy LMS',
'mautic' => 'Mautic',
'memberful' => 'Memberful',
'metaconversions' => 'Meta Conversions API',
'mindbody' => 'Mindbody',
'monday' => 'Monday.com',
'moneybird' => 'Moneybird',
'moosend' => 'Moosend',
'msteams' => 'Microsoft Teams',
'mstodo' => 'Microsoft To Do',
'mumara' => 'Mumara',
'mycred' => 'myCred',
'myob' => 'MYOB',
'neoncrm' => 'NeonCRM',
'newsletter' => 'Newsletter',
'nimble' => 'Nimble',
'ninjatables' => 'Ninja Tables',
'nocrmio' => 'noCRM.io',
'notion' => 'Notion',
'nutshell' => 'Nutshell CRM',
'omnisend' => 'Omnisend',
'on24' => 'ON24',
'onehash' => 'Onehash',
'ontraport' => 'Ontraport',
'openphone' => 'OpenPhone',
'outreach' => 'Outreach',
'pabbly' => 'Pabbly',
'paidmembershippproac' => 'Paid Memberships Pro',
'pandadoc' => 'PandaDoc',
'pardot' => 'Pardot (Account Engagement)',
'personio' => 'Personio',
'pipedrive' => 'Pipedrive',
'planningcenter' => 'Planning Center',
'podio' => 'Podio',
'practicebetter' => 'Practice Better',
'practicepanther' => 'PracticePanther',
'propertyhive' => 'PropertyHive',
'pushover' => 'Pushover',
'quickbase' => 'Quickbase',
'quickbooksonline' => 'QuickBooks Online',
'ragic' => 'Ragic',
'rapidmail' => 'Rapidmail',
'realgeeks' => 'Real Geeks',
'reamaze' => 'Re:amaze',
'recruitee' => 'Recruitee',
'resend' => 'Resend',
'robly' => 'Robly',
'salesflare' => 'Salesflare',
'salesforce' => 'Salesforce',
'saleshandy' => 'SalesHandy',
'salesloft' => 'Salesloft',
'salesmate' => 'Salesmate',
'salesrocks' => 'Sales Rocks',
'sarbacane' => 'Sarbacane',
'scoro' => 'Scoro CRM',
'segment' => 'Twilio Segment',
'selzy' => 'Selzy',
'sender' => 'Sender',
'sendfox' => 'Sendfox',
'sendgrid' => 'SendGrid',
'sendlane' => 'Sendlane',
'sendpulse' => 'Sendpulse',
'sendx' => 'SendX',
'sendy' => 'Sendy',
'senseilmsac' => 'Sensei LMS',
'servicem8' => 'ServiceM8',
'servicetitan' => 'ServiceTitan',
'sevdesk' => 'sevDesk',
'shapesoftware' => 'Shape Software',
'sharpspring' => 'SharpSpring',
'shopify' => 'Shopify',
'sierrainteractive' => 'Sierra Interactive',
'signnow' => 'signNow',
'sitereviewsac' => 'Site Reviews',
'skyslope' => 'SkySlope',
'slack' => 'Slack',
'slicewpac' => 'SliceWP',
'slicktext' => 'SlickText',
'smartlead' => 'Smartlead',
'smartrmail' => 'SmartrMail',
'smartsheet' => 'Smartsheet',
'snovio' => 'Snov.io',
'softr' => 'Softr',
'stripe' => 'Stripe',
'successai' => 'Success.ai',
'suitedash' => 'SuiteDash',
'superoffice' => 'SuperOffice CRM',
'supportcandy' => 'SupportCandy',
'suremembersac' => 'SureMembers',
'systemeio' => 'Systeme.io',
'tawkto' => 'Tawk.to',
'teachable' => 'Teachable',
'teamleader' => 'Teamleader Focus',
'teamwork' => 'Teamwork',
'tebra' => 'Tebra (Kareo)',
'telegram' => 'Telegram',
'theeventscalendar' => 'The Events Calendar',
'thinkific' => 'Thinkific',
'tidio' => 'Tidio',
'todoist' => 'Todoist',
'totalexpert' => 'Total Expert',
'trello' => 'Trello',
'tutorlmsac' => 'Tutor LMS',
'twilio' => 'Twilio',
'ultimatememberac' => 'Ultimate Member',
'velocify' => 'Velocify',
'verticalresponse' => 'Vertical Response',
'vision6' => 'Vision6',
'vismaeaccounting' => 'Visma eAccounting',
'vtiger' => 'Vtiger CRM',
'wealthbox' => 'Wealthbox',
'webhook' => 'Webhook',
'webinargeek' => 'WebinarGeek',
'webinarjam' => 'WebinarJam',
'whatsapp' => 'WhatsApp Business Platform',
'wiseagent' => 'Wise Agent',
'woocommerce' => 'WooCommerce',
'woodpecker' => 'Woodpecker.co',
'wordpress' => 'WordPress',
'workable' => 'Workable',
'wpbookingcalendarac' => 'WP Booking Calendar',
'wpcrmsystem' => 'WP CRM System',
'wpeventmanagerac' => 'WP Event Manager',
'wpformsac' => 'WPForms',
'wpjobmanagerac' => 'WP Job Manager',
'wrike' => 'Wrike',
'xero' => 'Xero',
'zapier' => 'Zapier',
'zendesk' => 'Zendesk Support',
'zendesksell' => 'Zendesk Sell',
'zohobooks' => 'Zoho Books',
'zohocampaigns' => 'Zoho Campaigns',
'zohocreator' => 'Zoho Creator',
'zohocrm' => 'Zoho CRM',
'zohodesk' => 'Zoho Desk',
'zohofsm' => 'Zoho FSM',
'zohoinventory' => 'Zoho Inventory',
'zohoinvoice' => 'Zoho Invoice',
'zohoma' => 'Zoho Marketing Automation',
'zohomeeting' => 'Zoho Meeting',
'zohopeople' => 'Zoho People',
'zohorecruit' => 'Zoho Recruit',
'zohosheet' => 'Zoho Sheet',
'zohosubscriptions' => 'Zoho Billing (Subscriptions)',
'zoomwebinar' => 'Zoom Webinar',
);
}
/**
* Cache key for the merged platform-settings result.
*
* Bumped manually when the cached payload's shape changes (so old serialized
* values from a prior plugin version are ignored without needing a manual
* purge).
*/
if ( !defined( 'ADFOIN_PLATFORM_SETTINGS_CACHE_KEY' ) ) {
define( 'ADFOIN_PLATFORM_SETTINGS_CACHE_KEY', 'adfoin_action_platform_settings_v1' );
}
/**
*
* Retrieves the action platform settings.
*
* The merged settings array (option-stored toggles + active integrations'
* providers) is memoized within the request and cached in a transient
* across requests. The cache is invalidated by
* adfoin_clear_action_platform_settings_cache() — wired to the
* `adfoin_general_settings_platforms` option's add/update hooks, and called
* directly from the integration save/update/toggle/delete paths.
*
* @global object $wpdb WordPress database access object.
*
* @return array The action platform settings.
*/
function adfoin_get_action_platform_settings() {
static $memo = null;
if ( null !== $memo ) {
return $memo;
}
$cached = get_transient( ADFOIN_PLATFORM_SETTINGS_CACHE_KEY );
if ( is_array( $cached ) ) {
$memo = $cached;
return $memo;
}
global $wpdb;
$settings = ( get_option( 'adfoin_general_settings_platforms' ) ? get_option( 'adfoin_general_settings_platforms' ) : array() );
$saved_records = $wpdb->get_results( "SELECT form_provider, action_provider FROM {$wpdb->prefix}adfoin_integration WHERE status = 1", ARRAY_A );
if ( !is_wp_error( $saved_records ) ) {
if ( $saved_records && is_array( $saved_records ) ) {
$action_providers = wp_list_pluck( $saved_records, 'action_provider' );
if ( $action_providers ) {
$action_providers = array_unique( $action_providers );
foreach ( $action_providers as $single ) {
$settings[$single] = true;
}
}
}
}
if ( !is_array( $settings ) ) {
$settings = array();
}
set_transient( ADFOIN_PLATFORM_SETTINGS_CACHE_KEY, $settings, HOUR_IN_SECONDS );
$memo = $settings;
return $memo;
}
/**
* Invalidate the cached action-platform settings.
*
* Call this any time the `adfoin_integration` table is mutated in a way that
* could change which platforms have an active integration, or when the
* `adfoin_general_settings_platforms` option changes.
*
* @return void
*/
function adfoin_clear_action_platform_settings_cache() {
delete_transient( ADFOIN_PLATFORM_SETTINGS_CACHE_KEY );
}
add_action( 'add_option_adfoin_general_settings_platforms', 'adfoin_clear_action_platform_settings_cache' );
add_action( 'update_option_adfoin_general_settings_platforms', 'adfoin_clear_action_platform_settings_cache' );
add_action( 'delete_option_adfoin_general_settings_platforms', 'adfoin_clear_action_platform_settings_cache' );
/**
* Build a map of action-provider key => component-script URL.
*
* Each platform's Vue component lives at
* `platforms//-component.js`. The map is consumed by
* `adfoinComponentLoader.loadPlatform()` in core.js to fetch only the
* file for the provider the user is currently editing.
*
* Pro versions of a platform may override the basic file by providing
* `pro/pro/pro-component.js` (the basic mapping is kept
* if no pro file exists).
*
* Plugins can extend or modify the map via the `adfoin_platform_scripts`
* filter.
*
* @return array map of provider key to component script URL.
*/
function adfoin_get_platform_scripts() {
// Memoize the expensive scandir part within a request — the platforms
// and pro directories don't change during a single pageload, but the
// filter is left out of the cache so filters added between calls still
// apply (e.g., a platform that hooks 'adfoin_platform_scripts' on its
// own bootstrap path later in the request).
static $scanned_map = null;
if ( null === $scanned_map ) {
$scanned_map = array();
$platforms_dir = ADVANCED_FORM_INTEGRATION_PLATFORMS;
$platforms_url = ADVANCED_FORM_INTEGRATION_URL . '/platforms';
if ( is_dir( $platforms_dir ) ) {
$entries = scandir( $platforms_dir );
if ( is_array( $entries ) ) {
foreach ( $entries as $entry ) {
if ( '.' === $entry || '..' === $entry ) {
continue;
}
$component_file = $platforms_dir . '/' . $entry . '/' . $entry . '-component.js';
if ( file_exists( $component_file ) ) {
$scanned_map[$entry] = $platforms_url . '/' . $entry . '/' . $entry . '-component.js';
}
}
}
}
}
return apply_filters( 'adfoin_platform_scripts', $scanned_map );
}
/**
* Renders the general settings view for Adfo.in plugin.
*/
add_action(
'adfoin_settings_view',
'adfoin_general_settings_view',
10,
1
);
/**
* Displays the view for general settings of Advanced Form Integration plugin
*
* @param string $current_tab The current tab name
*
* @return void
*/
function adfoin_general_settings_view( $current_tab ) {
if ( 'general' !== $current_tab ) {
return;
}
$nonce = wp_create_nonce( 'adfoin_general_settings' );
$reset_nonce = wp_create_nonce( 'adfoin_reset_general_settings' );
$log_settings = get_option( 'adfoin_general_settings_log', '' );
$log_retention = absint( get_option( 'adfoin_general_settings_log_retention', 0 ) );
$error_email = get_option( 'adfoin_general_settings_error_email', '' );
$st_settings = get_option( 'adfoin_general_settings_st', '' );
$utm_settings = get_option( 'adfoin_general_settings_utm', '' );
$job_queue = get_option( 'adfoin_general_settings_job_queue', '' );
$job_queue_stats = adfoin_get_job_queue_stats();
$platform_settings = adfoin_get_action_platform_settings();
$platforms = adfoin_get_action_platform_list();
include ADVANCED_FORM_INTEGRATION_VIEWS . '/partials/general-settings.php';
}
add_action(
'admin_post_adfoin_save_general_settings',
'adfoin_save_general_settings',
10,
0
);
function adfoin_save_general_settings() {
// Capability check — admin-post handlers in this plugin use wp_die() (matches adfoin_clear_all_logs).
if ( !current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to do this.', 'advanced-form-integration' ) );
}
$nonce = ( isset( $_POST['_nonce'] ) ? wp_unslash( $_POST['_nonce'] ) : '' );
if ( !wp_verify_nonce( $nonce, 'adfoin_general_settings' ) ) {
wp_die( esc_html__( 'Security check Failed', 'advanced-form-integration' ) );
}
$log_settings = ( isset( $_POST['adfoin_disable_log'] ) ? sanitize_text_field( wp_unslash( $_POST['adfoin_disable_log'] ) ) : '' );
$log_retention = ( isset( $_POST['adfoin_log_retention'] ) ? absint( wp_unslash( $_POST['adfoin_log_retention'] ) ) : 0 );
$error_email = ( isset( $_POST['adfoin_error_email'] ) ? sanitize_text_field( wp_unslash( $_POST['adfoin_error_email'] ) ) : '' );
$st_settings = ( isset( $_POST['adfoin_disable_st'] ) ? sanitize_text_field( wp_unslash( $_POST['adfoin_disable_st'] ) ) : '' );
$utm_settings = ( isset( $_POST['adfoin_enable_utm'] ) ? sanitize_text_field( wp_unslash( $_POST['adfoin_enable_utm'] ) ) : '' );
$job_queue = ( isset( $_POST['adfoin_job_queue'] ) ? sanitize_text_field( wp_unslash( $_POST['adfoin_job_queue'] ) ) : '' );
$default_platforms = array_fill_keys( array_keys( adfoin_get_action_platform_list() ), false );
$activated_platforms = ( isset( $_POST['platforms'] ) ? adfoin_sanitize_text_or_array_field( $_POST['platforms'] ) : array() );
$all_platforms = array_merge( $default_platforms, array_fill_keys( array_keys( array_intersect_key( $activated_platforms, $default_platforms ) ), true ) );
// Save
update_option( 'adfoin_general_settings_platforms', $all_platforms );
update_option( 'adfoin_general_settings_log', $log_settings );
update_option( 'adfoin_general_settings_log_retention', $log_retention );
update_option( 'adfoin_general_settings_error_email', $error_email );
update_option( 'adfoin_general_settings_st', $st_settings );
update_option( 'adfoin_general_settings_utm', $utm_settings );
update_option( 'adfoin_general_settings_job_queue', $job_queue );
adfoin_sync_log_cleanup_schedule( $log_retention );
advanced_form_integration_redirect( 'admin.php?page=advanced-form-integration-settings&settings-updated=true' );
}
/**
* Reconcile the daily log-cleanup recurring action with the retention setting.
*
* Scheduled when retention > 0, unscheduled when 0. Called from the save handler;
* if the recurring action is ever externally cleared, hitting Save again restores it.
*
* @param int $days Number of days to retain log rows. 0 disables auto-cleanup.
*/
function adfoin_sync_log_cleanup_schedule( $days ) {
if ( !function_exists( 'as_schedule_recurring_action' ) ) {
return;
}
if ( $days > 0 ) {
if ( !as_has_scheduled_action( 'adfoin_log_cleanup' ) ) {
as_schedule_recurring_action(
time() + DAY_IN_SECONDS,
DAY_IN_SECONDS,
'adfoin_log_cleanup',
array(),
'adfoin'
);
}
} else {
as_unschedule_all_actions( 'adfoin_log_cleanup' );
}
}
add_action( 'adfoin_log_cleanup', 'adfoin_run_log_cleanup' );
/**
* Daily Action Scheduler hook — delete log rows older than the retention window.
*/
function adfoin_run_log_cleanup() {
$days = absint( get_option( 'adfoin_general_settings_log_retention', 0 ) );
if ( $days <= 0 ) {
return;
}
global $wpdb;
$table = $wpdb->prefix . 'adfoin_log';
$wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE time < (NOW() - INTERVAL %d DAY)", $days ) );
}
/**
* Quick health summary of AFI's Action Scheduler queue, scoped to hooks of the
* shape `adfoin__job_queue` (the dispatch hooks fired by
* adfoin_dispatch_integrations()). Used to render the Settings → General queue
* health card under the "Enable Job Queue" toggle so users can spot a stuck
* cron / backed-up queue without leaving the Settings screen.
*
* Returns null when Action Scheduler isn't loaded or its actions table doesn't
* exist — render code uses that as a "hide the card" signal.
*
* @return array{pending:int,failed:int,last_run:?string}|null
*/
function adfoin_get_job_queue_stats() {
if ( !function_exists( 'as_enqueue_async_action' ) ) {
return null;
}
global $wpdb;
$table = $wpdb->prefix . 'actionscheduler_actions';
// Defense: if the AS schema isn't installed (rare upgrade scenarios), bail.
$exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
if ( $exists !== $table ) {
return null;
}
// Match every dispatch hook AFI emits via as_enqueue_async_action.
// `\_` escapes the underscores so MySQL LIKE treats them as literals
// (otherwise `_` matches any single char and the pattern would over-match).
$hook_pattern = 'adfoin\\_%\\_job\\_queue';
$pending = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE status = %s AND hook LIKE %s", 'pending', $hook_pattern ) );
$failed = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE status = %s AND hook LIKE %s", 'failed', $hook_pattern ) );
$last_run = $wpdb->get_var( $wpdb->prepare( "SELECT last_attempt_gmt FROM {$table} WHERE status = %s AND hook LIKE %s ORDER BY last_attempt_gmt DESC LIMIT 1", 'complete', $hook_pattern ) );
return array(
'pending' => $pending,
'failed' => $failed,
'last_run' => $last_run,
);
}
add_action( 'wp_ajax_adfoin_send_test_email', 'adfoin_send_test_email_ajax' );
/**
* AJAX handler — send a test of the integration error email so users can verify
* deliverability without waiting for a real failure.
*/
function adfoin_send_test_email_ajax() {
adfoin_require_manage_options();
if ( !wp_verify_nonce( ( isset( $_POST['_nonce'] ) ? wp_unslash( $_POST['_nonce'] ) : '' ), 'adfoin_send_test_email' ) ) {
wp_send_json_error( array(
'message' => __( 'Security check failed', 'advanced-form-integration' ),
), 403 );
}
$admin_email = get_option( 'admin_email' );
$subject = __( '[TEST] Error with AFI Integration', 'advanced-form-integration' );
$message = sprintf(
/* translators: %s: site URL */
__( 'This is a test email from Advanced Form Integration on %s. If you received this, the Send Error Email setting is wired up correctly.', 'advanced-form-integration' ),
get_bloginfo( 'url' )
);
$sent = wp_mail( $admin_email, $subject, $message );
if ( $sent ) {
wp_send_json_success( array(
'message' => sprintf(
/* translators: %s: admin email address */
__( 'Test email sent to %s.', 'advanced-form-integration' ),
$admin_email
),
) );
}
wp_send_json_error( array(
'message' => __( 'Failed to send test email. Check your site\'s email configuration.', 'advanced-form-integration' ),
) );
}
add_action( 'admin_post_adfoin_reset_general_settings', 'adfoin_reset_general_settings' );
/**
* admin-post handler — reset the five toggle options + log retention to defaults.
* Platform activation map is NOT touched (that's a deliberate scope choice; users
* have far more invested in which platforms they've turned on).
*/
function adfoin_reset_general_settings() {
if ( !current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to do this.', 'advanced-form-integration' ) );
}
$nonce = ( isset( $_POST['_nonce'] ) ? wp_unslash( $_POST['_nonce'] ) : '' );
if ( !wp_verify_nonce( $nonce, 'adfoin_reset_general_settings' ) ) {
wp_die( esc_html__( 'Security check Failed', 'advanced-form-integration' ) );
}
delete_option( 'adfoin_general_settings_log' );
delete_option( 'adfoin_general_settings_log_retention' );
delete_option( 'adfoin_general_settings_error_email' );
delete_option( 'adfoin_general_settings_st' );
delete_option( 'adfoin_general_settings_utm' );
delete_option( 'adfoin_general_settings_job_queue' );
adfoin_sync_log_cleanup_schedule( 0 );
advanced_form_integration_redirect( 'admin.php?page=advanced-form-integration-settings&settings-updated=reset' );
}
/**
* Dispatch a batch of integration records for a single form submission.
*
* Centralizes the Job Queue toggle logic that was previously copy-pasted across
* ~47 trigger files. Reads the global toggle once and routes each record to
* either Action Scheduler (async) or `call_user_func` (sync).
*
* Adds two safety nets the inline blocks didn't have:
* 1. If `adfoin_{slug}_job_queue` has no listeners (a future platform forgets
* to register one, like the Tier-A Freshdesk/Intercom/Zendesk Sell bugs),
* fall back to sync rather than silently dropping the submission.
* 2. If `adfoin_{slug}_send_data` doesn't exist (broken plugin install),
* skip the record instead of emitting a PHP warning.
*
* The async payload shape `array( 'data' => array( 'record' => ..., 'posted_data' => ... ) )`
* is preserved exactly to keep every existing platform's `_job_queue` handler working.
*
* @param array $saved_records Integration records matched for this submission.
* @param array $posted_data The form's posted-data array.
* @return void
*/
function adfoin_dispatch_integrations( $saved_records, $posted_data ) {
if ( empty( $saved_records ) || !is_array( $saved_records ) ) {
return;
}
$job_queue_enabled = (bool) get_option( 'adfoin_general_settings_job_queue', '' );
$can_queue = $job_queue_enabled && function_exists( 'as_enqueue_async_action' );
foreach ( $saved_records as $record ) {
if ( empty( $record['action_provider'] ) ) {
continue;
}
$action_provider = $record['action_provider'];
$sync_fn = "adfoin_{$action_provider}_send_data";
$hook = "adfoin_{$action_provider}_job_queue";
/**
* Whether this specific record should run via Action Scheduler.
* Pro extensions or third parties can force sync for platforms that
* need synchronous response handling (e.g. webhook-style integrations
* that mutate the parent form's response).
*
* @param bool $should_queue Default: the global Job Queue toggle.
* @param array $record The integration record.
* @param array $posted_data Form-submission payload.
*/
$should_queue = (bool) apply_filters(
'adfoin_should_queue',
$can_queue,
$record,
$posted_data
);
if ( $should_queue && has_action( $hook ) ) {
$action_id = as_enqueue_async_action( $hook, array(
'data' => array(
'record' => $record,
'posted_data' => $posted_data,
),
) );
// Action Scheduler silently REJECTS a job whose JSON-encoded args
// exceed its column limit (8000 chars for the DB store): it throws
// internally, the as_* wrapper swallows the exception and returns 0,
// so the integration would just vanish — no run, no log. A real
// WooCommerce order easily exceeds this (full items JSON, taxes,
// order-attribution meta, merged line-item arrays, ~80+ fields). When
// queuing fails for ANY reason (oversized payload, AS not ready),
// fall through to the synchronous path so the submission is never
// lost. A truthy action id means it was queued successfully.
if ( $action_id ) {
continue;
}
}
// Sync path (and async-fallback when no listeners are registered for
// $hook, or when the queue rejected the job above).
if ( function_exists( $sync_fn ) ) {
call_user_func( $sync_fn, $record, $posted_data );
}
}
}
/**
* Sanitize text or array field.
*
* @param mixed $array_or_string The string or array to sanitize.
* @return mixed The sanitized string or array.
*/
function adfoin_sanitize_text_or_array_field( $array_or_string ) {
if ( is_string( $array_or_string ) ) {
$array_or_string = stripslashes( $array_or_string );
} elseif ( is_array( $array_or_string ) ) {
foreach ( $array_or_string as $key => &$value ) {
if ( is_array( $value ) ) {
$value = adfoin_sanitize_text_or_array_field( $value );
} else {
$value = stripslashes( $value );
}
}
}
return $array_or_string;
}
/*
* Get parsed value
*/
function adfoin_get_parsed_values( $field, $posted_data ) {
// Bridge canonical `_form_id` / `_form_name` tags to the legacy `form_id` /
// `form_name` keys triggers have always pushed into $posted_data. Lets users
// pick {{_form_id}} from the dropdown without having to migrate every trigger
// to push the underscored key directly.
if ( is_array( $posted_data ) ) {
$aliases = array(
'_form_id' => 'form_id',
'_form_name' => 'form_name',
);
foreach ( $aliases as $tag => $legacy_key ) {
if ( !array_key_exists( $tag, $posted_data ) && !empty( $posted_data[$legacy_key] ) ) {
$posted_data[$tag] = $posted_data[$legacy_key];
}
}
}
// Submitted values must never be evaluated as shortcodes, only the admin's
// template. Square brackets in values are swapped for private-use markers
// before do_shortcode() runs and restored afterwards.
$bracket_markers = array("", "");
foreach ( $posted_data as $key => $value ) {
if ( is_array( $value ) ) {
$multi = 0;
foreach ( $value as $single ) {
if ( is_array( $single ) ) {
$multi = 1;
break;
}
}
if ( $multi ) {
$value = wp_json_encode( $value );
} else {
$value = @implode( ",", $value );
}
}
// Keep a legitimate "0" (e.g. quantity, rating, checkbox) — only skip empties.
if ( null !== $value && false !== $value && '' !== $value ) {
if ( is_string( $value ) ) {
$value = str_replace( array('[', ']'), $bracket_markers, $value );
}
$field = str_replace( '{{' . $key . '}}', $value, $field );
}
}
$field = preg_replace( "/{{.+?}}/", "", $field );
/**
* Whether to run do_shortcode() on the parsed field value before sending
* to the integration. Defaults to true to preserve existing behavior.
*
* Only shortcodes written in the field template run; brackets inside
* submitted values are neutralised above. Opt out if you don't use
* shortcodes in templates: in Action Scheduler / cron context there's no
* live request, so shortcodes that depend on `is_singular()`, `$_POST`,
* or current query state may error or return nonsense.
*
* add_filter( 'adfoin_parse_shortcodes', '__return_false' );
*
* @param bool $run_shortcodes Default true.
* @param string $field The parsed field value (after tag substitution).
* @param array $posted_data The full posted-data context.
*/
$run_shortcodes = apply_filters(
'adfoin_parse_shortcodes',
true,
$field,
$posted_data
);
if ( $run_shortcodes && strpos( $field, '[' ) !== false && strpos( $field, ']' ) !== false ) {
$field = do_shortcode( $field );
}
return str_replace( $bracket_markers, array('[', ']'), $field );
}
/**
* Add a log entry for an integration request/response.
*
* This function adds a log entry for an integration request and response. The log entry includes
* information about the URL, arguments, response data, and integration ID.
*
* @param mixed $return The response data from the integration request.
* @param string $url The URL of the integration request.
* @param array $args The arguments or data sent with the integration request.
* @param array $record An array containing integration record data.
* @param string|null $log_id Optional. The ID of the log entry to update. Default is an empty string.
* @return void
*/
function adfoin_add_to_log(
$return,
$url,
$args,
$record,
$log_id = ''
) {
$log_settings = get_option( 'adfoin_general_settings_log', '' );
if ( "1" == $log_settings ) {
return;
}
if ( isset( $args['body'] ) ) {
if ( !is_array( $args['body'] ) ) {
if ( null != json_decode( $args['body'] ) ) {
$args['body'] = json_decode( $args['body'] );
}
}
}
/**
* Allow sites to redact PII before request payloads hit the log table.
* Hook here if you store contact data in your forms and need to keep
* the integration log GDPR-friendly. Receives the args array and the
* platform-side URL; return a modified args array.
*
* @param array $args Request args (body, headers, etc.).
* @param string $url Outgoing URL.
* @param array $record Integration record (for context).
*/
$args = apply_filters(
'adfoin_log_request_args',
$args,
$url,
$record
);
$request_data = wp_json_encode( array(
'url' => $url,
'args' => $args,
) );
if ( is_wp_error( $return ) ) {
$data = array(
'response_code' => 0,
'response_message' => 'WP Error',
'integration_id' => ( isset( $record['id'] ) ? (int) $record['id'] : 0 ),
'request_data' => $request_data,
'response_data' => wp_json_encode( $return ),
);
} else {
$response_body = $return["body"];
if ( is_array( $response_body ) || is_object( $response_body ) ) {
$response_body = wp_json_encode( $response_body );
}
if ( is_string( $response_body ) ) {
$trimmed_body = trim( $response_body );
if ( '' !== $trimmed_body ) {
// Skip tag-stripping for an obviously-JSON body — running
// wp_strip_all_tags() over a large JSON payload is pure
// overhead, and JSON has no markup that needs scrubbing.
$first = $trimmed_body[0];
if ( '{' !== $first && '[' !== $first && '"' !== $first ) {
$stripped = wp_strip_all_tags( $trimmed_body, true );
if ( $stripped !== $trimmed_body ) {
$trimmed_body = preg_replace( '/\\s+/', ' ', $stripped );
}
}
$response_body = $trimmed_body;
}
} else {
$response_body = '';
}
/**
* Allow sites to redact PII in the response body before it hits the
* log table. Receives the (string) body — typically JSON — and the
* integration record; return the modified body string.
*
* @param string $response_body Body content about to be stored.
* @param array $record Integration record (for context).
* @param string $url Outgoing URL.
*/
$response_body = apply_filters(
'adfoin_log_response_body',
$response_body,
$record,
$url
);
$data = array(
'response_code' => $return["response"]["code"],
'response_message' => mb_substr( (string) $return["response"]["message"], 0, 255 ),
'integration_id' => ( isset( $record['id'] ) ? (int) $record['id'] : 0 ),
'request_data' => $request_data,
'response_data' => $response_body,
);
}
$log = new Advanced_Form_Integration_Log();
if ( $log_id ) {
$log->update( $data, $log_id );
$effective_log_id = (int) $log_id;
} else {
$log->insert( $data );
global $wpdb;
$effective_log_id = (int) $wpdb->insert_id;
}
// Fire the error-email action for both transport failures (WP_Error) and
// non-2xx HTTP responses. Previously only WP_Error fired the email, which
// meant 401/403/422/5xx — the cases users actually want notifications for —
// were silently logged but never alerted on.
if ( get_option( 'adfoin_general_settings_error_email', '' ) ) {
$response_code = ( is_wp_error( $return ) ? 0 : (( isset( $return['response']['code'] ) ? (int) $return['response']['code'] : 0 )) );
$is_failure = is_wp_error( $return ) || $response_code < 200 || $response_code >= 300;
if ( $is_failure ) {
$context = array(
'log_id' => $effective_log_id,
'response_code' => $response_code,
'response_message' => ( is_wp_error( $return ) ? $return->get_error_message() : (( isset( $return['response']['message'] ) ? $return['response']['message'] : '' )) ),
);
do_action(
'adfoin_send_api_error_email',
$return,
$record,
$request_data,
$context
);
}
}
return;
}
add_action(
'adfoin_send_api_error_email',
'adfoin_send_api_error_email',
10,
4
);
/**
* Send an email notification for an API error.
*
* Composes a contextual email with the integration title, platform, form name,
* HTTP status, and a direct link to the log entry. Throttles per
* (integration_id, response_code) to avoid spamming the inbox when one broken
* credential causes hundreds of submissions to fail.
*
* @param array $return The response from wp_remote_post (WP_Error or array).
* @param array $record The integration record (id, title, action_provider, form_name, ...).
* @param string $request_data JSON-encoded request payload (kept for back-compat / hookers).
* @param array $context Extra fields: log_id, response_code, response_message.
* @return void
*/
function adfoin_send_api_error_email(
$return,
$record,
$request_data,
$context = array()
) {
$context = array_merge( array(
'log_id' => 0,
'response_code' => 0,
'response_message' => '',
), (array) $context );
$integration_id = ( isset( $record['id'] ) ? (int) $record['id'] : 0 );
$integration_title = ( isset( $record['title'] ) ? (string) $record['title'] : '' );
$action_provider = ( isset( $record['action_provider'] ) ? (string) $record['action_provider'] : '' );
$form_name = ( isset( $record['form_name'] ) ? (string) $record['form_name'] : '' );
$response_code = (int) $context['response_code'];
$response_message = (string) $context['response_message'];
// Throttle: at most one email per (integration_id, response_code) per hour.
// A broken credential firing on every form submission must not spam the inbox.
$throttle_key = 'adfoin_err_email_' . md5( $integration_id . '|' . $response_code );
if ( get_transient( $throttle_key ) ) {
return;
}
set_transient( $throttle_key, 1, HOUR_IN_SECONDS );
$admin_email = get_option( 'admin_email' );
$site_name = wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES );
$site_url = get_bloginfo( 'url' );
$code_label = ( $response_code > 0 ? (string) $response_code : __( 'Transport error', 'advanced-form-integration' ) );
$log_url = ( $context['log_id'] ? admin_url( 'admin.php?page=advanced-form-integration-log&action=view&id=' . $context['log_id'] ) : admin_url( 'admin.php?page=advanced-form-integration-log' ) );
$integration_label = ( '' !== $integration_title ? sprintf( '%s (#%d)', $integration_title, $integration_id ) : '#' . $integration_id );
$subject = sprintf(
/* translators: 1: site name, 2: action provider slug, 3: HTTP status code or "Transport error" */
__( '[%1$s] AFI integration error: %2$s (%3$s)', 'advanced-form-integration' ),
$site_name,
( '' !== $action_provider ? $action_provider : __( 'integration', 'advanced-form-integration' ) ),
$code_label
);
$lines = array();
$lines[] = sprintf(
/* translators: %s: site URL */
__( 'An integration on %s failed.', 'advanced-form-integration' ),
$site_url
);
$lines[] = '';
$lines[] = sprintf( __( 'Integration: %s', 'advanced-form-integration' ), $integration_label );
$lines[] = sprintf( __( 'Platform: %s', 'advanced-form-integration' ), ( '' !== $action_provider ? $action_provider : '—' ) );
$lines[] = sprintf( __( 'Form: %s', 'advanced-form-integration' ), ( '' !== $form_name ? $form_name : '—' ) );
$lines[] = sprintf(
/* translators: 1: HTTP status code or "Transport error", 2: response message */
__( 'Status: %1$s %2$s', 'advanced-form-integration' ),
$code_label,
$response_message
);
$lines[] = '';
$lines[] = sprintf( __( 'Log entry: %s', 'advanced-form-integration' ), $log_url );
$message = implode( "\n", $lines );
/**
* Override the recipient for the integration-error email.
*
* @param string $to Default: site admin_email.
* @param array $context log_id, response_code, response_message.
* @param array $record The integration record.
* @param mixed $return The wp_remote_post response (WP_Error or array).
*/
$to = apply_filters(
'adfoin_error_email_to',
$admin_email,
$context,
$record,
$return
);
$subject = apply_filters(
'adfoin_error_email_subject',
$subject,
$context,
$record,
$return
);
$message = apply_filters(
'adfoin_error_email_message',
$message,
$context,
$record,
$return
);
wp_mail( $to, $subject, $message );
}
/*
* Get User IP
*/
function adfoin_get_user_ip() {
// Respect CloudFlare connecting IP if provided and valid
if ( isset( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
$cf_ip = ( is_string( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ? trim( $_SERVER['HTTP_CF_CONNECTING_IP'] ) : '' );
if ( filter_var( $cf_ip, FILTER_VALIDATE_IP ) ) {
$_SERVER['REMOTE_ADDR'] = $cf_ip;
$_SERVER['HTTP_CLIENT_IP'] = $cf_ip;
} else {
// don't overwrite with an invalid value
$_SERVER['HTTP_CLIENT_IP'] = '';
}
}
// Collect candidate IPs in order of preference
$candidates = array();
if ( !empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
$candidates[] = $_SERVER['HTTP_CLIENT_IP'];
}
if ( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
// X-Forwarded-For can contain a list of IPs
$forward_list = explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] );
foreach ( $forward_list as $fip ) {
$candidates[] = $fip;
}
}
if ( !empty( $_SERVER['REMOTE_ADDR'] ) ) {
$candidates[] = $_SERVER['REMOTE_ADDR'];
}
// Normalize and validate candidates, return first valid IP
foreach ( $candidates as $candidate ) {
$candidate = trim( (string) $candidate );
if ( $candidate === '' ) {
continue;
}
// Remove brackets used in some IPv6 representations
$candidate = trim( $candidate, "[] \t\n\r\x00\v" );
// If IPv4:port format (e.g. 1.2.3.4:5678), strip the port.
// Avoid stripping for IPv6 which contains multiple colons.
if ( strpos( $candidate, ':' ) !== false && substr_count( $candidate, ':' ) === 1 ) {
$candidate = strstr( $candidate, ':', true );
}
if ( filter_var( $candidate, FILTER_VALIDATE_IP ) ) {
return $candidate;
}
}
// If no valid IP found, return empty string instead of invalid values like "1"
return '';
}
function adfoin_get_cl_conditions() {
return array(
"equal_to" => __( 'Equal to', 'advanced-form-integration' ),
"not_equal_to" => __( 'Not equal to', 'advanced-form-integration' ),
"contains" => __( 'Contains', 'advanced-form-integration' ),
"does_not_contain" => __( 'Does not Contain', 'advanced-form-integration' ),
"starts_with" => __( 'Starts with', 'advanced-form-integration' ),
"ends_with" => __( 'Ends with', 'advanced-form-integration' ),
"in_list" => __( 'Is one of', 'advanced-form-integration' ),
"not_in_list" => __( 'Is not one of', 'advanced-form-integration' ),
"matches_regex" => __( 'Matches regex', 'advanced-form-integration' ),
"does_not_match_regex" => __( 'Does not match regex', 'advanced-form-integration' ),
"greater_than" => __( 'Greater than', 'advanced-form-integration' ),
"less_than" => __( 'Less than', 'advanced-form-integration' ),
"between" => __( 'Between (inclusive)', 'advanced-form-integration' ),
"not_between" => __( 'Not between', 'advanced-form-integration' ),
"date_before" => __( 'Date is before', 'advanced-form-integration' ),
"date_after" => __( 'Date is after', 'advanced-form-integration' ),
"date_on" => __( 'Date is on', 'advanced-form-integration' ),
"date_not_on" => __( 'Date is not on', 'advanced-form-integration' ),
"is_empty" => __( 'Is empty', 'advanced-form-integration' ),
"is_not_empty" => __( 'Is not empty', 'advanced-form-integration' ),
);
}
/**
* Operators grouped into