Skip to content

Commit

Permalink
MV3: use offscreen document to access textarea if possible
Browse files Browse the repository at this point in the history
  • Loading branch information
gdh1995 committed Jul 2, 2024
1 parent 2ebc5ba commit 96c28fc
Show file tree
Hide file tree
Showing 12 changed files with 146 additions and 50 deletions.
2 changes: 1 addition & 1 deletion background/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ set_paste_(((sed, newLenLimit?: number, exOut?: InfoOnSed): string | Promise<str
return reformat_(readInnerClipboard_(clip), null, exOut)
}
if (Build.MV3 || OnFirefox && (navClipboard || (navClipboard = navigator.clipboard))) {
return (Build.MV3 ? runOnTee_(kTeeTask.Paste, null, null)
return (Build.MV3 ? runOnTee_(kTeeTask.Paste, newLenLimit || 0, null)
: navClipboard!.readText!().catch(() => null)).then(s => typeof s === "string"
? s && reformat_(s.slice(0, GlobalConsts.MaxBufferLengthForPastingLongURL), sed, exOut) : null)
}
Expand Down
66 changes: 53 additions & 13 deletions background/frame_commands.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
Expand All @@ -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<boolean | string> => {
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 - <number> 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_<boolean | string>()
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(<T> (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)
Expand All @@ -56,7 +96,7 @@ set_runOnTee_(((task, serializable, data): Promise<boolean | string> => {
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 => {
Expand Down Expand Up @@ -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<Result> => {
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!)
Expand Down
49 changes: 30 additions & 19 deletions background/ports.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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<typeof teeTask_>): 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 => {
Expand Down
6 changes: 3 additions & 3 deletions background/request_handlers.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -778,7 +778,7 @@ Build.MV3 || (( // @ts-ignore
window as BgExports
).onPagesReq = (req): Promise<FgRes[kFgReq.pages]> => {
if (req.i === GlobalConsts.TeeReqId) {
const teeTask = setTeeTask_(null, null)
const teeTask = replaceTeeTask_(null, null)
teeTask && clearTimeout(teeTask.i)
return teeTask as never
}
Expand Down
11 changes: 7 additions & 4 deletions background/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ export let runOneMapping_: (key: string, port: Port | null, fStatus: NonNullable
export let inlineRunKey_: (rootRegistry: Writable<CommandsNS.Item>, path?: CommandsNS.Item[]) => void
export let focusAndExecuteOn_: <T extends FgCmdAcrossFrames> (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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions front/offscreen.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!DOCTYPE html>
<script src="tee.js"></script>
41 changes: 34 additions & 7 deletions front/tee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> = K extends keyof TeeTasks ? Req.tee<K> : 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<keyof TeeTasks>
const runTask = (): void | Promise<unknown> => {
// 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:
Expand Down Expand Up @@ -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
Expand All @@ -124,19 +151,19 @@
}
}
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()
return
}
port.onMessage.addListener((_response: unknown): void => {
const response = _response as Req.bg<kBgReq>
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)
}
})
Expand Down
3 changes: 2 additions & 1 deletion gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
11 changes: 11 additions & 0 deletions typings/base/base.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions typings/compatibility.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 96c28fc

Please sign in to comment.