// +---------------------------------------------------------------------- declare(strict_types=1); namespace ywxapp\middleware; use ywxapp\library\SiteTheme; /** * 整站 layui 主题注入中间件(全局注册于 app/middleware.php)。 * * 为什么用中间件而不是改各 Base 控制器: * - 现有 SkinVariables 只在「插件上下文」注入(FrontendBase/MemberBase/BackendBase * 各自 fetch 钩子),非插件页面拿不到任何皮肤变量,不是真正的整站。 * - 中间件在响应发出前统一处理,天然覆盖全部应用(backend/member/frontend/api) * 与全部插件页面,且不侵入任何控制器代码。 * * 注入内容(插入到 之前,即排在页面自有 CSS 之后): * 1. /static/theme/layui-theme.css —— layui 主色已变量化的覆写层 * 2. —— 站点变量(config/theme.php) * * 层叠关系(后加载者胜): * layui.css -> 页面/插件自有 CSS -> 本站主题层 -> 站点变量 * 因此:站点变量控制 layui 组件底色;插件模板又能通过自身 CSS 覆盖细节。 * * 严格限制只处理 HTML 页面响应,避免污染 JSON / 文件下载 / 重定向等。 * * @author ywxapp */ class ThemeInject { public function handle($request, \Closure $next) { $response = $next($request); try { if (! SiteTheme::isEnabled()) { return $response; } // 只看 HTML 文档响应 if (! $this->isHtmlPage($response)) { return $response; } $html = (string) $response->getContent(); if ($html === '' || stripos($html, '') === false) { return $response; } // 幂等:已注入过则跳过(例如视图里显式引入了主题层) if (stripos($html, 'ywx-site-theme') !== false || stripos($html, 'layui-theme.css') !== false) { return $response; } $inject = SiteTheme::linkTag() . SiteTheme::styleTag() . "\n"; $html = preg_replace('/<\/head>/i', $inject . '', $html, 1); $response->content($html); } catch (\Throwable $e) { // 主题注入绝不能影响页面输出:失败时原样返回 } return $response; } /** * 判定是否为可注入的 HTML 页面响应。 * 先看 Content-Type,缺失时用内容兜底(避免 API JSON / 下载被污染)。 */ protected function isHtmlPage($response): bool { $contentType = ''; try { $header = $response->getHeader('Content-Type'); $contentType = is_array($header) ? (string) ($header[0] ?? '') : (string) $header; } catch (\Throwable $e) { $contentType = ''; } if ($contentType !== '') { return stripos($contentType, 'text/html') !== false; } return true; // 无 Content-Type 时交给后续 检测兜底 } }