Files
wordpress-kratos-yang/inc/theme-option-defaults.php
T
laoyang ef9a58dc51 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 组全绿。
2026-10-23 19:50:07 +08:00

307 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 图片类选项的默认值
*
* ── 为什么需要这个文件 ───────────────────────────────────────────────
* 主题设置里有一批图片字段(Logo、favicon、国旗、404 配图、分享图、
* 默认特色图、无内容图、头像、背景图、二维码…)。新装一个站点时它们
* 全是空的,页面上就会出现:`<link href="">` 的空 favicon、
* `<img src="">` 的破图、一片空白的 404 配图。
*
* ── 原来的 std 为什么不够 ───────────────────────────────────────────
* 选项数组里的 `std` 只在两处起作用:
* ① 后台表单首次打开时的预填值;
* ② 点「恢复默认」按钮时写进库的那批值。
* 而运行时的 kratos_option() **完全不看 std** —— 它只查库,查不到才用
* 调用方传进来的第二个参数。所以:
* · 新装站点(库里没有该键)→ 模板若没写第二参数,就是空;
* · 用户保存过表单但把那格删空了 → 键存在且值是 '',
* kratos_option() 用 isset() 判断,'' 也算"已设置",
* 连调用方传的默认值都会被绕过。
* 两个口子合起来,才导致"明明填了 std 却还是空的"。
*
* ── 本文件的职责 ───────────────────────────────────────────────────
* 用一张集中的「字段 → 默认图」注册表 + 一个访问函数,把上面两个口子
* 一起堵住:
* · 值是空字符串 / 只有空白 → 用默认图;
* · 默认图本身若不存在(有人删了包内文件)→ 返回空,绝不输出假路径。
* 图片文件都在 assets/img/ 下,体积已按用途压到合计约 300KB。
*
* ── 有意不放默认图的字段 ───────────────────────────────────────────
* `g_logo`(图片 Logo)不做默认图。它空着时模板回落到站点名称文字,
* 那比给所有站点塞同一张通用 logo 图好看得多 —— 一张共用的占位 logo
* 会让每个站看起来都一样,反而更难看。
*
* @author Yongquan Yang <yangyq.net>
* @license MIT License
*/
if (!function_exists('yang_image_defaults')):
/**
* 字段 => 主题包内的相对路径
*
* 只登记**确实需要**默认图的字段。新增字段时加一行即可,
* 模板里用 yang_option_image('xxx') 读取。
*/
function yang_image_defaults()
{
return array(
// 站点图标。不用站点所有者自己的那枚(那是他专属的),
// 用一枚中性的 WordPress 风格图标(品牌蓝圆 + 白 W)。
'g_icon' => '/assets/img/favicon.ico',
// 页脚国旗。页面上固定 width:50px,100px 宽足够 2x 屏。
'g_flag_url' => '/assets/img/default-flag.png',
// 404 配图。整块背景,1000px 宽够用。
'g_404' => '/assets/img/default-404.jpg',
// 分享图与默认特色图:同一类用途(社交卡片 / 列表缩略),
// 共用一张,避免包里放两份几乎一样的图。
'seo_shareimg' => '/assets/img/default-share.jpg',
'g_postthumbnail' => '/assets/img/default-share.jpg',
// 搜不到内容时的插画。
'g_nothing' => '/assets/img/default-empty.jpg',
// 站长头像。
'a_gravatar' => '/assets/img/default-avatar.jpg',
// 顶部图片导航的背景。CSS 是 height:300px + cover。
'top_img' => '/assets/img/default-banner.jpg',
// 下面三个主题包里本来就有图,只是这里显式登记,
// 让"所有图片字段都有默认"这件事可被一处查全。
's_wechat_url' => '/assets/img/wechat.png',
'g_donate_alipay' => '/assets/img/donate.png',
'g_donate_wechat' => '/assets/img/donate.png',
);
}
endif;
if (!function_exists('yang_image_default')):
/**
* 取某个字段的默认图 URL;没有登记或文件不存在则返回 ''。
*
* 为什么要探文件存在:包被精简、或有人手工删了图片时,
* 返回一个 404 的地址比返回空更糟 —— 页面能打开但满屏破图。
* 探存在略有一点 IO 开销,但 WP 对象缓存 + 文件系统缓存下可忽略,
* 且本函数只在拼 HTML 时调用,不在循环里大批量跑。
*/
function yang_image_default($id)
{
$map = yang_image_defaults();
if (!isset($map[$id])) {
return '';
}
$rel = $map[$id];
// ASSET_PATH 由 theme-core.php 定义(可能是 CDN 前缀)。
// 它没定义时退回主题目录,避免在极早期调用时炸掉。
$base = defined('ASSET_PATH') ? ASSET_PATH : get_template_directory_uri();
// 文件存在性只能查本地目录;CDN 模式下本地也有一份,照查不误。
if (!file_exists(get_template_directory() . $rel)) {
return '';
}
return $base . $rel;
}
endif;
/* ── 给后台选项数组用的安全包装 ─────────────────────────────
*
* inc/theme-options.php 只在 admin_init 之后才被 require,正常时序下
* yang_image_default() 一定已存在。但那个文件在某些自定义场景
* (别处的脚本直接 include 它、或子主题换了加载顺序)可能先被读到,
* 那时调用未定义函数会 **Fatal**,整个后台白屏。
*
* 这个包装在函数缺失时返回空串,让后台退化成"没有预填默认值",
* 而不是白屏 —— 降级要降得能继续用,不能降成打不开。
*/
if (!function_exists('yang_options_default_img')):
function yang_options_default_img($id)
{
return function_exists('yang_image_default')
? yang_image_default($id)
: '';
}
endif;
if (!function_exists('yang_image_url_is_stale')):
/**
* 判断库里存的图片地址是不是"指向已不存在的主题目录"的陈旧地址。
*
* 为什么需要这个:
* options 表里可能躺着 `https://站点/wp-content/themes/<旧目录名>/assets/img/x.png`
* 这样的绝对地址(曾经的 std 被保存进库,或用户手动填过)。主题目录
* 一旦改名(kratos-master → kratos_yang),这些地址全部 404,页面上
* 就是破图。它们是"非空值",会绕过一切"空则用默认"的兜底,
* 所以必须单独识别。
*
* 判据刻意保守:只有当路径里出现 /wp-content/themes/<某目录>/ 且
* 那个目录**不是**当前主题目录时,才认定陈旧。用户把图传到
* uploads 目录、或填外链,都不受影响。
*/
function yang_image_url_is_stale($url)
{
if (!is_string($url) || $url === '') {
return false;
}
// 只关心主题目录下的资源;uploads / 外链一律不碰
if (strpos($url, '/wp-content/themes/') === false) {
return false;
}
/* 当前主题的目录名(slug)。
*
* 用 get_template() 而不是 basename(get_stylesheet_directory()):
* 后者取的是**文件系统目录名**,而 URL 里的那段是**主题 slug**。
* 正常安装时两者相同,但本地开发(仓库目录叫 wordpress-kratos-yang)
* 或符号链接部署时就会不一致,导致把"当前主题自己的地址"
* 误判成陈旧地址 ⇒ 用户明明填对了图却被换成默认图。
* get_template() 返回的就是 WP 认定的主题 slug,与 URL 同源。 */
$current = function_exists('get_template') ? get_template() : '';
if ($current === '') {
$current = basename(get_stylesheet_directory());
}
// 取出 /wp-content/themes/ 之后的第一段
$tail = substr($url, strpos($url, '/wp-content/themes/') + strlen('/wp-content/themes/'));
$slash = strpos($tail, '/');
$dir = $slash === false ? $tail : substr($tail, 0, $slash);
$dir = trim($dir);
if ($dir === '' || $dir === $current) {
return false;
}
return true;
}
endif;
if (!function_exists('yang_option_image')):
/**
* 读一个图片类选项,空值自动落到默认图。
*
* 这是模板里应该用的函数,取代直接调 kratos_option() ——
* 后者对空字符串不做兜底。
*
* @param string $id 选项 id
* @return string 图片 URL,可能为空(表示"确实没有可用图")
*/
function yang_option_image($id)
{
$val = kratos_option($id, '');
if (is_string($val)) {
$val = trim($val);
}
// 空值 → 用默认图
if ($val === '' || $val === false || $val === null) {
return yang_image_default($id);
}
/* 非空但指向已删除的主题目录(例如早先被写进库的
* .../themes/kratos-master/...)→ 同样落到默认图。
* 这种值不识别的话会一直是破图,而且比"空着"更难发现:
* 后台看着是填了图的。 */
if (yang_image_url_is_stale($val)) {
return yang_image_default($id);
}
return $val;
}
endif;
if (!function_exists('yang_flag_block')):
/**
* 页脚国旗那一块 HTML。
*
* ── 为什么抽成函数而不是写在 footer.php 里 ──────────────────
* 原来这段是内联的 if/else。内联写法有个测试上的致命问题:
* 断言只能"照着这段逻辑再抄一遍"来测,于是测的是抄件、
* 不是模板本身 —— 模板里真改坏了,测试照样全绿(自己测自己)。
* 抽成函数后,测试直接调它,调的就是线上跑的那份代码。
*
* ── 三种情况 ────────────────────────────────────────────
* 1. 没勾「是否添加国旗」 → 输出空(用户就是不想显示)
* 2. 勾了、但拿不到任何图片地址 → 输出空(**绝不输出 <img src="">**)
* 3. 勾了、有地址 → 输出国旗
*
* 第 2 种是老代码漏掉的:src 直接取选项,空值就成了破图图标。
* 破图比不显示更难看,也更容易被误以为"站坏了"。
*/
function yang_flag_block()
{
if (!kratos_option('g_flag')) {
return '';
}
$url = yang_option_image('g_flag_url');
if ($url === '') {
return '';
}
return '<div id="flag"><img style="width:50px" src="'
. esc_url($url) . '" alt=""><div>';
}
endif;
if (!function_exists('yang_favicon_tag')):
/**
* favicon 的 <link> 标签;拿不到地址时返回空(不输出空 href)。
*
* 空 href 的害处:浏览器会退而请求当前页面当作图标,
* 白跑一次请求、图标还是错的(显示成一个空白方块)。
*/
function yang_favicon_tag()
{
$url = yang_option_image('g_icon');
if ($url === '') {
return '';
}
return '<link rel="shortcut icon" href="' . esc_url($url) . '">';
}
endif;
if (!function_exists('yang_option_image_or')):
/**
* 同 yang_option_image(),但允许调用方指定一个"就地兜底"的回退值。
*
* 用在语义上不该共用默认图的场合:例如 Logo 空着时该显示站点名称,
* 而不是某张图。
*
* @param string $id 选项 id
* @param string $fallback 连默认图都没有时用的值
* @return string
*/
function yang_option_image_or($id, $fallback = '')
{
$img = yang_option_image($id);
return $img !== '' ? $img : $fallback;
}
endif;