| function menavExtractDomain(url) { |
| if (!url) return ''; |
|
|
| try { |
| |
| 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); |
| } |
| } |
|
|
| |
| 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; |
|
|
| |
| 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 '#'; |
| } |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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'; |
| } |
| } |
|
|
| |
| 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, |
| }; |
|
|