AI摘要

Typecho后台主题配置保存时空值被还原。根因是Request::get()将空字符串误判为null,导致数据库存储JSON null,下次加载时isset失效回退默认值。修复方案是在Form::getParams()中,对已提交但值为null的参数强制设为空字符串。

Typecho后台主题配置保存时空值被还原的Bug分析与修复

问题描述

Typecho 后台「外观设置」页面中,当用户清空某个已有默认值的输入框(如"首页名称"、"博主名称"等),点击保存后,该字段会自动恢复为 themeConfig() 中定义的硬编码默认值。用户预期的是空值被持久化,但实际上每次保存后都回退到默认值。

根因定位

数据链路

用户提交表单到最终渲染的完整链路:

用户清空文本框 → HTTP POST (value = "")
    → Typecho\Request::get() 
        → Typecho\Widget\Helper\Form::getParams()
            → Typecho\Widget\Helper\Form::getAllRequest()
                → Widget\Themes\Edit::config()
                    → json_encode() → 数据库
                        → 下次请求读取 → Typecho\Widget\Themes\Config::config()
                            → isset($options->field) → 表单渲染

断点一:Typecho\Request::get()

问题出现在 var/Typecho/Request.phpget() 方法(约第 181 行):

public function get(string $key, $default = null, ?bool &$exists = true)
{
    // … 从 params / sandbox / $_GET / $_POST 取值 …

    if (isset($value) && $value !== '') {   // ← 问题在这里
        $exists = true;
        return $value;
    } else {
        $exists = false;
        return $default;   // 返回 null
    }
}

当用户在表单中清空输入框并提交时,$_POST['field'] 的值为空字符串 ""

  • isset($_POST[$key])true,进入对应分支,$value = ""
  • 到达条件判断:isset("")true,但 "" !== ""false
  • 整个条件失败,进入 else 分支,返回 $default(即 null
  • 引用参数 $exists 被设为 false

设计意图很明确:作者希望过滤掉"空参数",将其视为未提交。这在某些场景下是合理的(如 URL 路由中丢弃空参数),但对于表单提交而言,空字符串是一个有意义的合法值,不应被抹除。

断点二:Form::getParams()

var/Typecho/Widget/Helper/Form.phpgetParams() 方法负责收集表单元素的提交值:

public function getParams(array $params): array
{
    $result = [];
    $request = Request::getInstance();
    foreach ($params as $param) {
        $result[$param] = $request->get($param, 
            is_array($this->getInput($param)->value) ? [] : null);
    }
    return $result;
}

默认值为 null,当 Request::get() 遇上空字符串返回 null 时,这里没有任何补救逻辑——直接接受 null

连锁反应

null 沿数据链路传播,形成一连串"误判":

步骤位置输入输出问题
1Request::get()$_POST["field"] = ""null空串被当作未提交
2Form::getParams()nullnull无补救逻辑
3Form::getAllRequest()["field" => null]["field" => null]
4Edit::config()nulljson_encode 写入 {"field":null}DB 存了 JSON null
5下次加载,Config::config()DB 读出nullisset($options->field)false跳过赋值
6表单渲染未赋值保持themeConfig() 中的默认值用户看到还原

关键在第 4 步:json_encode(["field" => null]) 输出 {"field":null},而非 {"field":""}。这直接导致第 5 步 isset() 失效——PHP 的 isset()null 返回 false,Typecho 据此判断"用户没有保存过该值",于是跳过赋值,表单元素保留了 themeConfig() 中设定的默认值。

修复方案

错误方案:全局修改 Request::get()

// ❌ 切勿这样改
if (isset($value)) {  // 去掉 $value !== ''

Request::get() 是全局方法,被路由解析、URL 参数判断等众多子系统调用。部分子系统依赖"空参数视为未提交"的语义来正常工作。移除该检查会导致 URL 路由匹配异常,表现为文章发布、页面管理等接口返回 404。

正确方案:在表单取值层兜底

修复位置:var/Typecho/Widget/Helper/Form.phpgetParams() 方法。

public function getParams(array $params): array
{
    $result = [];
    $request = Request::getInstance();

    foreach ($params as $param) {
        $value = $request->get($param, 
            is_array($this->getInput($param)->value) ? [] : null);

        // 修复:参数已提交但值为空时,Request::get() 会返回 null,
        // 导致空字符串被当作"未提交",进而 json_encode 为 null 写入数据库,
        // 下次加载时 isset 判断为 false,退回到 themeConfig 默认值
        if ($value === null && (isset($_POST[$param]) || isset($_GET[$param]))) {
            $value = '';
        }

        $result[$param] = $value;
    }

    return $result;
}

修复后三种场景均能正确区分:

场景$_POSTRequest::get()修复前修复后
字段未提交无此 keynullnullnull(保持)
字段提交空值""null(误判)null""
字段提交正常值"xxx""xxx""xxx""xxx"

为什么选择这个位置

  1. 影响范围最小 — 仅在表单上下文纠正,不影响路由、URL 参数判断等其他子系统。
  2. 语义正确Form::getParams() 的职责就是获取表单提交值,空字符串应被视作合法的表单值。
  3. 兼容性好 — 不破坏现有验证逻辑。字段没有 required 规则时,空字符串通过验证;有 required 规则时,空字符串被验证器拦截并提示错误,行为符合预期。

结语

这个 Bug 的表现极具迷惑性——"清空字段 → 保存 → 又变回默认"看起来像是某个前端脚本或浏览器自动填充在作祟,实则是一条隐蔽的数据链路:"" → null → JSON null → isset 失效 → 默认值

投币支持一下吧