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, };