' . $label . ''; } /** * 公告正文渲染:先整体转义,再仅放行 http(s) / 站内相对链接,保留换行。 * 支持 [文字](链接) 显式写法,也自动识别裸 http(s) 链接。 */ function notice_html(string $text): string { $esc = he($text); // 1) Markdown 风格链接:[文字](链接) $esc = preg_replace_callback('/\[([^\]]+)\]\(([^)\s]+)\)/u', static function (array $m): string { return notice_link($m[1], $m[2]); }, $esc); // 2) 裸链接自动识别(跳过已在标签 / href="..." 内的地址) $esc = preg_replace_callback('/(?])https?:\/\/[^\s<"\')\]]+/u', static function (array $m): string { $url = rtrim($m[0], '.,;:!?'); if ($url === '') { return $m[0]; } $tail = substr($m[0], strlen($url)); return '' . $url . '' . $tail; }, $esc); return nl2br($esc); } /** 当前 logo 的站点相对路径(后台配置优先,空则默认) */ function site_logo_rel(): string { $logo = setting_get('site_logo', ''); return $logo !== '' ? $logo : 'assets/img/logo.svg'; } /** 输出页面 :标题/favicon/主题防闪烁/公共样式 */ function layout_head(string $title = ''): void { global $P; $site = setting_get('site_name', 'SecHome'); $logo = site_logo_rel(); $fullTitle = ($title !== '' ? $title . ' - ' : '') . $site; $prefix = $P; echo ''; echo ''; echo ''; // 主题防闪烁:在任何样式加载前应用 echo ''; echo '' . he($fullTitle) . ''; echo ''; echo ''; // 站长提示开关(后台可关):关闭时统一隐藏带 st-tip 标记的辅助文案 if ((setting_get('show_st_tips', '1')) !== '1') { echo ''; } echo '' . "\n"; } /** 昼夜切换浮动按钮 + 返回顶部按钮(全站页面右下角) */ function layout_theme_fab(): void { echo '' . "\n"; echo '' . "\n"; } /** 站内 logo 图片(内容区使用,同 favicon) */ function layout_logo_img(string $cls = 'logo', string $alt = 'logo'): void { global $P; echo '' . he($alt) . ''; } /** 全站页脚 */ function layout_footer(): void { global $P; $site = setting_get('site_name', 'SecHome'); $icp = setting_get('icp_no', ''); $gongan = setting_get('gongan_no', ''); $gonganLink = setting_get('gongan_link', ''); $copyright = setting_get('copyright', ''); $extra = setting_get('footer_text', ''); echo '' . "\n"; } /** 首页访客信息(hero 信息模式):IP 与浏览器/系统/内核由服务端解析 UA 提供,屏幕/语言由前端补齐 */ function visitor_brief(): array { $ip = trim((string) ($_SERVER['REMOTE_ADDR'] ?? '')); if ($ip === '' || !filter_var($ip, FILTER_VALIDATE_IP)) { $ip = '未知'; } $ua = (string) ($_SERVER['HTTP_USER_AGENT'] ?? ''); $browser = '未知'; $kernel = '未知'; $os = '未知'; if ($ua !== '') { // 浏览器 / 内核 if (strpos($ua, 'MicroMessenger') !== false) { $browser = '微信内置浏览器'; $kernel = 'WebKit'; } elseif (preg_match('/Edg\/([\d.]+)/', $ua, $m)) { $browser = 'Edge ' . $m[1]; $kernel = 'Blink'; } elseif (preg_match('/OPR\/([\d.]+)/', $ua, $m)) { $browser = 'Opera ' . $m[1]; $kernel = 'Blink'; } elseif (strpos($ua, 'Firefox/') !== false) { $browser = 'Firefox'; $kernel = 'Gecko'; if (preg_match('/Firefox\/([\d.]+)/', $ua, $m)) $browser .= ' ' . $m[1]; } elseif (preg_match('/(?:Chrome|CriOS)\/([\d.]+)/', $ua, $m)) { $browser = 'Chrome ' . $m[1]; $kernel = 'Blink'; } elseif (strpos($ua, 'Safari/') !== false) { $browser = 'Safari'; $kernel = 'WebKit'; if (preg_match('/Version\/([\d.]+)/', $ua, $m)) $browser .= ' ' . $m[1]; } if ($kernel === '未知') { if (strpos($ua, 'Gecko/') !== false) $kernel = 'Gecko'; elseif (strpos($ua, 'AppleWebKit/') !== false) $kernel = 'WebKit'; } // 操作系统 if (strpos($ua, 'Windows NT 10.0') !== false) { $os = 'Windows 10 / 11'; } elseif (strpos($ua, 'Windows NT 6.3') !== false) { $os = 'Windows 8.1'; } elseif (strpos($ua, 'Windows NT 6.2') !== false) { $os = 'Windows 8'; } elseif (strpos($ua, 'Windows NT 6.1') !== false) { $os = 'Windows 7'; } elseif (strpos($ua, 'Windows NT 5.1') !== false) { $os = 'Windows XP'; } elseif (strpos($ua, 'Windows Phone') !== false) { $os = 'Windows Phone'; } elseif (strpos($ua, 'Android') !== false) { $os = 'Android'; if (preg_match('/Android ([\d.]+)/', $ua, $m)) $os .= ' ' . $m[1]; } elseif (strpos($ua, 'iPad') !== false || strpos($ua, 'iPhone') !== false || strpos($ua, 'iPod') !== false) { $os = 'iOS'; if (preg_match('/OS ([\d_]+)/', $ua, $m)) $os .= ' ' . str_replace('_', '.', $m[1]); } elseif (strpos($ua, 'Mac OS X') !== false) { $os = 'macOS'; if (preg_match('/Mac OS X ([\d_]+)/', $ua, $m)) $os .= ' ' . str_replace('_', '.', $m[1]); } elseif (strpos($ua, 'CrOS') !== false) { $os = 'ChromeOS'; } elseif (strpos($ua, 'Linux') !== false) { $os = 'Linux'; } } return ['ip' => $ip, 'browser' => $browser, 'os' => $os, 'kernel' => $kernel]; } /** 图标值是否属于图片(dataURL / http / 常见图片扩展 / 相对路径) */ function icon_is_image(string $raw): bool { $v = trim($raw); if ($v === '') { return false; } if (preg_match('/^data:image\//i', $v) || preg_match('#^https?://#i', $v)) { return true; } if (preg_match('/\.(png|jpe?g|gif|svg|webp)(\?|#|$)/i', $v)) { return true; } return $v[0] === '/' || strpos($v, './') === 0 || strpos($v, '../') === 0; } /** 渲染图标:图片返回 ,否则返回转义文本(供前台/后台预览复用) */ function site_icon(?string $raw, string $alt = 'icon'): string { $v = trim((string) $raw); if ($v === '') { return ''; } if (icon_is_image($v)) { return '' . he($alt) . ''; } return he($v); } /** 默认 CDN IPv4 段库(每行:CIDR + 空格 + 厂商名;# 开头为注释,由后台可覆盖) */ function default_cdn_ranges(): string { return implode("\n", [ '# 内置默认:Cloudflare 官方 IPv4 段(可在后台“CDN IP 段库”补充其它厂商)', '103.21.244.0/22 Cloudflare', '103.22.200.0/22 Cloudflare', '103.31.4.0/22 Cloudflare', '104.16.0.0/13 Cloudflare', '104.24.0.0/14 Cloudflare', '108.162.192.0/18 Cloudflare', '131.0.72.0/22 Cloudflare', '141.101.64.0/18 Cloudflare', '162.158.0.0/15 Cloudflare', '172.64.0.0/13 Cloudflare', '173.245.48.0/20 Cloudflare', '188.114.96.0/20 Cloudflare', '190.93.240.0/20 Cloudflare', '197.234.240.0/22 Cloudflare', '198.41.128.0/17 Cloudflare', ]); } /** 当前生效的 CDN 段库文本(后台配置优先,为空回退默认内置列表) */ function cdn_ranges_text(): string { $v = setting_get('cdn_ranges', ''); return $v !== '' ? $v : default_cdn_ranges(); } /** 默认搜索引擎列表(后台未配置时的兜底) */ function default_search_engines(): array { return [ ['key' => 'bing', 'name' => 'Bing', 'icon' => 'B', 'url' => 'https://www.bing.com/search?q={kw}'], ['key' => 'baidu', 'name' => '百度', 'icon' => '度', 'url' => 'https://www.baidu.com/s?wd={kw}'], ['key' => 'github', 'name' => 'GitHub', 'icon' => 'G', 'url' => 'https://github.com/search?q={kw}'], ['key' => 'google', 'name' => 'Google', 'icon' => 'G', 'url' => 'https://www.google.com/search?q={kw}'], ]; } /** 读取当前生效的搜索引擎列表(优先后台配置,无效或为空回退默认) */ function search_engines(): array { $raw = setting_get('search_engines', ''); if ($raw === '') { return default_search_engines(); } $arr = json_decode($raw, true); if (!is_array($arr) || count($arr) === 0) { return default_search_engines(); } $out = []; foreach ($arr as $i => $e) { if (!is_array($e)) continue; $name = trim((string) ($e['name'] ?? '')); $url = trim((string) ($e['url'] ?? '')); $icon = trim((string) ($e['icon'] ?? '')); if ($name === '' || $url === '' || strpos($url, '{kw}') === false) continue; $key = trim((string) ($e['key'] ?? '')); if ($key === '') { $key = 'e' . ((int) $i + 1); } $out[] = ['key' => $key, 'name' => $name, 'icon' => ($icon !== '' ? $icon : 'S'), 'url' => $url]; } return $out !== [] ? $out : default_search_engines(); }