v3.2.13 图片选项全部自带默认图,新装站点不再出现空洞/破图

根本原因:kratos_option() 运行时只读数据库,`std` 仅在后台表单预填和
「恢复默认」时生效,前台根本读不到。因此新装站点所有图片项都是空值。
另外 isset() 挡不住「用户清空」——of_sanitize_upload() 对空输入返回 '',
键存在且值为空串,调用方传的兜底值被绕过。

同时不能把绝对 URL 写进 std:它会随保存一起写库,主题目录改名/换 CDN/
迁站后永久 404。本站 s_wechat_url / g_donate_alipay / g_donate_wechat
三项就指向已删除的 kratos-master 目录,一直在破图(这三项是 hidden 字段,
后台界面上看不到,所以坏了很久没人发现)。

做法:
- 新增 inc/theme-option-defaults.php,图片默认值注册表(11 项),
  默认图一律运行时给出,不写数据库。
- yang_option_image() 三层判断:空/纯空白 → 默认图;指向已不存在的主题
  目录 → 默认图;文件不存在则不输出地址(避免换来一个 404)。
- 陈旧判断用 get_template()(主题 slug)而非 basename(目录名),避免本地
  开发目录名与 slug 不一致时误伤当前主题自己的图。
- 模板统一走取值函数:header(logo/favicon/banner)、footer(微信码/国旗)、
  404、index、article 缩略图、widgets 头像、page-toolbar、theme-core 赞赏码。
- 国旗与 favicon 的渲染抽成 yang_flag_block() / yang_favicon_tag(),
  空值时不输出空 src / 空 href。
- 后台 9 处 std 改为 yang_options_default_img(),与前台默认图同源;
  该包装函数自带 function_exists 防护,单独加载 theme-options.php 不会 Fatal。

新增图片(本地生成,合计 291KB):default-404 / default-share /
default-empty / default-avatar / default-banner / default-flag / favicon.ico。
favicon 为 WordPress 风格自绘(品牌蓝圆底 + 白色 W),非站长个人专用那张。

验证:run_tests 146 条(新增第 4 组图片默认值 22+ 条);falsify 35 条注入
全部能被抓住(新增 10 条);verify_all.sh 14 组全绿。
This commit is contained in:
2026-10-23 19:50:07 +08:00
parent 704a9346e1
commit ef9a58dc51
19 changed files with 410 additions and 49 deletions
+51 -12
View File
@@ -20,6 +20,29 @@ function getrobots()
return $robots;
}
/**
* 图片字段的默认值(后台预填用)。
*
* ── 为什么不直接调 yang_image_default() ──────────────────────
* 本文件由 optionsframework_init() 在 init 钩子上 require,
* 而 yang_image_default() 来自 inc/theme-option-defaults.php。
* 正常时序下后者先加载,但**加载顺序被打乱时**(子主题、外部脚本
* 直接 include 本文件、插件在更早的钩子里触发本函数),
* 直接调用就会 Fatal —— 后台整个白屏,比"少一个预填值"严重得多。
*
* 这里先用 function_exists() 探一下,缺了就返回空串:
* 后台退化成没有预填默认值,但页面能打开、能保存、能干活。
* 降级必须降到"还能用",不能降到"打不开"。
*/
if (!function_exists('yang_options_default_img')) {
function yang_options_default_img($id)
{
return function_exists('yang_image_default')
? yang_image_default($id)
: '';
}
}
function kratos_options()
{
$sitename = get_bloginfo('name');
@@ -55,12 +78,24 @@ function kratos_options()
'desc' => __('不选择图片则显示文字标题', 'kratos'),
'id' => 'g_logo',
'type' => 'upload',
/* 有意不设 std:这里空着时显示站点名称文字,比塞一张通用占位
logo 好看。其它图片字段见 inc/theme-option-defaults.php。 */
);
$options[] = array(
'name' => __('Favicon 图片', 'kratos'),
'desc' => __('浏览器收藏夹和地址栏中显示的图标', 'kratos'),
'desc' => __('浏览器收藏夹和地址栏中显示的图标。<b>留空则自动使用主题自带的默认图标</b>,不会出现空白。', 'kratos'),
'id' => 'g_icon',
/* 有意不写 std。
*
* std 里存的是**绝对 URL**,一旦用户保存表单就会被写进数据库。
* 数据库里躺着一个写死的主题目录地址是隐患:主题目录改名、
* 换 CDN、或站点迁移域名之后,那条记录就指向不存在的文件
* (线上 s_wechat_url 指向早已删除的 kratos-master 目录,就是这么坏掉的)。
*
* 所以默认值一律由运行时提供(yang_option_image()),库里保持空。
* 这样默认图地址永远由当前请求现算,改目录也不用改数据。
*/
'type' => 'upload',
);
$options[] = array(
@@ -72,8 +107,9 @@ $options[] = array(
);
$options[] = array(
'name' => __('国旗地址', 'kratos'),
'desc' => __('选择国旗图片地址', 'kratos'),
'desc' => __('选择国旗图片地址。<b>留空则自动使用主题自带的默认国旗</b>。', 'kratos'),
'id' => 'g_flag_url',
/* 不写 std —— 理由同 g_icon:默认值由运行时给,库里不存绝对地址。 */
'type' => 'upload',
);
$options[] = array(
@@ -158,16 +194,19 @@ $options[] = array(
$options[] = array(
'id' => 's_wechat_url',
'std' => ASSET_PATH . '/assets/img/wechat.png',
/* 后台显示的初始值必须与前台实际用的默认图**同一份来源**,
否则用户在后台看到 A 图、前台显示 B 图,一保存又把 A 写进库,
前台跟着退回 A —— 前后台像两个产品。
所以这里统一从 yang_image_defaults() 取,不在两处各写一遍路径。 */
'std' => yang_options_default_img('s_wechat_url'),
'type' => 'upload',
'class' => 'hidden',
'type' => 'upload',
);
$options[] = array(
'name' => __('404 页面图片', 'kratos'),
'id' => 'g_404',
'std' => ASSET_PATH . '/assets/img/404.jpg',
'std' => yang_options_default_img('g_404'),
'type' => 'upload',
);
@@ -180,7 +219,7 @@ $options[] = array(
'name' => __('分享图片', 'kratos'),
'desc' => __('搜索引擎或者社交工具分享首页时抓取的图片', 'kratos'),
'id' => 'seo_shareimg',
'std' => ASSET_PATH . '/assets/img/default.jpg',
'std' => yang_options_default_img('seo_shareimg'),
'type' => 'upload',
);
@@ -314,7 +353,7 @@ $options[] = array(
'desc' => __('当文章中没有图片并且没有设置特色图时在首页显示', 'kratos'),
'id' => 'g_postthumbnail',
'class' => 'hidden',
'std' => ASSET_PATH . '/assets/img/default.jpg',
'std' => yang_options_default_img('g_postthumbnail'),
'type' => 'upload',
);
@@ -322,7 +361,7 @@ $options[] = array(
'name' => __('无内容图片', 'kratos'),
'desc' => __('当搜索不到文章或文章分类中没有文章时显示', 'kratos'),
'id' => 'g_nothing',
'std' => ASSET_PATH . '/assets/img/nothing.svg',
'std' => yang_options_default_img('g_nothing'),
'type' => 'upload',
);
@@ -342,7 +381,7 @@ $options[] = array(
$options[] = array(
'name' => __('支付宝二维码', 'kratos'),
'id' => 'g_donate_alipay',
'std' => ASSET_PATH . '/assets/img/donate.png',
'std' => yang_options_default_img('g_donate_alipay'),
'type' => 'upload',
'class' => 'hidden',
'type' => 'upload',
@@ -351,7 +390,7 @@ $options[] = array(
$options[] = array(
'name' => __('微信二维码', 'kratos'),
'id' => 'g_donate_wechat',
'std' => ASSET_PATH . '/assets/img/donate.png',
'std' => yang_options_default_img('g_donate_wechat'),
'type' => 'upload',
'class' => 'hidden',
'type' => 'upload',
@@ -376,7 +415,7 @@ $options[] = array(
$options[] = array(
'name' => __('个人头像', 'kratos'),
'id' => 'a_gravatar',
'std' => ASSET_PATH . '/assets/img/gravatar.png',
'std' => yang_options_default_img('a_gravatar'),
'type' => 'upload',
);
@@ -477,7 +516,7 @@ $options[] = array(
$options[] = array(
'name' => __('图片导航', 'kratos'),
'id' => 'top_img',
'std' => ASSET_PATH . '/assets/img/background.png',
'std' => yang_options_default_img('top_img'),
'class' => 'hidden',
'type' => 'upload',
);