' . esc_html__( 'All in Box is installed twice. Keep a single copy active: both share the same settings, so you can deactivate the older folder without losing anything.', 'all-in-box' ) . '
'; } ); return; } define( 'AIB_LEAD_SYNC_VERSION', '1.10.0' ); // The declaration is wrapped so that PHP cannot early-bind it: a class declared at file // level is registered when the file is linked, before any statement of the file runs, so a // second copy of the plugin would hit "cannot declare class" before the guard above could // return. Inside a condition, the declaration becomes a runtime step and is simply skipped. if ( ! class_exists( 'AIB_Lead_Sync' ) ) : class AIB_Lead_Sync { const VERSION = AIB_LEAD_SYNC_VERSION; const OPT = 'aib_lead_sync_settings'; const SEEN_FORMS = 'aib_lead_sync_seen_forms'; const LAST_ERROR = 'aib_lead_sync_last_error'; /** Slug of the top-level menu, also the slug of its first page (Connection). */ const MENU_SLUG = 'all-in-box'; /** Look of an [allinbox_form] that sets no colour nor radius of its own: the brand's. */ const FORM_ACCENT = '#101010'; const FORM_TEXT = '#101010'; const FORM_RADIUS = 20; /** * The brand mark, black on transparent. WordPress repaints a base64 SVG menu icon in * the colours of the admin scheme, which a plain image URL would not get. */ const MENU_ICON = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MC4wMDEgNjEuMjE0Ij48cGF0aCBmaWxsPSJibGFjayIgZD0iTTEzLjI0OCwxLjI4NkEzLjU0MSwzLjU0MSwwLDAsMSwxNy42MjUuNzMyTDI4LjM4NSw3LjI3YTMuNTM1LDMuNTM1LDAsMCwwLDMuODczLS4xMjdsOS4yNzQtNi41YTMuNTQxLDMuNTQxLDAsMCwxLDQuNTcyLjQyN0w1OC45NSwxNC4xNThhMy43MDYsMy43MDYsMCwwLDEsLjM3Nyw0LjcxOWwtNS44NDksOC4yOWEzLjU0OCwzLjU0OCwwLDAsMS01LjQ1My40NTJMMzIuNTM4LDExLjg0NGEzLjU0NCwzLjU0NCwwLDAsMC01LjA3NiwwTDEyLjM0NywyNy4yMzhhMy43LDMuNywwLDAsMCwwLDUuMTdMMjguMjg2LDQ4LjY0M2EzLjU0MywzLjU0MywwLDAsMCw1LjA3NiwwTDQ4LjM3NywzMy4zNTFBMy41NTIsMy41NTIsMCwwLDEsNTQsMzQuMDYybDUuMzkxLDkuMmEzLjcwNywzLjcwNywwLDAsMS0uNTQ0LDQuNDU4bC0xMi4yLDEyLjQyMmEzLjU0MSwzLjU0MSwwLDAsMS00LjY2Ny4zNThsLTkuNDktNy4xMmEzLjUzNiwzLjUzNiwwLDAsMC00LjE2My0uMDY5bC05LjE4Miw2LjQzMmEzLjU0MSwzLjU0MSwwLDAsMS00LjUzOS0uMzk0TDEuNzg1LDQ2LjYzYTMuNzA2LDMuNzA2LDAsMCwxLS40NC00LjcwOWw2LjQ2NC05LjQ0OWEzLjcxMSwzLjcxMSwwLDAsMC0uMDU1LTQuMjU4TC43LDE4LjQ2MWEzLjcwNiwzLjcwNiwwLDAsMSwuMzUyLTQuNzUzWiIvPjwvc3ZnPg=='; /** Hook suffix WordPress gave each of our pages, by page key, so asset checks follow any slug change. */ private $page_hooks = []; /** Slugs of the pages kept out of the menu until the site is connected. */ private $hidden_pages = []; /** Data attributes of the enqueued SDK scripts, by handle. The SDKs read them from their own tag. */ private $sdk_attributes = []; public function __construct() { add_action( 'admin_menu', [ $this, 'menu' ] ); add_action( 'admin_init', [ $this, 'register_settings' ] ); // Up to 1.9 the settings lived under Settings > All in Box. WordPress still opens // that address (it falls back on the top-level page of the same slug), but under // the Settings menu: send it on to the new address instead. add_action( 'admin_init', [ $this, 'redirect_legacy_settings_url' ] ); // A page removed from the menu during admin_menu would fail the access check: // the pages stay registered, and leave the menu only once that check has passed. add_action( 'admin_head', [ $this, 'hide_unconnected_pages' ] ); add_action( 'init', [ $this, 'register_adapters' ] ); // Web Push install: serve the service worker and enqueue the SDK script. add_action( 'init', [ $this, 'maybe_serve_service_worker' ], 0 ); add_action( 'wp_enqueue_scripts', [ $this, 'webpush_script' ] ); add_filter( 'script_loader_tag', [ $this, 'sdk_script_attributes' ], 10, 2 ); // 1.8.0 also wrote a physical file at the site root. It is no longer written; a // leftover one is cleaned up, since it would shadow the route above. add_action( 'admin_init', [ $this, 'remove_legacy_service_worker' ] ); register_activation_hook( __FILE__, [ $this, 'remove_legacy_service_worker' ] ); // Webchat install: enqueue the chat widget script. Like Web Push, it only needs // the public key of the bot, so it works on a site that never syncs a contact. add_action( 'wp_enqueue_scripts', [ $this, 'webchat_script' ] ); // Embedded HTML form capture (a third-party form pasted into the page that posts // straight to an external service): a front-end script mirrors the submit of forms // marked data-aib-capture to a server-side REST endpoint, so the token stays server-side. add_action( 'rest_api_init', [ $this, 'register_rest' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_capture_assets' ] ); // Native All in Box form generator. Drop [allinbox_form] anywhere (Divi Text // module, page, widget). Multiple instances with their own fields/styling/language. add_shortcode( 'allinbox_form', [ $this, 'shortcode' ] ); // Visual form builder on its own admin page (live preview → shortcode). add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); } /** Loads the brand stylesheet on our pages, and the visual form builder on its page. */ public function enqueue_admin_assets( $hook ) { $page = '' !== $hook ? array_search( $hook, $this->page_hooks, true ) : false; if ( false === $page ) { return; } wp_enqueue_style( 'aib-lead-sync-admin', plugins_url( 'assets/admin.css', __FILE__ ), [], self::VERSION ); if ( 'form' !== $page ) { return; } wp_enqueue_script( 'aib-lead-sync-builder', plugins_url( 'assets/builder.js', __FILE__ ), [], self::VERSION, true ); $s = $this->settings(); wp_localize_script( 'aib-lead-sync-builder', 'AIBFormBuilder', [ 'i18n' => $this->form_i18n(), 'langs' => [ 'fr', 'en', 'es', 'de', 'it', 'nl', 'pt' ], 'fields' => [ 'firstname', 'lastname', 'email', 'phone', 'birthdate', 'address', 'postal_code', 'country', 'gender' ], 'defaults' => [ 'accent' => self::FORM_ACCENT, 'bg' => '#ffffff', 'text' => self::FORM_TEXT, 'radius' => self::FORM_RADIUS, 'width' => 420 ], 'trackConsent' => ! empty( $s['track_consent'] ), ] ); } /* ===================================================================== * Settings * ================================================================== */ public function menu() { $connected = $this->is_connected( $this->settings() ); $this->page_hooks['connection'] = (string) add_menu_page( 'All in Box', 'All in Box', 'manage_options', self::MENU_SLUG, [ $this, 'page_connection' ], self::MENU_ICON ); // The first submenu shares the top-level slug, so it renames "All in Box" to // "Connection" in the submenu without adding a page. foreach ( $this->admin_pages() as $key => $p ) { if ( ! empty( $p['woo'] ) && ! class_exists( 'WooCommerce' ) ) { continue; } $this->page_hooks[ $key ] = (string) add_submenu_page( self::MENU_SLUG, $p['title'] . ' - All in Box', $p['title'], 'manage_options', $p['slug'], [ $this, 'page_' . $key ] ); if ( $p['connected'] && ! $connected ) { $this->hidden_pages[] = $p['slug']; } } } public function register_settings() { register_setting( 'aib_lead_sync_group', self::OPT, [ $this, 'sanitize' ] ); } public function sanitize( $input ) { $input = is_array( $input ) ? $input : []; // A panel that is not rendered posts none of its fields (a tab hidden while the // connection is invalid, or WooCommerce deactivated). Falling back to the stored // value means saving from the Web Push or Webchat tab can no longer wipe the // contacts settings. Every checkbox is paired with a hidden "0" in the form, so // unticking one still posts and is honoured. $stored = get_option( self::OPT, [] ); $stored = is_array( $stored ) ? $stored : []; $kept = function ( $key, $default = '' ) use ( $stored ) { return array_key_exists( $key, $stored ) ? $stored[ $key ] : $default; }; $text = function ( $key, $default = '' ) use ( $input, $kept ) { return array_key_exists( $key, $input ) ? sanitize_text_field( $input[ $key ] ) : (string) $kept( $key, $default ); }; $flag = function ( $key, $default = 0 ) use ( $input, $kept ) { $value = array_key_exists( $key, $input ) ? $input[ $key ] : $kept( $key, $default ); return ! empty( $value ) ? 1 : 0; }; // The token is never printed back into the form, so an empty field means "keep the // stored one". Clearing it stays explicit, through the "forget" checkbox. $posted_token = array_key_exists( 'token', $input ) ? trim( sanitize_text_field( $input['token'] ) ) : ''; if ( ! empty( $input['token_clear'] ) ) { $token_value = ''; } elseif ( '' !== $posted_token ) { $token_value = $posted_token; } else { $token_value = (string) $kept( 'token' ); } $clean = [ 'base_url' => array_key_exists( 'base_url', $input ) ? esc_url_raw( rtrim( $input['base_url'], '/' ) ) : (string) $kept( 'base_url' ), 'token' => $token_value, 'list_id' => array_key_exists( 'list_id', $input ) ? absint( $input['list_id'] ) : absint( $kept( 'list_id', 0 ) ), 'form_ids' => $text( 'form_ids' ), 'default_subscribed' => $flag( 'default_subscribed', 0 ), 'track_consent' => $flag( 'track_consent', 1 ), 'email_field' => $text( 'email_field' ), 'firstname_field' => $text( 'firstname_field' ), 'lastname_field' => $text( 'lastname_field' ), 'phone_field' => $text( 'phone_field' ), 'postal_code_field' => $text( 'postal_code_field' ), 'country' => array_key_exists( 'country', $input ) ? strtoupper( substr( preg_replace( '/[^a-zA-Z]/', '', $input['country'] ), 0, 2 ) ) : (string) $kept( 'country' ), 'webpush_token' => array_key_exists( 'webpush_token', $input ) ? trim( sanitize_text_field( $input['webpush_token'] ) ) : (string) $kept( 'webpush_token' ), 'chat_key' => $this->sanitize_chat_key( array_key_exists( 'chat_key', $input ) ? $input['chat_key'] : $kept( 'chat_key' ) ), 'woo_order_on' => $flag( 'woo_order_on', 1 ), 'woo_order' => $text( 'woo_order', 'order_placed' ), 'woo_payment_on' => $flag( 'woo_payment_on', 1 ), 'woo_payment' => $text( 'woo_payment', 'payment_accepted' ), 'woo_cart_on' => $flag( 'woo_cart_on', 0 ), 'woo_cart' => $text( 'woo_cart', 'cart_add' ), ]; // Validate the token on every save (runs even when the value is unchanged), // so the connection badge and the other pages reflect reality immediately. $token = $clean['token'] !== '' ? $clean['token'] : ( ( defined( 'AIB_LEAD_SYNC_TOKEN' ) && AIB_LEAD_SYNC_TOKEN ) ? AIB_LEAD_SYNC_TOKEN : '' ); $clean['validated_sig'] = ''; if ( $clean['base_url'] === '' ) { add_settings_error( 'aib_lead_sync', 'aib_conn', __( 'Enter the API URL: every All in Box feature is served from it.', 'all-in-box' ), 'warning' ); } elseif ( $token === '' ) { // Web Push and Webchat need no API token. On such an install, staying quiet is // the correct behaviour rather than nagging about a connection nobody asked for. if ( '' === $clean['webpush_token'] && '' === $clean['chat_key'] ) { add_settings_error( 'aib_lead_sync', 'aib_conn', __( 'Enter the API URL and token to connect.', 'all-in-box' ), 'warning' ); } } else { $status = $this->validate_token( $clean['base_url'], $token ); if ( 'ok' === $status ) { $clean['validated_sig'] = md5( $clean['base_url'] . '|' . $token ); } elseif ( 'unauthorized' === $status ) { add_settings_error( 'aib_lead_sync', 'aib_conn', __( 'Invalid API token: All in Box rejected it. Check the token and that it has the contacts.create_or_update ability.', 'all-in-box' ), 'error' ); } elseif ( 'unreachable' === $status ) { add_settings_error( 'aib_lead_sync', 'aib_conn', __( 'Could not reach the API URL. Check the URL and that this site can make outbound HTTPS requests.', 'all-in-box' ), 'error' ); } else { add_settings_error( 'aib_lead_sync', 'aib_conn', __( 'Unexpected response from the API. Check the API URL.', 'all-in-box' ), 'error' ); } } return $clean; } private function settings() { // The token can also be defined in wp-config.php (recommended in production): // define( 'AIB_LEAD_SYNC_TOKEN', 'aib_skey_xxx' ); $s = wp_parse_args( get_option( self::OPT, [] ), [ 'validated_sig' => '', 'base_url' => 'https://app.allinbox.com', 'token' => '', 'list_id' => 0, 'form_ids' => '', 'default_subscribed' => 0, 'track_consent' => 1, 'email_field' => '', 'firstname_field' => '', 'lastname_field' => '', 'phone_field' => '', 'postal_code_field' => '', 'country' => '', 'webpush_token' => '', 'chat_key' => '', 'woo_order_on' => 1, 'woo_order' => 'order_placed', 'woo_payment_on' => 1, 'woo_payment' => 'payment_accepted', 'woo_cart_on' => 0, 'woo_cart' => 'cart_add', ] ); if ( defined( 'AIB_LEAD_SYNC_TOKEN' ) && AIB_LEAD_SYNC_TOKEN ) { $s['token'] = AIB_LEAD_SYNC_TOKEN; } return $s; } /** * The plugin's admin pages, in menu order. A "connected" page needs a validated API * token, like the tab it replaces; the WooCommerce page needs WooCommerce. */ private function admin_pages() { return [ 'connection' => [ 'slug' => self::MENU_SLUG, 'title' => __( 'Connection', 'all-in-box' ), 'lead' => __( 'Connect this site to your All in Box workspace.', 'all-in-box' ), 'connected' => false, ], 'contacts' => [ 'slug' => 'all-in-box-contacts', 'title' => __( 'Contacts', 'all-in-box' ), 'lead' => __( 'Form submissions on this site are sent to your All in Box list.', 'all-in-box' ), 'connected' => true, ], 'form' => [ 'slug' => 'all-in-box-form', 'title' => __( 'Newsletter form', 'all-in-box' ), 'lead' => __( 'Build a newsletter form, preview it, and copy its shortcode into a page.', 'all-in-box' ), 'connected' => true, ], 'webpush' => [ 'slug' => 'all-in-box-push', 'title' => __( 'Web Push', 'all-in-box' ), 'lead' => __( 'Send push notifications to the visitors of this site.', 'all-in-box' ), 'connected' => false, ], 'webchat' => [ 'slug' => 'all-in-box-chat', 'title' => __( 'Webchat', 'all-in-box' ), 'lead' => __( 'Add your All in Box chatbot to every page of this site.', 'all-in-box' ), 'connected' => false, ], 'woocommerce' => [ 'slug' => 'all-in-box-woocommerce', 'title' => 'WooCommerce', 'lead' => __( 'Orders, payments and carts reach All in Box as events on the contact.', 'all-in-box' ), 'connected' => true, 'woo' => true, ], ]; } public function page_connection() { $this->render_page( 'connection' ); } public function page_contacts() { $this->render_page( 'contacts' ); } public function page_form() { $this->render_page( 'form' ); } public function page_webpush() { $this->render_page( 'webpush' ); } public function page_webchat() { $this->render_page( 'webchat' ); } public function page_woocommerce() { $this->render_page( 'woocommerce' ); } /** Hides the pages that need a connection until the site is connected, like the tabs before. */ public function hide_unconnected_pages() { foreach ( $this->hidden_pages as $slug ) { remove_submenu_page( self::MENU_SLUG, $slug ); } } /** Sends the 1.9 address (Settings > All in Box) on to the Connection page. */ public function redirect_legacy_settings_url() { global $pagenow; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing, nothing is changed. $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; if ( 'options-general.php' === $pagenow && self::MENU_SLUG === $page && current_user_can( 'manage_options' ) ) { wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG ) ); exit; } } /** One admin page: the brand header, the title, the notices, then the page's own panel. */ private function render_page( $key ) { if ( ! current_user_can( 'manage_options' ) ) { return; } $pages = $this->admin_pages(); $p = $pages[ $key ]; $s = $this->settings(); $f = function ( $k ) { return self::OPT . '[' . $k . ']'; }; $connected = $this->is_connected( $s ); $woo_notice = ''; if ( 'woocommerce' === $key && $connected && isset( $_GET['aib_woo_create'] ) && check_admin_referer( 'aib_woo_create' ) ) { $n = $this->create_standard_woo_event_types(); $s = $this->settings(); // refresh after the handles are mapped $woo_notice = $n > 0 /* translators: %d: number of event types created or mapped. */ ? '' . sprintf( esc_html__( '%d event type(s) created/mapped in All in Box.', 'all-in-box' ), $n ) . '
' . esc_html__( 'Could not create event types. Check the API URL and token first.', 'all-in-box' ) . '
Enter the address of All in Box and the token of your workspace. The Contacts, Newsletter form and WooCommerce pages appear once the token is validated.
No trailing slash. Usually https://app.allinbox.com.
Workspace key aib_skey_… with the contacts.create_or_update ability.
It is never displayed again once saved.
Defined in wp-config.php.'; ?>
Enter your URL and token, then click Save settings below. The token is validated on save, and the other pages appear in the menu.
ID of the All in Box list that receives the contacts.
By default every form on the site is synced.
Optional. Comma-separated form identifiers (see the list below).
Leave empty to sync every form. IDs are unique per builder, so if two builders share an
ID use the qualified form like metform:11.
Add a native All in Box form anywhere with the
[allinbox_form] shortcode.
[allinbox_form] into a page, a widget or a Divi Text module (a Divi Code module does not run shortcodes). Choose the
language, fields, colors and target list via attributes, e.g.
[allinbox_form lang="fr" fields="firstname,email" accent="#101010" redirect="/thank-you"].
You can place several, each with its own settings.
data-aib-capture to its
<form> tag and the plugin handles the submit (sends to All in Box, then
shows a success message or redirects). Email/consent fields are auto-detected, or mark them
with data-aib-email / data-aib-optin; bots are filtered via a
honeypot (data-aib-hp). The API token never reaches the browser.
Opt-in only. A contact is always created; it is only subscribed with consent.
Leave empty for auto-detection. Set the exact field name to force it.
A postal code is only sent with a country (API requirement). The country
is auto-detected from a country/pays field, or this default is used.
Each form that triggers a sync appears here. Copy the qualified identifier (e.g. metform:11) into “Restrict to forms”.
None yet. Submit a form once and it will show up here.
| Builder | Identifier | Name | Last submission |
|---|---|---|---|
builder_slug( $row['builder'] ) . ':' . $row['id'] ); ?>
or |
Send purchase events to the contact. Each event maps to an All in Box event-type handle.
fetch_event_types() as $t ) { if ( ! empty( $t['handle'] ) && ! empty( $t['schema'] ) ) { $complete[ $t['handle'] ] = ! empty( $t['display_name'] ) ? $t['display_name'] . ' (' . $t['handle'] . ')' : $t['handle']; } } $create_url = wp_nonce_url( add_query_arg( 'aib_woo_create', 1 ), 'aib_woo_create' ); ?>Create standard event types in All in Box Creates Order placed / Payment accepted / Add to cart and maps them below.
woo_event_defs() as $def ) : $cur = (string) $s[ $def['key'] ]; ?>Must exist in All in Box. Set the API token then use the button above.
Install the All in Box Web Push SDK without touching the theme or uploading files. No API token needed: this works on its own, even without contacts sync.
From your All in Box Web Push channel (the data-aib-token value).
When set, the SDK script is injected on every page and the service worker is served at
.
sw_scope() ) : ?>
This site is not at the root of its domain, so notifications cover
sw_scope() ); ?> and its pages only.
Check setup. Runs the same verification as All in Box, for the script and the worker.
Add the All in Box chat bubble to every page of the site. No API token needed: this works on its own, even without contacts sync.
From your All in Box chatbot, section Integrate on your website
(the data-aib-chatbot-key value). Once saved, the chat script is added to
every page of the site.
into its Your website field and publish the chatbot, otherwise the bubble stays silent.
Check setup. Verifies the key, the published status and the declared domain, from this site.
' . esc_html__( 'All in Box: finish the setup in the All in Box menu (API URL and token on Connection, list on Contacts) for this form to appear.', 'all-in-box' ) . '
'; } return ''; } $i18n = $this->form_i18n(); $lang = strtolower( substr( preg_replace( '/[^a-zA-Z]/', '', $a['lang'] ), 0, 2 ) ); if ( ! isset( $i18n[ $lang ] ) ) { $loc = strtolower( substr( get_locale(), 0, 2 ) ); $lang = isset( $i18n[ $loc ] ) ? $loc : 'en'; } $t = $i18n[ $lang ]; // Selected fields (email is always present). $allowed = [ 'firstname', 'lastname', 'email', 'phone', 'birthdate', 'address', 'postal_code', 'country', 'gender' ]; $fields = array_values( array_intersect( $allowed, array_map( 'trim', explode( ',', strtolower( $a['fields'] ) ) ) ) ); if ( ! in_array( 'email', $fields, true ) ) { $fields[] = 'email'; } $id = wp_unique_id( 'aib-form-' ); $accent = $this->sanitize_color( $a['accent'], self::FORM_ACCENT ); $bg = $this->sanitize_color( $a['bg'], '#ffffff' ); $text = $this->sanitize_color( $a['text'], self::FORM_TEXT ); $radius = max( 0, (int) $a['radius'] ); $width = max( 200, (int) $a['width'] ); $fs = 'inherit' === strtolower( trim( (string) $a['font_size'] ) ) ? 'inherit' : ( (int) $a['font_size'] > 0 ? (int) $a['font_size'] . 'px' : '16px' ); $type_map = [ 'email' => 'email', 'phone' => 'tel', 'birthdate' => 'date' ]; $inputs = ''; foreach ( $fields as $f ) { $label = isset( $t[ $f ] ) ? $t[ $f ] : ucfirst( $f ); if ( 'gender' === $f ) { $inputs .= ''; } else { $type = isset( $type_map[ $f ] ) ? $type_map[ $f ] : 'text'; $extra = ( 'country' === $f ? ' maxlength="2" placeholder="FR"' : '' ) . ( 'email' === $f ? ' required' : '' ); $inputs .= ''; } } $button = '' !== $a['button'] ? $a['button'] : $t['button']; $success = '' !== $a['success'] ? $a['success'] : $t['success']; $error = '' !== $a['error'] ? $a['error'] : $t['error']; $consent_html = ''; if ( 'no' !== strtolower( $a['consent'] ) ) { $is_default = ( '' === $a['consent'] || 'yes' === strtolower( $a['consent'] ) ); // The default text mentions open-tracking only when the pixel is actually enabled. $default_key = ! empty( $s['track_consent'] ) ? 'consent_track' : 'consent'; $consent_text = $is_default ? $t[ $default_key ] : $a['consent']; // Optional link to the privacy policy: turn the policy phrase into a link. $policy_url = trim( (string) $a['policy'] ); if ( '' !== $policy_url ) { $inner = $this->linkify_policy( $consent_text, $t['policy_phrase'], $policy_url ); } else { $inner = esc_html( $consent_text ); } $req = ! in_array( strtolower( (string) $a['consent_required'] ), [ 'no', '0', 'false' ], true ) ? ' required' : ''; $consent_html = ''; } // data-aib-t carries the signature of this form's target list, so the endpoint can // trust data-aib-list. Both are emitted together, never one without the other. $list_attr = ' data-aib-list="' . $list_id . '" data-aib-t="' . esc_attr( $this->capture_token( $list_id ) ) . '"'; // Only a path or a URL on this site: wp_validate_redirect() drops javascript: and // any other host, so a shortcode cannot turn into a redirect to a third party. $redirect = wp_validate_redirect( trim( (string) $a['redirect'] ), '' ); $redir_attr = '' !== $redirect ? ' data-aib-redirect="' . esc_attr( $redirect ) . '"' : ''; // Fields and button stay at 10px at most: a card of 20px with fields of 18px reads // as pills. A radius of 12px or less keeps the fields 2px under the card, as before. $ir = min( max( 0, $radius - 2 ), 10 ); // The brand's ink accent gets the brand's lime ring and a dark hover; any other // accent keeps its own translucent ring and the lighter hover it always had. $brand = 0 === strcasecmp( $accent, self::FORM_ACCENT ); $ring = $brand ? '#E2F87E' : $accent . '22'; $hover = $brand ? 'background:#2d2a2a;' : 'filter:brightness(1.08);'; $css = ''; $title_html = '' !== trim( (string) $a['title'] ) ? '' . esc_html( $a['description'] ) . '
' : ''; // method/action matter for the day the script does not run (a JS optimiser, an // error earlier in the page): the browser then reloads the page instead of putting // the address in the query string. $form = ''; return $css . $form; } private function sanitize_color( $value, $default ) { $v = trim( (string) $value ); return preg_match( '/^#[0-9a-fA-F]{3,8}$/', $v ) ? $v : $default; } /** Turns the privacy-policy phrase inside a consent sentence into a link (escaped). */ private function linkify_policy( $text, $phrase, $url ) { $link = '' . esc_html( $phrase ) . ''; $pos = $phrase !== '' ? mb_stripos( $text, $phrase ) : false; if ( false === $pos ) { // Phrase not found (custom text): append the link. return esc_html( $text ) . ' ' . $link; } return esc_html( mb_substr( $text, 0, $pos ) ) . $link . esc_html( mb_substr( $text, $pos + mb_strlen( $phrase ) ) ); } /** Field labels + messages for the 7 All in Box languages. */ private function form_i18n() { return [ 'fr' => [ 'email' => 'E-mail', 'firstname' => 'Prénom', 'lastname' => 'Nom', 'phone' => 'Téléphone', 'birthdate' => 'Date de naissance', 'address' => 'Adresse', 'postal_code' => 'Code postal', 'country' => 'Pays', 'gender' => 'Civilité', 'gender_m' => 'Homme', 'gender_f' => 'Femme', 'consent' => "J'accepte de recevoir des e-mails et j'ai pris connaissance de la politique de confidentialité.", 'consent_track' => "J'accepte de recevoir des e-mails et le suivi de leur ouverture, et j'ai pris connaissance de la politique de confidentialité.", 'policy_phrase' => 'politique de confidentialité', 'button' => "Je m'inscris", 'success' => 'Merci, votre inscription est bien enregistrée.', 'error' => 'Une erreur est survenue, merci de réessayer.' ], 'en' => [ 'email' => 'Email', 'firstname' => 'First name', 'lastname' => 'Last name', 'phone' => 'Phone', 'birthdate' => 'Date of birth', 'address' => 'Address', 'postal_code' => 'Postal code', 'country' => 'Country', 'gender' => 'Title', 'gender_m' => 'Male', 'gender_f' => 'Female', 'consent' => 'I agree to receive emails and have read the privacy policy.', 'consent_track' => 'I agree to receive emails and the associated open tracking, and have read the privacy policy.', 'policy_phrase' => 'privacy policy', 'button' => 'Subscribe', 'success' => "Thanks, you're subscribed!", 'error' => 'Something went wrong, please try again.' ], 'es' => [ 'email' => 'Correo electrónico', 'firstname' => 'Nombre', 'lastname' => 'Apellido', 'phone' => 'Teléfono', 'birthdate' => 'Fecha de nacimiento', 'address' => 'Dirección', 'postal_code' => 'Código postal', 'country' => 'País', 'gender' => 'Tratamiento', 'gender_m' => 'Hombre', 'gender_f' => 'Mujer', 'consent' => 'Acepto recibir correos y he leído la política de privacidad.', 'consent_track' => 'Acepto recibir correos y el seguimiento de su apertura, y he leído la política de privacidad.', 'policy_phrase' => 'política de privacidad', 'button' => 'Suscribirme', 'success' => '¡Gracias, su suscripción se ha registrado!', 'error' => 'Se produjo un error, inténtelo de nuevo.' ], 'de' => [ 'email' => 'E-Mail', 'firstname' => 'Vorname', 'lastname' => 'Nachname', 'phone' => 'Telefon', 'birthdate' => 'Geburtsdatum', 'address' => 'Adresse', 'postal_code' => 'Postleitzahl', 'country' => 'Land', 'gender' => 'Anrede', 'gender_m' => 'Mann', 'gender_f' => 'Frau', 'consent' => 'Ich willige ein, E-Mails zu erhalten, und habe die Datenschutzerklärung gelesen.', 'consent_track' => 'Ich willige ein, E-Mails und das damit verbundene Öffnungs-Tracking zu erhalten, und habe die Datenschutzerklärung gelesen.', 'policy_phrase' => 'Datenschutzerklärung', 'button' => 'Abonnieren', 'success' => 'Danke, Ihre Anmeldung wurde gespeichert!', 'error' => 'Ein Fehler ist aufgetreten, bitte versuchen Sie es erneut.' ], 'it' => [ 'email' => 'Email', 'firstname' => 'Nome', 'lastname' => 'Cognome', 'phone' => 'Telefono', 'birthdate' => 'Data di nascita', 'address' => 'Indirizzo', 'postal_code' => 'Codice postale', 'country' => 'Paese', 'gender' => 'Titolo', 'gender_m' => 'Uomo', 'gender_f' => 'Donna', 'consent' => "Accetto di ricevere email e ho letto l'informativa sulla privacy.", 'consent_track' => "Accetto di ricevere email e il monitoraggio della loro apertura, e ho letto l'informativa sulla privacy.", 'policy_phrase' => 'informativa sulla privacy', 'button' => 'Iscriviti', 'success' => 'Grazie, la tua iscrizione è stata registrata!', 'error' => 'Si è verificato un errore, riprova.' ], 'nl' => [ 'email' => 'E-mail', 'firstname' => 'Voornaam', 'lastname' => 'Achternaam', 'phone' => 'Telefoon', 'birthdate' => 'Geboortedatum', 'address' => 'Adres', 'postal_code' => 'Postcode', 'country' => 'Land', 'gender' => 'Aanhef', 'gender_m' => 'Man', 'gender_f' => 'Vrouw', 'consent' => 'Ik ga akkoord met het ontvangen van e-mails en heb het privacybeleid gelezen.', 'consent_track' => 'Ik ga akkoord met het ontvangen van e-mails en de bijbehorende openingstracking, en heb het privacybeleid gelezen.', 'policy_phrase' => 'privacybeleid', 'button' => 'Inschrijven', 'success' => 'Bedankt, je inschrijving is geregistreerd!', 'error' => 'Er is iets misgegaan, probeer het opnieuw.' ], 'pt' => [ 'email' => 'E-mail', 'firstname' => 'Nome', 'lastname' => 'Apelido', 'phone' => 'Telefone', 'birthdate' => 'Data de nascimento', 'address' => 'Endereço', 'postal_code' => 'Código postal', 'country' => 'País', 'gender' => 'Tratamento', 'gender_m' => 'Homem', 'gender_f' => 'Mulher', 'consent' => 'Aceito receber e-mails e li a política de privacidade.', 'consent_track' => 'Aceito receber e-mails e o acompanhamento da sua abertura, e li a política de privacidade.', 'policy_phrase' => 'política de privacidade', 'button' => 'Subscrever', 'success' => 'Obrigado, a sua inscrição foi registada!', 'error' => 'Ocorreu um erro, tente novamente.' ], ]; } /* ===================================================================== * Web Push install * ================================================================== */ /** Exact body expected by All in Box for the service worker. */ private function sw_content( array $s ) { return 'importScripts("' . rtrim( $s['base_url'], '/' ) . '/sdk/webpush/v1/AIB.sw.js");'; } /** * Scope the worker has to cover, taken from the site address: "/" on an ordinary * install, "/grasse/" on a subdirectory multisite or a WordPress living in a subfolder. * A service worker cannot claim a scope above its own directory, so serving it next to * the pages it covers is what makes those installs work at all, and it keeps one * sub-site of a network from stealing the notifications of another. */ private function sw_scope() { $path = wp_parse_url( home_url( '/' ), PHP_URL_PATH ); return trailingslashit( ( is_string( $path ) && '' !== $path ) ? $path : '/' ); } /** Path the worker answers on. "/AIBSDKWorker.js" on an ordinary install. */ private function sw_request_path() { return $this->sw_scope() . 'AIBSDKWorker.js'; } /** Serves the service worker from PHP, so nothing has to be written to the filesystem. */ public function maybe_serve_service_worker() { if ( empty( $_SERVER['REQUEST_URI'] ) ) { return; } $path = wp_parse_url( esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ), PHP_URL_PATH ); if ( $this->sw_request_path() !== $path ) { return; } $s = $this->settings(); if ( empty( $s['webpush_token'] ) || empty( $s['base_url'] ) ) { return; // not configured → let WordPress handle it (404) } header( 'Content-Type: application/javascript; charset=UTF-8' ); header( 'Service-Worker-Allowed: ' . $this->sw_scope() ); nocache_headers(); echo $this->sw_content( $s ); // phpcs:ignore -- exact body required, no escaping exit; } /** Enqueues the SDK script in the page head on the front-end. */ public function webpush_script() { $s = $this->settings(); if ( empty( $s['webpush_token'] ) || empty( $s['base_url'] ) ) { return; } $handle = 'aib-lead-sync-webpush'; $attrs = [ 'data-aib-token' => $s['webpush_token'], 'data-aib-subscriber-id' => '', ]; // The SDK defaults to /AIBSDKWorker.js with scope "/". Off the root, tell it where // the worker actually is; on an ordinary install the markup stays untouched. $scope = $this->sw_scope(); if ( '/' !== $scope ) { $attrs['data-sw-path'] = $this->sw_request_path(); $attrs['data-sw-scope'] = $scope; } $attrs['charset'] = 'utf-8'; $attrs['defer'] = true; $this->sdk_attributes[ $handle ] = $attrs; wp_enqueue_script( $handle, rtrim( $s['base_url'], '/' ) . '/sdk/webpush/v1/AIB.sdk.js', [], self::VERSION, false ); } /** * Adds the data attributes the SDKs read from their own script tag. wp_enqueue_script() * cannot set arbitrary attributes, and "defer" only from WordPress 6.3 on. */ public function sdk_script_attributes( $tag, $handle ) { if ( empty( $this->sdk_attributes[ $handle ] ) ) { return $tag; } $attrs = ''; foreach ( $this->sdk_attributes[ $handle ] as $name => $value ) { $attrs .= true === $value ? ' ' . $name : ' ' . $name . '="' . esc_attr( $value ) . '"'; } return preg_replace( '/