用户要求:附件存储里所有限制项(几张图、几个视频、大小、总量、保留天数、 每小时次数)都能填 0,0 表示没有任何限制。 ## 最危险的地方:填 0 原本是「全禁」不是「不限」 两处逻辑把 0 判成了禁止,语义正好相反: 1. `fb_rate_check()` —— `count < limit`,limit=0 时 `0 < 0` 为假 ⇒ 每小时上传次数填 0 = 一次都不许传 2. 每帖数量校验 —— `count > max`,max=0 时传一个就超限 ⇒ 每帖图片填 0 = 一张都不许传 都改成「limit <= 0 直接放行」。 ## 校验区间与 input 下限全部放开到 0 | 字段 | 原下限 | 现下限 | |---|---|---| | image_max_per_post | 0(但界面上限 20) | 0,上限 1000 | | video_max_per_post | 0(上限 10) | 0,上限 1000 | | image_max_bytes | 0.2 MB | 0,上限 1024 MB | | video_max_bytes | 1 MB | 0,上限 10240 MB | | total_cap_bytes | 10 MB | 0,上限 1048576 MB | | retention_days | 1 天 | 0,上限 36500 | | upload_per_hour | 1 次 | 0,上限 100000 | | image_max_side | 480 | **保持 480** —— 这是压缩目标边长,不是限制项 | 服务端 `max($range[0], ...)` 不再把 0 拉回下限;input 的 min 一并改 0, 否则浏览器直接拦住连提交都到不了后端。 ## 语义冲突:0=不限与「0=关闭该类上传」冲突 原先 `fb_upload_kind_on()` 靠「数量上限是否为 0」判断开关, 0 现在表示不限个数,两者冲突。 按用户选择:**新增独立总开关** `upload.image_on` / `upload.video_on`(默认开), `fb_upload_kind_on()` 改看开关,不再看数量。 两个新键已加入 `fb_setting_keys()` 白名单(这个坑踩过多次)。 ## 前端同步 - `maxCount > 0` 才拦,0 不拦 - 提示文案:数量 0 显示「数量不限」、大小 0 显示「大小不限」, 不再出现「最多 0 张」这种荒唐文案 ## 风险提示(按用户选择用文字告知,不阻止保存) 单张图片/单个视频/全站总量填 0 时,字段下方提示「填 0 时注意:磁盘可能被写满」。 ## 验证 0 专项 31/31,关键几条: - 七项输入框 min 均为 0,image_max_side 保持 480 - 填 0 后数据库里就是 0,没被拉回下限 - **填 0 后行为是放行**:每小时放行、数量不限、总量放行、不清理 - 数量填 0 时上传仍是开的(靠独立开关) - 取消勾选开关后确实关闭,且与数量填 0 互不干扰 - 非 0 值全部正确(4 张 / 2 个 / 5MB / 50MB / 1024MB / 30 天 / 60 次) 回归全绿:全新安装 33/33、附件 22/22、老基线 25/25、安装向导 60/60、约束检查 20/20。
169 lines
7.4 KiB
PHP
169 lines
7.4 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);
|
||
|
||
/* ---- 按软件换主题色 ----
|
||
* 通过 $opt['software'] 传入软件行;全站主题只靠 --brand / --brand-600 /
|
||
* --brand-soft 三个变量,覆写它们即可,不用为每个软件写一套 CSS。
|
||
* 没传软件(如后台、安装页)就用主题默认色,保持中性。 */
|
||
$swColors = null;
|
||
if (!empty($opt['software'])) {
|
||
list($cMain, $cDark, $cSoft) = fb_software_colors($opt['software']);
|
||
if ($cMain !== '') { $swColors = [$cMain, $cDark, $cSoft]; }
|
||
}
|
||
$themeColor = $swColors ? $swColors[0] : '#0057B8';
|
||
?><!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="<?php echo e($themeColor); ?>">
|
||
<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=<?php echo (int)FB_ASSET_V; ?>">
|
||
<?php if ($swColors): ?>
|
||
<?php /* 按软件覆写主题色:只改这三个变量,其它样式自动跟着变 */ ?>
|
||
<style>:root{
|
||
--brand:<?php echo e($swColors[0]); ?>;
|
||
--brand-600:<?php echo e($swColors[1]); ?>;
|
||
--brand-soft:<?php echo e($swColors[2]); ?>;
|
||
}</style>
|
||
<?php endif; ?>
|
||
</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=<?php echo (int)FB_ASSET_V; ?>"></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 ? '图片' : '视频';
|
||
// 0 表示不限制,文案别写成「最多 0 张」
|
||
$cntTxt = $maxN > 0 ? ('最多 ' . $maxN . ($isImg ? ' 张' : ' 个')) : '数量不限';
|
||
$sizeTxt = $maxB > 0
|
||
? ('单' . ($isImg ? '张' : '个') . '不超过 ' . fb_size_h($maxB))
|
||
: '大小不限';
|
||
$desc = $cntTxt . ',' . $sizeTxt . ($isImg ? ';大图会自动压缩后上传' : '');
|
||
?>
|
||
<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
|
||
}
|