'noindex,nofollow']); ?>
请输入管理员用户名与口令(当前口令仍是 config.php 里的明文,登录后请到「站点设置」改成加密存储)。
这是一封来自 ' . htmlspecialchars($site, ENT_QUOTES, 'UTF-8') . ' 的测试邮件。
' . '如果你收到了它,说明 SMTP 配置正确,回复通知可以正常发出。
' . '发送时间:' . date('Y-m-d H:i:s') . '
'; list($ok, $err) = fb_smtp_send($sc, $to, '【' . $site . '】邮件测试', $body); if ($ok) { $msg = '测试邮件已发出,请查收 ' . $to . '(也看一下垃圾箱)。'; $msgType = 'ok'; } else { $msg = '发送失败:' . $err; $msgType = 'error'; } } } elseif (fb_admin_is_taxonomy_act($act)) { list($msg, $msgType) = fb_admin_taxonomy_action($pdo, $act, $_POST); } elseif ($act === 'settings') { $msg = fb_admin_save_settings(); } elseif ($act === 'password') { $old = (string)(isset($_POST['old_password']) ? $_POST['old_password'] : ''); $n1 = (string)(isset($_POST['new_password']) ? $_POST['new_password'] : ''); $n2 = (string)(isset($_POST['new_password2']) ? $_POST['new_password2'] : ''); // 从没设过口令(或仍是出厂占位值)时,跳过"当前口令"校验 —— 否则谁也进不来、又改不了 $firstSet = !fb_admin_password_is_set(); if (!$firstSet && !fb_admin_verify_password($old)) { $msg = '当前口令不正确。'; $msgType = 'error'; } elseif (fb_len($n1) < 8) { $msg = '新口令至少 8 个字符。'; $msgType = 'error'; } elseif ($n1 !== $n2) { $msg = '两次输入的新口令不一致。'; $msgType = 'error'; } else { fb_admin_set_password($n1); fb_mod_log('password', null, $firstSet ? '首次设定登录口令(bcrypt 哈希存库)' : '修改登录口令'); $msg = $firstSet ? '口令已设定,下次登录请使用它。' : '口令已修改,下次登录请使用新口令。'; } } } } /** * 保存站点设置(把表单值写进 settings 表;config.php 仍是出厂默认值) * @return string 提示语 */ function fb_admin_save_settings() { // PHP 会把表单字段名里的「.」替换成「_」,所以两种写法都读一遍 $post = function ($k, $def = null) { if (isset($_POST[$k])) { return $_POST[$k]; } $alt = str_replace('.', '_', $k); if (isset($_POST[$alt])) { return $_POST[$alt]; } return $def; }; /** * 这个字段本次请求到底有没有被提交? * * 🔴 为什么必须有这个:后台有多个 tab(站点设置 / 邮件通知), * 但它们**共用同一个保存处理**(都提交 act=settings)。 * 如果在站点设置页点保存,该表单里根本没有 smtp_* 字段, * `$post('smtp.host', '')` 会取到空串,然后被写进数据库 —— * **把用户之前填好的邮件配置清空**。 * * 同类事故还会发生在 checkbox 上:没提交 = 视为取消勾选, * 于是另一个 tab 保存一次就把通知开关全关掉。 * * 所以写库前一律先问 has():没提交就跳过,绝不写空值。 */ $has = function ($k) { if (array_key_exists($k, $_POST)) { return true; } return array_key_exists(str_replace('.', '_', $k), $_POST); }; // ============ 站点设置组(含站点信息/页脚/附件/OSS)============ // 🔴 整组包在 has() 判断里:邮件页只提交邮件字段,没有 site_*/oss_*/upload_*, // 无条件写入会把它们全清空。反过来也一样。 $isSiteForm = $has('oss_endpoint') || $has('site_name') || $has('attachment_storage') || $has('site_desc'); if ($isSiteForm) { // 站点信息(对外展示,用于说明「本系统为哪个软件提供反馈服务」) // 留空 = 删除该设置项,回落到 config.php 的出厂值,便于恢复默认。 foreach (['site_name', 'site_desc'] as $k) { if (!$has($k)) { continue; } $v = trim((string)$post($k, '')); if ($v === '') { fb_unset_setting($k); } else { fb_set_setting($k, $v); } } // 页脚:是否显示「由 FeedLite 驱动」这句版权,以及链接指向的开源仓库地址 if ($has('footer_show_credit')) { fb_set_setting('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); } $ints = [ // 区间下限统一改 0:0 = 不限制(见 #4)。 // 只有 image_max_side 例外 —— 边长 0 没有意义,它是压缩目标不是限制。 'upload.image_max_per_post' => [0, 1000], 'upload.video_max_per_post' => [0, 1000], 'upload.retention_days' => [0, 36500], 'upload.upload_per_hour' => [0, 100000], 'upload.image_max_side' => [480, 4096], ]; foreach ($ints as $k => $range) { if (!$has($k)) { continue; } $v = (int)$post($k, 0); $v = max($range[0], min($range[1], $v)); fb_set_setting($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')); } // MB → 字节 // 下限改 0 = 不限制(见 #4) $mbs = [ 'upload.image_max_bytes' => [0, 1024], 'upload.video_max_bytes' => [0, 10240], 'upload.total_cap_bytes' => [0, 1048576], ]; foreach ($mbs as $k => $range) { 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)); } $storage = (string)$post('attachment.storage', 'off'); if (!in_array($storage, ['off', 'local', 'oss'], true)) { $storage = 'off'; } fb_set_setting('attachment.storage', $storage); // OSS 模式下 oss.enabled 必须跟着开;其它模式关掉,避免误判 fb_set_setting('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) { if (!$has($k)) { continue; } // 没提交就不动 $v = trim((string)$post($k, '')); // 这两个留空都有"出厂默认"兜底,必须删键而不是存空串: // - oss.prefix 留空会让对象键变成 "/2026/...",默认是 'fb' // - close_mail_subject 留空会让邮件标题变成空字符串, // sprintf('') 出来的主题是空的,必须回落到默认模板 if (($k === 'oss.prefix' || $k === 'moderation.close_mail_subject') && $v === '') { fb_unset_setting($k); continue; } // RAM 角色留空 = 让程序自己去元数据服务探测,不写死一个空值 if ($k === 'oss.instance_role' && $v === '') { list($rOk, $rVal) = fb_oss_detect_instance_role(); if ($rOk) { fb_set_setting($k, (string)$rVal); } continue; // 探测不到就保持原值不动 } fb_set_setting($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', ''))); $sk = trim((string)$post('oss.secret_key', '')); if ($sk !== '') { fb_set_setting('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); } // ← end if ($isSiteForm) /* ---------------- 邮件通知(SMTP) ---------------- * * 🔴🔴 必须整段包在「本页到底有没有提交邮件表单」的判断里。 * * 事故:站点设置页和邮件通知页共用 act=settings。在站点设置页点保存时, * 该表单里没有 smtp_* 字段,$post() 取到空串,被无条件写进数据库 —— * **把用户已经填好的 SMTP 服务器/账号/发件地址全清空了**。 * 用户看到的现象就是「我明明配过邮件,怎么全没了、信也发不出」。 * * 判据用 smtp_enabled:它是邮件表单里的总开关,一定存在。 * 站点设置页没有这个字段 ⇒ 整段跳过,一个键都不动。 */ $isMailForm = $has('smtp_enabled') || $has('smtp.host') || $has('smtp_host'); if ($isMailForm) { $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, ''))); } // 口令留空 = 不修改(避免每次保存都得重填) $sp = (string)(isset($_POST['smtp_pass']) ? $_POST['smtp_pass'] : ''); if (trim($sp) !== '') { fb_set_setting('smtp.pass', $sp); } // 端口留空 = 用默认 465。删键回落,不要存成 0(0 端口根本连不上) $portRaw = trim((string)$post('smtp.port', '')); if ($portRaw === '') { fb_unset_setting('smtp.port'); } else { $smtpPort = (int)$portRaw; if ($smtpPort <= 0 || $smtpPort > 65535) { $smtpPort = 465; } fb_set_setting('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); // 超时留空 = 用默认 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))); } fb_set_setting('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', ''))); } // 只有 host 填了才允许开启,否则开着一个空配置只会每次发信都失败 $wantOn = (bool)$post('smtp.enabled'); $hostNow = trim((string)fb_cfg('smtp.host', '')); fb_set_setting('smtp.enabled', ($wantOn && $hostNow !== '') ? 1 : 0); if ($wantOn && $hostNow === '') { $msg = '邮件通知未开启:请先填写 SMTP 服务器地址。'; $msgType = 'error'; } /* ---------------- 通知开关 ---------------- * 全是 checkbox:未提交会被当成"取消勾选"。 * 所以只有本页确实提交了邮件表单时才写,否则在站点设置页保存一次 * 就会把「有人回复时通知」这些开关全部关掉。 */ 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')); } // 冷却秒数留空 = 用默认 300(config 里的出厂值)。注意 0 是合法值(表示不限制), // 所以要用「空串」而不是「0」来区分"没填"和"填了 0" if ($has('notify_cooldown_seconds')) { $cdRaw = trim((string)$post('notify.cooldown_seconds', '')); if ($cdRaw === '') { fb_unset_setting('notify.cooldown_seconds'); } else { fb_set_setting('notify.cooldown_seconds', max(0, min(86400, (int)$cdRaw))); } } } // ← end if ($isMailForm) // 审核开关属于「站点设置」,只在站点设置页提交时才写 if ($has('moderation_enabled')) { fb_set_setting('moderation.enabled', $post('moderation.enabled') ? 1 : 0); } fb_mod_log('settings', null, '保存站点设置:存储=' . $storage); return '设置已保存。'; } /* ================================================================== * 页面 * ================================================================== */ $tab = (string)(isset($_GET['tab']) ? $_GET['tab'] : 'pending'); if (!in_array($tab, ['pending', 'all', 'thread', 'taxonomy', 'statuses', 'settings', 'mail', 'log'], true)) { $tab = 'pending'; } fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']); ?>登录状态 小时内有效,超时自动退出。
当前没有待审核的反馈。
没有符合条件的问题。
问题不存在。返回列表
—
鉴权方式:
凭证状态:
Bucket:
Endpoint(配置):
Endpoint(实际连接): —
连通性:
↳ 内网 Endpoint 跨地域不可达:请取消勾选上方「使用内网 Endpoint」后保存,或把桶换到与本机同地域。
测一下桶连通性
(会真实访问一次 OSS,跨地域不通时可能等待数秒)
已用附件: / 上限
settings 表(键名 admin.password_hash)
config.php 的 admin_password
尚未设置
是否加密:
是 —— 存的是 password_hash() 生成的 bcrypt 单向哈希($2y$…),
不可逆、无法还原明文;校验用 password_verify()。
否 —— 目前是明文写在 config.php 里。
在下面改一次口令后即会转存为数据库中的 bcrypt 哈希。
—— 还没有可用口令,当前后台无法登录。请用下方表单设定一个。
数据库文件在 data/feedback.db(已由 .htaccess 拒绝外部访问),且只存哈希,明文不落盘。
配置 SMTP 后,反馈有变动会自动发信通知提交人。
还没有操作记录。