用户要求:首页下方除了「管理」,再放开源地址与反馈链接。 - config.php 新增 REPO_URL / FEEDBACK_URL 两个常量,集中管理外链, 以后换仓库或换反馈系统只改一处,不用翻模板 - index.php 页脚顺序:管理 · 开源 · 反馈 · 站点名 - 外链带 target=_blank + rel=noopener(新标签打开,且不给 window.opener) 反馈地址用 https://feedlite.yangyq.net/index.php?sw=simplenav (sw= 是 FeedLite 区分来源的约定;原来的 ?sw=SimpleNav 写法是错的) 测试:_sn_footer_e2e.py 15 项(地址精确匹配 / target / rel / 顺序 / 常量未定义会露馅) 反向验证 8 变体全精确命中,含「反馈地址写成旧值」——正是本次修掉的那个错。
149 lines
6.0 KiB
PHP
149 lines
6.0 KiB
PHP
<?php
|
||
/**
|
||
* 简航 SimpleNav —— 基础配置
|
||
* 面向个人的轻量级网站导航系统
|
||
*/
|
||
declare(strict_types=1);
|
||
|
||
date_default_timezone_set('Asia/Shanghai');
|
||
if (function_exists('mb_internal_encoding')) {
|
||
mb_internal_encoding('UTF-8');
|
||
}
|
||
|
||
define('APP_NAME', '简航');
|
||
define('APP_PATH', __DIR__);
|
||
define('DATA_PATH', APP_PATH . '/data'); // 数据库等敏感数据(禁止 Web 访问)
|
||
define('DB_PATH', DATA_PATH . '/nav.db');
|
||
define('ICON_PATH', APP_PATH . '/uploads/icons'); // 抓取的网站图标(可公开访问)
|
||
define('ICON_URL', 'uploads/icons'); // 相对站点根
|
||
define('ICON_STORE', 128); // 图标统一存成 128×128(高清屏下 45~64px 显示仍锐利)
|
||
|
||
/**
|
||
* 排序步长(稀疏排序)。
|
||
*
|
||
* 为什么不用「连续整数 + 每次全表重排」:
|
||
* · 拖拽排序只需要改**被拖动那一条**(在两邻居之间取中间值),不必重写整组;
|
||
* · 新增条目一律 `同组 MAX + SORT_STEP`,天然排在最后,**永远不会插进用户已排好的
|
||
* 顺序中间**(这是用户明确要求的行为);
|
||
* · 只有「间隙用尽」或「用户拖完一次」时才做一次归一化(见 apply_sort_order())。
|
||
*
|
||
* 取值 1000:一个小组最多能容纳约 1000 次「往同两邻居之间插」而不归一化,
|
||
* 个人导航站的规模下基本永远不会退化。
|
||
*/
|
||
define('SORT_STEP', 1000);
|
||
|
||
/**
|
||
* 页脚的项目链接。集中定义在这里,而不是散落在模板里 ——
|
||
* 换仓库地址或换反馈系统时只改这一处。
|
||
*
|
||
* REPO_URL :开源仓库(Gitea)
|
||
* FEEDBACK_URL:反馈收集,sw= 参数用于区分是哪个软件来的(FeedLite 的约定)
|
||
*/
|
||
define('REPO_URL', 'https://code.yangyq.net/laoyang/SimpleNav');
|
||
define('FEEDBACK_URL', 'https://feedlite.yangyq.net/index.php?sw=simplenav');
|
||
|
||
/**
|
||
* 「站点整体图标」(浏览器标签页 favicon + 首页品牌图标)—— 用户上传后自动生成的一整套尺寸。
|
||
*
|
||
* ⚠️ 与上面的 ICON_PATH 是**两件事**,目录也刻意分开:
|
||
* ICON_PATH = 导航格子里**某个网站**自己的小图标,一个网站一份(键 = icon_key(网址));
|
||
* SITE_ICON_PATH = **本站自己**的图标,全局唯一(键 = 随机 16 位 hex)。
|
||
* 混在一起的话,清理图标目录时两边会互相误伤。
|
||
*/
|
||
define('SITE_ICON_PATH', APP_PATH . '/uploads/site');
|
||
define('SITE_ICON_URL', 'uploads/site');
|
||
|
||
/**
|
||
* 是否「由 PHP 再发一颗」绕开服务器侧页面缓存的标记 cookie。
|
||
*
|
||
* 背景(2026-09-28 线上实测):服务器装了宝塔面板的「网站加速」,它是一层基于
|
||
* memcached 的整页缓存,本站实测其过期时间是 30 秒。实测它
|
||
* **读不到** FastCGI 后端返回的 `Cache-Control` 响应头,所以 `no-store` 之类对它
|
||
* 无效;它唯一认的信号是**请求** cookie `X_CACHE=1`。
|
||
*
|
||
* 主要解法在根目录 `.htaccess` 里(用 `RequestHeader` 在请求侧注入该标记),
|
||
* 对所有访客一次生效、无需 PHP 配合;下面这个开关是**第二道保险**,
|
||
* 用于 Nginx 等不读 .htaccess 的环境。
|
||
*
|
||
* true = 动态页顺带把该 cookie 种到访客浏览器(首次访问各发一次)
|
||
* false = 只发响应头,完全依赖 .htaccess / 服务器配置
|
||
*/
|
||
define('BYPASS_PAGE_CACHE', true);
|
||
|
||
/**
|
||
* 自动探测应用根 URL,兼容「放在子目录 /simplenav」与「放在根目录」两种部署。
|
||
* 返回形如 ''(根目录)或 '/simplenav'。
|
||
*/
|
||
function app_base(): string
|
||
{
|
||
$dir = isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : '/';
|
||
$dir = str_replace('\\', '/', dirname($dir));
|
||
$dir = preg_replace('#/admin$#', '', $dir); // admin/ 下的脚本回退到应用根
|
||
$dir = rtrim($dir, '/');
|
||
return $dir === '/' ? '' : $dir;
|
||
}
|
||
|
||
/** 生成带应用根的 URL */
|
||
function url(string $path = ''): string
|
||
{
|
||
return app_base() . '/' . ltrim($path, '/');
|
||
}
|
||
|
||
/** 站点名(可在后台设置里覆盖)
|
||
*
|
||
* ⚠️ 这是**唯一**允许「空则回退默认」的设置:站点名留空会让 <title> / <h1> / alt
|
||
* 变成空白,页面连名字都没有。
|
||
* 这里显式写出来,而不是指望 setting() 全局回退 —— 那正是「副标题删不掉」的病根。
|
||
*/
|
||
function site_name(): string
|
||
{
|
||
$n = setting('site_name');
|
||
return $n !== '' ? $n : APP_NAME;
|
||
}
|
||
|
||
/**
|
||
* 设置表缓存(可失效)。
|
||
*
|
||
* ⚠️ 为什么必须能失效:这个缓存只在**首次读取**时查库,之后整进程复用。
|
||
* 后台「保存设置 → 重定向」是两次请求,所以以前没暴露问题;但只要有**同一次请求里
|
||
* 先写设置、再读回来**的路径(上传站点图标就是:写完马上要用新键拼 URL),
|
||
* 就会读到写之前的旧值 —— 表现为「上传成功但图标没变」。
|
||
* 任何写入设置的地方,写完都要调 settings_reload()。
|
||
*/
|
||
function settings_cache(bool $reload = false): array
|
||
{
|
||
static $cache = null;
|
||
if ($cache === null || $reload) {
|
||
$cache = [];
|
||
try {
|
||
foreach (db()->query('SELECT k, v FROM settings') as $row) {
|
||
$cache[$row['k']] = $row['v'];
|
||
}
|
||
} catch (Throwable $e) {
|
||
$cache = [];
|
||
}
|
||
}
|
||
return $cache;
|
||
}
|
||
|
||
/** 写入设置后调用,让本次请求后续的 setting() 读到新值 */
|
||
function settings_reload(): void
|
||
{
|
||
settings_cache(true);
|
||
}
|
||
|
||
/**
|
||
* 读取设置项。
|
||
*
|
||
* ⚠️ 空串是一等公民:**只有「这一行不存在」才回退 $default**。
|
||
* 曾经这里写的是 `$cache[$key] !== ''`,把「用户主动清空」和「从未配置过」当成一回事,
|
||
* 于是后台删掉副标题后前台照样显示默认的「个人网站导航」——删不掉。
|
||
* 需要「空则回退」的设置请在自己的读取点显式写明(如 site_name()),
|
||
* 不要把这个语义塞回函数里。
|
||
*/
|
||
function setting(string $key, string $default = ''): string
|
||
{
|
||
$cache = settings_cache();
|
||
return array_key_exists($key, $cache) ? $cache[$key] : $default;
|
||
}
|