diff --git a/src/app/service/content/create_context.ts b/src/app/service/content/create_context.ts index 1bbe7f151..999a78b3a 100644 --- a/src/app/service/content/create_context.ts +++ b/src/app/service/content/create_context.ts @@ -7,6 +7,7 @@ import { protect } from "./gm_api/gm_context"; import { isEarlyStartScript } from "./utils"; import { ListenerManager } from "./listener_manager"; import { createGMBase } from "./gm_api/gm_api"; +import { attachNavigateHandler, type UrlChangeEvent } from "./gm_api/navigation_handle"; // 构建沙盒上下文 export const createContext = ( @@ -102,6 +103,10 @@ export const createContext = ( } } context.unsafeWindow = window; + if (scriptGrants.has("window.onurlchange") && context.onurlchange === undefined) { + context.onurlchange = null; + attachNavigateHandler(window as any); + } return context; }; @@ -370,6 +375,23 @@ export const createProxyContext = (context }, }; + // @grant window.onurlchange + if (context?.onurlchange === null) { + let currentValue: ((this: GlobalEventHandlers, ev: UrlChangeEvent) => any) | null = null; + ownDescs.onurlchange = { + enumerable: true, + configurable: true, + get() { + return currentValue; + }, + set(nv) { + if (typeof nv !== "function") nv = null; + currentValue = nv; + return true; + }, + }; + } + // 把初始Copy加上特殊变量后,生成一份新Copy mySandbox = Object.create(Object.getPrototypeOf(sharedInitCopy), ownDescs); @@ -389,7 +411,7 @@ export const createProxyContext = (context // 把 GM context物件的 window属性内容移至exposedWindow // 由于目前只有 window.close, window.open, window.onurlchange, 不需要循环 window - const cWindow = context.window; + const cWindow = context.window as (Window & Record) | undefined; // @grant window.close if (cWindow?.close) { @@ -402,9 +424,11 @@ export const createProxyContext = (context } // @grant window.onurlchange - if (cWindow?.onurlchange === null) { - // 目前 TM 只支援 null. ScriptCat不需要grant预设启用? - mySandbox.onurlchange = null; + if (context?.onurlchange === null) { + const handle = function (this: Window & Record, e: UrlChangeEvent) { + this.onurlchange?.(e); + } as EventListener; + (window).addEventListener("urlchange", handle.bind(mySandbox), false); } // 从网页 console 隔离出来的沙盒 console diff --git a/src/app/service/content/gm_api/navigation_handle.ts b/src/app/service/content/gm_api/navigation_handle.ts new file mode 100644 index 000000000..f64b581d4 --- /dev/null +++ b/src/app/service/content/gm_api/navigation_handle.ts @@ -0,0 +1,58 @@ +export class UrlChangeEvent extends Event { + readonly url: string; + constructor(type: string, url: string) { + super(type); + this.url = url; + } +} + +let attached = false; + +const getPropGetter = (obj: any, key: string) => { + // 避免直接 obj[key] 读取。或会被 hack + let t = obj; + let pd: PropertyDescriptor | undefined; + while (t) { + pd = Object.getOwnPropertyDescriptor(t, key); + if (pd) break; + t = Object.getPrototypeOf(t); + } + if (pd) { + return pd?.get?.bind(obj); + } +}; + +// Chrome 102+, Firefox 147+ +// https://developer.chrome.com/docs/web-platform/navigation-api +// https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API#browser_compatibility +export const attachNavigateHandler = (win: Window & { navigation: EventTarget }) => { + if (attached) return; + attached = true; + // 以 location.href 判断避免 replaceState/pushState 重复执行重复触发 + const loc = win.location; + const getUrl = getPropGetter(loc, "href"); + const dispatch = win.dispatchEvent.bind(win); + let lastUrl = getUrl?.(); + let callSeq = 0; + const handler = async (ev: Event): Promise => { + callSeq = callSeq > 512 ? 1 : callSeq + 1; + const seq = callSeq; + let newUrl = getUrl?.(); // 取得当前 location.href + const destUrl = (ev as any).destination?.url; + if (destUrl !== newUrl && newUrl === lastUrl) { + // 某些情况,location.href 未更新就触发了 + // 用 postMessage 推迟到下一个 macrotask 阶段 + await new Promise((resolve) => { + self.addEventListener("message", resolve, { once: true }); + self.postMessage({ [`${Math.random()}`]: {} }, "*"); // 传一个 dummy message + }); + if (seq !== callSeq) return; // 等待时,或许已经触发了其他 navigate + newUrl = getUrl?.(); // 再次取得当前 location.href + } + if (newUrl === lastUrl) return; + lastUrl = newUrl; + const urlChangeEv = new UrlChangeEvent("urlchange", (destUrl || newUrl) as string); + dispatch(urlChangeEv); + }; + win.navigation?.addEventListener("navigate", handler, false); +};