self::CLASS_OVERLAY, 'exit_intent' => self::CLASS_OVERLAY, 'promo_' => self::CLASS_REGION, 'slot_' => self::CLASS_INLINE, 'sidecart_' => self::CLASS_INLINE, ); const CLASS_LIMITS = array( self::CLASS_OVERLAY => 1, self::CLASS_INLINE => 0, self::CLASS_REGION => 0, ); const TRIGGERS = array('load', 'delay', 'scroll', 'idle', 'exit', 'approach', 'interaction'); const EXIT_TOUCH_FALLBACK = 'idle'; const VERB_MAP = array( 'impression' => 'shown', 'ei_impression' => 'shown', 'promo_impression' => 'shown', 'reco_click' => 'engaged', 'ei_click' => 'engaged', 'promo_click' => 'engaged', 'ei_dismiss' => 'dismissed', 'ei_convert' => 'converted', 'suppressed' => 'suppressed', ); const SUPPRESS_REASONS = array('cap', 'schedule', 'device', 'visitor', 'cart', 'audience', 'placement', 'instigator', 'arbitration'); public static function defaults() { return array( 'start' => 0, 'end' => 0, 'trigger' => 'load', 'delayMs' => 0, 'scrollPct' => 0, 'idleS' => 0, 'rootMargin' => 600, 'freq' => 'always', 'freqDays' => 0, 'maxImpr' => 0, 'maxPerSession' => 0, 'reserveHeight' => 0, 'device' => 'all', 'visitor' => 'all', 'cart' => 'any', 'cartMin' => 0.0, 'audience' => 0, 'instigator' => 'any', 'instigatorRef' => '', 'priority' => 10, 'test' => false, 'v' => 0, ); } const CLIENT_PAINTED_ONLY = array('trigger', 'delayMs', 'scrollPct', 'idleS', 'rootMargin', 'reserveHeight'); const OVERRIDABLE = array( 'trigger', 'delayMs', 'scrollPct', 'idleS', 'rootMargin', 'reserveHeight', 'freq', 'freqDays', 'maxImpr', 'maxPerSession', 'device', 'visitor', 'cart', 'cartMin', 'audience', 'priority', 'instigator', 'instigatorRef', ); const INSTIGATORS = array('any', 'web_push', 'newsletter', 'paid', 'referral', 'direct'); const ENUMS = array( 'trigger' => self::TRIGGERS, 'freq' => array('always', 'session', 'once', 'days'), 'device' => array('all', 'desktop', 'mobile'), 'visitor' => array('all', 'guest', 'logged_in', 'new', 'returning'), 'cart' => array('any', 'empty', 'has_items', 'min_total'), 'instigator' => self::INSTIGATORS, ); public static function sanitize($raw) { $raw = is_array($raw) ? $raw : array(); $out = self::defaults(); foreach ($out as $key => $default) { if (!array_key_exists($key, $raw)) { continue; } $val = $raw[$key]; if (isset(self::ENUMS[$key])) { $val = is_scalar($val) ? (string) $val : ''; $out[$key] = in_array($val, self::ENUMS[$key], true) ? $val : $default; continue; } switch ($key) { case 'test': $out[$key] = (bool) $val; break; case 'cartMin': $out[$key] = max(0.0, (float) $val); break; case 'start': case 'end': case 'v': $out[$key] = max(0, (int) $val); break; case 'scrollPct': $out[$key] = max(0, min(100, (int) $val)); break; case 'delayMs': $out[$key] = max(0, min(600000, (int) $val)); break; case 'idleS': $out[$key] = max(0, min(600, (int) $val)); break; case 'rootMargin': case 'reserveHeight': $out[$key] = max(0, min(4000, (int) $val)); break; case 'priority': $out[$key] = max(0, min(9999, (int) $val)); break; case 'instigatorRef': $out[$key] = substr(sanitize_text_field((string) (is_scalar($val) ? $val : '')), 0, 191); break; default: $out[$key] = max(0, (int) $val); } } return $out; } public static function client_config($stored, $identity = array()) { $cfg = self::sanitize($stored); $identity = is_array($identity) ? $identity : array(); $surface = isset($identity['surface']) ? (string) $identity['surface'] : ''; $cfg['id'] = isset($identity['id']) ? (int) $identity['id'] : 0; $cfg['surface'] = $surface; $cfg['class'] = self::surface_class($surface); if (isset($identity['v'])) { $cfg['v'] = max(0, (int) $identity['v']); } if ($surface !== '' && self::ensure_experiment()) { $ab = AIEO_Display_Experiment::client_ab($surface); if (!empty($ab)) { $cfg['_ab'] = $ab; } } return $cfg; } public static function ensure_origin() { if (class_exists('AIEO_Display_Origin')) { return true; } $local = __DIR__ . '/class-aieo-display-origin.php'; if (file_exists($local)) { require_once $local; } if (!class_exists('AIEO_Display_Origin') && defined('AIEO_DIR') && file_exists(AIEO_DIR . 'includes/class-aieo-display-origin.php')) { require_once AIEO_DIR . 'includes/class-aieo-display-origin.php'; } return class_exists('AIEO_Display_Origin'); } private static function ensure_experiment() { if (class_exists('AIEO_Display_Experiment')) { return true; } $local = __DIR__ . '/class-aieo-display-experiment.php'; if (file_exists($local)) { require_once $local; } if (!class_exists('AIEO_Display_Experiment') && defined('AIEO_DIR') && file_exists(AIEO_DIR . 'includes/class-aieo-display-experiment.php')) { require_once AIEO_DIR . 'includes/class-aieo-display-experiment.php'; } return class_exists('AIEO_Display_Experiment'); } public static function surface_class($surface) { $surface = (string) $surface; foreach (self::CLASS_PREFIXES as $prefix => $class) { if (strpos($surface, $prefix) === 0) { return $class; } } return self::CLASS_INLINE; } public static function class_limit($class) { $limits = self::CLASS_LIMITS; return isset($limits[$class]) ? (int) $limits[$class] : 0; } const ARM_DRIFT_KEY = 'aieo_display_arm_drift'; const ARM_DRIFT_WRITE_GUARD = 'aieo_display_arm_drift_w'; public static function event_arm($surface, $sid, $reported, $fallback = '') { $surface = (string) $surface; $sid = (string) $sid; if ($surface === '' || $sid === '' || !self::ensure_experiment()) { return (string) $fallback; } static $memo = array(); $key = $sid . '|' . $surface; if (!array_key_exists($key, $memo)) { $exp = AIEO_Display_Experiment::for_surface($surface); $memo[$key] = $exp ? array('arm' => AIEO_Display_Experiment::arm_for($surface, $sid), 'arms' => array_keys($exp['arms'])) : null; } if (null === $memo[$key]) { return (string) $fallback; } $server = (string) $memo[$key]['arm']; $reported = is_scalar($reported) ? (string) $reported : ''; if ($reported !== '' && $reported !== $server && in_array($reported, $memo[$key]['arms'], true)) { self::record_arm_drift($surface, $server, $reported); } return $server; } private static function record_arm_drift($surface, $server, $reported) { if (get_transient(self::ARM_DRIFT_WRITE_GUARD)) { return; } set_transient(self::ARM_DRIFT_WRITE_GUARD, 1, MINUTE_IN_SECONDS); $rec = get_transient(self::ARM_DRIFT_KEY); if (!is_array($rec)) { $rec = array('n' => 0, 'first' => '', 'last' => '', 'sample' => array()); } $rec['n'] = (int) $rec['n'] + 1; $rec['last'] = gmdate('Y-m-d H:i:s'); if ($rec['first'] === '') { $rec['first'] = $rec['last']; } $rec['sample'] = array( 'surface' => (string) $surface, 'server' => (string) $server, 'reported' => (string) $reported, ); set_transient(self::ARM_DRIFT_KEY, $rec, 30 * DAY_IN_SECONDS); } public static function arm_drift() { $rec = get_transient(self::ARM_DRIFT_KEY); if (!is_array($rec)) { return array('n' => 0, 'first' => '', 'last' => '', 'sample' => array()); } return array( 'n' => isset($rec['n']) ? (int) $rec['n'] : 0, 'first' => isset($rec['first']) ? (string) $rec['first'] : '', 'last' => isset($rec['last']) ? (string) $rec['last'] : '', 'sample' => isset($rec['sample']) && is_array($rec['sample']) ? $rec['sample'] : array(), ); } public static function clear_arm_drift() { delete_transient(self::ARM_DRIFT_KEY); delete_transient(self::ARM_DRIFT_WRITE_GUARD); } const CONVERSION_PAGES = array('cart', 'checkout', 'account'); const PLACEMENTS = array('all', 'front', 'blog', 'shop', 'product', 'product_cat', 'page', 'cart', 'checkout', 'account', 'ids'); public static function placement_defaults() { return array( 'pages' => array('all'), 'exclude' => array(), 'ids' => array(), 'url' => '', ); } public static function sanitize_placement($raw) { $out = self::placement_defaults(); if (!is_array($raw)) { return $out; } foreach (array('pages', 'exclude') as $key) { if (!array_key_exists($key, $raw)) { continue; } $tokens = array(); foreach ((array) $raw[$key] as $token) { $token = is_scalar($token) ? sanitize_key((string) $token) : ''; if ($token !== '' && in_array($token, self::PLACEMENTS, true) && !in_array($token, $tokens, true)) { $tokens[] = $token; } } $out[$key] = $tokens; } if (array_key_exists('ids', $raw)) { $ids = array(); foreach ((array) $raw['ids'] as $id) { $id = (int) $id; if ($id > 0 && !in_array($id, $ids, true)) { $ids[] = $id; } } $out['ids'] = array_slice($ids, 0, 200); } if (array_key_exists('url', $raw)) { $out['url'] = substr(sanitize_text_field((string) (is_scalar($raw['url']) ? $raw['url'] : '')), 0, 191); } return $out; } public static function placement_matches($rules, $deny_conversion = false) { $rules = is_array($rules) ? $rules : array(); $include = isset($rules['pages']) ? (array) $rules['pages'] : array('all'); $exclude = isset($rules['exclude']) ? (array) $rules['exclude'] : array(); $ids = isset($rules['ids']) ? (array) $rules['ids'] : array(); $needle = isset($rules['url']) ? (string) $rules['url'] : ''; if ($deny_conversion) { foreach (self::CONVERSION_PAGES as $token) { if (self::token_matches($token, array())) { return false; } } if (function_exists('is_wc_endpoint_url') && (is_wc_endpoint_url('order-pay') || is_wc_endpoint_url('order-received'))) { return false; } } if ($needle !== '') { $uri = isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : ''; if (stripos($uri, $needle) === false) { return false; } } foreach ($exclude as $token) { if (self::token_matches($token, $ids)) { return false; } } if (in_array('all', $include, true)) { return true; } foreach ($include as $token) { if (self::token_matches($token, $ids)) { return true; } } return false; } public static function token_matches($token, $ids = array()) { $ids = is_array($ids) ? $ids : array(); switch ($token) { case 'all': return true; case 'front': return is_front_page(); case 'blog': return is_home() || is_singular('post') || is_category() || is_tag(); case 'shop': return function_exists('is_shop') && (is_shop() || (function_exists('is_product_taxonomy') && is_product_taxonomy())); case 'product': return function_exists('is_product') && is_product(); case 'product_cat': return function_exists('is_product_category') && is_product_category(); case 'cart': return function_exists('is_cart') && is_cart(); case 'checkout': return function_exists('is_checkout') && is_checkout(); case 'account': return function_exists('is_account_page') && is_account_page(); case 'page': return is_page(); case 'ids': return $ids && is_singular() && in_array((int) get_queried_object_id(), array_map('intval', $ids), true); } return false; } public static function canonical_verb($event_type) { $map = self::VERB_MAP; $key = (string) $event_type; return isset($map[$key]) ? $map[$key] : ''; } public static function canonical_verb_sql($col = 'event_type') { $cases = ''; foreach (self::VERB_MAP as $raw => $verb) { $cases .= sprintf(" WHEN '%s' THEN '%s'", esc_sql($raw), esc_sql($verb)); } return 'CASE ' . $col . $cases . " ELSE '' END"; } public static function raw_types_for($verb) { $out = array(); foreach (self::VERB_MAP as $raw => $v) { if ($v === (string) $verb) { $out[] = $raw; } } return $out; } public static function raw_types_sql($verb) { $types = self::raw_types_for($verb); if (empty($types)) { return "''"; } return "'" . implode("','", array_map('esc_sql', $types)) . "'"; } private static function asset_uri() { return trailingslashit(dirname(plugins_url('', __FILE__))); } private static function asset_dir() { return trailingslashit(dirname(__DIR__)); } public static function enqueue_runtime() { self::enqueue_triggers(); if (self::$runtime_served) { return; } self::$runtime_served = true; $version = defined('AIEO_VERSION') ? AIEO_VERSION : '1.0'; $dir = self::asset_dir(); $mt = @filemtime($dir . 'assets/js/aieo-display-conditions.js'); wp_enqueue_script( self::HANDLE, self::asset_uri() . 'assets/js/aieo-display-conditions.js', array(), $mt ? $version . '.' . $mt : $version, array('in_footer' => true, 'strategy' => 'defer') ); wp_localize_script(self::HANDLE, 'aieoDisplayCfg', self::runtime_config()); } public static function enqueue_triggers() { if (self::$triggers_served) { return; } self::$triggers_served = true; $version = defined('AIEO_VERSION') ? AIEO_VERSION : '1.0'; $mt = @filemtime(self::asset_dir() . 'assets/js/aieo-display-triggers.js'); if (!wp_script_is(self::HANDLE, 'registered')) { $cmt = @filemtime(self::asset_dir() . 'assets/js/aieo-display-conditions.js'); wp_register_script( self::HANDLE, self::asset_uri() . 'assets/js/aieo-display-conditions.js', array(), $cmt ? $version . '.' . $cmt : $version, array('in_footer' => true, 'strategy' => 'defer') ); } wp_enqueue_script( self::TRIGGERS_HANDLE, self::asset_uri() . 'assets/js/aieo-display-triggers.js', array(self::HANDLE), $mt ? $version . '.' . $mt : $version, array('in_footer' => true, 'strategy' => 'defer') ); } public static function runtime_config() { return array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aieo_popup_audience'), 'consentRequired' => self::consent_required(), 'defaultConsent' => ('1' === (string) get_option('aieo_session_tracking_default_consent', '1')), 'cookiePath' => defined('COOKIEPATH') && COOKIEPATH ? COOKIEPATH : '/', 'cookieDomain' => defined('COOKIE_DOMAIN') && COOKIE_DOMAIN ? COOKIE_DOMAIN : '', 'originRules' => self::ensure_origin() ? AIEO_Display_Origin::client_rules() : null, ); } private static $runtime_served = false; private static $triggers_served = false; public static function mark_runtime_served() { self::$runtime_served = true; } private static function retire_runtime_handle() { $scripts = wp_scripts(); if (!$scripts) { return; } wp_dequeue_script(self::HANDLE); if (!in_array(self::HANDLE, (array) $scripts->done, true)) { $scripts->done[] = self::HANDLE; } } public static function consent_required() { $enabled = get_option('aieo_session_tracking_enabled', '0'); if ($enabled !== '1' && $enabled !== 1) { return false; } $bypass = get_option('aieo_session_bypass_consent', '1'); if ($bypass == '1' || $bypass === 1) { return false; } $require = get_option('aieo_session_tracking_require_consent', '1'); return ($require == '1' || $require === 1); } const GATE_ID = 'aieo-display-gate'; private static $static_surfaces = array(); public static function register_static_surface($surface, $stored, $selector, $identity = array(), $anchor = 0, $emit_shown = true) { $surface = sanitize_key((string) $surface); if ($surface === '' || $selector === '') { return; } $cfg = self::sanitize(is_array($stored) ? $stored : array()); if (!self::can_deny($cfg) && !self::has_experiment($surface)) { return; } $identity = is_array($identity) ? $identity : array(); $identity['surface'] = $surface; self::$static_surfaces[$surface] = array( 'cfg' => self::client_config($stored, $identity), 'sel' => (string) $selector, 'anchor' => (int) $anchor, 'emitShown' => (bool) $emit_shown, ); if (!has_action('wp_head', array(__CLASS__, 'print_static_gate'))) { add_action('wp_head', array(__CLASS__, 'print_static_gate'), 2); } } public static function can_deny(array $cfg) { if (!empty($cfg['start']) || !empty($cfg['end'])) { return true; } if (isset($cfg['device']) && 'all' !== $cfg['device']) { return true; } if (isset($cfg['visitor']) && 'all' !== $cfg['visitor']) { return true; } if (isset($cfg['cart']) && 'any' !== $cfg['cart']) { return true; } if (!empty($cfg['audience'])) { return true; } if (isset($cfg['instigator']) && 'any' !== $cfg['instigator']) { return true; } if (!empty($cfg['maxPerSession']) || !empty($cfg['maxImpr'])) { return true; } if (isset($cfg['freq']) && 'always' !== $cfg['freq']) { return true; } return false; } private static function has_experiment($surface) { if (!self::ensure_experiment()) { return false; } return null !== AIEO_Display_Experiment::for_surface($surface); } public static function has_static_surfaces() { return !empty(self::$static_surfaces); } public static function print_static_gate() { if (empty(self::$static_surfaces) || is_admin()) { return; } $src = self::runtime_source(); if ('' === $src) { return; } $rules = array(); $list = array(); foreach (self::$static_surfaces as $surface => $entry) { $rules[] = sprintf( 'html.aieo-off-%s %s{display:none !important}', $surface, $entry['sel'] ); $item = $entry['cfg']; $item['sel'] = $entry['sel']; $item['anchor'] = $entry['anchor']; $item['emitShown'] = $entry['emitShown']; $list[] = $item; } echo '' . "\n"; printf( '' . "\n", esc_attr(self::GATE_ID . '-cfg'), wp_json_encode(self::runtime_config()) ); echo '' . "\n"; self::mark_runtime_served(); self::retire_runtime_handle(); self::enqueue_triggers(); } private static function runtime_source() { $dir = self::asset_dir(); $min = $dir . 'assets/js/aieo-display-conditions.min.js'; $full = $dir . 'assets/js/aieo-display-conditions.js'; $path = (!defined('SCRIPT_DEBUG') || !SCRIPT_DEBUG) && file_exists($min) ? $min : $full; if (!file_exists($path)) { return ''; } $src = file_get_contents($path); return is_string($src) ? $src : ''; } public static function rocket_needles() { return array(self::HANDLE, self::TRIGGERS_HANDLE, self::GATE_ID); } } }