diff --git a/admin.php b/admin.php index 39be23c..0477bdd 100644 --- a/admin.php +++ b/admin.php @@ -366,6 +366,34 @@ function fb_admin_save_settings() { return array_key_exists(str_replace('.', '_', $k), $_POST); }; + /** + * 🔴🔴 受守卫的写库:**字段没提交就一个字都不写**。 + * + * 这是本文件唯一允许写设置的入口(新增设置项也必须走它)。 + * 为什么要把这件事做成强制: + * 这个 bug 已经犯过三次 —— 每次都是「某个 tab 保存时, + * 另一个 tab 的字段没提交,被当成空值写回去」。 + * 根因不是某个字段忘了判断,而是**写入入口本身没有约束**: + * 32 处 fb_set_setting 只有 8 处手写了 has(),漏一处就是一次事故。 + * + * 用法: + * $save('oss.bucket', trim((string)$post('oss.bucket', ''))); + * $save('oss.enabled', $storage === 'oss' ? 1 : 0); + * 字段没提交 → 直接 return false,数据库不动。 + * + * ⚠️ 少数场景确实要在「字段没提交」时也写(例如用户主动清空某项), + * 那时用 $saveForce(),但必须在注释里写清楚为什么。 + */ + $save = function ($k, $v) use ($has) { + if (!$has($k)) { return false; } // ← 铁律:没提交就不动 + return fb_set_setting($k, $v); + }; + + /** 显式强制写入(字段没提交也写),仅在「清空某项」这类确有必要时用 */ + $saveForce = function ($k, $v) { + return fb_set_setting($k, $v); + }; + // ============ 站点设置组(含站点信息/页脚/附件/OSS)============ // 🔴 整组包在 has() 判断里:邮件页只提交邮件字段,没有 site_*/oss_*/upload_*, // 无条件写入会把它们全清空。反过来也一样。 @@ -379,18 +407,18 @@ function fb_admin_save_settings() { if (!$has($k)) { continue; } $v = trim((string)$post($k, '')); if ($v === '') { fb_unset_setting($k); } - else { fb_set_setting($k, $v); } + else { $save($k, $v); } } // 页脚:是否显示「由 FeedLite 驱动」这句版权,以及链接指向的开源仓库地址 - if ($has('footer_show_credit')) { fb_set_setting('footer.show_credit', $post('footer_show_credit') ? 1 : 0); } + $save('footer.show_credit', $post('footer_show_credit') ? 1 : 0); $curl = trim((string)$post('footer_credit_url', '')); // 只接受 http/https:这条会被直接渲染成 ,不能让 javascript:/data: 混进来 if ($curl !== '' && !preg_match('~^https?://~i', $curl)) { return '页脚链接地址需以 http:// 或 https:// 开头。'; } if ($curl === '' || $curl === fb_credit_url_default()) { fb_unset_setting('footer.credit_url'); } - else { fb_set_setting('footer.credit_url', $curl); } + else { $save('footer.credit_url', $curl); } $ints = [ // 区间下限统一改 0:0 = 不限制(见 #4)。 @@ -405,11 +433,11 @@ function fb_admin_save_settings() { if (!$has($k)) { continue; } $v = (int)$post($k, 0); $v = max($range[0], min($range[1], $v)); - fb_set_setting($k, $v); + $save($k, $v); } // 图片/视频上传总开关(0=不限个数后,开关必须独立存在) - if ($has('upload_image_on')) { fb_set_setting('upload.image_on', (int)(bool)$post('upload.image_on')); } - if ($has('upload_video_on')) { fb_set_setting('upload.video_on', (int)(bool)$post('upload.video_on')); } + $save('upload.image_on', (int)(bool)$post('upload.image_on')); + $save('upload.video_on', (int)(bool)$post('upload.video_on')); // MB → 字节 // 下限改 0 = 不限制(见 #4) @@ -422,14 +450,14 @@ function fb_admin_save_settings() { if (!$has($k)) { continue; } // 没提交就不动(防跨 tab 清空) $mb = (float)$post($k, 0); $mb = max($range[0], min($range[1], $mb)); - fb_set_setting($k, (int)round($mb * 1048576)); + $save($k, (int)round($mb * 1048576)); } $storage = (string)$post('attachment.storage', 'off'); if (!in_array($storage, ['off', 'local', 'oss'], true)) { $storage = 'off'; } - fb_set_setting('attachment.storage', $storage); + $save('attachment.storage', $storage); // OSS 模式下 oss.enabled 必须跟着开;其它模式关掉,避免误判 - fb_set_setting('oss.enabled', $storage === 'oss' ? 1 : 0); + $save('oss.enabled', $storage === 'oss' ? 1 : 0); $texts = ['oss.endpoint', 'oss.bucket', 'oss.instance_role', 'oss.domain', 'oss.prefix', 'moderation.close_mail_subject']; foreach ($texts as $k) { @@ -446,23 +474,23 @@ function fb_admin_save_settings() { // RAM 角色留空 = 让程序自己去元数据服务探测,不写死一个空值 if ($k === 'oss.instance_role' && $v === '') { list($rOk, $rVal) = fb_oss_detect_instance_role(); - if ($rOk) { fb_set_setting($k, (string)$rVal); } + if ($rOk) { $save($k, (string)$rVal); } continue; // 探测不到就保持原值不动 } - fb_set_setting($k, $v); + $save($k, $v); } $src = (string)$post('oss.credential_source', 'instance_role'); - fb_set_setting('oss.credential_source', $src === 'static' ? 'static' : 'instance_role'); - fb_set_setting('oss.access_key', trim((string)$post('oss.access_key', ''))); + $save('oss.credential_source', $src === 'static' ? 'static' : 'instance_role'); + $save('oss.access_key', trim((string)$post('oss.access_key', ''))); $sk = trim((string)$post('oss.secret_key', '')); - if ($sk !== '') { fb_set_setting('oss.secret_key', $sk); } + if ($sk !== '') { $save('oss.secret_key', $sk); } - fb_set_setting('oss.visibility', ((string)$post('oss.visibility', 'public')) === 'private' ? 'private' : 'public'); - fb_set_setting('oss.url_scheme', ((string)$post('oss.url_scheme', 'https')) === 'http' ? 'http' : 'https'); - fb_set_setting('oss.verify', $post('oss.verify') ? 1 : 0); - fb_set_setting('oss.image_process', $post('oss.image_process') ? 1 : 0); - fb_set_setting('oss.use_internal_endpoint', $post('oss.use_internal_endpoint') ? 1 : 0); + $save('oss.visibility', ((string)$post('oss.visibility', 'public')) === 'private' ? 'private' : 'public'); + $save('oss.url_scheme', ((string)$post('oss.url_scheme', 'https')) === 'http' ? 'http' : 'https'); + $save('oss.verify', $post('oss.verify') ? 1 : 0); + $save('oss.image_process', $post('oss.image_process') ? 1 : 0); + $save('oss.use_internal_endpoint', $post('oss.use_internal_endpoint') ? 1 : 0); } // ← end if ($isSiteForm) @@ -483,11 +511,11 @@ function fb_admin_save_settings() { $smtpText = ['smtp.host', 'smtp.user', 'smtp.from', 'smtp.from_name']; foreach ($smtpText as $k) { if (!$has($k)) { continue; } // 没提交就不动它 - fb_set_setting($k, trim((string)$post($k, ''))); + $save($k, trim((string)$post($k, ''))); } // 口令留空 = 不修改(避免每次保存都得重填) $sp = (string)(isset($_POST['smtp_pass']) ? $_POST['smtp_pass'] : ''); - if (trim($sp) !== '') { fb_set_setting('smtp.pass', $sp); } + if (trim($sp) !== '') { $save('smtp.pass', $sp); } // 端口留空 = 用默认 465。删键回落,不要存成 0(0 端口根本连不上) $portRaw = trim((string)$post('smtp.port', '')); @@ -496,31 +524,31 @@ function fb_admin_save_settings() { } else { $smtpPort = (int)$portRaw; if ($smtpPort <= 0 || $smtpPort > 65535) { $smtpPort = 465; } - fb_set_setting('smtp.port', $smtpPort); + $save('smtp.port', $smtpPort); } $sec = strtolower(trim((string)$post('smtp.secure', 'ssl'))); if (!in_array($sec, ['ssl', 'tls', ''], true)) { $sec = 'ssl'; } - fb_set_setting('smtp.secure', $sec); + $save('smtp.secure', $sec); // 超时留空 = 用默认 15 $tmoRaw = trim((string)$post('smtp.timeout', '')); if ($tmoRaw === '') { fb_unset_setting('smtp.timeout'); } else { - fb_set_setting('smtp.timeout', max(5, min(60, (int)$tmoRaw))); + $save('smtp.timeout', max(5, min(60, (int)$tmoRaw))); } - fb_set_setting('smtp.verify_ssl', (int)(bool)$post('smtp.verify_ssl')); + $save('smtp.verify_ssl', (int)(bool)$post('smtp.verify_ssl')); // 没填 from 时用 user 兜底(绝大多数 SMTP 要求两者一致) - if (trim((string)fb_cfg('smtp.from', '')) === '') { - fb_set_setting('smtp.from', trim((string)fb_cfg('smtp.user', ''))); + if ($has('smtp.from') && trim((string)$post('smtp.from', '')) === '') { + $save('smtp.from', trim((string)fb_cfg('smtp.user', ''))); } // 只有 host 填了才允许开启,否则开着一个空配置只会每次发信都失败 $wantOn = (bool)$post('smtp.enabled'); $hostNow = trim((string)fb_cfg('smtp.host', '')); - fb_set_setting('smtp.enabled', ($wantOn && $hostNow !== '') ? 1 : 0); + $save('smtp.enabled', ($wantOn && $hostNow !== '') ? 1 : 0); if ($wantOn && $hostNow === '') { $msg = '邮件通知未开启:请先填写 SMTP 服务器地址。'; $msgType = 'error'; @@ -531,9 +559,9 @@ function fb_admin_save_settings() { * 所以只有本页确实提交了邮件表单时才写,否则在站点设置页保存一次 * 就会把「有人回复时通知」这些开关全部关掉。 */ - if ($has('notify_enabled')) { fb_set_setting('notify.enabled', (int)(bool)$post('notify.enabled')); } - if ($has('notify_on_every_reply')) { fb_set_setting('notify.on_every_reply', (int)(bool)$post('notify.on_every_reply')); } - if ($has('notify_admin_new_thread')) { fb_set_setting('notify.admin_new_thread', (int)(bool)$post('notify.admin_new_thread')); } + $save('notify.enabled', (int)(bool)$post('notify.enabled')); + $save('notify.on_every_reply', (int)(bool)$post('notify.on_every_reply')); + $save('notify.admin_new_thread', (int)(bool)$post('notify.admin_new_thread')); // 冷却秒数留空 = 用默认 300(config 里的出厂值)。注意 0 是合法值(表示不限制), // 所以要用「空串」而不是「0」来区分"没填"和"填了 0" @@ -542,7 +570,7 @@ function fb_admin_save_settings() { if ($cdRaw === '') { fb_unset_setting('notify.cooldown_seconds'); } else { - fb_set_setting('notify.cooldown_seconds', max(0, min(86400, (int)$cdRaw))); + $save('notify.cooldown_seconds', max(0, min(86400, (int)$cdRaw))); } } @@ -550,7 +578,7 @@ function fb_admin_save_settings() { // 审核开关属于「站点设置」,只在站点设置页提交时才写 if ($has('moderation_enabled')) { - fb_set_setting('moderation.enabled', $post('moderation.enabled') ? 1 : 0); + $save('moderation.enabled', $post('moderation.enabled') ? 1 : 0); } fb_mod_log('settings', null, '保存站点设置:存储=' . $storage);