true, 'hash_ip' => true, 'salt' => 'd!&5wIPA0Tnc0SlGrGso', 'table_log' => 'rcp_paginas_querys', 'table_click' => 'rcp_paginas_querys_log', ]; /** * Get analytics configuration */ public static function get_config(): array { $config = get_option('roi_apu_search_analytics', []); return array_merge(self::DEFAULT_CONFIG, $config); } /** * Create/read first-party cookie to identify visitors */ public static function ensureVisitorId(): string { $name = 'apu_vid'; $vid = $_COOKIE[$name] ?? ''; if (!preg_match('/^[a-f0-9]{32}$/', $vid)) { $vid = bin2hex(random_bytes(16)); setcookie($name, $vid, [ 'expires' => time() + 31536000, // 1 year 'path' => '/', 'secure' => !empty($_SERVER['HTTPS']), 'httponly' => true, 'samesite' => 'Lax', ]); $_COOKIE[$name] = $vid; } return $vid; } /** * Hash IP address (without storing clear IP) */ private static function ipHash(?string $salt): ?string { $ip = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? ''; if ($ip === '') { return null; } if (strpos($ip, ',') !== false) { $ip = trim(explode(',', $ip)[0]); } return sha1($ip . (string) $salt); } /** * Parse User-Agent string */ private static function parseUA(string $ua): array { $uaL = strtolower($ua); // device if (str_contains($uaL, 'ipad') || str_contains($uaL, 'tablet')) { $device = 'tablet'; } elseif ( str_contains($uaL, 'mobile') || str_contains($uaL, 'iphone') || str_contains($uaL, 'android') ) { $device = 'mobile'; } else { $device = 'desktop'; } // browser if (str_contains($uaL, 'edg/')) { $browser = 'edge'; } elseif (str_contains($uaL, 'opr/') || str_contains($uaL, 'opera')) { $browser = 'opera'; } elseif (str_contains($uaL, 'chrome') && !str_contains($uaL, 'edg/')) { $browser = 'chrome'; } elseif (str_contains($uaL, 'safari') && !str_contains($uaL, 'chrome')) { $browser = 'safari'; } elseif (str_contains($uaL, 'firefox')) { $browser = 'firefox'; } elseif (str_contains($uaL, 'msie') || str_contains($uaL, 'trident')) { $browser = 'ie'; } else { $browser = 'other'; } // os if (str_contains($uaL, 'android')) { $os = 'android'; } elseif (str_contains($uaL, 'iphone') || str_contains($uaL, 'ipad')) { $os = 'ios'; } elseif (str_contains($uaL, 'mac os x')) { $os = 'mac'; } elseif (str_contains($uaL, 'windows')) { $os = 'windows'; } elseif (str_contains($uaL, 'linux')) { $os = 'linux'; } else { $os = 'other'; } return [$device, $browser, $os]; } /** * Extract host and path from referer */ private static function refParts(?string $ref): array { $host = $path = null; if ($ref) { $p = parse_url($ref); $host = $p['host'] ?? null; $path = ($p['path'] ?? '') . (isset($p['query']) ? '?' . $p['query'] : ''); $path = $path !== '' ? substr($path, 0, 255) : null; } return [$host, $path]; } /** * Simple tokenization for analytics */ private static function simpleTokens(string $qTerm): array { $s = mb_strtolower($qTerm, 'UTF-8'); $x = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s); if ($x !== false) { $s = $x; } $s = preg_replace('/[^a-z0-9 ]+/i', ' ', $s); $s = preg_replace('/\s+/', ' ', trim($s)); $parts = array_filter(explode(' ', $s), static fn($w) => strlen($w) >= 2); return array_values($parts); } /** * Log a search and return its ID */ public static function logSearch( PDO $pdo, string $prefix, array $cfg, string $qRaw, string $qTerm, array $results, int $page, int $perPage ): ?int { try { if (empty($cfg['enabled'])) { return null; } if (session_status() !== PHP_SESSION_ACTIVE) { @session_start(); } $vid = self::ensureVisitorId(); $sid = session_id() ?: null; $uid = (function_exists('is_user_logged_in') && is_user_logged_in()) ? get_current_user_id() : null; $ua = substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255); [$device, $browser, $os] = self::parseUA($ua); $country = substr((string) ($_SERVER['HTTP_CF_IPCOUNTRY'] ?? ''), 0, 2) ?: null; $ipHash = !empty($cfg['hash_ip']) ? self::ipHash($cfg['salt'] ?? '') : null; [$refHost, $refPath] = self::refParts($_SERVER['HTTP_REFERER'] ?? null); $tokens = self::simpleTokens($qTerm); $tokensJson = json_encode($tokens, JSON_UNESCAPED_UNICODE); $qLen = mb_strlen($qTerm, 'UTF-8'); $total = (int) ($results['total'] ?? 0); $shown = (isset($results['rows']) && is_array($results['rows'])) ? count($results['rows']) : 0; $table = $prefix . ($cfg['table_log'] ?? 'rcp_paginas_querys'); $sql = "INSERT INTO `{$table}` (visitor_id, session_id, user_id, ip_hash, country, user_agent, device, browser, os, referer_host, referer_path, q_raw, q_term, q_tokens, q_len, page, per_page, total_results, result_count, zero_results) VALUES (:vid, :sid, :uid, :ip_hash, :country, :ua, :device, :browser, :os, :rhost, :rpath, :q_raw, :q_term, :q_tokens, :q_len, :page, :per_page, :total, :shown, :zero)"; $st = $pdo->prepare($sql); $st->bindValue(':vid', $vid); $st->bindValue(':sid', $sid); $st->bindValue(':uid', $uid); $st->bindValue(':ip_hash', $ipHash); $st->bindValue(':country', $country); $st->bindValue(':ua', $ua); $st->bindValue(':device', $device); $st->bindValue(':browser', $browser); $st->bindValue(':os', $os); $st->bindValue(':rhost', $refHost); $st->bindValue(':rpath', $refPath); $st->bindValue(':q_raw', $qRaw); $st->bindValue(':q_term', $qTerm); $st->bindValue(':q_tokens', $tokensJson); $st->bindValue(':q_len', $qLen, PDO::PARAM_INT); $st->bindValue(':page', $page, PDO::PARAM_INT); $st->bindValue(':per_page', $perPage, PDO::PARAM_INT); $st->bindValue(':total', $total, PDO::PARAM_INT); $st->bindValue(':shown', $shown, PDO::PARAM_INT); $st->bindValue(':zero', $total === 0 ? 1 : 0, PDO::PARAM_INT); $st->execute(); return (int) $pdo->lastInsertId(); } catch (\Throwable $e) { error_log('ROI APU Search Analytics Error (logSearch): ' . $e->getMessage()); return null; // Never break search due to analytics } } /** * Log a click - does not interrupt navigation if it fails */ public static function logClick( PDO $pdo, string $prefix, array $cfg, int $searchId, int $postId, int $position, int $page, string $destUrl ): void { try { if (empty($cfg['enabled'])) { return; } if (session_status() !== PHP_SESSION_ACTIVE) { @session_start(); } $vid = self::ensureVisitorId(); $sid = session_id() ?: null; $uid = (function_exists('is_user_logged_in') && is_user_logged_in()) ? get_current_user_id() : null; $table = $prefix . ($cfg['table_click'] ?? 'rcp_paginas_querys_log'); $sql = "INSERT INTO `{$table}` (search_id, visitor_id, session_id, user_id, post_id, position, page, dest_url) VALUES (:sid, :vid, :sess, :uid, :pid, :pos, :page, :dest)"; $st = $pdo->prepare($sql); $st->bindValue(':sid', $searchId, PDO::PARAM_INT); $st->bindValue(':vid', $vid); $st->bindValue(':sess', $sid); $st->bindValue(':uid', $uid); $st->bindValue(':pid', $postId, PDO::PARAM_INT); $st->bindValue(':pos', $position, PDO::PARAM_INT); $st->bindValue(':page', $page, PDO::PARAM_INT); $st->bindValue(':dest', substr($destUrl, 0, 255)); $st->execute(); } catch (\Throwable $e) { error_log('ROI APU Search Analytics Error (logClick): ' . $e->getMessage()); } } }