Files
SimpleNav/db.php
T
laoyang 47b45cfa23 后台新增「搜索与链接的打开方式」,并让站点链接跟随设置
原先的设置项只作用于顶部搜索框,文案也写作「搜索结果新窗口打开」;而站点格
与合集里的链接 target 是写死的 _blank —— 于是切到「当前页面打开」时会出现
半生效:搜索留在本页,点网站却仍然弹新标签。

- index.php:把打开方式收敛成一个 open_target(),搜索框与所有站点格共用同一
  来源,任何一处都不再写死 target;页面输出 data-open="blank|self" 便于断言与排查。
- admin/index.php:后台由含义含糊的复选框改为明确的二选一(新标签页 / 当前页面),
  并补一段说明它管哪两处、不影响后台管理界面。
- admin/actions.php:保存走白名单回退 —— '0' 之外(含字段缺失、被篡改成怪值)
  一律回到 '1'(新标签页)。默认值与改造前的旧行为一致,老库不受影响。
- 当前页面模式下不再输出无意义的 rel="noopener"。

验证:php -l 4 个文件通过;e2e 113/113(新增 20 项,并做了反向验证——把链接
改回写死 _blank 后,相关断言精确失败 4 条);线上部署 0 FAIL(双版本语法检查、
62 条静态断言、38 文件哈希逐字节一致、HTTP 冒烟、真机上切换两种模式并已复原)。
2026-09-28 19:28:57 +08:00

248 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 简航 SimpleNav —— 数据库(PDO SQLite)
* 首次访问自动建表并写入一份通用的默认数据(示例链接,可在后台随意增删)。
*/
declare(strict_types=1);
require_once __DIR__ . '/config.php';
function db(): PDO
{
static $pdo = null;
if ($pdo === null) {
if (!is_dir(DATA_PATH)) {
@mkdir(DATA_PATH, 0755, true);
}
$pdo = new PDO('sqlite:' . DB_PATH);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec('PRAGMA foreign_keys = ON');
db_init($pdo);
}
return $pdo;
}
function db_init(PDO $pdo): void
{
$pdo->exec(<<<'SQL'
CREATE TABLE IF NOT EXISTS groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
slug TEXT DEFAULT '',
display TEXT NOT NULL DEFAULT 'grid', -- grid=普通网格板块 tabs=合集标签页
sort_order INTEGER NOT NULL DEFAULT 0,
is_visible INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE IF NOT EXISTS sites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id INTEGER NOT NULL,
url TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '', -- 自动抓取的网站名称
short_name TEXT NOT NULL DEFAULT '', -- 用户自定义简称(页面展示用)
icon TEXT NOT NULL DEFAULT '', -- 本地图标文件名
icon_custom INTEGER NOT NULL DEFAULT 0, -- 1=用户自定义图标("全部更新"不再覆盖)
sort_order INTEGER NOT NULL DEFAULT 0,
is_visible INTEGER NOT NULL DEFAULT 1,
created_at TEXT DEFAULT '',
updated_at TEXT DEFAULT '',
FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS engines (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
slug TEXT NOT NULL,
search_url TEXT NOT NULL,
param TEXT NOT NULL DEFAULT 'q',
suggest_url TEXT NOT NULL DEFAULT '',
suggest_type TEXT NOT NULL DEFAULT '',
color TEXT NOT NULL DEFAULT '#2F6BF7',
is_active INTEGER NOT NULL DEFAULT 0,
sort_order INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS settings (
k TEXT PRIMARY KEY,
v TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT ''
);
SQL);
// 老库升级:为已存在的 sites 表补 icon_custom 列
$cols = [];
foreach ($pdo->query('PRAGMA table_info(sites)') as $c) {
$cols[(string)$c['name']] = true;
}
if (!isset($cols['icon_custom'])) {
$pdo->exec('ALTER TABLE sites ADD COLUMN icon_custom INTEGER NOT NULL DEFAULT 0');
}
// 老库升级:清掉「搜狗联想」这个已经彻底失效的默认值
// (2026-09-28 实测:suggnew/ajajjson 返回 200 但 0 字节,联想域名 w.sugg.sogou.com 已不解析)
// 只匹配默认种子里的那个 URL —— 用户自己改过的地址绝不动。
$pdo->prepare('UPDATE engines SET suggest_url=?, suggest_type=? WHERE slug=? AND suggest_url=?')
->execute(['', '', 'sogou', 'https://www.sogou.com/suggnew/ajajjson?type=web&key={q}']);
// 是否为空库(用于判断是否写入默认数据)
$isEmpty = (int)$pdo->query('SELECT COUNT(*) FROM groups')->fetchColumn() === 0;
seed_settings($pdo);
seed_engines($pdo);
if ($isEmpty) {
seed_groups_and_sites($pdo);
}
}
function seed_settings(PDO $pdo): void
{
$defaults = [
'site_name' => '简航',
'subtitle' => '个人网站导航',
'default_engine' => 'bing',
'primary_color' => '#2F6BF7',
// '1' = 顶部搜索框与所有站点/合集链接都在**新标签页**打开;'0' = 都在**当前页面**打开。
// 默认 '1' 与引入该设置前的旧行为一致;老库若已有这一行不会被覆盖(INSERT OR IGNORE)。
'open_blank' => '1',
];
$stmt = $pdo->prepare('INSERT OR IGNORE INTO settings (k, v) VALUES (?, ?)');
foreach ($defaults as $k => $v) {
$stmt->execute([$k, $v]);
}
}
function seed_engines(PDO $pdo): void
{
if ((int)$pdo->query('SELECT COUNT(*) FROM engines')->fetchColumn() > 0) {
return;
}
// name, slug, search_url, param, suggest_url, suggest_type, color, active, sort
// ⚠️ suggest_url 留空的含义:该引擎自己没有可用的联想接口。
// 反正默认「联想词来源 = 百度」,留空只会让 auto 模式下少一次无谓的超时等待。
// 搜狗(suggnew/ajajjson 已失效)、谷歌 / DuckDuckGo(国内不可达)都不填。
$rows = [
['必应', 'bing', 'https://cn.bing.com/search', 'q', 'https://api.bing.com/osjson.aspx?query={q}', 'bing_osjson', '#60A504', 1, 1],
['百度', 'baidu', 'https://www.baidu.com/s', 'wd', 'https://www.baidu.com/sugrec?prod=pc&wd={q}', 'baidu_sugrec', '#2100E0', 1, 2],
['谷歌', 'google', 'https://www.google.com/search', 'q', '', '', '#3B83FA', 0, 3],
['搜狗', 'sogou', 'https://www.sogou.com/web', 'query', '', '', '#FE6B14', 0, 4],
['360', 'so360', 'https://www.so.com/s', 'q', 'https://sug.so.360.cn/suggest?encodein=utf-8&encodeout=utf-8&format=json&word={q}', 'so360', '#1BB04A', 0, 5],
['DuckGo','ddg', 'https://duckduckgo.com/', 'q', '', '', '#DE5833', 0, 6],
];
$stmt = $pdo->prepare('INSERT INTO engines (name,slug,search_url,param,suggest_url,suggest_type,color,is_active,sort_order) VALUES (?,?,?,?,?,?,?,?,?)');
foreach ($rows as $r) {
$stmt->execute($r);
}
}
function seed_groups_and_sites(PDO $pdo): void
{
// 板块定义: [名称, 类型, 排序, [ [名称, 网址], ... ] ]
$data = [
['常用', 'grid', 1, [
['GitHub', 'https://github.com/'],
['阿里云', 'https://www.aliyun.com/'],
['知乎', 'https://www.zhihu.com/'],
['天翼云盘', 'https://cloud.189.cn/'],
['快科技', 'http://www.mydrivers.com/'],
['豆瓣', 'https://www.douban.com/'],
['少数派', 'https://sspai.com/'],
['红围脖', 'https://hongbowei.com/'],
]],
['工具', 'grid', 2, [
['宝塔面板', 'https://www.bt.cn/'],
['阿里云盘', 'https://www.alipan.com/'],
['网易云课堂', 'https://study.163.com/'],
['学堂在线', 'https://www.xuetangx.com/'],
['CSDN', 'https://www.csdn.net/'],
['稀土掘金', 'https://juejin.cn/'],
['淘宝', 'https://www.taobao.com/'],
['京东', 'https://www.jd.com/'],
['腾讯文档', 'https://docs.qq.com/'],
['石墨文档', 'https://shimo.im/'],
['飞书', 'https://www.feishu.cn/'],
['Gitee', 'https://gitee.com/'],
['百度网盘', 'https://pan.baidu.com/'],
['豆瓣电影', 'https://movie.douban.com/'],
['花瓣网', 'https://huaban.com/'],
['蓝奏云', 'https://www.lanzou.com/'],
['bilibili', 'https://www.bilibili.com/'],
['优酷', 'https://www.youku.com/'],
['爱奇艺', 'https://www.iqiyi.com/'],
['腾讯视频', 'https://v.qq.com/'],
['芒果TV', 'https://www.mgtv.com/'],
['draw.io', 'https://app.diagrams.net/'],
['稿定PS', 'https://ps.gaoding.com/'],
['Bitwarden', 'https://bitwarden.com/'],
['PDF工具', 'https://pdf.wps.cn/'],
['微信读书', 'https://weread.qq.com/'],
['高德地图', 'https://gaode.com/'],
['百度地图', 'https://map.baidu.com/'],
['建设银行', 'http://ccb.com/cn/home/index.html'],
['支付宝', 'https://www.alipay.com/'],
['MOOC', 'https://www.icourse163.org/'],
]],
['常用软件', 'tabs', 10, [
['WPS', 'https://www.wps.cn/'],
['Chrome', 'https://www.google.cn/chrome/index.html'],
['VS Code', 'https://code.visualstudio.com/'],
['Draw.IO', 'https://github.com/jgraph/drawio-desktop/releases'],
['火绒', 'https://www.huorong.cn/'],
['7-Zip', 'https://sparanoid.com/lab/7z/'],
['Tabby', 'https://github.com/Eugeny/tabby/'],
['GitAhead', 'https://gitahead.github.io/gitahead.com/'],
['Everything', 'https://www.voidtools.com/zh-cn/'],
['Wox', 'https://github.com/Wox-launcher/Wox'],
['BCUninstaller', 'https://github.com/Klocman/Bulk-Crap-Uninstaller'],
['DBeaver', 'https://github.com/dbeaver/dbeaver'],
['Thunderbird', 'https://www.thunderbird.net/zh-CN/'],
['FDM', 'https://www.freedownloadmanager.org/zh/'],
['GIMP', 'https://www.gimp.org/'],
['VLC', 'https://www.videolan.org/vlc/'],
['Zotero', 'https://www.zotero.org/'],
['QQ音乐', 'https://y.qq.com/'],
['微信', 'https://weixin.qq.com/'],
['TIM', 'https://tim.qq.com/'],
['Shotcut', 'https://github.com/mltframework/shotcut'],
]],
['图片资源', 'tabs', 11, [
['图标库', 'https://www.iconfont.cn/'],
['插画库', 'https://undraw.co/illustrations'],
['Pixabay', 'https://pixabay.com/zh/'],
['Pexels', 'https://www.pexels.com/'],
]],
['科研论文', 'tabs', 12, [
['DeepL翻译', 'https://www.deepl.com/'],
['Linggle', 'https://linggle.com/'],
['Netspeak', 'https://netspeak.org/'],
['Semantic Scholar', 'https://www.semanticscholar.org/'],
['LaTex Table', 'https://www.tablesgenerator.com/'],
]],
];
$g = $pdo->prepare('INSERT INTO groups (name, slug, display, sort_order, is_visible) VALUES (?,?,?,?,1)');
$s = $pdo->prepare('INSERT INTO sites (group_id, url, name, short_name, icon, sort_order, is_visible, created_at, updated_at) VALUES (?,?,?,?,\'\',?,1,?,?)');
$now = date('Y-m-d H:i:s');
foreach ($data as $row) {
[$name, $display, $order, $sites] = $row;
$g->execute([$name, slugify($name), $display, $order]);
$gid = (int)$pdo->lastInsertId();
$i = 0;
foreach ($sites as $site) {
$s->execute([$gid, $site[1], $site[0], $site[0], $i++, $now, $now]);
}
}
}
/** 生成 slug(中文退化为拼音不可用时用 md5 短串) */
function slugify(string $s): string
{
$s = trim($s);
if (preg_match('/^[a-z0-9\-]+$/i', $s)) {
return strtolower($s);
}
return 'g' . substr(md5($s), 0, 6);
}