附件限制全部支持填 0 = 不限制 (#4)
用户要求:附件存储里所有限制项(几张图、几个视频、大小、总量、保留天数、 每小时次数)都能填 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。
This commit is contained in:
@@ -287,10 +287,12 @@ function fb_admin_save_settings() {
|
|||||||
else { fb_set_setting('footer.credit_url', $curl); }
|
else { fb_set_setting('footer.credit_url', $curl); }
|
||||||
|
|
||||||
$ints = [
|
$ints = [
|
||||||
'upload.image_max_per_post' => [0, 20],
|
// 区间下限统一改 0:0 = 不限制(见 #4)。
|
||||||
'upload.video_max_per_post' => [0, 10],
|
// 只有 image_max_side 例外 —— 边长 0 没有意义,它是压缩目标不是限制。
|
||||||
'upload.retention_days' => [1, 3650],
|
'upload.image_max_per_post' => [0, 1000],
|
||||||
'upload.upload_per_hour' => [1, 2000],
|
'upload.video_max_per_post' => [0, 1000],
|
||||||
|
'upload.retention_days' => [0, 36500],
|
||||||
|
'upload.upload_per_hour' => [0, 100000],
|
||||||
'upload.image_max_side' => [480, 4096],
|
'upload.image_max_side' => [480, 4096],
|
||||||
];
|
];
|
||||||
foreach ($ints as $k => $range) {
|
foreach ($ints as $k => $range) {
|
||||||
@@ -298,12 +300,16 @@ function fb_admin_save_settings() {
|
|||||||
$v = max($range[0], min($range[1], $v));
|
$v = max($range[0], min($range[1], $v));
|
||||||
fb_set_setting($k, $v);
|
fb_set_setting($k, $v);
|
||||||
}
|
}
|
||||||
|
// 图片/视频上传总开关(0=不限个数后,开关必须独立存在)
|
||||||
|
fb_set_setting('upload.image_on', (int)(bool)$post('upload.image_on'));
|
||||||
|
fb_set_setting('upload.video_on', (int)(bool)$post('upload.video_on'));
|
||||||
|
|
||||||
// MB → 字节
|
// MB → 字节
|
||||||
|
// 下限改 0 = 不限制(见 #4)
|
||||||
$mbs = [
|
$mbs = [
|
||||||
'upload.image_max_bytes' => [0.2, 50],
|
'upload.image_max_bytes' => [0, 1024],
|
||||||
'upload.video_max_bytes' => [1, 500],
|
'upload.video_max_bytes' => [0, 10240],
|
||||||
'upload.total_cap_bytes' => [10, 100000],
|
'upload.total_cap_bytes' => [0, 1048576],
|
||||||
];
|
];
|
||||||
foreach ($mbs as $k => $range) {
|
foreach ($mbs as $k => $range) {
|
||||||
$mb = (float)$post($k, 0);
|
$mb = (float)$post($k, 0);
|
||||||
@@ -762,23 +768,48 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
|||||||
<i class="hint">本机:文件落在站点 uploads/ 目录;OSS:文件经服务器代理写入私有桶(走内网 Endpoint),公网无法直连桶。两种方式都需要服务器磁盘能放下单个上传文件。<b>前台展示一律经 media.php 代理</b>——无论存哪,页面上的图片/视频地址都是 <code>media.php?k=…</code>,不暴露 uploads/ 真实路径,也不出现任何 OSS 桶信息。</i>
|
<i class="hint">本机:文件落在站点 uploads/ 目录;OSS:文件经服务器代理写入私有桶(走内网 Endpoint),公网无法直连桶。两种方式都需要服务器磁盘能放下单个上传文件。<b>前台展示一律经 media.php 代理</b>——无论存哪,页面上的图片/视频地址都是 <code>media.php?k=…</code>,不暴露 uploads/ 真实路径,也不出现任何 OSS 桶信息。</i>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="grid2">
|
<?php
|
||||||
|
// 所有「限制类」输入框:min=0,填 0 = 不限制(见 #4)。
|
||||||
|
// 只有 image_max_side 例外 —— 它是压缩目标边长,0 没有意义。
|
||||||
|
$zero = '<i class="hint">填 0 表示不限制。</i>';
|
||||||
|
?>
|
||||||
|
<label class="check"><input type="checkbox" name="upload_image_on" value="1"<?php echo fb_upload_cfg('image_on', true) ? ' checked' : ''; ?>> 允许上传图片
|
||||||
|
<span class="hint" style="display:block;margin-left:24px">关掉后前台不再显示图片上传入口。与下面「填 0 不限个数」互不干扰。</span></label>
|
||||||
|
<label class="check"><input type="checkbox" name="upload_video_on" value="1"<?php echo fb_upload_cfg('video_on', true) ? ' checked' : ''; ?>> 允许上传视频</label>
|
||||||
|
|
||||||
|
<div class="grid2" style="margin-top:10px">
|
||||||
<label class="field"><span>每个问题最多几张图片</span>
|
<label class="field"><span>每个问题最多几张图片</span>
|
||||||
<input type="number" name="upload_image_max_per_post" min="0" max="20" value="<?php echo (int)fb_upload_cfg('image_max_per_post', 4); ?>"></label>
|
<input type="number" name="upload_image_max_per_post" min="0" max="1000"
|
||||||
|
value="<?php echo (int)fb_upload_cfg('image_max_per_post', 4); ?>">
|
||||||
|
<?php echo $zero; ?></label>
|
||||||
<label class="field"><span>单张图片上限(MB)</span>
|
<label class="field"><span>单张图片上限(MB)</span>
|
||||||
<input type="number" step="0.1" min="0.2" max="50" name="upload_image_max_bytes" value="<?php echo $mb(fb_upload_cfg('image_max_bytes', 5242880)); ?>"></label>
|
<input type="number" step="any" min="0" max="1024" name="upload_image_max_bytes"
|
||||||
|
value="<?php echo $mb(fb_upload_cfg('image_max_bytes', 5242880)); ?>">
|
||||||
|
<?php echo $zero; ?><i class="hint">填 0 时注意:服务器磁盘可能被大图写满。</i></label>
|
||||||
<label class="field"><span>每个问题最多几个视频</span>
|
<label class="field"><span>每个问题最多几个视频</span>
|
||||||
<input type="number" name="upload_video_max_per_post" min="0" max="10" value="<?php echo (int)fb_upload_cfg('video_max_per_post', 1); ?>"></label>
|
<input type="number" name="upload_video_max_per_post" min="0" max="1000"
|
||||||
|
value="<?php echo (int)fb_upload_cfg('video_max_per_post', 1); ?>">
|
||||||
|
<?php echo $zero; ?></label>
|
||||||
<label class="field"><span>单个视频上限(MB)</span>
|
<label class="field"><span>单个视频上限(MB)</span>
|
||||||
<input type="number" step="1" min="1" max="500" name="upload_video_max_bytes" value="<?php echo $mb(fb_upload_cfg('video_max_bytes', 52428800)); ?>"></label>
|
<input type="number" step="any" min="0" max="10240" name="upload_video_max_bytes"
|
||||||
|
value="<?php echo $mb(fb_upload_cfg('video_max_bytes', 52428800)); ?>">
|
||||||
|
<?php echo $zero; ?><i class="hint">填 0 时注意:服务器磁盘可能被大视频写满。</i></label>
|
||||||
<label class="field"><span>全站附件总量上限(MB)</span>
|
<label class="field"><span>全站附件总量上限(MB)</span>
|
||||||
<input type="number" step="any" min="10" max="100000" name="upload_total_cap_bytes" value="<?php echo $mb(fb_upload_cfg('total_cap_bytes', 1073741824)); ?>"></label>
|
<input type="number" step="any" min="0" max="1048576" name="upload_total_cap_bytes"
|
||||||
|
value="<?php echo $mb(fb_upload_cfg('total_cap_bytes', 1073741824)); ?>">
|
||||||
|
<?php echo $zero; ?><i class="hint">填 0 表示不限总量,磁盘写满后上传会失败。</i></label>
|
||||||
<label class="field"><span>附件保留天数</span>
|
<label class="field"><span>附件保留天数</span>
|
||||||
<input type="number" name="upload_retention_days" min="1" max="3650" value="<?php echo (int)fb_upload_cfg('retention_days', 365); ?>"></label>
|
<input type="number" name="upload_retention_days" min="0" max="36500"
|
||||||
|
value="<?php echo (int)fb_upload_cfg('retention_days', 365); ?>">
|
||||||
|
<?php echo $zero; ?><i class="hint">填 0 = 永不自动清理。</i></label>
|
||||||
<label class="field"><span>每 IP 每小时最多上传几次</span>
|
<label class="field"><span>每 IP 每小时最多上传几次</span>
|
||||||
<input type="number" name="upload_upload_per_hour" min="1" max="2000" value="<?php echo (int)fb_upload_cfg('upload_per_hour', 60); ?>"></label>
|
<input type="number" name="upload_upload_per_hour" min="0" max="100000"
|
||||||
|
value="<?php echo (int)fb_upload_cfg('upload_per_hour', 60); ?>">
|
||||||
|
<?php echo $zero; ?></label>
|
||||||
<label class="field"><span>图片压缩后的最长边(像素)</span>
|
<label class="field"><span>图片压缩后的最长边(像素)</span>
|
||||||
<input type="number" name="upload_image_max_side" min="480" max="4096" value="<?php echo (int)fb_upload_cfg('image_max_side', 1600); ?>"></label>
|
<input type="number" name="upload_image_max_side" min="480" max="4096"
|
||||||
|
value="<?php echo (int)fb_upload_cfg('image_max_side', 1600); ?>">
|
||||||
|
<i class="hint">这是压缩目标边长,不是限制项,最小 480。</i></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 style="margin:24px 0 10px">阿里云 OSS(选「OSS」时才需要填)</h2>
|
<h2 style="margin:24px 0 10px">阿里云 OSS(选「OSS」时才需要填)</h2>
|
||||||
|
|||||||
+3
-1
@@ -301,7 +301,9 @@
|
|||||||
alert('「' + file.name + '」超过 ' + human(maxBytes) + ' 上限。');
|
alert('「' + file.name + '」超过 ' + human(maxBytes) + ' 上限。');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (countOf(kind) >= maxCount) {
|
// maxCount = 0 表示不限个数(后台可填 0),此时不做任何拦截。
|
||||||
|
// 别写成 `count >= 0`,那样等于一个都不让传。
|
||||||
|
if (maxCount > 0 && countOf(kind) >= maxCount) {
|
||||||
alert(label + '最多上传 ' + maxCount + ' 个,请先移除已上传的文件。');
|
alert(label + '最多上传 ' + maxCount + ' 个,请先移除已上传的文件。');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-4
@@ -201,10 +201,17 @@ function fb_attach_delete($key) {
|
|||||||
return fb_oss_delete($key);
|
return fb_oss_delete($key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 该类型是否可用(数量上限为 0 或未配置则视为关闭) */
|
/**
|
||||||
|
* 该类型是否可用。
|
||||||
|
*
|
||||||
|
* ⚠️ 原先靠「数量上限是否为 0」判断开关 —— 但 0 现在表示「不限个数」,
|
||||||
|
* 两者语义冲突(一个说"关掉"、一个说"不限制")。改为看独立开关:
|
||||||
|
* upload.image_on / upload.video_on,默认都开。
|
||||||
|
*/
|
||||||
function fb_upload_kind_on($kind) {
|
function fb_upload_kind_on($kind) {
|
||||||
if (!fb_store_enabled()) { return false; }
|
if (!fb_store_enabled()) { return false; }
|
||||||
return (int)fb_upload_cfg($kind . '_max_per_post', 0) > 0;
|
$key = $kind . '_on';
|
||||||
|
return (bool)fb_upload_cfg($key, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 归一化扩展名 */
|
/** 归一化扩展名 */
|
||||||
@@ -313,9 +320,12 @@ function fb_attach_parse($raw) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$counts[$kind]++;
|
$counts[$kind]++;
|
||||||
if ($counts[$kind] > (int)fb_upload_cfg($kind . '_max_per_post', 0)) {
|
// 0 = 不限个数。注意别写成 `count > max`,那样 max=0 时传一个就超限,
|
||||||
|
// 等于「填 0 反而一个都不让传」,与用户意图相反。
|
||||||
|
$maxPerPost = (int)fb_upload_cfg($kind . '_max_per_post', 0);
|
||||||
|
if ($maxPerPost > 0 && $counts[$kind] > $maxPerPost) {
|
||||||
return [false, [], ($kind === 'image' ? '图片最多上传 ' : '视频最多上传 ')
|
return [false, [], ($kind === 'image' ? '图片最多上传 ' : '视频最多上传 ')
|
||||||
. (int)fb_upload_cfg($kind . '_max_per_post', 0) . ' 个。'];
|
. $maxPerPost . ' 个。'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$seen[$key] = true;
|
$seen[$key] = true;
|
||||||
|
|||||||
+6
-3
@@ -139,9 +139,12 @@ function fb_uploaders(array $existing = []) {
|
|||||||
$maxB = (int)fb_upload_cfg($kind . '_max_bytes', 0);
|
$maxB = (int)fb_upload_cfg($kind . '_max_bytes', 0);
|
||||||
$accept = implode(',', (array)fb_upload_cfg($kind . '_mimes', []));
|
$accept = implode(',', (array)fb_upload_cfg($kind . '_mimes', []));
|
||||||
$label = $isImg ? '图片' : '视频';
|
$label = $isImg ? '图片' : '视频';
|
||||||
$desc = $isImg
|
// 0 表示不限制,文案别写成「最多 0 张」
|
||||||
? ('最多 ' . $maxN . ' 张,单张不超过 ' . fb_size_h($maxB) . ';大图会自动压缩后上传')
|
$cntTxt = $maxN > 0 ? ('最多 ' . $maxN . ($isImg ? ' 张' : ' 个')) : '数量不限';
|
||||||
: ('最多 ' . $maxN . ' 个,单个不超过 ' . fb_size_h($maxB));
|
$sizeTxt = $maxB > 0
|
||||||
|
? ('单' . ($isImg ? '张' : '个') . '不超过 ' . fb_size_h($maxB))
|
||||||
|
: '大小不限';
|
||||||
|
$desc = $cntTxt . ',' . $sizeTxt . ($isImg ? ';大图会自动压缩后上传' : '');
|
||||||
?>
|
?>
|
||||||
<div class="uploader" data-uploader data-kind="<?php echo e($kind); ?>"
|
<div class="uploader" data-uploader data-kind="<?php echo e($kind); ?>"
|
||||||
data-max-count="<?php echo $maxN; ?>"
|
data-max-count="<?php echo $maxN; ?>"
|
||||||
|
|||||||
+12
-1
@@ -279,12 +279,22 @@ function fb_csrf_ok($t) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 频率限制 */
|
/** 频率限制 */
|
||||||
|
/**
|
||||||
|
* 频率检查。
|
||||||
|
* @param int $limit 每小时的允许次数;**0 或负数表示不限制**
|
||||||
|
*
|
||||||
|
* ⚠️ 曾经写成 `count < limit` —— limit=0 时 `0 < 0` 为假,
|
||||||
|
* 用户把「每小时上传次数」设成 0(本意是不限制)会变成「一次都不许传」。
|
||||||
|
* 语义正好相反,属于"填 0 反而全禁掉"的陷阱。
|
||||||
|
*/
|
||||||
function fb_rate_check($action, $limit) {
|
function fb_rate_check($action, $limit) {
|
||||||
|
$limit = (int)$limit;
|
||||||
|
if ($limit <= 0) { return true; } // 0 = 不限制,直接放行
|
||||||
$pdo = fb_db();
|
$pdo = fb_db();
|
||||||
$since = time() - 3600;
|
$since = time() - 3600;
|
||||||
$st = $pdo->prepare('SELECT COUNT(*) FROM rate_limit WHERE ip_hash=? AND action=? AND created_at>=?');
|
$st = $pdo->prepare('SELECT COUNT(*) FROM rate_limit WHERE ip_hash=? AND action=? AND created_at>=?');
|
||||||
$st->execute([fb_ip_hash(), $action, $since]);
|
$st->execute([fb_ip_hash(), $action, $since]);
|
||||||
return ((int)$st->fetchColumn()) < (int)$limit;
|
return ((int)$st->fetchColumn()) < $limit;
|
||||||
}
|
}
|
||||||
function fb_rate_hit($action) {
|
function fb_rate_hit($action) {
|
||||||
$pdo = fb_db();
|
$pdo = fb_db();
|
||||||
@@ -382,6 +392,7 @@ function fb_setting_keys() {
|
|||||||
'footer.show_credit', 'footer.credit_url',
|
'footer.show_credit', 'footer.credit_url',
|
||||||
'installed_at',
|
'installed_at',
|
||||||
'attachment.storage',
|
'attachment.storage',
|
||||||
|
'upload.image_on', 'upload.video_on',
|
||||||
'upload.image_max_per_post', 'upload.image_max_bytes',
|
'upload.image_max_per_post', 'upload.image_max_bytes',
|
||||||
'upload.video_max_per_post', 'upload.video_max_bytes',
|
'upload.video_max_per_post', 'upload.video_max_bytes',
|
||||||
'upload.total_cap_bytes', 'upload.retention_days',
|
'upload.total_cap_bytes', 'upload.retention_days',
|
||||||
|
|||||||
Reference in New Issue
Block a user