' . $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 '
';
}
/** 全站页脚 */
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 '
';
}
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();
}