&slug=bing&q=关键词` * * ⚠️⚠️ **这个接口也是免登录的**(展示页本身免登录,它的联想接口必须一起免登录, * 否则展示页的搜索框就没法联想)。因此它必须有自己的门禁 —— 见下面"门禁"那段: * **必须带一个有效的短语**才放行,否则任何人都能拿它当免费代理刷外网请求。 */ declare(strict_types=1); require __DIR__ . '/app/bootstrap.php'; use App\Nav; use App\NavFetch; header('Content-Type: application/json; charset=utf-8'); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('X-Robots-Tag: noindex'); // ---- 门禁:必须凭一个有效的短语(见文件头说明)---- // // ⚠️ 这**不是**身份校验(短语本来就是公开的),它挡的是"随便谁来都能刷": // 没有它,这个 URL 就是一个匿名的外网请求放大器(每次 GET 都会去打百度/必应)。 $phrase = trim((string)($_GET['p'] ?? '')); /* 🔴 v30 用户第 2 条:容错 —— 短语里**不允许**出现 `?` 或 `&`(短语本身是 `[A-Za-z0-9_-]{4,32}`,见 Nav::PHRASE_RE,永远不含这两个字符)。 为什么要有这一段:前端曾经把 suggest 地址硬拼成 `…?p=abc?slug=bing&q=…` (suggest 地址本身已带 `?`,旧代码又补了一个 `?`)。PHP 对重复 `?` 的处理是 "第一个 `?` 之后整串都算查询值" → `p` 变成了 `abc?slug=bing` → 403 → 展示页下拉**永远是空的**,而服务端单测是好的(探针自己拼对了)→ 极难查。 修法在前端(`withQuery()`,已改)。这里再做一层"把第一个 `?` 之后的垃圾切掉", 是为了让**任何**老前端 / 缓存住的旧 JS 都不至于整条联想失效 —— 属于廉价兜底: 合法短语不可能含 `?`,切掉不会误伤。 */ if ($phrase !== '') { $cut = strcspn($phrase, '?&'); if ($cut < strlen($phrase)) { $phrase = substr($phrase, 0, $cut); } } $owner = $phrase === '' ? 0 : Nav::ownerByPhrase($phrase); if ($owner <= 0) { http_response_code(403); echo json_encode(['q' => '', 'items' => [], 'src' => '', 'error' => 'forbidden'], JSON_UNESCAPED_UNICODE); exit; } $slug = isset($_GET['slug']) ? (string)preg_replace('/[^a-z0-9\-_]/i', '', (string)$_GET['slug']) : ''; $q = isset($_GET['q']) ? trim((string)$_GET['q']) : ''; // 联想词是给人看着点的短词,限长免得被拿去当搜索接口刷 $q = mb_substr($q, 0, 60, 'UTF-8'); if ($q === '') { echo json_encode(['q' => '', 'items' => [], 'src' => ''], JSON_UNESCAPED_UNICODE); exit; } // ---- 取这个人的引擎表(联想来源也按**他的**引擎配置走)---- $engines = Nav::engines($owner); /** 按 slug 找他的某个引擎 */ $engineBy = static function (string $s) use ($engines): ?array { if ($s === '') { return null; } foreach ($engines as $e) { if ((string)$e['slug'] === $s) { return $e; } } return null; }; /** * 排出「先试谁、再试谁」的候选顺序 —— 照搬 SimpleNav 的 suggest_order()。 * * 兜底链固定「百度 → 必应 → 360」(实测可用的三家,按快慢排序), * 只保留库里真实存在、且配了 suggest_url 的引擎。 * * ⚠️ SimpleNav 那个 setting('suggest_source') 开关这里**没搬** —— * 它依赖项目级设置,而本模块是**每人一份**的;为了不给用户多一个看不懂的开关, * 直接用最有用的那一种(即 auto 的兜底链行为)。这是本批唯一有意"少搬"的一处, * 在此记明,免得下一个人以为是漏了。 */ $order = []; if ($slug !== '') { $own = $engineBy($slug); if ($own !== null && (string)$own['suggest_url'] !== '') { $order[] = $own; } } foreach (['baidu', 'bing', 'so360'] as $s) { $row = $engineBy($s); if ($row !== null && (string)$row['suggest_url'] !== '') { $order[] = $row; } } $items = []; $used = ''; foreach ($order as $en) { $items = nav_fetch_suggest($en, $q); if ($items) { $used = (string)$en['slug']; break; } } $items = array_values(array_unique(array_filter(array_map('trim', $items)))); $items = array_slice($items, 0, 10); echo json_encode(['q' => $q, 'items' => $items, 'src' => $used], JSON_UNESCAPED_UNICODE); exit; /* ========================================================================== */ /** 抓一次联想并解析;任何异常都退化成空数组,绝不让下拉框卡住(照搬原函数) */ function nav_fetch_suggest(array $engine, string $q): array { if ((string)($engine['suggest_url'] ?? '') === '') { return []; } $u = str_replace('{q}', rawurlencode($q), (string)$engine['suggest_url']); // 3 秒。既然有兜底链,宁快速失败也不要让输入框等太久。 $resp = NavFetch::get($u, 100000, 3); if ($resp === null) { return []; } return nav_parse_suggest((string)$engine['suggest_type'], (string)$resp['body'], $q); } /** 按引擎类型解析联想结果(照搬原函数,逻辑一行未改) */ function nav_parse_suggest(string $type, string $body, string $q): array { $body = trim($body); $out = []; unset($q); switch ($type) { case 'bing_osjson': case 'google_firefox': // ["query", ["s1","s2",...]] $j = json_decode($body, true); if (is_array($j) && isset($j[1]) && is_array($j[1])) { $out = $j[1]; } break; case 'baidu_sugrec': // {"q":"x","g":[{"type":"..","q":".."}], ...} $j = json_decode($body, true); if (is_array($j)) { if (!empty($j['g']) && is_array($j['g'])) { foreach ($j['g'] as $it) { if (isset($it['q'])) { $out[] = $it['q']; } } } elseif (!empty($j['s']) && is_array($j['s'])) { $out = $j['s']; } } break; case 'so360': // {"s":[".."]} 或 {"result":[{"word":".."}]} $j = json_decode($body, true); if (is_array($j)) { if (!empty($j['s']) && is_array($j['s'])) { $out = $j['s']; } elseif (!empty($j['result']) && is_array($j['result'])) { foreach ($j['result'] as $it) { if (isset($it['word'])) { $out[] = $it['word']; } } } } break; case 'sogou_ajaj': // {"query":"x","items":["a","b"]}(可能 GBK,已被 NavFetch 转码) $j = json_decode($body, true); if (is_array($j) && !empty($j['items']) && is_array($j['items'])) { $out = $j['items']; } elseif (preg_match_all('#"([^"]{1,40})"#u', $body, $m)) { $out = array_slice($m[1], 1); // 跳过 query 自身 } break; case 'ddg': // [{"phrase":".."},...] $j = json_decode($body, true); if (is_array($j)) { foreach ($j as $it) { if (is_array($it) && isset($it['phrase'])) { $out[] = $it['phrase']; } elseif (is_string($it)) { $out[] = $it; } } } break; // ---- v28 用户第 10 条新增的三个联想来源(见 Nav::SUGGEST_LIBRARY)---- case 'sogou': // {"query":"x","items":["a","b"]} // ⚠️ 与旧的 `sogou_ajaj` 分开:那条是 GBK 老接口(现已不解析), // 这条是新的 suggestion.sogou.com,返回 utf-8 JSON。 $j = json_decode($body, true); if (is_array($j) && !empty($j['items']) && is_array($j['items'])) { $out = $j['items']; } break; case 'zhihu': // {"data":[{"query":".."}]} 或 {"suggest":[{"query":".."}]} $j = json_decode($body, true); if (is_array($j)) { foreach (['data', 'suggest'] as $k) { if (!empty($j[$k]) && is_array($j[$k])) { foreach ($j[$k] as $it) { if (is_array($it)) { foreach (['query', 'title', 'name'] as $f) { if (isset($it[$f]) && is_string($it[$f])) { $out[] = $it[$f]; break; } } } elseif (is_string($it)) { $out[] = $it; } } break; } } } break; case 'bili': // {"code":0,"data":{"tag":[{"value":".."}]}} // 也兼容 {"result":[{"term":".."}]} $j = json_decode($body, true); if (is_array($j)) { if (!empty($j['data']['tag']) && is_array($j['data']['tag'])) { foreach ($j['data']['tag'] as $it) { if (is_array($it) && isset($it['value'])) { $out[] = $it['value']; } } } elseif (!empty($j['result']) && is_array($j['result'])) { foreach ($j['result'] as $it) { if (is_array($it)) { foreach (['term', 'name', 'value'] as $f) { if (isset($it[$f]) && is_string($it[$f])) { $out[] = $it[$f]; break; } } } } } } break; default: // 通用兜底:先试 JSON,再退化正则(照搬) $j = json_decode($body, true); if (is_array($j)) { if (isset($j[1]) && is_array($j[1])) { $out = $j[1]; } elseif (!empty($j['s']) && is_array($j['s'])) { $out = $j['s']; } } } return is_array($out) ? $out : []; }