Files
wordpress-kratos-yang/inc/theme-cdn.php
T
laoyang 40e09b6309 3.2.34 新增「CDN 加速」:地址 + 白名单 + 黑名单,黑名单优先 #3
后台「主题设置」新增第十一个标签页(邮件通知之后、自定义代码之前),
只有三个输入框:CDN 地址、白名单、黑名单。地址留空即关闭功能。

实现(inc/theme-cdn.php,新增):
- 输出缓冲挂在 template_redirect 优先级 1,改整页 HTML 的资源地址,
  覆盖主题 enqueue 之外的文章图片、插件输出、区块图片。
- 规则按字面形态自动识别七种写法:* / 扩展名 / 扩展名列表 /
  路径前缀 / 通配符 / 路径子串 / ~正则~。
- 单个纯字母词(jpg 与 uploads 分不出谁是扩展名)同时按扩展名与
  路径子串匹配,两个含义互斥,避免 uploads 规则静默失效。
- 黑名单先于白名单判定;硬性排除不受两套名单影响:以 / 结尾、
  页面/动态后缀、无扩展名、wp-admin/wp-json/wp-cron/xmlrpc/feed、
  非本站域名、非 http(s) 协议。
- 后台/REST/AJAX/feed/cron/定制器预览/CLI 一律不改写。
- 查询串与锚点原样保留(?ver= 丢了会一直吃旧缓存)。
- 子目录站:判规则用完整路径,拼 CDN 时剥掉站点子目录。
- 写错的正则编译时丢弃,避免每个资源抛一条 warning 刷满日志。

验证:
- inc/theme-cdn.php 逻辑探针 92/0,反向验证 21/0(M1~M15 逐条注入,
  含 M5 拆成三条覆盖协议相对/绝对/整页三条分支,M10/M13 组合注入)。
- 后台探针 56/0(真实渲染 + 几何 + 类型 + 说明块),反向验证 15/0。
- 包契约 69/0,反向验证 23/0(改 zip 内字节,不改源码)。
- 旧探针回归:toc330 101/0、admin330 88/0、admin328 13/0、
  pkgcheck331/332/333 全绿、e2e322/323 通过。
- 线上仅在 theme-update/ 发 zip + readme + update.json,未解包覆盖服务器主题目录。
2026-10-25 16:15:09 +08:00

728 lines
25 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
/**
* CDN 加速
*
* ── 做什么 ────────────────────────────────────────────────────
* 把页面里指向本站的静态资源地址,前缀换成用户配置的 CDN 地址。
* 参考 CDN Enabler 插件的思路,但只保留三件事:CDN 地址、白名单、黑名单。
*
* ── 为什么在输出缓冲层做,而不是改 enqueue ────────────────────
* 主题自己 enqueue 的资源只有十来个,可以精确改;
* 但**文章正文里的图片、插件输出的样式、Gutenberg 区块的图片**都绕过
* enqueue,直接写死在 HTML 里。要覆盖这些,只能在最终 HTML 上做。
* 而且这样做完,主题自身资源也自动被覆盖,不必两套逻辑。
*
* ── 安全底线(硬编码,不受规则影响)──────────────────────────
* 站点被 CDN 改坏最常见的原因是「把页面地址也改了」:
* 导航链接变成 CDN 域名 → 用户点一下就离开本站 → 全站 404。
* 所以下面这些一律不改写,且**不能**通过白名单放开:
* · 以 / 结尾的地址(页面、栏目页)
* · .php / .html / .htm 等页面或动态后缀
* · wp-admin / wp-login / wp-json / wp-cron 等后台与接口路径
* · 非本站域名(外链、别人家的 CDN)
* · data: / mailto: / tel: / javascript: 等非 http(s) 协议
*
* @author Yongquan Yang <yangyq.net>
* @license MIT License
*/
/**
* 运行时默认值
*
* 与 3.2.32 的教训一致:`std` 只在后台预填和恢复默认时有用,
* **运行时取不到**。所以这里单独给一份,供 kratos_option() 回落。
*/
function yang_cdn_defaults()
{
return array(
'cdn_url' => '',
'cdn_include' => '',
'cdn_exclude' => '',
);
}
/**
* 读一个 CDN 配置
*
* 注意空字符串的处理:用户可能明确把 CDN 地址清空(想关掉功能),
* 那时库里存的是 '',键存在。`isset()` 挡不住这种情况 ——
* 它只判"键在不在"。所以直接返回,由调用方判空。
*/
function yang_cdn_opt($key)
{
$d = yang_cdn_defaults();
$v = kratos_option($key, isset($d[$key]) ? $d[$key] : '');
return is_string($v) ? $v : '';
}
/**
* 解析 CDN 地址
*
* 返回 array(scheme, host, base_path),无法解析时返回 null。
* 允许用户只填 `cdn.example.com`(不带协议)—— 这时按当前站点协议补全,
* 因为在 HTTPS 站点上引用 http 资源会被浏览器拦成混合内容。
*/
function yang_cdn_parse_url($raw)
{
$raw = trim($raw);
if ($raw === '') {
return null;
}
// 去掉尾部斜杠(后面拼路径时会自己加),但保留根路径语义
$raw = rtrim($raw, '/');
if (strpos($raw, '//') === 0) {
$raw = 'https:' . $raw;
} elseif (!preg_match('#^[a-z][a-z0-9+.\-]*://#i', $raw)) {
$raw = 'https://' . $raw;
}
$p = wp_parse_url($raw);
if (empty($p['host'])) {
return null;
}
$scheme = !empty($p['scheme']) ? strtolower($p['scheme']) : 'https';
if ($scheme !== 'http' && $scheme !== 'https') {
return null;
}
return array(
'scheme' => $scheme,
'host' => strtolower($p['host']),
'path' => isset($p['path']) ? rtrim($p['path'], '/') : '',
);
}
/**
* 把一行规则编译成一个判断函数
*
* ── 规则语法(一行一条)──────────────────────────────────────
* `*` 全部
* `jpg` / `.jpg` / `*.jpg` 扩展名
* `jpg,png` / `.jpg .png` 扩展名列表(逗号或空格分隔)
* `/wp-content/uploads/` 路径前缀(以 / 开头且不含通配符)
* 两端带 `*` 的写法 shell 通配,`*` 可跨 `/`
* (例:星号 + `/uploads/` + 星号)
* `uploads` 路径子串
* `~regex~` 正则,对完整 URL 做 preg_match
* `#` `;` 开头 注释
*
* ── 为什么把「前缀 / 子串 / 通配」混在一个字段里 ──────────────
* 用户明确要求「规则的制定要非常的灵活」,而分成多个字段会让后台变复杂
* (这正是他说"不要弄得太复杂"的地方)。所以按**字面形态**判断,
* 让一行文字自己说明它是什么规则。
*
* 返回 null 表示这行不是有效规则(空行、注释、写错的括号)。
*/
function yang_cdn_compile_rule($line)
{
$line = trim($line);
if ($line === '') {
return null;
}
if ($line[0] === '#' || $line[0] === ';') {
return null;
}
// 正则:用 ~...~ 包裹。必须是整行包裹,避免把路径里的 ~ 误当正则。
if (strlen($line) > 2 && $line[0] === '~' && substr($line, -1) === '~') {
$pattern = $line;
// 先校验一次,写错的正则不能留着 —— 它会在每个资源上抛 warning,
// 一个页面几十个资源就是几十条日志,把日志刷满还托慢页面。
if (@preg_match($pattern, '') === false) {
return null;
}
return function ($url, $path) use ($pattern) {
return preg_match($pattern, $url) === 1;
};
}
// 全部
if ($line === '*') {
return function ($url, $path) {
return true;
};
}
// 扩展名列表:`jpg,png,webp` / `.jpg .png` / `*.jpg,*.png`
// 判据 = 每一段都长得像扩展名(字母数字开头,不含 / 和 *)
$parts = preg_split('/[\s,]+/', $line, -1, PREG_SPLIT_NO_EMPTY);
if (count($parts) > 0) {
$exts = array();
$all_ext = true;
foreach ($parts as $p) {
$p = ltrim($p, '*');
$p = ltrim($p, '.');
if ($p === '' || !preg_match('/^[A-Za-z0-9]+$/', $p)) {
$all_ext = false;
break;
}
$exts[] = strtolower($p);
}
if ($all_ext && !empty($exts)) {
// ── 单个纯字母词的两义并存 ──────────────────────────
//
// `jpg` 和 `uploads` 在字面上分不出谁是扩展名、谁是路径片段:
// 两个都是纯字母,都过得了上面的检查。若一律当扩展名,
// 用户写 `uploads` 想按目录筛就会完全失效(而且没有任何提示,
// 只是"规则没反应",最难查)。
//
// 所以单个词同时按**扩展名**和**路径子串**匹配:
// `jpg` → 扩展名命中 .jpg 文件(路径里也极少含 jpg)
// `uploads` → 子串命中 /wp-content/uploads/...(几乎没有
// 扩展名叫 uploads 的文件,不会误伤)
// 两个含义天然互斥,同时挂上不会造成歧义,反而省掉一次猜。
//
// 多个词(`jpg,png`)**只**按扩展名列表处理:那串文字整体
// 不可能出现在路径里,按子串匹配恒假,留着只会让人误解。
$one_word = (count($exts) === 1);
$needle = $one_word ? $exts[0] : '';
return function ($url, $path) use ($exts, $one_word, $needle) {
$e = strtolower(pathinfo($path, PATHINFO_EXTENSION));
if ($e !== '' && in_array($e, $exts, true)) {
return true;
}
if ($one_word && strlen($needle) > 1) {
return strpos(strtolower($path), $needle) !== false;
}
return false;
};
}
}
// 含通配符 → shell 通配(`*` 跨 `/`)
if (strpos($line, '*') !== false || strpos($line, '?') !== false) {
// 用 preg_quote 转义其余字符,再把占位符换回通配语义
$re = preg_quote($line, '#');
$re = str_replace(array('\*', '\?'), array('.*', '.'), $re);
$pattern = '#^' . $re . '$#i';
return function ($url, $path) use ($pattern) {
return preg_match($pattern, $path) === 1
|| preg_match($pattern, $url) === 1;
};
}
// 以 / 开头 → 路径前缀(含自身)
if ($line[0] === '/') {
$prefix = strtolower($line);
return function ($url, $path) use ($prefix) {
$p = strtolower($path);
return $p === rtrim($prefix, '/') || stripos($p, $prefix) === 0;
};
}
// 其余 → 路径子串(不区分大小写)
$needle = strtolower($line);
return function ($url, $path) use ($needle) {
return strpos(strtolower($path), $needle) !== false;
};
}
/**
* 把整段文本编译成一串规则
*/
function yang_cdn_compile_list($raw)
{
$out = array();
foreach (preg_split('/\r\n|\r|\n/', (string) $raw) as $line) {
$fn = yang_cdn_compile_rule($line);
if ($fn !== null) {
$out[] = $fn;
}
}
return $out;
}
/**
* 规则编译缓存的登记处
*
* ── 为什么需要手动清 ────────────────────────────────────────
* 规则编译是有成本的(每行都要判形态、正则还要先试编译),所以
* yang_cdn_should_rewrite() 里用 static 缓存一份。但 static 一旦写进去
* 就跟着进程走,任何"改了配置马上要看到新结果"的场合都会拿到旧规则:
* · 后台的「测试这条地址」按钮 —— 用户刚在输入框里改完规则,还没保存
* · 自测脚本 —— 一个进程里要试几十种配置
* 所以把缓存句柄放在全局里,提供这个函数显式清掉。
*
* 后台测试用的是"临时覆盖层"(改 $GLOBALS['__yang_cdn_override']),
* 不写库、不触发 sanitize,所以不会像 3.2.11 的 SMTP 那样递归。
*/
function yang_cdn_reset_cache()
{
$GLOBALS['__yang_cdn_rules'] = null;
$GLOBALS['__yang_cdn_hosts'] = null;
$GLOBALS['__yang_cdn_base'] = null;
}
/**
* 读一份(编译好的)规则,带缓存
*/
function yang_cdn_rules()
{
if (isset($GLOBALS['__yang_cdn_rules']) && is_array($GLOBALS['__yang_cdn_rules'])) {
return $GLOBALS['__yang_cdn_rules'];
}
$GLOBALS['__yang_cdn_rules'] = array(
'include' => yang_cdn_compile_list(yang_cdn_opt('cdn_include')),
'exclude' => yang_cdn_compile_list(yang_cdn_opt('cdn_exclude')),
);
return $GLOBALS['__yang_cdn_rules'];
}
/**
* 判断某个地址是否该走 CDN
*
* $url 完整地址(可能是绝对地址,也可能是 /path 这种相对地址)
* $path 已经剥掉域名与查询串的纯路径
*
* ── 顺序很重要 ───────────────────────────────────────────────
* ① 硬性排除(页面/动态/后台/非本站)→ 无论规则怎么写都不改
* ② 黑名单 → 命中就退出
* ③ 白名单为空 = 全通过;否则必须命中至少一条
*/
function yang_cdn_should_rewrite($url, $path)
{
$rules = yang_cdn_rules();
$include_cache = $rules['include'];
$exclude_cache = $rules['exclude'];
// ① 硬性排除 ----------------------------------------------------
$lower_path = strtolower($path);
// 以 / 结尾 = 页面/栏目页。改了会让导航跳到 CDN 域名,全站 404。
if ($lower_path === '' || substr($lower_path, -1) === '/') {
return false;
}
// 页面与动态后缀
$ext = strtolower(pathinfo($lower_path, PATHINFO_EXTENSION));
$never = array('php', 'php3', 'php4', 'php5', 'phtml', 'html', 'htm', 'shtml',
'asp', 'aspx', 'jsp', 'cgi', 'pl', 'do', 'action');
if ($ext !== '' && in_array($ext, $never, true)) {
return false;
}
// 没有扩展名也不改:通常是接口、短链、伪静态页
if ($ext === '' && strpos(basename($lower_path), '.') === false) {
return false;
}
// 后台与接口路径
foreach (array('/wp-admin/', '/wp-login.php', '/wp-json/', '/wp-cron.php',
'/xmlrpc.php', '/wp-comments-post.php', '/feed/') as $bad) {
if (strpos($lower_path, $bad) !== false) {
return false;
}
}
// ② 黑名单 ------------------------------------------------------
foreach ($exclude_cache as $fn) {
if ($fn($url, $path)) {
return false;
}
}
// ③ 白名单(空 = 全通过)----------------------------------------
if (empty($include_cache)) {
return true;
}
foreach ($include_cache as $fn) {
if ($fn($url, $path)) {
return true;
}
}
return false;
}
/**
* 把一个地址换成 CDN 地址
*
* 返回 false 表示不改。返回字符串表示换好的地址。
*/
function yang_cdn_rewrite_one($url, $site_hosts, $cdn)
{
$raw = $url;
// 非 http(s) 一律不动:data:、mailto:、tel:、javascript:、about:blank …
if (preg_match('#^(data|mailto|tel|javascript|about|blob|ftp):#i', $raw)) {
return false;
}
// 协议相对地址 `//host/path` —— 必须按绝对地址处理,
// 否则下面按「相对路径」拼 base 会得到 `https://cdn.com//host/path`。
if (strpos($raw, '//') === 0) {
$abs = 'https:' . $raw;
$p = wp_parse_url($abs);
$host = isset($p['host']) ? strtolower($p['host']) : '';
if ($host === '' || !in_array($host, $site_hosts, true)) {
return false;
}
return yang_cdn_build($p, $cdn);
}
// 绝对地址:只有本站域名才改
if (preg_match('#^https?://#i', $raw)) {
$p = wp_parse_url($raw);
if (empty($p['host'])) {
return false;
}
$host = strtolower($p['host']);
if (!in_array($host, $site_hosts, true)) {
return false;
}
return yang_cdn_build($p, $cdn);
}
// 相对地址(以 / 开头,或者纯相对路径)
$p = wp_parse_url($raw);
if (empty($p['path'])) {
return false;
}
$p['host'] = '';
return yang_cdn_build($p, $cdn);
}
/**
* 用解析好的 URL 分量拼出 CDN 地址
*
* 查询串与锚点原样保留 —— 版本号参数(`?ver=3.2.33`)丢了会让浏览器
* 一直用 CDN 上的旧缓存,是这类功能最常见的事故。
*/
function yang_cdn_build($p, $cdn)
{
$path = isset($p['path']) ? $p['path'] : '';
if ($path === '' || $path[0] !== '/') {
$path = '/' . $path;
}
if (!yang_cdn_should_rewrite(
(isset($p['host']) && $p['host'] !== '' ? 'https://' . $p['host'] : '') . $path,
$path
)) {
return false;
}
$out = $cdn['scheme'] . '://' . $cdn['host'] . $cdn['path'] . $path;
if (isset($p['query']) && $p['query'] !== '') {
$out .= '?' . $p['query'];
}
if (isset($p['fragment']) && $p['fragment'] !== '') {
$out .= '#' . $p['fragment'];
}
return $out;
}
/**
* 站点自己的域名列表(要改写哪些域名)
*
* home_url / site_url 都算 —— WordPress 允许把站点装在子目录里,
* 也允许 home 与 site 不同域(前台与后台分开)。漏掉一个就会有一半
* 资源不走 CDN,而且很难发现是哪个域名漏了。
*/
function yang_cdn_site_hosts()
{
if (isset($GLOBALS['__yang_cdn_hosts']) && is_array($GLOBALS['__yang_cdn_hosts'])) {
return $GLOBALS['__yang_cdn_hosts'];
}
$hosts = array();
foreach (array(home_url('/'), site_url('/')) as $u) {
$p = wp_parse_url($u);
if (!empty($p['host'])) {
$hosts[] = strtolower($p['host']);
}
}
// 带 www 与不带 www 都要认:站点地址填了 www,而正文里可能写裸域名
foreach ($hosts as $h) {
if (strpos($h, 'www.') === 0) {
$hosts[] = substr($h, 4);
} else {
$hosts[] = 'www.' . $h;
}
}
$hosts = array_values(array_unique($hosts));
$GLOBALS['__yang_cdn_hosts'] = $hosts;
return $hosts;
}
/**
* 主题站点的根路径
*
* 站点装在子目录(`example.com/blog`)时,资源地址是
* `/blog/wp-content/...`。拼 CDN 地址时要把这段剥掉,否则会变成
* `cdn.com/blog/wp-content/...`,而 CDN 回源根目录下并没有 blog。
* 但用户的规则是按完整路径写的,所以**判规则时用原路径,拼接时才剥**。
*/
function yang_cdn_site_base()
{
if (isset($GLOBALS['__yang_cdn_base'])) {
return $GLOBALS['__yang_cdn_base'];
}
$p = wp_parse_url(home_url('/'));
$GLOBALS['__yang_cdn_base'] = isset($p['path']) ? rtrim($p['path'], '/') : '';
return $GLOBALS['__yang_cdn_base'];
}
/**
* 改写一整段 HTML
*
* 只碰「资源地址可能出现的位置」,不去正则所有 http 字符串 ——
* 后者会把正文里提到的网址、分享链接、评论内容一起改掉,
* 而且这类误改极难发现(读者点文章里的引用链接会跑到 CDN)。
*
* 覆盖:
* src= / href= 的静态资源(脚本、样式、图片、视频、字体)
* srcset=(响应式图片,一个属性里多组地址 + 宽度描述符)
* 内联样式与 <style> 里的 url(...)
*
* 不改 href 里指向页面/文档的链接 —— 由 yang_cdn_should_rewrite 的
* 硬性排除兜住(页面与 .html/.php 都不改)。
*/
function yang_cdn_rewrite_html($html)
{
$cdn = yang_cdn_parse_url(yang_cdn_opt('cdn_url'));
if ($cdn === null) {
return $html;
}
if (strpos($html, 'http') === false && strpos($html, 'src=') === false
&& strpos($html, 'href=') === false && strpos($html, 'url(') === false) {
return $html;
}
$hosts = yang_cdn_site_hosts();
$base = yang_cdn_site_base();
// 包一层:把站点子目录剥掉再拼 CDN
$build = function ($p) use ($cdn, $base) {
if ($base !== '' && !empty($p['path'])) {
$pp = $p['path'];
if ($pp === $base || strpos($pp, $base . '/') === 0) {
$stripped = substr($pp, strlen($base));
$p['path'] = ($stripped === '' ? '/' : $stripped);
}
}
return yang_cdn_build($p, $cdn);
};
// ── srcset:先处理,因为它的格式(多组 + 描述符)不通用 ──
$html = preg_replace_callback(
'#(\ssrcset\s*=\s*)(["\'])(.*?)\2#is',
function ($m) use ($hosts, $build) {
$items = array();
$changed = false;
foreach (explode(',', $m[3]) as $item) {
$item = trim($item);
if ($item === '') {
continue;
}
// 每组 = 地址 + 可选描述符(`2x`、`480w`)
$sp = preg_split('/\s+/', $item, 2);
$u = $sp[0];
$desc = isset($sp[1]) ? ' ' . $sp[1] : '';
if (preg_match('#^(https?:)?//#i', $u)) {
$p = wp_parse_url(preg_match('#^//#', $u) ? 'https:' . $u : $u);
$host = isset($p['host']) ? strtolower($p['host']) : '';
if ($host !== '' && in_array($host, $hosts, true)) {
$new = $build($p);
if ($new !== false) {
$u = $new;
$changed = true;
}
}
} elseif ($u !== '' && $u[0] === '/' && substr($u, 0, 2) !== '//') {
$p = wp_parse_url($u);
$p['host'] = '';
$new = $build($p);
if ($new !== false) {
$u = $new;
$changed = true;
}
}
$items[] = $u . $desc;
}
if (!$changed) {
return $m[0];
}
return $m[1] . $m[2] . implode(', ', $items) . $m[2];
},
$html
);
// ── src= / href= ──
// 值里可能带查询串(`?ver=`),用 [^"']* 而不是「到 ? 为止」,
// 否则会截断成半截地址。
$html = preg_replace_callback(
'#(\s(?:src|href)\s*=\s*)(["\'])((?:(?:https?:)?//|/)[^"\']*)\2#is',
function ($m) use ($hosts, $build) {
$new = yang_cdn_rewrite_abs($m[3], $hosts, $build);
if ($new === false) {
return $m[0];
}
return $m[1] . $m[2] . $new . $m[2];
},
$html
);
// ── 内联样式与 <style> 里的 url() ──
$html = preg_replace_callback(
'#url\(\s*(["\']?)((?:(?:https?:)?//|/)[^)"\']*)\1\s*\)#i',
function ($m) use ($hosts, $build) {
$new = yang_cdn_rewrite_abs($m[2], $hosts, $build);
if ($new === false) {
return $m[0];
}
return 'url(' . $m[1] . $new . $m[1] . ')';
},
$html
);
return $html;
}
/**
* 改写一个「属性值里的地址」
*
* 与 yang_cdn_rewrite_one 的区别:这个函数只负责认形态(绝对/协议相对/相对),
* 真正的拼接交给外部传进来的 $build(它负责剥站点子目录)。
*/
function yang_cdn_rewrite_abs($u, $hosts, $build)
{
if ($u === '') {
return false;
}
if (preg_match('#^//#', $u)) {
$p = wp_parse_url('https:' . $u);
$host = isset($p['host']) ? strtolower($p['host']) : '';
if ($host === '' || !in_array($host, $hosts, true)) {
return false;
}
return $build($p);
}
if (preg_match('#^https?://#i', $u)) {
$p = wp_parse_url($u);
$host = isset($p['host']) ? strtolower($p['host']) : '';
if ($host === '' || !in_array($host, $hosts, true)) {
return false;
}
return $build($p);
}
if ($u[0] === '/') {
$p = wp_parse_url($u);
if (empty($p['path'])) {
return false;
}
$p['host'] = '';
return $build($p);
}
return false;
}
/**
* 输出缓冲:整页改写
*
* ── 为什么用 ob_start 而不是各种 the_content 过滤器 ──────────
* 资源地址散落在主题模板、插件输出、区块渲染、小工具里,
* 逐个挂钩子必然漏。整页缓冲是唯一能一次覆盖全的地方。
*
* ── 什么时候不改 ────────────────────────────────────────────
* 后台、REST、AJAX、feed、cron、预览、定制器:这些地方的地址带 CDN
* 会妨碍编辑与调试(编辑器里看不到真实图片、REST 返回的地址对不上库)。
*/
function yang_cdn_buffer_start()
{
if (is_admin() || wp_doing_ajax() || wp_doing_cron()) {
return;
}
if (defined('REST_REQUEST') && REST_REQUEST) {
return;
}
if (defined('WP_CLI') && WP_CLI) {
return;
}
if (function_exists('is_feed') && is_feed()) {
return;
}
// 定制器预览:用户正在改主题设置,看到的应该是真实地址
if (isset($_GET['customize_changeset_uuid']) || isset($_GET['customize_preview'])) {
return;
}
// 站点还没配 CDN 地址 —— 直接不接管,零开销
if (yang_cdn_parse_url(yang_cdn_opt('cdn_url')) === null) {
return;
}
ob_start('yang_cdn_buffer_callback');
}
add_action('template_redirect', 'yang_cdn_buffer_start', 1);
/**
* 输出缓冲回调
*
* 只处理 HTML。图片、CSS、JS、XML 直接原样返回 ——
* 对二进制内容调 preg_replace 既没用又可能损坏数据。
*/
function yang_cdn_buffer_callback($html)
{
if (!is_string($html) || $html === '') {
return $html;
}
// 只看开头一小段判断类型,别对整页做正则
$head = substr($html, 0, 200);
if (stripos($head, '<!doctype') === false && stripos($head, '<html') === false
&& stripos($head, '<head') === false) {
return $html;
}
return yang_cdn_rewrite_html($html);
}
/**
* 后台「测试这条地址走不走 CDN」
*
* 给后台一个只读的诊断入口:用户填了规则,不用去前台翻源码就能确认
* 「我这个图片到底走不走 CDN」。返回数组,供 AJAX 与自测脚本共用。
*/
function yang_cdn_test_url($url)
{
$cdn = yang_cdn_parse_url(yang_cdn_opt('cdn_url'));
$r = array(
'input' => $url,
'cdn' => $cdn === null ? '' : ($cdn['scheme'] . '://' . $cdn['host'] . $cdn['path']),
'matched' => false,
'output' => $url,
'reason' => '',
);
if ($cdn === null) {
$r['reason'] = 'CDN 地址未填写或无法解析';
return $r;
}
$p = wp_parse_url($url);
if (empty($p['path'])) {
$r['reason'] = '这个地址没有路径部分';
return $r;
}
$path = $p['path'];
$r['path'] = $path;
if (!yang_cdn_should_rewrite($url, $path)) {
$r['reason'] = '被排除(硬性规则或黑名单),或未命中白名单';
return $r;
}
$r['matched'] = true;
$r['output'] = yang_cdn_rewrite_one($url, yang_cdn_site_hosts(), $cdn);
if ($r['output'] === false) {
$r['matched'] = false;
$r['output'] = $url;
$r['reason'] = '域名不是本站(或地址形态无法识别)';
} else {
$r['reason'] = '已命中,将改写到 CDN';
}
return $r;
}