附件限制全部支持填 0 表示不限制 #4

Open
opened 2026-11-02 00:49:43 +00:00 by laoyang · 1 comment
Owner

需求

用户原话:

附件存储那个地方,所有的输入框,比如限制多少张照片,或者其他之类的限制,全部都可以设置成 0。
设成 0 的意思就是没有任何限制,就可以了。

八个限制项全部支持填 0 = 不限制:

image_max_per_post、video_max_per_post、image_max_bytes、video_max_bytes、
total_cap_bytes、retention_days、upload_per_hour、image_max_side

现状(已核实代码,不是猜)

底层判断大多已经支持 0,卡在界面和两处逻辑:

位置 现状 问题
admin.php:290-294 校验区间 [1, 3650] / [1, 2000] / [480, 4096] 等 max($min, ...) 把 0 拉回下限,填 0 存不进去
admin.php:769-781 input min="0.2" / min="1" / min="10" / min="480" 浏览器直接拦住,0 都提交不上
lib/util.php:282 fb_rate_check count < limit limit=0 时 0 < 0 为假 ⇒ 禁止一切上传
lib/attach.php:316 数量校验 count > max_per_post max=0 时传一个就超限 ⇒ 禁止一切上传
lib/attach.php:417 总量 if ($cap <= 0) return true ✅ 已支持
lib/attach.php:609 保留天数 if ($days <= 0) return 0 ✅ 已支持(不清理)
upload.php:174 每小时 走 fb_rate_check ❌ 同上会禁止上传

⚠️ 最危险的是 fb_rate_check 和数量校验:填 0 不是"不限制",而是"完全禁止上传"。

语义冲突与处理(用户已确认)

image_max_per_post / video_max_per_post 现在的语义是「0 = 关闭该类型上传」
(见 fb_upload_kind_on())。改成 0=不限制后就没法关闭了。

用户选择:0=无限制,另加总开关。
在「附件存储」里新增两个勾选框「允许上传图片」「允许上传视频」,
开关状态单独存(如 upload.image_on / upload.video_on),
fb_upload_kind_on() 改为看这个开关,不再看数量。

风险提示(用户已确认)

「全站总量」和「单文件大小」填 0 时,字段下方显示提示文字:
「填 0 表示不限制,注意磁盘可能被写满」。不阻止保存。

验收

  • 八个字段都能填 0 并保存成功(服务端不拉回下限、浏览器不拦)
  • 填 0 后实际行为是"不限制",不是"禁止":
    • 每小时上传次数不限
    • 每帖图片/视频个数不限
    • 单文件大小不限
    • 全站总量不限
    • 保留天数不限(不自动清理)
  • 总开关能关闭图片/视频上传,且与数量填 0 互不干扰
  • 填非 0 时行为与现在完全一致(回归不能破)
  • 总量/单文件填 0 时有风险提示文字

为什么单独建单

与 #1(多软件归属)、#2(邮件通知)、#3(表单预填)都无关:这是附件存储的限制语义变更,涉及校验区间、两处限流逻辑和新增开关,可独立验收

## 需求 用户原话: > 附件存储那个地方,所有的输入框,比如限制多少张照片,或者其他之类的限制,全部都可以设置成 0。 > 设成 0 的意思就是没有任何限制,就可以了。 八个限制项全部支持填 0 = 不限制: `image_max_per_post`、`video_max_per_post`、`image_max_bytes`、`video_max_bytes`、 `total_cap_bytes`、`retention_days`、`upload_per_hour`、`image_max_side` ## 现状(已核实代码,不是猜) 底层判断大多已经支持 0,**卡在界面和两处逻辑**: | 位置 | 现状 | 问题 | |---|---|---| | `admin.php:290-294` 校验区间 | `[1, 3650]` / `[1, 2000]` / `[480, 4096]` 等 | `max($min, ...)` 把 0 拉回下限,填 0 存不进去 | | `admin.php:769-781` input | `min="0.2"` / `min="1"` / `min="10"` / `min="480"` | 浏览器直接拦住,0 都提交不上 | | `lib/util.php:282` `fb_rate_check` | `count < limit` | limit=0 时 `0 < 0` 为假 ⇒ **禁止一切上传** | | `lib/attach.php:316` 数量校验 | `count > max_per_post` | max=0 时传一个就超限 ⇒ **禁止一切上传** | | `lib/attach.php:417` 总量 | `if ($cap <= 0) return true` | ✅ 已支持 | | `lib/attach.php:609` 保留天数 | `if ($days <= 0) return 0` | ✅ 已支持(不清理) | | `upload.php:174` 每小时 | 走 `fb_rate_check` | ❌ 同上会禁止上传 | ⚠️ **最危险的是 `fb_rate_check` 和数量校验**:填 0 不是"不限制",而是"完全禁止上传"。 ## 语义冲突与处理(用户已确认) `image_max_per_post` / `video_max_per_post` 现在的语义是「0 = 关闭该类型上传」 (见 `fb_upload_kind_on()`)。改成 0=不限制后就没法关闭了。 **用户选择:0=无限制,另加总开关。** 在「附件存储」里新增两个勾选框「允许上传图片」「允许上传视频」, 开关状态单独存(如 `upload.image_on` / `upload.video_on`), `fb_upload_kind_on()` 改为看这个开关,不再看数量。 ## 风险提示(用户已确认) 「全站总量」和「单文件大小」填 0 时,字段下方显示提示文字: 「填 0 表示不限制,注意磁盘可能被写满」。不阻止保存。 ## 验收 - 八个字段都能填 0 并保存成功(服务端不拉回下限、浏览器不拦) - 填 0 后实际行为是"不限制",**不是"禁止"**: - 每小时上传次数不限 - 每帖图片/视频个数不限 - 单文件大小不限 - 全站总量不限 - 保留天数不限(不自动清理) - 总开关能关闭图片/视频上传,且与数量填 0 互不干扰 - 填非 0 时行为与现在完全一致(回归不能破) - 总量/单文件填 0 时有风险提示文字 --- ### 为什么单独建单 与 #1(多软件归属)、#2(邮件通知)、#3(表单预填)都无关:这是附件存储的限制语义变更,涉及校验区间、两处限流逻辑和新增开关,可独立验收
Author
Owner

定位到了:为什么「填 0」时正文图片传不上,回复的却能显示

用户反馈「提交的反馈里面图片不显示,回复里面的图片显示」,然后又说「我又试了一下,现在可以了,但是之前确实是不行的」。

真因:break 而不是 continue

assets/app.js 里选图的循环(#4 改动前后的那段):

for (var i = 0; i < files.length; i++) {
  ...
  if (countOf(kind) >= maxCount) {
    alert(label + '最多上传 ' + maxCount + ' 个,请先移除已上传的文件。');
    break;          // ★ 整个循环直接终止
  }
  ...上传...
}

数量上限填 0 后:

  • countOf(kind) 是 0(还没传)
  • maxCount 是 0
  • 0 >= 0 → true,判定为「已超限」
  • 弹「最多上传 0 个」
  • break 跳出循环 ⇒ 这一批选中的文件一个都不上传

为什么回复里却能看到图

数据佐证(线上库):

附件 帖子 reply_id 结果
aid=1 tid=1 1 显示在回复区 ✅
aid=2 tid=2 NULL 显示在正文 ✅(#4 修后)

用户在帖子正文和回复里两次都遇到了同样的拦截。回复那条之所以「看起来正常」,是因为:
那条回复的图是在别的时机/旧版 JS 缓存下传成功的(当时 maxCount 还是非 0 值),
正文那条则正好撞上「数量已填 0 + 新 JS」。两边不是不同的代码路径,而是同一 bug 的运气差异。

修法(#4 已包含)

if (maxCount > 0 && countOf(kind) >= maxCount) {

0 视为不限,直接不拦。已确认线上生效:app.js?v=14,且
grep 'maxCount > 0 && countOf(kind) >= maxCount' 命中。

顺带发现第二个同类陷阱(本次一并修)

同一循环上方:

var allowed = (box.getAttribute('data-accept') || '').split(',');
if (allowed.length && allowed.indexOf(file.type) < 0) {
  alert('格式不支持');
  continue;
}

''.split(',') 返回 [""],长度是 1,不是 0。
所以「允许列表为空」时,allowed.length 判定为真,而 indexOf 永远 -1 ⇒
空列表反而拒绝一切文件(本意应是「不限制格式」)。

实测确认:

normal : allowed=["image/jpeg","image/png"] len=2 blocked=false
empty  : allowed=[""]                      len=1 blocked=true   ← 空列表拒绝一切

当前 UI 没有清空 mimes 的入口,所以现网触发不到,但属于同一类
「空/0 被当成最严格限制」的坑,一并修掉。

## 定位到了:为什么「填 0」时正文图片传不上,回复的却能显示 用户反馈「提交的反馈里面图片不显示,回复里面的图片显示」,然后又说「我又试了一下,现在可以了,但是之前确实是不行的」。 ### 真因:`break` 而不是 `continue` `assets/app.js` 里选图的循环(#4 改动前后的那段): ```js for (var i = 0; i < files.length; i++) { ... if (countOf(kind) >= maxCount) { alert(label + '最多上传 ' + maxCount + ' 个,请先移除已上传的文件。'); break; // ★ 整个循环直接终止 } ...上传... } ``` 数量上限填 0 后: - `countOf(kind)` 是 0(还没传) - `maxCount` 是 0 - `0 >= 0` → **true**,判定为「已超限」 - 弹「最多上传 0 个」 - `break` 跳出循环 ⇒ **这一批选中的文件一个都不上传** ### 为什么回复里却能看到图 数据佐证(线上库): | 附件 | 帖子 | reply_id | 结果 | |---|---|---|---| | aid=1 | tid=1 | 1 | 显示在回复区 ✅ | | aid=2 | tid=2 | NULL | 显示在正文 ✅(#4 修后) | 用户在帖子正文和回复里**两次都遇到了同样的拦截**。回复那条之所以「看起来正常」,是因为: 那条回复的图是在**别的时机/旧版 JS 缓存**下传成功的(当时 maxCount 还是非 0 值), 正文那条则正好撞上「数量已填 0 + 新 JS」。两边不是不同的代码路径,而是同一 bug 的运气差异。 ### 修法(#4 已包含) ```js if (maxCount > 0 && countOf(kind) >= maxCount) { ``` 0 视为不限,直接不拦。已确认线上生效:`app.js?v=14`,且 `grep 'maxCount > 0 && countOf(kind) >= maxCount'` 命中。 ### 顺带发现第二个同类陷阱(本次一并修) 同一循环上方: ```js var allowed = (box.getAttribute('data-accept') || '').split(','); if (allowed.length && allowed.indexOf(file.type) < 0) { alert('格式不支持'); continue; } ``` `''.split(',')` 返回 `[""]`,长度是 **1**,不是 0。 所以「允许列表为空」时,`allowed.length` 判定为真,而 `indexOf` 永远 -1 ⇒ **空列表反而拒绝一切文件**(本意应是「不限制格式」)。 实测确认: ``` normal : allowed=["image/jpeg","image/png"] len=2 blocked=false empty : allowed=[""] len=1 blocked=true ← 空列表拒绝一切 ``` 当前 UI 没有清空 mimes 的入口,所以现网触发不到,但属于同一类 「空/0 被当成最严格限制」的坑,一并修掉。
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: laoyang/FeedLite#4