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);
}
}
}