prefix . 'aieo_addcart_events'; } public static function get_reco_table_name() { global $wpdb; return $wpdb->prefix . 'aieo_reco_events'; } public function maybe_install() { $installed = get_option(self::SCHEMA_OPTION, ''); if ($installed !== self::SCHEMA_VERSION && AIEO_Schema_Lock::acquire(self::SCHEMA_OPTION)) { self::install(); update_option(self::SCHEMA_OPTION, self::SCHEMA_VERSION, false); } $reco_installed = get_option(self::RECO_SCHEMA_OPTION, ''); if ($reco_installed !== self::RECO_SCHEMA_VERSION && AIEO_Schema_Lock::acquire(self::RECO_SCHEMA_OPTION)) { self::install_reco(); update_option(self::RECO_SCHEMA_OPTION, self::RECO_SCHEMA_VERSION, false); } if (get_option(self::COLUMNS_OPTION, '') !== self::COLUMNS_VERSION && ! get_transient(self::COLUMNS_RETRY_TRANSIENT) && AIEO_Schema_Lock::acquire(self::COLUMNS_OPTION)) { set_transient(self::COLUMNS_RETRY_TRANSIENT, 1, HOUR_IN_SECONDS); $ok = self::ensure_session_channel_columns(); $ok = self::ensure_badge_context_column() && $ok; if ($ok) { update_option(self::COLUMNS_OPTION, self::COLUMNS_VERSION, true); delete_transient(self::COLUMNS_RETRY_TRANSIENT); } } } public static function ensure_badge_context_column() { global $wpdb; $tbl = self::get_table_name(); if ((string) $wpdb->get_var("SHOW TABLES LIKE '{$tbl}'") !== $tbl) { return false; } $bcs = $wpdb->get_var("SHOW COLUMNS FROM {$tbl} LIKE 'badge_context_snapshot'"); if (!$bcs) { $wpdb->query("ALTER TABLE {$tbl} ADD COLUMN badge_context_snapshot LONGTEXT NULL DEFAULT NULL AFTER source_meta"); } $bf = $wpdb->get_var("SHOW COLUMNS FROM {$tbl} LIKE 'backfilled_at'"); if (!$bf) { $wpdb->query("ALTER TABLE {$tbl} ADD COLUMN backfilled_at DATETIME NULL DEFAULT NULL"); } $bf_idx = $wpdb->get_var("SHOW INDEX FROM {$tbl} WHERE Key_name = 'idx_backfilled_at'"); if (!$bf_idx) { @$wpdb->query("ALTER TABLE {$tbl} ADD INDEX idx_backfilled_at (backfilled_at, id)"); } $wf_fired = $wpdb->get_var("SHOW COLUMNS FROM {$tbl} LIKE 'aieo_wf_fired_at'"); if (!$wf_fired) { $wpdb->query("ALTER TABLE {$tbl} ADD COLUMN aieo_wf_fired_at DATETIME NULL DEFAULT NULL"); } $wf_idx = $wpdb->get_var("SHOW INDEX FROM {$tbl} WHERE Key_name = 'idx_wf_fired'"); if (!$wf_idx) { @$wpdb->query("ALTER TABLE {$tbl} ADD INDEX idx_wf_fired (aieo_wf_fired_at)"); } $virtuals = (function_exists('aieo_db_supports_analytics') && !aieo_db_supports_analytics()) ? array() : array( 'bcs_was_on_sale' => "TINYINT GENERATED ALWAYS AS (CAST(JSON_VALUE(badge_context_snapshot, '\$.was_on_sale') AS UNSIGNED)) VIRTUAL", 'bcs_is_new' => "TINYINT GENERATED ALWAYS AS (CAST(JSON_VALUE(badge_context_snapshot, '\$.is_new_product') AS UNSIGNED)) VIRTUAL", 'bcs_gift_offer_id' => "INT GENERATED ALWAYS AS (CAST(JSON_VALUE(badge_context_snapshot, '\$.eligible_gift_offer.offer_id') AS UNSIGNED)) VIRTUAL", 'bcs_monthly_offer_id' => "INT GENERATED ALWAYS AS (CAST(JSON_VALUE(badge_context_snapshot, '\$.monthly_offer.offer_id') AS UNSIGNED)) VIRTUAL", ); foreach ($virtuals as $name => $def) { $exists = $wpdb->get_var("SHOW COLUMNS FROM {$tbl} LIKE '{$name}'"); if (!$exists) { @$wpdb->query("ALTER TABLE {$tbl} ADD COLUMN {$name} {$def}"); } } $idx_names = (function_exists('aieo_db_supports_analytics') && !aieo_db_supports_analytics()) ? array() : array( 'idx_bcs_sale' => 'bcs_was_on_sale', 'idx_bcs_new' => 'bcs_is_new', 'idx_bcs_gift' => 'bcs_gift_offer_id', 'idx_bcs_mo' => 'bcs_monthly_offer_id', ); foreach ($idx_names as $idx => $col) { $exists = $wpdb->get_var("SHOW INDEX FROM {$tbl} WHERE Key_name = '{$idx}'"); if (!$exists) { @$wpdb->query("ALTER TABLE {$tbl} ADD INDEX {$idx} ({$col})"); } } self::ensure_archive_column_parity($tbl); return true; } public static function capture_badge_context($product_id, $variation_id = 0, $user_id = 0) { $product_id = (int) $product_id; $variation_id = (int) $variation_id; $priced_pid = $variation_id > 0 ? $variation_id : $product_id; $audience_key = class_exists('AIEO_Identity_Bridge') ? AIEO_Identity_Bridge::audience_key_for_current_user() : 'guest'; $snap = array( 'schema_v' => 1, 'captured_at' => gmdate('c'), 'audience_key' => $audience_key, 'priced_pid' => $priced_pid, ); if (class_exists('AIEO_Effective_Prices')) { $row = AIEO_Effective_Prices::get_for($product_id, $audience_key); if (is_array($row)) { $snap['was_on_sale'] = !empty($row['was_on_sale']) ? 1 : 0; $snap['sale_discount_pct'] = isset($row['sale_discount_pct']) ? $row['sale_discount_pct'] : null; $snap['regular_price'] = isset($row['base_price']) ? (float) $row['base_price'] : null; $snap['effective_price'] = isset($row['sale_price']) && $row['sale_price'] !== null ? (float) $row['sale_price'] : (isset($row['base_price']) ? (float) $row['base_price'] : null); $snap['role_price_applied'] = !empty($row['role_price_applied']) ? 1 : 0; $snap['monthly_offer'] = array( 'active' => !empty($row['monthly_offer_active']) ? 1 : 0, 'offer_id' => null, ); $snap['source_rule'] = array( 'type' => isset($row['source_rule_type']) ? $row['source_rule_type'] : null, 'id' => isset($row['source_rule_id']) ? (int) $row['source_rule_id'] : null, ); } } if (class_exists('AIEO_DMM_Brand_Discounts')) { $snap['is_new_product'] = AIEO_DMM_Brand_Discounts::is_new_product($product_id) ? 1 : 0; $post_time = get_post_time('U', true, $product_id); $snap['days_since_publish'] = $post_time ? (int) floor((time() - (int) $post_time) / DAY_IN_SECONDS) : null; } if (class_exists('AIEO_Effective_Eligibility')) { foreach (array('brand_gift', 'product_label', 'complementary') as $surface) { $row = AIEO_Effective_Eligibility::get_for($surface, $audience_key, $product_id); if (!is_array($row) || empty($row['eligible'])) { continue; } switch ($surface) { case 'brand_gift': $snap['eligible_gift_offer'] = array( 'offer_id' => isset($row['offer_id']) ? (int) $row['offer_id'] : null, 'label' => isset($row['payload']['label']) ? (string) $row['payload']['label'] : '', 'qualified_at_addtime' => 0, ); break; case 'product_label': if (!isset($row['payload']['rule'])) { break; } $snap['product_label'] = array( 'rule' => (string) $row['payload']['rule'], 'label' => isset($row['payload']['label']) ? (string) $row['payload']['label'] : '', ); break; case 'complementary': $snap['complementary'] = array( 'recipe_id' => isset($row['offer_id']) ? (int) $row['offer_id'] : null, 'count' => isset($row['payload']['count']) ? (int) $row['payload']['count'] : 0, ); break; } } } if (!empty($snap['monthly_offer']['active']) && class_exists('AIEO_DMM_Brand_Discounts')) { $batch = get_post_meta($product_id, AIEO_DMM_Brand_Discounts::META_MONTHLY_BATCH, true); if (!empty($batch)) { $snap['monthly_offer']['offer_id'] = is_numeric($batch) ? (int) $batch : (string) $batch; } } return $snap; } public static function ensure_session_channel_columns() { global $wpdb; $tbl = $wpdb->prefix . 'aieo_sessions_statics'; if ((string) $wpdb->get_var("SHOW TABLES LIKE '{$tbl}'") !== $tbl) { return false; } $cols = array( 'entry_channel' => "VARCHAR(40) NOT NULL DEFAULT '' AFTER referrer", 'entry_campaign' => "VARCHAR(120) NOT NULL DEFAULT '' AFTER entry_channel", 'entry_medium' => "VARCHAR(40) NOT NULL DEFAULT '' AFTER entry_campaign", 'entry_term' => "VARCHAR(80) NOT NULL DEFAULT '' AFTER entry_medium", 'entry_content' => "VARCHAR(80) NOT NULL DEFAULT '' AFTER entry_term", 'entry_source' => "VARCHAR(80) NOT NULL DEFAULT '' AFTER entry_channel", 'entry_click_id' => "VARCHAR(255) NOT NULL DEFAULT '' AFTER entry_content", 'entry_ad_id' => "VARCHAR(40) NOT NULL DEFAULT '' AFTER entry_click_id", 'entry_adset_id' => "VARCHAR(40) NOT NULL DEFAULT '' AFTER entry_ad_id", 'entry_ad_campaign_id' => "VARCHAR(40) NOT NULL DEFAULT '' AFTER entry_adset_id", ); $prev_suppress = $wpdb->suppress_errors(true); foreach ($cols as $name => $def) { $exists = $wpdb->get_var("SHOW COLUMNS FROM {$tbl} LIKE '{$name}'"); if (!$exists) { $wpdb->query("ALTER TABLE {$tbl} ADD COLUMN {$name} {$def}"); } } $idx = $wpdb->get_var("SHOW INDEX FROM {$tbl} WHERE Key_name = 'idx_entry_channel'"); if (!$idx) { $wpdb->query("ALTER TABLE {$tbl} ADD INDEX idx_entry_channel (entry_channel)"); } $wpdb->suppress_errors($prev_suppress); $coll = function_exists('aieo_analytics_collation') ? aieo_analytics_collation() : 'utf8mb4_uca1400_ai_ci'; self::reconcile_session_id_collation($coll); self::reconcile_column_collation('email', $coll); return true; } public static function reconcile_session_id_collation(string $target): void { self::reconcile_column_collation('session_id', $target); } public static function reconcile_column_collation(string $column, string $target): void { global $wpdb; $column = preg_replace('/[^a-z0-9_]/i', '', $column); if ($column === '') { return; } $supported = (string) $wpdb->get_var($wpdb->prepare( "SELECT COLLATION_NAME FROM information_schema.COLLATIONS WHERE COLLATION_NAME = %s LIMIT 1", $target )); if ($supported === '') { $addcart_tbl = $wpdb->prefix . 'aieo_addcart_events'; $fallback = (string) $wpdb->get_var($wpdb->prepare( "SELECT COLLATION_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = %s", $addcart_tbl, $column )); if ($fallback === '' || $fallback === $target) { return; } $target = $fallback; } $prefix_like = $wpdb->esc_like($wpdb->prefix . 'aieo_') . '%'; $rows = $wpdb->get_results($wpdb->prepare( "SELECT TABLE_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME LIKE %s AND COLUMN_NAME = %s AND COLLATION_NAME <> %s", $prefix_like, $column, $target ), ARRAY_A); if (empty($rows)) { return; } foreach ($rows as $r) { $tbl = $r['TABLE_NAME']; $type = $r['COLUMN_TYPE']; $null = strtoupper($r['IS_NULLABLE']) === 'YES' ? 'NULL' : 'NOT NULL'; if ($r['COLUMN_DEFAULT'] === null) { $default = ($null === 'NOT NULL') ? "DEFAULT ''" : ''; } else { $default = "DEFAULT '" . esc_sql((string) $r['COLUMN_DEFAULT']) . "'"; } $wpdb->query( "ALTER TABLE `{$tbl}` MODIFY `{$column}` {$type} CHARACTER SET utf8mb4 COLLATE {$target} {$null} {$default}" ); } } public static function install() { global $wpdb; $table = self::get_table_name(); $charset = aieo_get_charset_collate(); $sql = "CREATE TABLE {$table} ( id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, event_id varchar(36) NOT NULL DEFAULT '', session_id varchar(64) NOT NULL DEFAULT '', user_id bigint(20) UNSIGNED NOT NULL DEFAULT 0, product_id bigint(20) UNSIGNED NOT NULL, variation_id bigint(20) UNSIGNED NOT NULL DEFAULT 0, quantity int NOT NULL DEFAULT 1, unit_price decimal(18,4) NOT NULL DEFAULT 0.0000, source varchar(40) NOT NULL DEFAULT 'unknown', source_pid bigint(20) UNSIGNED NOT NULL DEFAULT 0, source_meta text NULL, cart_position int NOT NULL DEFAULT 0, ts_added datetime NOT NULL, ts_viewed datetime NULL DEFAULT NULL, order_id bigint(20) UNSIGNED NOT NULL DEFAULT 0, ga_sent tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY event_id (event_id), KEY session_id (session_id), KEY product_id (product_id), KEY source (source), KEY ts_added (ts_added), KEY order_id (order_id), KEY user_id (user_id) ) {$charset};"; if (!function_exists('dbDelta')) { require_once ABSPATH . 'wp-admin/includes/upgrade.php'; } dbDelta($sql); } public static function install_reco() { global $wpdb; $table = self::get_reco_table_name(); $charset = aieo_get_charset_collate(); $sql = "CREATE TABLE {$table} ( id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, event_id varchar(36) NOT NULL DEFAULT '', session_id varchar(64) NOT NULL DEFAULT '', user_id bigint(20) UNSIGNED NOT NULL DEFAULT 0, event_type varchar(16) NOT NULL DEFAULT 'impression', target_pid bigint(20) UNSIGNED NOT NULL, target_type varchar(20) NOT NULL DEFAULT '', anchor_pid bigint(20) UNSIGNED NOT NULL DEFAULT 0, anchor_type varchar(20) NOT NULL DEFAULT '', anchor_term_id bigint(20) UNSIGNED NOT NULL DEFAULT 0, anchor_term_tax varchar(32) NOT NULL DEFAULT '', surface varchar(40) NOT NULL DEFAULT 'unknown', position smallint UNSIGNED NOT NULL DEFAULT 0, arm varchar(16) NOT NULL DEFAULT '', reason varchar(16) NOT NULL DEFAULT '', origin_id int UNSIGNED NOT NULL DEFAULT 0, ts datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY event_id (event_id), KEY session_target (session_id, target_pid), KEY surface (surface), KEY anchor_pid (anchor_pid), KEY anchor_term_id (anchor_term_id), KEY arm_ts (arm, ts), KEY origin_ts (origin_id, ts), KEY ts (ts) ) {$charset};"; if (!function_exists('dbDelta')) { require_once ABSPATH . 'wp-admin/includes/upgrade.php'; } dbDelta($sql); self::ensure_archive_column_parity(); } public static function ensure_archive_column_parity($hot = '') { global $wpdb; $hot = '' !== (string) $hot ? (string) $hot : self::get_reco_table_name(); $archive = $hot . '_archive'; $have_archive = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s", $archive )); if ($have_archive < 1) { return 0; } $cols = $wpdb->get_results($wpdb->prepare( "SELECT TABLE_NAME, COLUMN_NAME, EXTRA FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (%s, %s)", $hot, $archive )); $hot_cols = array(); $arc_cols = array(); foreach ((array) $cols as $c) { if ($c->TABLE_NAME === $hot) { if (false === stripos((string) $c->EXTRA, 'GENERATED')) { $hot_cols[strtolower($c->COLUMN_NAME)] = $c; } } else { $arc_cols[strtolower($c->COLUMN_NAME)] = true; } } $missing = array_diff_key($hot_cols, $arc_cols); if (!$missing) { return 0; } $create = $wpdb->get_row("SHOW CREATE TABLE `{$hot}`", ARRAY_N); $defs = array(); foreach (preg_split('/\r?\n/', (string) ($create[1] ?? '')) as $line) { if (preg_match('/^\s*`([^`]+)`\s+(.+?),?\s*$/', $line, $m)) { $defs[strtolower($m[1])] = $m[2]; } } $added = 0; foreach ($missing as $name => $c) { if (!isset($defs[$name])) { continue; } $ok = $wpdb->query("ALTER TABLE `{$archive}` ADD COLUMN `{$c->COLUMN_NAME}` {$defs[$name]}"); if (false === $ok) { aieo_debug_log('AIEO_Attribution: archive column parity failed for ' . $c->COLUMN_NAME . ': ' . $wpdb->last_error); continue; } $added++; } return $added; } public function enqueue_assets() { if (is_admin()) { return; } if (!apply_filters('aieo_attribution_client_enabled', true)) { return; } $has_wc = class_exists('WooCommerce'); $has_reco_cards = self::content_reco_surface_present(); if (!$has_wc && !$has_reco_cards) { return; } if (!$has_wc) { self::enqueue_reco_observer(null); return; } $archive_context = $this->resolve_archive_context(); self::enqueue_reco_observer($archive_context); } public static function content_reco_surface_present() { return class_exists('AIEO_Semantic_Reco_Source') && AIEO_Semantic_Reco_Source::available(); } private static function enqueue_reco_observer($archive_context) { self::enqueue_cards_asset(); $reco_handle = 'aieo-reco-observer'; $reco_rel = 'assets/js/aieo-reco-observer.js'; $reco_url = plugins_url($reco_rel, AIEO_FILE); $reco_path = dirname(AIEO_FILE) . '/' . $reco_rel; $reco_ver = file_exists($reco_path) ? filemtime($reco_path) : (defined('AIEO_VERSION') ? AIEO_VERSION : false); wp_enqueue_script($reco_handle, $reco_url, array('aieo-cards'), $reco_ver, array('in_footer' => true, 'strategy' => 'defer')); add_filter('rooster/third_party/handles', static function ($handles) use ($reco_handle) { $handles = is_array($handles) ? $handles : array(); $handles[$reco_handle] = array('trigger' => 'human'); return $handles; }); add_filter('rocket_delay_js_exclusions', array(__CLASS__, 'rocket_delay_exclusions_reco')); wp_localize_script($reco_handle, 'aieoRecoObserver', array( 'enabled' => true, 'ajaxUrl' => admin_url('admin-ajax.php'), 'flushIntervalMs' => 5000, 'maxBatchSize' => 40, 'archive' => $archive_context, )); } public static function rocket_delay_exclusions_reco($list) { $list = is_array($list) ? $list : array(); $list[] = 'aieo-reco-observer'; $list[] = 'aieo-cards'; return array_values(array_unique($list)); } public static function enqueue_cards_asset() { if (wp_script_is('aieo-cards', 'enqueued') || wp_script_is('aieo-cards', 'registered')) { wp_enqueue_script('aieo-cards'); return; } $rel = 'assets/js/aieo-cards.js'; $path = dirname(AIEO_FILE) . '/' . $rel; $ver = file_exists($path) ? filemtime($path) : (defined('AIEO_VERSION') ? AIEO_VERSION : false); wp_enqueue_script('aieo-cards', plugins_url($rel, AIEO_FILE), array(), $ver, array('in_footer' => true, 'strategy' => 'defer')); } private function resolve_archive_context() { if (!function_exists('is_tax') || !function_exists('get_queried_object')) { return null; } $brand_taxonomies = array('product_brand', 'pwb-brand', 'yith_product_brand'); if (!is_tax()) { return null; } $term = get_queried_object(); if (!$term || empty($term->term_id) || empty($term->taxonomy)) { return null; } $tax = (string) $term->taxonomy; $tax_obj = function_exists('get_taxonomy') ? get_taxonomy($tax) : null; $object_types = ($tax_obj && !empty($tax_obj->object_type)) ? (array) $tax_obj->object_type : array(); $is_brand = in_array($tax, $brand_taxonomies, true); if (!$is_brand && !in_array('product', $object_types, true)) { return null; } if ($is_brand) { $tax = 'product_brand'; } return array( 'term_id' => (int) $term->term_id, 'tax' => $tax, 'slug' => (string) $term->slug, 'name' => (string) $term->name, ); } public function ajax_log() { if (!headers_sent()) { @header('Connection: close'); @header('Content-Length: 0'); } while (ob_get_level() > 0) { ob_end_clean(); } $ip = isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : ''; if ($ip !== '') { $cap = (int) apply_filters('aieo_attribution_log_rate_limit_per_minute', 60); if ($cap > 0) { $key = 'aieo_attr_rl_' . md5($ip); $hits = (int) get_transient($key); if ($hits >= $cap) { status_header(429); wp_die('', '', array('response' => 429)); } set_transient($key, $hits + 1, MINUTE_IN_SECONDS); } } $this->log_event(array( 'event_id' => isset($_POST['event_id']) ? sanitize_text_field((string) wp_unslash($_POST['event_id'])) : '', 'product_id' => isset($_POST['product_id']) ? absint(wp_unslash($_POST['product_id'])) : 0, 'variation_id' => isset($_POST['variation_id']) ? absint(wp_unslash($_POST['variation_id'])) : 0, 'quantity' => isset($_POST['quantity']) ? max(1, absint(wp_unslash($_POST['quantity']))) : 1, 'unit_price' => isset($_POST['unit_price']) ? (float) sanitize_text_field(wp_unslash($_POST['unit_price'])) : 0.0, 'source' => isset($_POST['source']) ? sanitize_key((string) wp_unslash($_POST['source'])) : 'unknown', 'source_pid' => isset($_POST['source_pid']) ? absint(wp_unslash($_POST['source_pid'])) : 0, 'source_meta' => isset($_POST['source_meta']) ? (is_array($_POST['source_meta']) ? map_deep(wp_unslash($_POST['source_meta']), 'sanitize_text_field') : sanitize_textarea_field((string) wp_unslash($_POST['source_meta']))) : null, )); status_header(204); wp_die('', '', array('response' => 204)); } public function ajax_reco_batch() { if (!headers_sent()) { @header('Connection: close'); @header('Content-Length: 0'); } while (ob_get_level() > 0) { ob_end_clean(); } if (class_exists('AIEO_Session_Tracker') && method_exists('AIEO_Session_Tracker', 'is_consent_blocking_writes') && AIEO_Session_Tracker::is_consent_blocking_writes()) { status_header(204); wp_die('', '', array('response' => 204)); } $ip = isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : ''; if ($ip !== '') { $cap = (int) apply_filters('aieo_reco_log_rate_limit_per_minute', 60); if ($cap > 0) { $key = 'aieo_reco_rl_' . md5($ip); $hits = (int) get_transient($key); if ($hits >= $cap) { status_header(429); wp_die('', '', array('response' => 429)); } set_transient($key, $hits + 1, MINUTE_IN_SECONDS); } } $raw = isset($_POST['events']) ? wp_unslash((string) $_POST['events']) : ''; $events = $raw !== '' ? json_decode($raw, true) : null; if (is_array($events) && !empty($events)) { $this->log_reco_events($events); } status_header(204); wp_die('', '', array('response' => 204)); } private function log_reco_events(array $events) { global $wpdb; $table = self::get_reco_table_name(); $session_id = sanitize_text_field($this->get_session_id()); $user_id = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0; $request_time = time(); $arm = class_exists('AIEO_Reco_Experiment') ? AIEO_Reco_Experiment::arm() : ''; $origin_id = self::resolve_origin_id(isset($_POST['origin']) ? wp_unslash($_POST['origin']) : ''); $rows = array(); $placeholders = array(); $cap = 100; $i = 0; foreach ($events as $e) { if ($i++ >= $cap) { break; } if (!is_array($e)) { continue; } $type = isset($e['type']) ? (string) $e['type'] : ''; if (!in_array($type, self::RECO_EVENT_TYPES, true)) { continue; } $target_pid = isset($e['target_pid']) ? (int) $e['target_pid'] : 0; if ($target_pid <= 0 && !in_array($type, self::RECO_NON_PRODUCT_EVENT_TYPES, true)) { continue; } $anchor_pid = isset($e['anchor_pid']) ? (int) $e['anchor_pid'] : 0; $target_type = isset($e['target_type']) ? substr(sanitize_key((string) $e['target_type']), 0, 20) : ''; $anchor_type = isset($e['anchor_type']) ? substr(sanitize_key((string) $e['anchor_type']), 0, 20) : ''; $surface = isset($e['surface']) ? sanitize_key((string) $e['surface']) : 'unknown'; $event_id = isset($e['event_id']) ? (string) $e['event_id'] : ''; if ($event_id === '' || strlen($event_id) > 36 || stripos($event_id, 'eic-') === 0) { $event_id = $this->generate_uuid_v4(); } $offset_ms = isset($e['ts_offset_ms']) ? (int) $e['ts_offset_ms'] : 0; if ($offset_ms < 0) { $offset_ms = 0; } if ($offset_ms > 6 * HOUR_IN_SECONDS * 1000) { $offset_ms = 0; } $ts = gmdate('Y-m-d H:i:s', $request_time - intdiv($offset_ms, 1000)); $position = isset($e['position']) ? max(0, min(999, (int) $e['position'])) : 0; $anchor_term_id = isset($e['anchor_term_id']) ? (int) $e['anchor_term_id'] : 0; $anchor_term_tax = isset($e['anchor_term_tax']) ? sanitize_key((string) $e['anchor_term_tax']) : ''; $allowed_tax = array('product_cat', 'product_tag', 'product_brand'); if ($anchor_term_tax !== '' && !in_array($anchor_term_tax, $allowed_tax, true)) { $anchor_term_tax = ''; $anchor_term_id = 0; } $reason = ''; if ('suppressed' === $type && isset($e['reason']) && is_scalar($e['reason'])) { $candidate = sanitize_key((string) $e['reason']); if (self::ensure_display_conditions() && in_array($candidate, AIEO_Display_Conditions::SUPPRESS_REASONS, true)) { $reason = $candidate; } } $row_arm = $arm; if (self::ensure_display_conditions()) { $row_arm = AIEO_Display_Conditions::event_arm( $surface, $session_id, isset($e['arm']) ? $e['arm'] : '', $arm ); } $rows[] = array( 'event_id' => $event_id, 'session_id' => $session_id, 'user_id' => $user_id, 'event_type' => $type, 'target_pid' => $target_pid, 'target_type' => $target_type, 'anchor_pid' => $anchor_pid, 'anchor_type' => $anchor_type, 'anchor_term_id' => $anchor_term_id, 'anchor_term_tax' => $anchor_term_tax, 'surface' => $surface, 'position' => $position, 'arm' => $row_arm, 'reason' => $reason, 'origin_id' => $origin_id, 'ts' => $ts, ); $placeholders[] = '(%s, %s, %d, %s, %d, %s, %d, %s, %d, %s, %s, %d, %s, %s, %d, %s)'; } if (empty($rows)) { return; } $values = array(); foreach ($rows as $r) { $values[] = $r['event_id']; $values[] = $r['session_id']; $values[] = $r['user_id']; $values[] = $r['event_type']; $values[] = $r['target_pid']; $values[] = $r['target_type']; $values[] = $r['anchor_pid']; $values[] = $r['anchor_type']; $values[] = $r['anchor_term_id']; $values[] = $r['anchor_term_tax']; $values[] = $r['surface']; $values[] = $r['position']; $values[] = $r['arm']; $values[] = $r['reason']; $values[] = $r['origin_id']; $values[] = $r['ts']; } $sql = "INSERT IGNORE INTO {$table} (event_id, session_id, user_id, event_type, target_pid, target_type, anchor_pid, anchor_type, anchor_term_id, anchor_term_tax, surface, position, arm, reason, origin_id, ts) VALUES " . implode(', ', $placeholders); $wpdb->query($wpdb->prepare($sql, $values)); } public static function resolve_origin_id($json) { if (!is_string($json) || '' === $json) { return 0; } if (strlen($json) > 2048) { return 0; } if (!self::ensure_display_origin()) { return 0; } $raw = json_decode($json, true); if (!is_array($raw)) { return 0; } return AIEO_Display_Origin::resolve_id(AIEO_Display_Origin::sanitize_tuple($raw)); } private static function ensure_display_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_display_conditions() { if (class_exists('AIEO_Display_Conditions')) { return true; } $local = __DIR__ . '/class-aieo-display-conditions.php'; if (file_exists($local)) { require_once $local; } if (!class_exists('AIEO_Display_Conditions') && defined('AIEO_DIR') && file_exists(AIEO_DIR . 'includes/class-aieo-display-conditions.php')) { require_once AIEO_DIR . 'includes/class-aieo-display-conditions.php'; } return class_exists('AIEO_Display_Conditions'); } public function log_event(array $args) { global $wpdb; $product_id = isset($args['product_id']) ? (int) $args['product_id'] : 0; if ($product_id <= 0) { return false; } if ( class_exists('AIEO_Session_Tracker') && method_exists('AIEO_Session_Tracker', 'is_consent_blocking_writes') ) { if ( AIEO_Session_Tracker::is_consent_blocking_writes() ) { return false; } } $source = isset($args['source']) ? (string) $args['source'] : 'unknown'; if (!in_array($source, self::VALID_SOURCES, true)) { $source = 'unknown'; } $event_id = isset($args['event_id']) ? (string) $args['event_id'] : ''; if ($event_id === '' || strlen($event_id) > 36) { $event_id = $this->generate_uuid_v4(); } $session_id = $this->get_session_id(); $user_id = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0; if ( $session_id !== '' && preg_match('/^[0-9]{1,3}$/', (string) $session_id) ) { if ( function_exists('aieo_debug_log') ) { aieo_debug_log( '[AIEO_Attribution] Numeric-trivial session_id "' . $session_id . '" — ST cookie may not have initialised yet. product_id=' . $product_id . ' source=' . $source ); } } $cart_position = 0; $session_id_clean = sanitize_text_field($session_id); if ($session_id_clean !== '') { $prev = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM " . self::get_table_name() . " WHERE session_id = %s AND order_id = 0", $session_id_clean )); $cart_position = $prev + 1; } $source_meta = null; if (isset($args['source_meta'])) { if (is_array($args['source_meta'])) { $source_meta = wp_json_encode($args['source_meta']); } else { $decoded = json_decode((string) $args['source_meta'], true); if ($decoded !== null || $args['source_meta'] === 'null') { $source_meta = (string) $args['source_meta']; } else { $source_meta = wp_json_encode(array('raw' => (string) $args['source_meta'])); } } $source_meta = substr((string) $source_meta, 0, 8192); } $variation_id_int = isset($args['variation_id']) ? (int) $args['variation_id'] : 0; $badge_snapshot = self::capture_badge_context($product_id, $variation_id_int, $user_id); $badge_snapshot_json = wp_json_encode($badge_snapshot); if (!is_string($badge_snapshot_json)) { $badge_snapshot_json = null; } $ok = $wpdb->insert( self::get_table_name(), array( 'event_id' => $event_id, 'session_id' => $session_id_clean, 'user_id' => $user_id, 'product_id' => $product_id, 'variation_id' => $variation_id_int, 'quantity' => isset($args['quantity']) ? max(1, (int) $args['quantity']) : 1, 'unit_price' => isset($args['unit_price']) ? (float) $args['unit_price'] : 0.0, 'source' => $source, 'source_pid' => isset($args['source_pid']) ? (int) $args['source_pid'] : 0, 'source_meta' => $source_meta, 'badge_context_snapshot' => $badge_snapshot_json, 'cart_position' => $cart_position, 'ts_added' => current_time('mysql', true), 'ga_sent' => 0, ), array('%s', '%s', '%d', '%d', '%d', '%d', '%f', '%s', '%d', '%s', '%s', '%d', '%s', '%d') ); return $ok === false ? false : (int) $wpdb->insert_id; } public function on_order_processed($order_id, $posted_data) { $this->backfill_order_id($order_id); } public function on_thankyou($order_id) { $this->backfill_order_id((int) $order_id); } private function backfill_order_id($order_id) { if (!$order_id) { return; } global $wpdb; $order = function_exists('wc_get_order') ? wc_get_order($order_id) : null; if (!$order) { return; } $candidates = array(); $current = $this->get_session_id(); if ($current !== '') { $candidates[] = $current; } $cust_id = (int) $order->get_customer_id(); if ($cust_id > 0) { $candidates[] = (string) $cust_id; } $candidates = array_values(array_unique(array_filter($candidates))); $since = gmdate('Y-m-d H:i:s', time() - (7 * DAY_IN_SECONDS)); $table = self::get_table_name(); $order_created_ts = $order->get_date_created() ? $order->get_date_created()->getTimestamp() : time(); $window_since = gmdate('Y-m-d H:i:s', $order_created_ts - (2 * HOUR_IN_SECONDS)); $window_until = gmdate('Y-m-d H:i:s', $order_created_ts + (10 * MINUTE_IN_SECONDS)); foreach ($order->get_items() as $item) { if (!is_a($item, 'WC_Order_Item_Product')) { continue; } $pid = (int) $item->get_product_id(); if ($pid <= 0) { continue; } $matched = 0; if (!empty($candidates)) { $placeholders = implode(',', array_fill(0, count($candidates), '%s')); $args = array_merge( array((int) $order_id), $candidates, array($pid, $since) ); $matched = (int) $wpdb->query($wpdb->prepare( "UPDATE {$table} SET order_id = %d, backfilled_at = UTC_TIMESTAMP() WHERE session_id IN ({$placeholders}) AND product_id = %d AND order_id = 0 AND ts_added >= %s", $args )); } if ($matched > 0) { continue; } $qty = max(1, (int) $item->get_quantity()); $ids_to_claim = $wpdb->get_col($wpdb->prepare( "SELECT id FROM {$table} WHERE product_id = %d AND order_id = 0 AND user_id = 0 AND session_id LIKE 't\_%%' AND ts_added >= %s AND ts_added <= %s ORDER BY ts_added DESC LIMIT %d", $pid, $window_since, $window_until, $qty )); if (!empty($ids_to_claim)) { $id_list = implode(',', array_map('intval', $ids_to_claim)); $wpdb->query($wpdb->prepare( "UPDATE {$table} SET order_id = %d, backfilled_at = UTC_TIMESTAMP() WHERE id IN ({$id_list})", (int) $order_id )); } } if (class_exists('AIEO_Subject_Resolver')) { $aieo_vk = $this->get_session_id(); if ('' !== $aieo_vk) { $aieo_sid = AIEO_Subject_Resolver::get_or_create_subject($aieo_vk); if ($aieo_sid > 0) { AIEO_Subject_Resolver::stamp_identity( $aieo_sid, (int) $order->get_customer_id(), (string) $order->get_billing_email() ); } } } } private function get_session_id() { $cookie = 'aieo_attr_sid'; if (isset($_COOKIE[$cookie]) && (string) $_COOKIE[$cookie] !== '') { return sanitize_text_field((string) wp_unslash($_COOKIE[$cookie])); } if (class_exists('AIEO_Session_Tracker') && method_exists('AIEO_Session_Tracker', 'is_consent_blocking_writes') && AIEO_Session_Tracker::is_consent_blocking_writes()) { return ''; } $sid = $this->generate_uuid_v4(); if (!headers_sent()) { setcookie($cookie, $sid, time() + (30 * DAY_IN_SECONDS), COOKIEPATH ? COOKIEPATH : '/', COOKIE_DOMAIN); } $_COOKIE[$cookie] = $sid; return $sid; } public static function read_visitor_key() { if (isset($_COOKIE['aieo_attr_sid']) && (string) $_COOKIE['aieo_attr_sid'] !== '') { return sanitize_text_field((string) wp_unslash($_COOKIE['aieo_attr_sid'])); } return (string) self::instance()->get_session_id(); } private function generate_uuid_v4() { try { $data = random_bytes(16); $data[6] = chr(ord($data[6]) & 0x0f | 0x40); $data[8] = chr(ord($data[8]) & 0x3f | 0x80); return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } catch (Exception $e) { return uniqid('aieo_', true); } } const VALID_CHANNELS = array( 'web_push', 'newsletter', 'paid_google', 'paid_facebook', 'paid_microsoft', 'paid_tiktok', 'paid_other', 'organic_search', 'social', 'referral', 'direct', 'internal', ); const CANONICAL_CHANNELS = array( 'web_push', 'newsletter', 'paid_google', 'paid_facebook', 'paid_microsoft', 'paid_tiktok', 'paid_other', 'organic_search', 'social', 'referral', 'direct', 'internal', 'skroutz', 'shopflix', 'bestprice', 'admin', 'unknown', ); const PAID_MEDIUM_REGEX = 'cpc|ppc|paid|display|cpm'; private static function click_id_channels() { return array( 'gclid' => 'paid_google', 'gbraid' => 'paid_google', 'wbraid' => 'paid_google', 'fbclid' => 'paid_facebook', 'ttclid' => 'paid_tiktok', 'msclkid' => 'paid_microsoft', ); } private static function source_platform_map() { return array( 'fb' => array( 'paid_facebook', 'social' ), 'facebook' => array( 'paid_facebook', 'social' ), 'ig' => array( 'paid_facebook', 'social' ), 'instagram' => array( 'paid_facebook', 'social' ), 'meta' => array( 'paid_facebook', 'social' ), 'tiktok' => array( 'paid_tiktok', 'social' ), 'google' => array( 'paid_google', 'organic_search' ), 'googleads' => array( 'paid_google', 'paid_google' ), 'adwords' => array( 'paid_google', 'paid_google' ), 'bing' => array( 'paid_microsoft', 'organic_search' ), 'microsoft' => array( 'paid_microsoft', 'paid_microsoft' ), 'skroutz' => array( 'skroutz', 'skroutz' ), 'shopflix' => array( 'shopflix', 'shopflix' ), 'bestprice' => array( 'bestprice', 'bestprice' ), 'newsletter' => array( 'newsletter', 'newsletter' ), 'youtube' => array( 'paid_google', 'social' ), 'yahoo' => array( 'paid_other', 'organic_search' ), 'duckduckgo' => array( 'paid_other', 'organic_search' ), 'ecosia' => array( 'paid_other', 'organic_search' ), 'yandex' => array( 'paid_other', 'organic_search' ), 'outbrain' => array( 'paid_other', 'paid_other' ), 'taboola' => array( 'paid_other', 'paid_other' ), 'chatgpt' => array( 'referral', 'referral' ), 'perplexity' => array( 'referral', 'referral' ), 'vivapayments' => array( 'internal', 'internal' ), 'paypal' => array( 'internal', 'internal' ), ); } public static function canonical_source_token( $src ) { $s = strtolower( trim( (string) $src ) ); if ( '' === $s ) { return ''; } $s = preg_replace( '#^https?://#', '', $s ); $s = preg_replace( '#^(www|l|m|lm|business|search)\.#', '', $s ); $dot = strpos( $s, '.' ); if ( false !== $dot ) { $s = substr( $s, 0, $dot ); } return preg_replace( '/[^a-z0-9_]/', '', $s ); } public static function canonical_channel( $raw_channel, $utm_source = '', $utm_medium = '', $landing_url = '', $source_type = '' ) { $raw = strtolower( trim( (string) $raw_channel ) ); $med = strtolower( trim( (string) $utm_medium ) ); $type = strtolower( trim( (string) $source_type ) ); $url = strtolower( (string) $landing_url ); $tok = self::canonical_source_token( $utm_source ); if ( '' !== $url ) { foreach ( self::click_id_channels() as $param => $channel ) { if ( false !== strpos( $url, $param . '=' ) ) { return $channel; } } } $is_paid = ( '' !== $med && preg_match( '/' . self::PAID_MEDIUM_REGEX . '/', $med ) ); $map = self::source_platform_map(); if ( '' !== $tok && isset( $map[ $tok ] ) ) { return $map[ $tok ][ $is_paid ? 0 : 1 ]; } if ( 'email' === $med || 0 === strpos( $tok, 'newsletter' ) || 0 === strpos( $tok, 'email' ) ) { return 'newsletter'; } if ( in_array( $raw, self::CANONICAL_CHANNELS, true ) ) { return $raw; } if ( $is_paid ) { return 'paid_other'; } $raw_tok = self::canonical_source_token( $raw ); if ( '' !== $raw_tok && isset( $map[ $raw_tok ] ) ) { return $map[ $raw_tok ][1]; } switch ( $type ) { case 'organic': return 'organic_search'; case 'typein': return 'direct'; case 'referral': return 'referral'; case 'admin': return 'admin'; } if ( 'organic' === $raw ) { return 'organic_search'; } if ( 0 === strpos( $raw, 'aio' ) || 0 === strpos( $raw, 'paid_aio' ) || 'woo' === $raw ) { return 'internal'; } return 'unknown'; } public static function canonical_channel_sql( $raw, $src, $med, $entry, $type ) { $paid = self::PAID_MEDIUM_REGEX; $lmed = "LOWER(COALESCE({$med},''))"; $lraw = "LOWER(COALESCE({$raw},''))"; $ltyp = "LOWER(COALESCE({$type},''))"; $tok = function ( $expr ) { return "REGEXP_REPLACE(REGEXP_REPLACE(REGEXP_REPLACE(LOWER(COALESCE({$expr},''))," . "'^https?://',''),'^(www|l|m|lm|business|search)\\\\.',''),'\\\\..*$','')"; }; $stok = $tok( $src ); $rtok = $tok( $raw ); $sql = "CASE\n"; foreach ( self::click_id_channels() as $param => $channel ) { $sql .= " WHEN LOWER(COALESCE({$entry},'')) LIKE '%{$param}=%' THEN '{$channel}'\n"; } foreach ( self::source_platform_map() as $token => $pair ) { $sql .= " WHEN {$stok} = '{$token}' AND {$lmed} REGEXP '{$paid}' THEN '{$pair[0]}'\n"; $sql .= " WHEN {$stok} = '{$token}' THEN '{$pair[1]}'\n"; } $sql .= " WHEN {$lmed} = 'email' OR {$stok} LIKE 'newsletter%' OR {$stok} LIKE 'email%' THEN 'newsletter'\n"; $quoted = array(); foreach ( self::CANONICAL_CHANNELS as $c ) { $quoted[] = "'{$c}'"; } $sql .= " WHEN {$lraw} IN (" . implode( ',', $quoted ) . ") THEN {$lraw}\n"; $sql .= " WHEN {$lmed} REGEXP '{$paid}' THEN 'paid_other'\n"; foreach ( self::source_platform_map() as $token => $pair ) { $sql .= " WHEN {$rtok} = '{$token}' THEN '{$pair[1]}'\n"; } $sql .= " WHEN {$ltyp} = 'organic' THEN 'organic_search'\n"; $sql .= " WHEN {$ltyp} = 'typein' THEN 'direct'\n"; $sql .= " WHEN {$ltyp} = 'referral' THEN 'referral'\n"; $sql .= " WHEN {$ltyp} = 'admin' THEN 'admin'\n"; $sql .= " WHEN {$lraw} = 'organic' THEN 'organic_search'\n"; $sql .= " WHEN {$lraw} LIKE 'aio%' OR {$lraw} LIKE 'paid\\_aio%' OR {$lraw} = 'woo' THEN 'internal'\n"; $sql .= " ELSE 'unknown'\n END"; return $sql; } public static function canonical_utm_value( $value ) { return trim( str_replace( '+', ' ', (string) $value ) ); } public static function canonical_utm_value_sql( $col ) { return "NULLIF(TRIM(REPLACE(COALESCE({$col},''),'+',' ')),'')"; } public static function resolve_channel($landing_page, $referrer) { $out = array('channel' => 'direct', 'source' => '', 'campaign' => '', 'medium' => '', 'term' => '', 'content' => '', 'click_id' => '', 'ad_id' => '', 'adset_id' => '', 'ad_campaign_id' => ''); $landing_page = (string) $landing_page; $referrer = (string) $referrer; $qs = ''; if ($landing_page !== '') { $parts = wp_parse_url($landing_page); if (isset($parts['query'])) { $qs = (string) $parts['query']; } } $params = array(); if ($qs !== '') { parse_str($qs, $params); } $utm_source = isset($params['utm_source']) ? strtolower(trim((string) $params['utm_source'])) : ''; $utm_medium = isset($params['utm_medium']) ? strtolower(trim((string) $params['utm_medium'])) : ''; $utm_campaign = isset($params['utm_campaign']) ? trim((string) $params['utm_campaign']) : ''; $utm_term = isset($params['utm_term']) ? trim((string) $params['utm_term']) : ''; $utm_content = isset($params['utm_content']) ? trim((string) $params['utm_content']) : ''; $aieo_src = isset($params['aieo_src']) ? strtolower(trim((string) $params['aieo_src'])) : ''; $out['source'] = $utm_source; $out['campaign'] = $utm_campaign; $out['medium'] = $utm_medium; $out['term'] = $utm_term; $out['content'] = $utm_content; foreach (array('fbclid', 'gclid', 'gbraid', 'wbraid', 'msclkid', 'ttclid', 'twclid', 'li_fat_id', 'epik', 'rdt_cid', 'sc_cid', 'yclid', 'dclid', 'srsltid') as $__cid) { if (!empty($params[$__cid])) { $out['click_id'] = (string) $params[$__cid]; break; } } if (isset($params['ad_id']) && $params['ad_id'] !== '') { $out['ad_id'] = (string) $params['ad_id']; } if (isset($params['adset_id']) && $params['adset_id'] !== '') { $out['adset_id'] = (string) $params['adset_id']; } elseif (isset($params['adgroup_id']) && $params['adgroup_id'] !== '') { $out['adset_id'] = (string) $params['adgroup_id']; } elseif (isset($params['adgroupid']) && $params['adgroupid'] !== '') { $out['adset_id'] = (string) $params['adgroupid']; } if (isset($params['campaign_id']) && $params['campaign_id'] !== '') { $out['ad_campaign_id'] = (string) $params['campaign_id']; } elseif (isset($params['utm_id']) && $params['utm_id'] !== '') { $out['ad_campaign_id'] = (string) $params['utm_id']; } if ($aieo_src === 'push') { $out['channel'] = 'web_push'; if ($out['campaign'] === '' && !empty($params['aieo_dispatch_id'])) { $out['campaign'] = 'dispatch_' . (int) $params['aieo_dispatch_id']; } return $out; } if ($aieo_src === 'newsletter') { $out['channel'] = 'newsletter'; if ($out['campaign'] === '' && !empty($params['aieo_email_id'])) { $out['campaign'] = 'tnp_email_' . (int) $params['aieo_email_id']; } return $out; } if ($aieo_src === 'social') { $out['channel'] = 'social'; if ($out['source'] === '' && $utm_source !== '') { $out['source'] = $utm_source; } if ($out['campaign'] === '' && !empty($params['aieo_post_id'])) { $out['campaign'] = 'social_' . (int) $params['aieo_post_id']; } if ($out['medium'] === '') { $out['medium'] = ($utm_medium === 'paid_social') ? 'paid_social' : 'social'; } return $out; } if (!empty($params['gclid']) || !empty($params['gbraid']) || !empty($params['wbraid'])) { $out['channel'] = 'paid_google'; if ($out['medium'] === '') $out['medium'] = 'cpc'; return $out; } if (!empty($params['fbclid']) || (!empty($params['ad_id']) && !empty($params['adset_id']))) { $out['channel'] = 'paid_facebook'; if ($out['medium'] === '') $out['medium'] = 'cpc'; return $out; } if (!empty($params['msclkid'])) { $out['channel'] = 'paid_microsoft'; if ($out['medium'] === '') $out['medium'] = 'cpc'; return $out; } if (!empty($params['ttclid'])) { $out['channel'] = 'paid_tiktok'; if ($out['medium'] === '') $out['medium'] = 'paid_social'; return $out; } if (in_array($utm_medium, array('email', 'newsletter'), true)) { $out['channel'] = 'newsletter'; return $out; } if (in_array($utm_source, array('newsletter', 'tnp', 'mailchimp', 'klaviyo', 'sendgrid', 'sendinblue', 'brevo'), true)) { $out['channel'] = 'newsletter'; return $out; } if (in_array($utm_medium, array('cpc', 'paid', 'ppc', 'paid-social', 'paidsocial'), true)) { if (in_array($utm_source, array('google', 'adwords', 'google_ads', 'googleads'), true)) { $out['channel'] = 'paid_google'; return $out; } if (in_array($utm_source, array('facebook', 'meta', 'instagram', 'fb'), true)) { $out['channel'] = 'paid_facebook'; return $out; } if (in_array($utm_source, array('tiktok', 'tiktokads', 'tiktok_ads', 'tt'), true)) { $out['channel'] = 'paid_tiktok'; return $out; } $out['channel'] = 'paid_other'; return $out; } if ($utm_medium === 'social') { $out['channel'] = 'social'; return $out; } $ref_host = ''; if ($referrer !== '') { $rp = wp_parse_url($referrer); $ref_host = isset($rp['host']) ? strtolower((string) $rp['host']) : ''; } $own_host = ''; $hp = wp_parse_url((string) home_url('/')); if (isset($hp['host'])) { $own_host = strtolower((string) $hp['host']); } if ($ref_host === '') { $out['channel'] = 'direct'; return $out; } if ($own_host !== '' && $ref_host === $own_host) { $out['channel'] = 'direct'; return $out; } $search_hosts = array('google.', 'bing.com', 'duckduckgo.com', 'yahoo.', 'ecosia.org', 'brave.com', 'qwant.com', 'startpage.com', 'yandex.', 'baidu.com'); foreach ($search_hosts as $h) { if (strpos($ref_host, $h) !== false) { $out['channel'] = 'organic_search'; return $out; } } $social_hosts = array('facebook.com', 'fb.com', 'm.facebook.com', 'l.facebook.com', 'instagram.com', 'twitter.com', 'x.com', 't.co', 'tiktok.com', 'pinterest.', 'youtube.com', 'youtu.be', 'reddit.com', 'linkedin.com', 'lnkd.in', 'whatsapp.com', 'telegram.', 'discord.'); foreach ($social_hosts as $h) { if (strpos($ref_host, $h) !== false) { $out['channel'] = 'social'; return $out; } } $out['channel'] = 'referral'; return $out; } public static function backfill_session_channels($limit = 5000) { global $wpdb; $tbl = $wpdb->prefix . 'aieo_sessions_statics'; $rows = $wpdb->get_results($wpdb->prepare( "SELECT id, landing_page, referrer FROM {$tbl} WHERE entry_channel = '' ORDER BY id ASC LIMIT %d", (int) $limit ), ARRAY_A); if (empty($rows)) { return array('processed' => 0, 'by_channel' => array()); } $by = array(); $now = current_time('mysql', true); foreach ($rows as $r) { $resolved = self::resolve_channel((string) $r['landing_page'], (string) $r['referrer']); $campaign_str = mb_substr((string) $resolved['campaign'], 0, 120); $campaign_id = ($campaign_str !== '' && class_exists('AIEO_DMM_Campaigns')) ? AIEO_DMM_Campaigns::touch($campaign_str, (string) $resolved['channel']) : null; $update_data = array( 'entry_channel' => $resolved['channel'], 'entry_source' => mb_substr((string) (isset($resolved['source']) ? $resolved['source'] : ''), 0, 80), 'entry_campaign' => $campaign_str, 'entry_medium' => mb_substr((string) $resolved['medium'], 0, 40), 'entry_term' => mb_substr((string) $resolved['term'], 0, 80), 'entry_content' => mb_substr((string) $resolved['content'], 0, 80), 'entry_click_id' => mb_substr((string) (isset($resolved['click_id']) ? $resolved['click_id'] : ''), 0, 255), 'entry_ad_id' => mb_substr((string) (isset($resolved['ad_id']) ? $resolved['ad_id'] : ''), 0, 40), 'entry_adset_id' => mb_substr((string) (isset($resolved['adset_id']) ? $resolved['adset_id'] : ''), 0, 40), 'entry_ad_campaign_id' => mb_substr((string) (isset($resolved['ad_campaign_id']) ? $resolved['ad_campaign_id'] : ''), 0, 40), 'updated_at' => $now, ); $update_fmt = array('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s'); if ($campaign_id) { $update_data['entry_campaign_id'] = (int) $campaign_id; $update_fmt[] = '%d'; } $wpdb->update($tbl, $update_data, array('id' => (int) $r['id']), $update_fmt, array('%d')); $ch = $resolved['channel']; if (!isset($by[$ch])) { $by[$ch] = 0; } $by[$ch]++; } return array('processed' => count($rows), 'by_channel' => $by); } public static function reconcile_session_from_order($order_id) { $order_id = (int) $order_id; if ($order_id <= 0 || !function_exists('wc_get_order')) { return false; } $order = wc_get_order($order_id); if (!$order) { return false; } $sid = (string) $order->get_meta('_aieo_attr_sid'); if ($sid === '') { return false; } global $wpdb; $tbl = $wpdb->prefix . 'aieo_sessions_statics'; $row = $wpdb->get_row($wpdb->prepare( "SELECT id, entry_channel, order_id FROM {$tbl} WHERE session_id = %s LIMIT 1", $sid ), ARRAY_A); if (!$row) { return false; } $landing = (string) $order->get_meta('_wc_order_attribution_session_entry'); $referrer = (string) $order->get_meta('_wc_order_attribution_referrer'); if ($landing === '') { $q = array(); $g = (string) $order->get_meta('_aieo_gclid'); if ($g !== '') { $q['gclid'] = $g; } foreach (array('utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content') as $u) { $v = (string) $order->get_meta('_wc_order_attribution_' . $u); if ($v !== '') { $q[$u] = $v; } } if (!empty($q)) { $landing = home_url('/?' . http_build_query($q)); } } $resolved = self::resolve_channel($landing, $referrer); $new_ch = (string) $resolved['channel']; $cur = (string) $row['entry_channel']; $do_channel = in_array($cur, array('', 'internal', 'direct'), true) && !in_array($new_ch, array('', 'internal', 'direct'), true); $campaign_str = ''; $data = array(); $fmt = array(); if ($do_channel) { $campaign_str = mb_substr((string) $resolved['campaign'], 0, 120); $data['entry_channel'] = $new_ch; $fmt[] = '%s'; $data['entry_source'] = mb_substr((string) $resolved['source'], 0, 80); $fmt[] = '%s'; $data['entry_campaign'] = $campaign_str; $fmt[] = '%s'; $data['entry_medium'] = mb_substr((string) $resolved['medium'], 0, 40); $fmt[] = '%s'; $data['entry_term'] = mb_substr((string) $resolved['term'], 0, 80); $fmt[] = '%s'; $data['entry_content'] = mb_substr((string) $resolved['content'], 0, 80); $fmt[] = '%s'; $data['entry_click_id'] = mb_substr((string) $resolved['click_id'], 0, 255); $fmt[] = '%s'; $data['entry_ad_id'] = mb_substr((string) $resolved['ad_id'], 0, 40); $fmt[] = '%s'; $data['entry_adset_id'] = mb_substr((string) $resolved['adset_id'], 0, 40); $fmt[] = '%s'; $data['entry_ad_campaign_id'] = mb_substr((string) $resolved['ad_campaign_id'], 0, 40); $fmt[] = '%s'; } if ((int) $row['order_id'] <= 0) { $data['order_id'] = $order_id; $fmt[] = '%d'; $data['order_total'] = (float) $order->get_total(); $fmt[] = '%f'; } if (empty($data)) { return false; } $data['updated_at'] = current_time('mysql', true); $fmt[] = '%s'; $wpdb->update($tbl, $data, array('id' => (int) $row['id']), $fmt, array('%d')); if ($do_channel && $campaign_str !== '' && class_exists('AIEO_DMM_Campaigns')) { $cid = AIEO_DMM_Campaigns::touch($campaign_str, $new_ch); if ($cid) { $wpdb->update($tbl, array('entry_campaign_id' => (int) $cid), array('id' => (int) $row['id']), array('%d'), array('%d')); } } return true; } }