File size: 4,206 Bytes
e1ae2c6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | function menavExtractDomain(url) {
if (!url) return '';
try {
// 移除协议部分 (http://, https://, etc.)
let domain = String(url).replace(/^[a-zA-Z]+:\/\//, '');
// 移除路径、查询参数和锚点
domain = domain.split('/')[0].split('?')[0].split('#')[0];
// 移除端口号(如果有)
domain = domain.split(':')[0];
return domain;
} catch (e) {
return String(url);
}
}
// URL 安全策略:默认仅允许 http/https(可加 mailto/tel)与相对链接;其他 scheme 降级为 '#'
function menavGetAllowedUrlSchemes() {
try {
const cfg =
window.MeNav && typeof window.MeNav.getConfig === 'function'
? window.MeNav.getConfig()
: null;
const fromConfig =
cfg &&
cfg.data &&
cfg.data.site &&
cfg.data.site.security &&
cfg.data.site.security.allowedSchemes;
if (Array.isArray(fromConfig) && fromConfig.length > 0) {
return fromConfig
.map((s) =>
String(s || '')
.trim()
.toLowerCase()
.replace(/:$/, '')
)
.filter(Boolean);
}
} catch (e) {
// 忽略,回退默认
}
return ['http', 'https', 'mailto', 'tel'];
}
function menavIsRelativeUrl(url) {
const s = String(url || '').trim();
return (
s.startsWith('#') ||
s.startsWith('/') ||
s.startsWith('./') ||
s.startsWith('../') ||
s.startsWith('?')
);
}
function menavSanitizeUrl(rawUrl, contextLabel) {
if (rawUrl === undefined || rawUrl === null) return '#';
const url = String(rawUrl).trim();
if (!url) return '#';
if (menavIsRelativeUrl(url)) return url;
// 明确拒绝协议相对 URL(//example.com),避免意外绕过策略
if (url.startsWith('//')) {
console.warn(`[MeNav][安全] 已拦截不安全 URL(协议相对形式):${contextLabel || ''}`, url);
return '#';
}
try {
const parsed = new URL(url);
const scheme = String(parsed.protocol || '')
.toLowerCase()
.replace(/:$/, '');
const allowed = menavGetAllowedUrlSchemes();
if (allowed.includes(scheme)) return url;
console.warn(`[MeNav][安全] 已拦截不安全 URL scheme:${contextLabel || ''}`, url);
return '#';
} catch (e) {
console.warn(`[MeNav][安全] 已拦截无法解析的 URL:${contextLabel || ''}`, url);
return '#';
}
}
// class token 清洗:仅允许字母/数字/下划线/中划线与空格分隔,避免属性/事件注入
function menavSanitizeClassList(rawClassList, contextLabel) {
const input = String(rawClassList || '').trim();
if (!input) return '';
const tokens = input
.split(/\s+/g)
.map((t) => t.trim())
.filter(Boolean)
.map((t) => t.replace(/[^\w-]/g, ''))
.filter(Boolean);
const sanitized = tokens.join(' ');
if (sanitized !== input) {
console.warn(`[MeNav][安全] 已清洗不安全的 icon class:${contextLabel || ''}`, rawClassList);
}
return sanitized;
}
// 版本号统一来源:优先读取 meta[menav-version],回退到 menav-config-data.version
function menavDetectVersion() {
try {
const meta = document.querySelector('meta[name="menav-version"]');
const v = meta ? String(meta.getAttribute('content') || '').trim() : '';
if (v) return v;
} catch (e) {
// 忽略
}
try {
const configData = document.getElementById('menav-config-data');
const raw = configData ? String(configData.textContent || '').trim() : '';
if (!raw) return '1.0.0';
const parsed = JSON.parse(raw);
const v = parsed && parsed.version ? String(parsed.version).trim() : '';
return v || '1.0.0';
} catch (e) {
return '1.0.0';
}
}
// 修复移动端 `100vh` 视口高度问题:用实际可视高度驱动布局,避免侧边栏/内容区底部被浏览器 UI 遮挡
function menavUpdateAppHeight() {
const viewportHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight;
document.documentElement.style.setProperty('--app-height', `${Math.round(viewportHeight)}px`);
}
module.exports = {
menavExtractDomain,
menavSanitizeUrl,
menavSanitizeClassList,
menavDetectVersion,
menavUpdateAppHeight,
};
|