需求落实: 1. 页脚常驻「管理后台」链接,不用再手敲 admin.php。 2. 页脚「由 FeedLite 驱动」中的 FeedLite 改为超链接(指向开源仓库); 后台新增「页脚」设置组,可关闭整句(关闭后只留管理后台链接),也可改链接地址。 3. 管理员口令存储方式在后台如实展示:数据库 settings.admin.password_hash, 存 bcrypt 单向哈希(不可逆),明文不落盘。 4. 新增安装向导 install.php:data/ 下没有数据库时自动 302 引导; 体检环境 -> 填站点信息与口令 -> 生成 config.php -> 建库建表 -> 完成。 顺带修掉一个真实安全隐患: config.php 里的出厂占位口令 CHANGE_ME_admin 原先可被当作有效口令登录后台 (等于公开仓库里就写着线上后门口令)。现改为占位/空值一律拒绝, 未设口令时后台明确提示去安装向导,不再含糊报「口令不正确」。 实现要点: - 新增 fb_cfg_file():只读配置文件、不叠加 settings 表、绝不触达数据库。 「是否已安装」以数据库文件是否存在为判据,而 fb_cfg() 读 settings 会触发 fb_db() 就地建库 —— 那会把安装态自己造出来,所以判定必须走不碰库的通道。 - bootstrap 只在已安装时建库;未安装则 302 到 install.php(CLI 下报错退出)。 - fb_settings_flat() 在未安装时直接返回空,杜绝任何隐式建库。 - install.php 用文本级替换生成 config.php,保留全部注释与 OSS/SMTP 段落; 生成的 PHP 用 token_get_all(TOKEN_PARSE) 校验后才落盘。 - 页脚链接只放行 http/https,后台保存时校验(防 javascript: 伪协议)。 - style.css 页脚行改 flex,版本号 v6 -> v7。 验证: - php -l 全量通过;现有回归 25/25、附件专项 13/13 保持通过。 - 安装向导专项 60/60(含「明文口令未写入任何文件」「数据库字节里搜不到明文」)。 - 反向验证:撤销三处修复后对应断言确实失败(断言非装饰)。 - 几何检查:本机 15/15 + 页脚专项全过;线上真实 URL 15/15 + 页脚专项全过。 - 线上真机冒烟 22/22。
147 lines
6.3 KiB
PHP
147 lines
6.3 KiB
PHP
<?php
|
|
/**
|
|
* 页面外壳(头部 / 底部)
|
|
*/
|
|
if (!defined('FB_ROOT')) { http_response_code(403); exit('forbidden'); }
|
|
|
|
/**
|
|
* 输出页面头部
|
|
* @param string $title 页面标题
|
|
* @param array $opt ['robots' => 'noindex', 'desc' => '...']
|
|
*/
|
|
function fb_layout_head($title, array $opt = []) {
|
|
$site = (string)fb_cfg('site_name');
|
|
$robots = isset($opt['robots']) ? (string)$opt['robots'] : 'index,follow';
|
|
$full = $title === '' ? $site : ($title . ' · ' . $site);
|
|
?><!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<meta name="theme-color" content="#0057B8">
|
|
<meta name="robots" content="<?php echo e($robots); ?>">
|
|
<title><?php echo e($full); ?></title>
|
|
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
|
|
<link rel="icon" type="image/x-icon" href="assets/favicon.ico">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon-32x32.png">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon-16x16.png">
|
|
<link rel="apple-touch-icon" sizes="180x180" href="assets/apple-touch-icon.png">
|
|
<link rel="manifest" href="assets/site.webmanifest">
|
|
<link rel="stylesheet" href="assets/style.css?v=7">
|
|
</head>
|
|
<body>
|
|
<header class="topbar">
|
|
<div class="wrap">
|
|
<a class="brand-mini" href="./" title="<?php echo e($site); ?>">
|
|
<span class="logo-mark" role="img" aria-label="轻反馈图标"></span>
|
|
<span class="brand-text"><?php echo e($site); ?></span>
|
|
</a>
|
|
<div class="spacer"></div>
|
|
<a class="navlink hide-sm" href="./">全部问题</a>
|
|
<?php if (fb_admin_logged_in()): ?><a class="navlink" href="admin.php">管理后台</a><?php endif; ?>
|
|
<a class="btn-mini" href="new.php">我要反馈</a>
|
|
</div>
|
|
</header>
|
|
<main class="wrap">
|
|
<?php
|
|
}
|
|
|
|
/** 输出页面底部 */
|
|
function fb_layout_foot() {
|
|
// 页脚两件事:① 常驻「管理后台」入口(省得每次手敲 admin.php);② 可关闭的 FeedLite 版权句。
|
|
// 版权句里的 FeedLite 指向开源仓库,地址可后台改;关掉开关则整句不渲染,只留管理后台链接。
|
|
$showCredit = (bool)fb_cfg('footer.show_credit', true);
|
|
$creditUrl = trim((string)fb_cfg('footer.credit_url', ''));
|
|
if ($creditUrl === '') { $creditUrl = fb_credit_url_default(); }
|
|
// 只放行 http/https:防止把 javascript:/data: 之类写进页脚链接(后台设置项,同样要防)
|
|
if (!preg_match('~^https?://~i', $creditUrl)) { $creditUrl = fb_credit_url_default(); }
|
|
?>
|
|
<footer class="foot">
|
|
<div class="wrap">
|
|
<p class="muted">本站仅保存你提交的昵称、邮箱与内容,用于问题跟进;页面不会公开展示邮箱与手机号。</p>
|
|
<p class="muted foot-line">
|
|
<?php if ($showCredit): ?><span><?php echo e((string)fb_cfg('site_name', '轻反馈')); ?> · 由 <a href="<?php echo e($creditUrl); ?>" target="_blank" rel="noopener">FeedLite</a> 驱动</span><?php endif; ?>
|
|
<?php if ($showCredit): ?><span class="dot">·</span><?php endif; ?>
|
|
<a href="admin.php">管理后台</a>
|
|
</p>
|
|
</div>
|
|
</footer>
|
|
<script src="assets/app.js?v=5"></script>
|
|
</body>
|
|
</html><?php
|
|
}
|
|
|
|
/** 渲染提示条 */
|
|
function fb_flash_box($msg, $type = 'error') {
|
|
if ($msg === '' || $msg === null) { return; }
|
|
echo '<div class="flash ' . e($type) . '">' . e($msg) . '</div>';
|
|
}
|
|
|
|
/**
|
|
* 渲染附件上传控件(图片 / 视频;上传经服务器中转落地,local/OSS 前端同一路径)
|
|
* 需置于 <form> 内部:会输出一个 name="attachments" 的隐藏字段,
|
|
* 由 assets/app.js 在文件上传成功后回填 JSON 描述。
|
|
*
|
|
* @param array $existing 校验失败时已上传的附件描述(避免用户重复上传)
|
|
*/
|
|
function fb_uploaders(array $existing = []) {
|
|
$kinds = [];
|
|
if (fb_upload_kind_on('image')) { $kinds[] = 'image'; }
|
|
if (fb_upload_kind_on('video')) { $kinds[] = 'video'; }
|
|
if (!$kinds) { return; }
|
|
|
|
$init = [];
|
|
foreach ($existing as $it) {
|
|
$kind = isset($it['kind']) ? (string)$it['kind'] : 'image';
|
|
$key = isset($it['key']) ? (string)$it['key'] : '';
|
|
if ($key === '') { continue; }
|
|
$init[] = [
|
|
'kind' => $kind,
|
|
'key' => $key,
|
|
'size' => isset($it['size']) ? (int)$it['size'] : 0,
|
|
'name' => isset($it['name']) ? (string)$it['name'] : '',
|
|
'mime' => isset($it['mime']) ? (string)$it['mime'] : '',
|
|
'w' => isset($it['width']) ? (int)$it['width'] : 0,
|
|
'h' => isset($it['height']) ? (int)$it['height'] : 0,
|
|
'url' => fb_attach_url($key),
|
|
'thumb' => $kind === 'image' ? fb_attach_thumb_url($key, 360) : '',
|
|
];
|
|
}
|
|
?>
|
|
<div class="uploader-box"
|
|
data-uploader-root
|
|
data-csrf="<?php echo e(fb_csrf_token()); ?>"
|
|
data-sign-url="upload.php"
|
|
data-init="<?php echo e(json_encode($init, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); ?>">
|
|
|
|
<?php foreach ($kinds as $kind):
|
|
$isImg = ($kind === 'image');
|
|
$maxN = (int)fb_upload_cfg($kind . '_max_per_post', 1);
|
|
$maxB = (int)fb_upload_cfg($kind . '_max_bytes', 0);
|
|
$accept = implode(',', (array)fb_upload_cfg($kind . '_mimes', []));
|
|
$label = $isImg ? '图片' : '视频';
|
|
$desc = $isImg
|
|
? ('最多 ' . $maxN . ' 张,单张不超过 ' . fb_size_h($maxB) . ';大图会自动压缩后上传')
|
|
: ('最多 ' . $maxN . ' 个,单个不超过 ' . fb_size_h($maxB));
|
|
?>
|
|
<div class="uploader" data-uploader data-kind="<?php echo e($kind); ?>"
|
|
data-max-count="<?php echo $maxN; ?>"
|
|
data-max-bytes="<?php echo $maxB; ?>"
|
|
data-max-side="<?php echo (int)fb_upload_cfg('image_max_side', 1600); ?>"
|
|
data-accept="<?php echo e($accept); ?>"
|
|
data-label="<?php echo e($label); ?>">
|
|
<div class="up-head">
|
|
<span class="up-title"><?php echo e($label); ?> <i class="hint"><?php echo e($desc); ?></i></span>
|
|
<button type="button" class="btn up-pick">选择<?php echo e($label); ?></button>
|
|
<input type="file" class="up-file" accept="<?php echo e($accept); ?>"
|
|
<?php echo $isImg ? 'multiple' : ''; ?> tabindex="-1">
|
|
</div>
|
|
<div class="up-list"></div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
|
|
<input type="hidden" name="attachments" class="att-field" value="">
|
|
</div>
|
|
<?php
|
|
}
|