Files
Genarrative/apps/mobile-shell/src/shell/webViewPolicy.ts
T
kdletters a54210b0d1 收口原生壳公开主站来源
新增共享 HostBridge 公开主站 origin 和默认 URL 常量

移动壳启动 URL、分享和 WebView 策略改为引用共享主站来源

桌面壳配置检查反查 Rust WEB_APP_ORIGIN 与共享契约一致

同步原生壳方案和共享决策记录
2026-06-19 01:40:22 +08:00

221 lines
5.3 KiB
TypeScript

import { DEFAULT_MOBILE_SHELL_WEB_URL } from './url';
const MOBILE_WEBVIEW_BLOCKED_DOWNLOAD_PROTOCOLS = new Set([
'blob:',
'data:',
'file:',
'filesystem:',
]);
export type MobileWebViewNavigationRequest = {
url?: string | null;
};
export function shouldBlockMobileWebViewDownloadUrl(
rawUrl: string | null | undefined,
) {
if (!rawUrl) {
return false;
}
try {
return MOBILE_WEBVIEW_BLOCKED_DOWNLOAD_PROTOCOLS.has(
new URL(rawUrl, DEFAULT_MOBILE_SHELL_WEB_URL).protocol,
);
} catch {
return false;
}
}
export function shouldBlockMobileWebViewNavigationRequest(
request: MobileWebViewNavigationRequest,
) {
return shouldBlockMobileWebViewDownloadUrl(request.url);
}
export const BLOCK_WEBVIEW_DOWNLOAD_SCRIPT = `
(function() {
var blockedDownloadProtocols = {
'blob:': true,
'data:': true,
'file:': true,
'filesystem:': true
};
function shouldBlockDownloadUrl(rawUrl) {
if (typeof rawUrl !== 'string') {
return false;
}
try {
return blockedDownloadProtocols[new URL(rawUrl, window.location.href).protocol] === true;
} catch (error) {
return false;
}
}
function findDownloadAnchor(target) {
if (!target) {
return null;
}
if (typeof target.closest === 'function') {
return target.closest('a');
}
var element = target;
while (element && element !== document) {
if (element.tagName === 'A') {
return element;
}
element = element.parentNode;
}
return null;
}
function shouldBlockAnchor(anchor) {
return Boolean(
anchor &&
(anchor.hasAttribute('download') || shouldBlockDownloadUrl(anchor.href))
);
}
function blockDownloadEvent(event) {
event.preventDefault();
event.stopImmediatePropagation();
event.stopPropagation();
return false;
}
document.addEventListener('click', function(event) {
if (shouldBlockAnchor(findDownloadAnchor(event.target))) {
return blockDownloadEvent(event);
}
}, true);
var originalOpen = window.open;
window.open = function(url) {
if (shouldBlockDownloadUrl(url)) {
return null;
}
return originalOpen.apply(window, arguments);
};
if (window.HTMLAnchorElement && HTMLAnchorElement.prototype.click) {
var originalAnchorClick = HTMLAnchorElement.prototype.click;
HTMLAnchorElement.prototype.click = function() {
if (shouldBlockAnchor(this)) {
return undefined;
}
return originalAnchorClick.apply(this, arguments);
};
}
})();
true;
`;
export const TRACK_MOBILE_WEBVIEW_HISTORY_SCRIPT = `
(function() {
if (window.__GENARRATIVE_MOBILE_HISTORY_TRACKER_INSTALLED__) {
if (typeof window.__GENARRATIVE_MOBILE_POST_NAVIGATION_STATE__ === 'function') {
window.__GENARRATIVE_MOBILE_POST_NAVIGATION_STATE__();
}
return true;
}
var historyIndexKey = '__genarrativeMobileHistoryIndex';
var originalPushState = window.history.pushState.bind(window.history);
var originalReplaceState = window.history.replaceState.bind(window.history);
function readIndex(state) {
if (!state || typeof state !== 'object') {
return null;
}
var value = state[historyIndexKey];
return Number.isInteger(value) && value >= 0 ? value : null;
}
function stateWithIndex(state, index) {
if (state && typeof state === 'object' && !Array.isArray(state)) {
var nextState = {};
Object.keys(state).forEach(function(key) {
nextState[key] = state[key];
});
nextState[historyIndexKey] = index;
return nextState;
}
var indexedState = {};
indexedState[historyIndexKey] = index;
return indexedState;
}
var currentIndex = readIndex(window.history.state) || 0;
function postNavigationState() {
var bridge = window.ReactNativeWebView;
if (!bridge || typeof bridge.postMessage !== 'function') {
return;
}
bridge.postMessage(JSON.stringify({
type: 'genarrative.mobile.historyState',
canGoBack: currentIndex > 0
}));
}
function replaceCurrentState() {
try {
originalReplaceState(
stateWithIndex(window.history.state, currentIndex),
'',
window.location.href
);
} catch (error) {}
}
window.history.pushState = function(state, title, url) {
var nextIndex = currentIndex + 1;
var result = originalPushState(stateWithIndex(state, nextIndex), title, url);
currentIndex = nextIndex;
postNavigationState();
return result;
};
window.history.replaceState = function(state, title, url) {
var result = originalReplaceState(
stateWithIndex(state, currentIndex),
title,
url
);
postNavigationState();
return result;
};
window.__GENARRATIVE_MOBILE_POST_NAVIGATION_STATE__ = postNavigationState;
window.addEventListener('popstate', function(event) {
var nextIndex = readIndex(event.state);
currentIndex = nextIndex === null ? 0 : nextIndex;
if (nextIndex === null) {
replaceCurrentState();
}
postNavigationState();
});
window.__GENARRATIVE_MOBILE_HISTORY_TRACKER_INSTALLED__ = true;
replaceCurrentState();
postNavigationState();
})();
true;
`;
export const MOBILE_WEBVIEW_BEFORE_CONTENT_SCRIPT = `
${BLOCK_WEBVIEW_DOWNLOAD_SCRIPT}
${TRACK_MOBILE_WEBVIEW_HISTORY_SCRIPT}
true;
`;