diff --git a/background/clipboard.ts b/background/clipboard.ts index ecbe02705..265e07fef 100644 --- a/background/clipboard.ts +++ b/background/clipboard.ts @@ -549,7 +549,7 @@ set_paste_(((sed, newLenLimit?: number, exOut?: InfoOnSed): string | Promise null)).then(s => typeof s === "string" ? s && reformat_(s.slice(0, GlobalConsts.MaxBufferLengthForPastingLongURL), sed, exOut) : null) } diff --git a/background/frame_commands.ts b/background/frame_commands.ts index 98de438cc..ef126fbbb 100644 --- a/background/frame_commands.ts +++ b/background/frame_commands.ts @@ -1,7 +1,7 @@ import { cPort, cRepeat, get_cOptions, set_cPort, set_cOptions, set_cRepeat, framesForTab_, findCSS_, cKey, reqH_, runOnTee_, curTabId_, settingsCache_, OnChrome, visualWordsRe_, CurCVer_, OnEdge, OnFirefox, substitute_, CONST_, set_runOnTee_, - curWndId_, vomnibarPage_f, vomnibarBgOptions_, setTeeTask_, blank_, + curWndId_, vomnibarPage_f, vomnibarBgOptions_, replaceTeeTask_, blank_, offscreenPort_, teeTask_, curIncognito_, OnOther_, keyToCommandMap_, Origin2_ } from "./store" import * as BgUtils_ from "./utils" @@ -12,7 +12,7 @@ import { import { convertToUrl_, createSearchUrl_, normalizeSVG_ } from "./normalize_urls" import { showHUD, complainLimits, ensureInnerCSS, getParentFrame, getPortUrl_, safePost, getCurFrames_, getFrames_, - waitForPorts_ + waitForPorts_, postTeeTask_, resetOffscreenPort_ } from "./ports" import { createSimpleUrlMatcher_, matchSimply_ } from "./exclusions" import { trans_ } from "./i18n" @@ -25,28 +25,68 @@ import { parseReuse, newTabIndex, openUrlWithActions } from "./open_urls" import { FindModeHistory_ } from "./tools" import C = kBgCmd +const DEBUG_OFFSCREEN: BOOL | boolean = false +let _lastOffscreenWndId = 0 +let _offscreenFailed = false + set_runOnTee_(((task, serializable, data): Promise => { - const frames = framesForTab_.get(curTabId_) || cPort && getCurFrames_() - let port = frames ? frames.cur_ : cPort as typeof cPort | null + if (Build.MV3 && task === kTeeTask.Paste && OnChrome && serializable >= 0) { + return navigator.permissions!.query({ name: "clipboard-read" }).catch(blank_) + .then((res) => !!res && res.state !== "denied" && runOnTee_(kTeeTask.Paste, -1 - serializable, null)) + } + const useOffscreen = !!Build.MV3 && !_offscreenFailed && OnChrome && (Build.MinCVer >= BrowserVer.MinOffscreenAPIs + || CurCVer_ > BrowserVer.MinOffscreenAPIs - 1) + && (task !== kTeeTask.CopyImage && task !== kTeeTask.DrawAndCopy) + const frames = useOffscreen ? null : framesForTab_.get(curTabId_) || cPort && getCurFrames_() + let port = useOffscreen ? null : frames ? frames.cur_ : cPort as typeof cPort | null if (frames && frames.top_ && port !== frames.top_ && !(frames.top_.s.flags_ & Frames.Flags.ResReleased) // here can not check `!port!.s.url_.startsWith(location.protocol)` - such an ext iframe is limited by default && (!BgUtils_.protocolRe_.test(frames.top_.s.url_) || port!.s.flags_ & Frames.Flags.ResReleased || !port!.s.url_.startsWith((BgUtils_.safeParseURL_(frames.top_.s.url_)?.origin || "") + "/"))) { port = frames.top_ } - if (Build.MV3 && task === kTeeTask.Paste && OnChrome && !serializable) { - return navigator.permissions!.query({ name: "clipboard-read" }).catch(blank_) - .then((res) => !!res && res.state !== "denied" && runOnTee_(kTeeTask.Paste, true, null)) - } const id = setTimeout((): void => { - const latest = setTeeTask_(id, null) + const latest = replaceTeeTask_(id, null) latest && latest.r && latest.r(false) }, 40_000) const deferred = BgUtils_.deferPromise_() - setTeeTask_(null, { i: id, t: task, s: serializable, d: Build.MV3 ? null : data, r: deferred.resolve_ }) + replaceTeeTask_(null, { i: id, t: task, s: serializable, d: Build.MV3 ? null : data, r: deferred.resolve_ }) + if (useOffscreen) { + if (offscreenPort_) { + try { + if (!Build.NDEBUG && DEBUG_OFFSCREEN) { + Windows_.update(_lastOffscreenWndId, { focused: true }, (): void => { + postTeeTask_(offscreenPort_!, teeTask_!) + }) + } else { + postTeeTask_(offscreenPort_, teeTask_!) + } + } catch { + resetOffscreenPort_() + } + } + if (offscreenPort_) { /* empty */ } + else if (!Build.NDEBUG && DEBUG_OFFSCREEN) { + Windows_.create({ url: CONST_.OffscreenFrame_ }, (wnd): void => { _lastOffscreenWndId = wnd.id }) + } else { + const all_reasons = browser_.offscreen.Reason + const reasons: chrome.offscreen.kReason[] = [all_reasons.BLOBS, all_reasons.CLIPBOARD, all_reasons.MATCH_MEDIA + ].filter( (i: T | undefined): i is T => !!i) + browser_.offscreen.createDocument({ + reasons: reasons.length > 0 ? reasons : ["CLIPBOARD"], url: CONST_.OffscreenFrame_, + justification: "read and write system clipboard", + }, (): void => { + const err = runtimeError_() + if (err) { + _offscreenFailed = true + resetOffscreenPort_() + return err + } + }) + } + } else if (port) { const allow = task === kTeeTask.CopyImage || task === kTeeTask.Copy || task === kTeeTask.DrawAndCopy || Build.MV3 && task === kTeeTask.Paste ? "clipboard-write; clipboard-read" : "" - if (port) { portSendFgCmd(port, kFgCmd.callTee, 1, { u: CONST_.TeeFrame_, c: "R TEE UI", a: allow, t: 3000, i: frames && port !== frames.cur_ && !(frames.cur_.s.flags_ & Frames.Flags.ResReleased) ? frames.cur_.s.frameId_ : 0 }, 1) @@ -56,7 +96,7 @@ set_runOnTee_(((task, serializable, data): Promise => { const lastWndId = curWnd ? curWnd.id : curWndId_ makeWindow({ type: "popup", url: CONST_.TeeFrame_, focused: true, incognito: false , left: 0, top: 0, width: 100, height: 32 }, "", (wnd): void => { - const teeTask = wnd ? null : setTeeTask_(null, null) + const teeTask = wnd ? null : replaceTeeTask_(null, null) if (wnd) { const newWndId = wnd.id void promise.then((): void => { @@ -355,7 +395,7 @@ export const handleImageUrl = (url: `data:${string}` | "", buffer: Blob | null : (url || !Build.MV3 && copyFromBlobUrl_mv2 ? Promise.resolve() : BgUtils_.convertToDataURL_(buffer!).then((u2): void => { url = u2 })) .then((): Promise => { - return runOnTee_(actions === kTeeTask.DrawAndCopy ? actions : kTeeTask.CopyImage, { + return runOnTee_(actions === kTeeTask.DrawAndCopy ? kTeeTask.DrawAndCopy : kTeeTask.CopyImage, { u: !Build.MV3 && copyFromBlobUrl_mv2 ? blobRef_mv2 : url, t: text, b: Build.BTypes && !(Build.BTypes & (Build.BTypes - 1)) ? Build.BTypes as number : OnOther_ }, buffer!) diff --git a/background/ports.ts b/background/ports.ts index 90df6a045..7b20566c7 100644 --- a/background/ports.ts +++ b/background/ports.ts @@ -1,13 +1,13 @@ import { needIcon_, cPort, set_cPort, reqH_, contentPayload_, omniPayload_, innerCSS_, extAllowList_, framesForTab_, findCSS_, framesForOmni_, getNextFakeTabId, curTabId_, vomnibarPage_f, OnChrome, CurCVer_, OnEdge, setIcon_, lastKeptTabId_, - keyFSM_, mappedKeyRegistry_, CONST_, mappedKeyTypes_, recencyForTab_, setTeeTask_, OnFirefox, UseZhLang_, blank_, - set_lastKeptTabId_, omniConfVer_, contentConfVer_, saveRecency_ + keyFSM_, mappedKeyRegistry_, CONST_, mappedKeyTypes_, recencyForTab_, replaceTeeTask_, OnFirefox, UseZhLang_, blank_, + set_lastKeptTabId_, omniConfVer_, contentConfVer_, saveRecency_, set_offscreenPort_, teeTask_ } from "./store" import { asyncIter_, deferPromise_, getOmniSecret_, isNotPriviledged, keys_ } from "./utils" import { removeTempTab, tabsGet, runtimeError_, getCurTab, getTabUrl, browserWebNav_, Q_, executeScript_, getFindCSS_cr_, - selectTab, selectWndIfNeed + selectTab, selectWndIfNeed, browser_ } from "./browser" import { exclusionListening_, getExcluded_, exclusionListenHash_ } from "./exclusions" import { I18nNames, transEx_ } from "./i18n" @@ -232,32 +232,43 @@ const onOmniDisconnect = (port: Port): void => { return runtimeError_() } -const _onPageConnect = (port: Port, type: PortType): void => { +export const postTeeTask_ = (port: Frames.BrowserPort, task: NonNullable): void => { + ; (port as Port).postMessage({ N: kBgReq.omni_runTeeTask, t: task.t, s: task.s }) +} + +const onTeeResult_ = (res: any): void => { + const task = replaceTeeTask_(null, null) + if (task) { + clearTimeout(task.i) + task.r && task.r(res) + } +} +const markTeeFail_ = (): void => { onTeeResult_(false) } +export const resetOffscreenPort_ = (): void => { + set_offscreenPort_(null); onTeeResult_(false) + browser_.offscreen.closeDocument(runtimeError_) +} + +const _onPageConnect = (port: Frames.BrowserPort, type: PortType): void => { if (type & PortType.otherExtension) { port.disconnect() return } - (port as Frames.Port).s = false as never if (type & PortType.Tee) { - let taskOnce = setTeeTask_(null, null) - if (taskOnce && taskOnce.t) { - taskOnce.d = null - port.postMessage({ N: kBgReq.omni_runTeeTask, t: taskOnce.t, s: taskOnce.s }) - const callback = (res: any): void => { - if (taskOnce) { - clearTimeout(taskOnce.i) - taskOnce.r && taskOnce.r(res) - } - taskOnce = null - } - port.onMessage.addListener(callback) - port.onDisconnect.addListener((): void => { callback(false) }) + if (teeTask_) { + const isOffscreen = Build.MV3 && type & PortType.Offscreen + Build.MV3 || (teeTask_.d = null) + port.onMessage.addListener(onTeeResult_) + postTeeTask_(port, teeTask_) + port.onDisconnect.addListener(isOffscreen ? resetOffscreenPort_ : markTeeFail_) + isOffscreen && set_offscreenPort_(port) } else { port.disconnect() } return } - port.onMessage.addListener(onMessage) + (port as Frames.Port).s = false as never + ; (port as Port).onMessage.addListener(onMessage) } const formatPortSender = (port: Port): Frames.Sender => { diff --git a/background/request_handlers.ts b/background/request_handlers.ts index b8fa66bc8..5ffc791fa 100644 --- a/background/request_handlers.ts +++ b/background/request_handlers.ts @@ -1,7 +1,7 @@ import { set_cPort, set_cRepeat, set_cOptions, needIcon_, set_cKey, cKey, get_cOptions, set_reqH_, reqH_, restoreSettings_, innerCSS_, framesForTab_, cRepeat, curTabId_, Completion_, CurCVer_, OnChrome, OnEdge, OnFirefox, setIcon_, blank_, - substitute_, paste_, keyToCommandMap_, CONST_, copy_, set_cEnv, settingsCache_, vomnibarBgOptions_, setTeeTask_, + substitute_, paste_, keyToCommandMap_, CONST_, copy_, set_cEnv, settingsCache_, vomnibarBgOptions_, replaceTeeTask_, curIncognito_, inlineRunKey_, CurFFVer_, Origin2_, focusAndExecuteOn_, set_focusAndExecuteOn_, curWndId_ } from "./store" import * as BgUtils_ from "./utils" @@ -620,7 +620,7 @@ set_reqH_([ runNextCmdBy(1, req.c.o) }, /** kFgReq.recheckTee: */ (): FgRes[kFgReq.recheckTee] => { - const taskOnce = setTeeTask_(null, null) + const taskOnce = replaceTeeTask_(null, null) if (taskOnce) { clearTimeout(taskOnce.i) taskOnce.r && taskOnce.r(false) @@ -778,7 +778,7 @@ Build.MV3 || (( // @ts-ignore window as BgExports ).onPagesReq = (req): Promise => { if (req.i === GlobalConsts.TeeReqId) { - const teeTask = setTeeTask_(null, null) + const teeTask = replaceTeeTask_(null, null) teeTask && clearTimeout(teeTask.i) return teeTask as never } diff --git a/background/store.ts b/background/store.ts index 2a4bb70b1..178d98838 100644 --- a/background/store.ts +++ b/background/store.ts @@ -150,7 +150,8 @@ export let runOneMapping_: (key: string, port: Port | null, fStatus: NonNullable export let inlineRunKey_: (rootRegistry: Writable, path?: CommandsNS.Item[]) => void export let focusAndExecuteOn_: (port: Port, cmd: T, options: CmdOptions[T], count: number , focusAndShowFrameBorder: BOOL) => void -let _teeTask: BaseTeeTask & { /** unique id */ i: number } | null = null +export let teeTask_: BaseTeeTask & { /** unique id */ i: number } | null = null +export let offscreenPort_: Frames.BrowserPort | null = null //#endregion //#region variable setter @@ -205,11 +206,12 @@ export const set_installation_ = (_newInstallation: typeof installation_): void export const set_runOneMapping_ = (_newF: typeof runOneMapping_): void => { runOneMapping_ = _newF } export const set_inlineRunKey_ = (_newInlineRunKey: typeof inlineRunKey_): void => { inlineRunKey_ = _newInlineRunKey } export const set_focusAndExecuteOn_ = (_newFAE: typeof focusAndExecuteOn_): void => { focusAndExecuteOn_ = _newFAE } -export const setTeeTask_ = (expected: number | null, newTask: typeof _teeTask): typeof _teeTask => { - const old = _teeTask, matches = !expected || old && old.i === expected - _teeTask = matches ? newTask : old +export const replaceTeeTask_ = (expected: number | null, newTask: typeof teeTask_): typeof teeTask_ => { + const old = teeTask_, matches = !expected || old && old.i === expected + teeTask_ = matches ? newTask : old return matches ? old : null } +export const set_offscreenPort_ = (_newOffscrPort: typeof offscreenPort_): void => { offscreenPort_ = _newOffscrPort} //#endregion //#region some shared util functions @@ -263,6 +265,7 @@ export const CONST_ = { GitVer: BuildStr.Commit as string, Injector_: "/lib/injector.js", TeeFrame_: "/front/vomnibar-tee.html", + OffscreenFrame_: "/front/offscreen.html", HelpDialogJS: "/background/help_dialog.js" as const, OptionsPage_: GlobalConsts.OptionsPage as string, Platform_: "browser", BrowserName_: "", HomePage_: "https://github.com/gdh1995/vimium-c", diff --git a/front/offscreen.html b/front/offscreen.html new file mode 100644 index 000000000..b98de5279 --- /dev/null +++ b/front/offscreen.html @@ -0,0 +1,2 @@ + + diff --git a/front/tee.ts b/front/tee.ts index c69e8c040..c9752e307 100644 --- a/front/tee.ts +++ b/front/tee.ts @@ -9,28 +9,55 @@ : !!(mayBrowser_ && mayBrowser_.runtime && mayBrowser_.runtime.connect) const browser_ = useBrowser ? (browser as typeof chrome) : chrome const runtime = browser_.runtime + const isOffscreen = !!Build.MV3 && location.pathname.endsWith("offscreen.html") const destroy = (): void => { - window !== top && (parent as Window).focus() + isOffscreen || (parent as Window).focus() window.closed || window.close() port = null } const onTask = (_response: BaseTeeTask): void => { type TaskTypes = K extends keyof TeeTasks ? Req.tee : never let onFinish = (ok: boolean | string): void => { + okResult = true if (Build.MV3 || port) { (port as any).postMessage(ok) } else { resolve!(ok) } - setTimeout(destroy, 0) // try to avoid a strange crashes on Chrome 103 + isOffscreen || setTimeout(destroy, 0) // try to avoid a strange crashes on Chrome 103 } const { t: taskId, s: serialized, d: data, r: resolve } = _response as TaskTypes const runTask = (): void | Promise => { + // MV3 + // || OnChrome && chromeVer_ >= MinEnsured$Clipboard$$write$and$ClipboardItem + // || OnFirefox && taskId === kTeeTask.DrawAndCopy if (Build.MV3) { switch (taskId) { case kTeeTask.Copy: case kTeeTask.Paste: const navClip = navigator.clipboard! + if (!(Build.BTypes & ~BrowserType.Chrome) && Build.MinCVer >= BrowserVer.MinOffscreenAPIs || isOffscreen) { + const doc = document, textArea = doc.createElement("textarea") + if (taskId === kTeeTask.Copy) { + textArea.value = serialized + doc.body!.appendChild(textArea) + textArea.select() + doc.execCommand("copy") + textArea.remove() + textArea.value = "" + } else { + const newLenLimit = serialized < 0 ? -1 - serialized : serialized + textArea.maxLength = newLenLimit || GlobalConsts.MaxBufferLengthForPastingNormalText + doc.body!.appendChild(textArea) + textArea.focus() + doc.execCommand("paste") + okResult = textArea.value.slice(0, newLenLimit || GlobalConsts.MaxBufferLengthForPastingNormalText) + textArea.value = "" + textArea.remove() + textArea.removeAttribute("maxlength") + } + return Promise.resolve() + } return taskId === kTeeTask.Copy ? navClip.writeText!(serialized) : navClip.readText!().then((result): void => { okResult = result }) case kTeeTask.Download: @@ -109,9 +136,9 @@ }) : onFinish(false) } let okResult: true | string = true - document.hasFocus() ? onFocus() : (window.onfocus = onFocus, window.focus()) + ; (isOffscreen || document.hasFocus()) ? onFocus() : (window.onfocus = onFocus, window.focus()) } - let port: chrome.runtime.Port | null, once = false + let port: chrome.runtime.Port | null, refusedMoreMessages = false if (!Build.MV3) { const getBg = browser_.extension.getBackgroundPage const bg = getBg && getBg() as unknown as BgExports | null @@ -124,7 +151,7 @@ } } try { - port = runtime.connect({ name: "" + (PortType.selfPages | PortType.Tee) }) + port = runtime.connect({ name: "" + (PortType.selfPages | PortType.Tee | (isOffscreen ? PortType.Offscreen : 0)) }) port.onDisconnect.addListener(destroy) } catch { destroy() @@ -132,11 +159,11 @@ } port.onMessage.addListener((_response: unknown): void => { const response = _response as Req.bg - if (response.N !== kBgReq.omni_runTeeTask || once) { + if (response.N !== kBgReq.omni_runTeeTask || refusedMoreMessages) { Build.NDEBUG || console.log("Vimium C: error: unknown message:", response) destroy() } else { - once = true + refusedMoreMessages = !isOffscreen onTask(response) } }) diff --git a/gulpfile.js b/gulpfile.js index d32839582..cc802c696 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -102,6 +102,7 @@ var Tasks = { }, "minify-html": function() { const arr = ["front/*.html", "pages/*.html", "!*/vomnibar.html"]; + if (!getBuildItem("MV3")) { arr.push("!*/offscreen.html") } if (!getBuildItem("NDEBUG")) { return copyByPath(arr) } return copyByPath(arr, file => { ToBuffer(file, require("html-minifier").minify(ToString(file), { collapseWhitespace: true, @@ -145,7 +146,7 @@ var Tasks = { "build/_clean_diff": function() { return cleanByPath([".build/**", "manifest.json", "*/vomnibar.html", "background/*.html", ".*.build" , ...JSON_TO_JS, "*/*.html", "*/*.css", "**/*.json", "**/*.js", "!helpers/*/*.js", ".snapshot.sh", LOCALES_EN - ], DEST) + , "*/offscreen.html"], DEST) }, "build/_all": ["build/scripts", "build/pages"], "build/ts": function(cb) { diff --git a/typings/base/base.d.ts b/typings/base/base.d.ts index b106a96e9..d72f38a62 100644 --- a/typings/base/base.d.ts +++ b/typings/base/base.d.ts @@ -246,6 +246,17 @@ declare namespace chrome.runtime { export const getFrameId: ((frame: Window | HTMLIFrameElement | HTMLFrameElement) => number) | undefined } +declare namespace chrome.offscreen { + export type kReason = "CLIPBOARD" | "MATCH_MEDIA" | "BLOBS" + export const Reason: { + BLOBS: "BLOBS", + CLIPBOARD: "CLIPBOARD", + MATCH_MEDIA?: "MATCH_MEDIA", + } + export function createDocument(args: { reasons: kReason[], url: string, justification: string }, cb: () => void): void + export function closeDocument(callback: (_fake: FakeArg) => void): void +} + declare module chrome.downloads { export interface DownloadOptions { url: string diff --git a/typings/compatibility.d.ts b/typings/compatibility.d.ts index 29c332fb5..fd6ea4399 100644 --- a/typings/compatibility.d.ts +++ b/typings/compatibility.d.ts @@ -609,6 +609,7 @@ declare const enum BrowserVer { Min$downloads$$setUiOptions = 105, // require `downloads.ui` MinURLPatternWith$ignoreCase = 107, // even if EXP or LEGACY MinMaybePopoverWith$popovershow = 109, // if EXP; use popovershow/popoverhide instead of toggle + MinOffscreenAPIs = 109, // even if EXP or LEGACY MinBgWorkerAliveIfOnlyAnyAction = 110, MinCSAcceptWorldInManifest = 111, // even if EXP or LEGACY MinMaybePopoverToggleEvent = 112, // if EXP diff --git a/typings/messages.d.ts b/typings/messages.d.ts index b7dc82432..c6a45b3ba 100644 --- a/typings/messages.d.ts +++ b/typings/messages.d.ts @@ -221,7 +221,7 @@ interface BaseTeeTask { interface ImageToCopy { /** url for binary data */ u: string, /** text */ t: string, /** browser */ b?: BrowserType } interface TeeTasks { [kTeeTask.Copy]: { s: string, d: null } - [kTeeTask.Paste]: { s: null | /** permitted */ true, d: null } + [kTeeTask.Paste]: { /** negative means permitted */ s: number, d: null } [kTeeTask.CopyImage]: { s: ImageToCopy, d: Blob } [kTeeTask.Download]: { s: ImageToCopy, d: null } [kTeeTask.DrawAndCopy]: { s: ImageToCopy, d: Blob } diff --git a/typings/vimium_c.d.ts b/typings/vimium_c.d.ts index 3c5725c66..93a3fbd6a 100644 --- a/typings/vimium_c.d.ts +++ b/typings/vimium_c.d.ts @@ -283,7 +283,7 @@ declare const enum PortNameEnum { declare const enum PortType { initing = 0, isTop = 1, hasFocus = 2, confInherited = 4, reconnect = 8, hasCSS = 16, onceFreezed = 32, aboutIframe = 64, selfPages = 128, omnibar = 256, refreshInBatch = 512, - otherExtension = 1024, Tee = 2048, OFFSET_SETTINGS = /** log2(4096) */ 12, + otherExtension = 1024, Tee = 2048, Offscreen = 4096, OFFSET_SETTINGS = /** log2(8192) */ 13, /** for external extensions like NewTab Adapter */ CloseSelf = 999, }