diff --git a/README.md b/README.md index f188e519..b000d558 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ Userscripts currently supports the following api methods. All methods are asynch > For the v4.5.x and earlier versions: > https://github.com/quoid/userscripts/tree/v4.5.4#api -For API type definitions, please refer to: [`types.d.ts`](https://github.com/userscriptsup/testscripts/blob/bfce18746cd6bcab0616727401fa7ab6ef4086ac/userscripts/types.d.ts) +For API type definitions, please refer to: [`types.d.ts`](https://github.com/userscriptsup/testscripts/blob/f2fcde4b556fa436fe806a44a89afb9eb5dccd0b/userscripts/types.d.ts) - `GM.addStyle(css)` - `css: String` @@ -278,6 +278,19 @@ For API type definitions, please refer to: [`types.d.ts`](https://github.com/use - `data: String | Blob | ArrayBuffer | TypedArray | DataView | FormData | URLSearchParams` - optional - `responseType: String` - optional - refer to [`XMLHttpRequests`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) + - upload event handlers: + - `upload: Object` - optional + - `onabort: Function` - optional + - `onerror: Function` - optional + - `onload: Function` - optional + - `onloadend: Function` - optional + - `onloadstart: Function` - optional + - `onprogress: Function` - optional + - `ontimeout: Function` - optional + - the progress object passed to the event handlers has the following properties: + - `lengthComputable` + - `loaded` + - `total` - event handlers: - `onabort: Function` - optional - `onerror: Function` - optional diff --git a/src/ext/background/main.js b/src/ext/background/main.js index c542d2d3..761eda72 100644 --- a/src/ext/background/main.js +++ b/src/ext/background/main.js @@ -364,6 +364,7 @@ async function handleMessage(message, sender) { // initializing an xhr instance const xhr = new XMLHttpRequest(); // establish a long-lived port connection to content script + /** @type {import("../global.d.ts").TypeBackground.XHRPort} */ const port = browser.tabs.connect(sender.tab.id, { name: message.xhrPortName, }); @@ -465,7 +466,21 @@ async function handleMessage(message, sender) { // transfer to content script via text and then parse to document if (responseType === "document") xhr.responseType = "text"; // add required listeners and send result back to the content script - const handlers = details.hasHandlers || {}; + if (details.hasUploadHandlers) { + for (const handler of Object.keys(details.hasUploadHandlers)) { + /** @param {ProgressEvent} event */ + xhr.upload[handler] = async (event) => { + /** @type {TypeExtMessages.XHRProgress} */ + const progress = { + lengthComputable: event.lengthComputable, + loaded: event.loaded, + total: event.total, + }; + port.postMessage({ handler, progress }); + }; + } + } + const handlers = details.hasHandlers ?? {}; for (const handler of Object.keys(handlers)) { xhr[handler] = async () => { // can not send xhr through postMessage @@ -513,7 +528,7 @@ async function handleMessage(message, sender) { // if onloadend not set in xhr details // onloadend event won't be passed to content script // if that happens port DISCONNECT message won't be posted - // if details lacks onloadend attach listener + // so if details lacks onloadend then attach the listener if (!handlers.onloadend) { xhr.onloadend = () => { port.postMessage({ handler: "onloadend" }); diff --git a/src/ext/content-scripts/api.js b/src/ext/content-scripts/api.js index fc7c447a..e2bf4cdb 100644 --- a/src/ext/content-scripts/api.js +++ b/src/ext/content-scripts/api.js @@ -296,6 +296,7 @@ async function xhr(details, control, promise) { url: String(details.url), user: String(details.user), hasHandlers: {}, + hasUploadHandlers: {}, }; // preprocess data key try { @@ -340,6 +341,30 @@ async function xhr(details, control, promise) { handlers[handler] = details[handler]; } } + // preprocess upload handlers + /** @type {TypeExtMessages.XHRUploadHandlersObj} */ + const uploadHandlers = {}; + /** @type {TypeExtMessages.XHRUploadHandlers} */ + const XHRUploadHandlers = [ + "onabort", + "onerror", + "onload", + "onloadend", + "onloadstart", + "onprogress", + "ontimeout", + ]; + if (typeof details.upload === "object") { + for (const handler of XHRUploadHandlers) { + if ( + handler in XMLHttpRequestEventTarget.prototype && + typeof details.upload[handler] === "function" + ) { + detailsParsed.hasUploadHandlers[handler] = true; + uploadHandlers[handler] = details.upload[handler]; + } + } + } // resolving asynchronous xmlHttpRequest if (promise) { detailsParsed.hasHandlers.onloadend = true; @@ -363,14 +388,26 @@ async function xhr(details, control, promise) { /** * port listener, most of the messaging logic goes here * @type {Parameters[0]} + * @param {import("../global.d.ts").TypeContentScripts.XHRPort} port */ const listener = (port) => { if (port.name !== xhrPortName) return; + // handle port messages port.onMessage.addListener(async (msg) => { - /** @type {TypeExtMessages.XHRHandlers[number]} */ const handler = msg.handler; + // handle upload progress + if ( + "progress" in msg && + detailsParsed.hasUploadHandlers[handler] && + typeof uploadHandlers[handler] === "function" + ) { + // call userscript handler + uploadHandlers[handler](msg.progress); + return; + } + // handle download events if ( - msg.response && + "response" in msg && detailsParsed.hasHandlers[handler] && typeof handlers[handler] === "function" ) { @@ -387,7 +424,7 @@ async function xhr(details, control, promise) { if (response.readyState === 4 && response.response !== null) { xhrResponseProcessor(msgResponse, response); } - // call userscript method + // call userscript handler handlers[handler](response); // call the deleted XHR.DONE handlers above if (response.readyState === 4) { diff --git a/src/ext/content-scripts/entry-userscripts.js b/src/ext/content-scripts/entry-userscripts.js index 1742d606..e0b0f7fb 100644 --- a/src/ext/content-scripts/entry-userscripts.js +++ b/src/ext/content-scripts/entry-userscripts.js @@ -198,7 +198,7 @@ async function injection() { // loop through each userscript @grant value, add methods as needed for (let j = 0; j < grants.length; j++) { const grant = grants[j]; - const method = grant.split(".")[1] || grant.split(".")[0]; + const method = grant.startsWith("GM.") ? grant.slice(3) : grant; // ensure API method exists in USAPI object if (!Object.keys(USAPI).includes(method)) continue; // add granted methods diff --git a/src/ext/global.d.ts b/src/ext/global.d.ts index 167d6d97..8b626e08 100644 --- a/src/ext/global.d.ts +++ b/src/ext/global.d.ts @@ -11,3 +11,31 @@ declare global { browser: Browser.Browser; } } + +declare namespace TypeBackground { + interface XHRMessage { + handler: string; + progress?: TypeExtMessages.XHRProgress; + response?: TypeExtMessages.XHRTransportableResponse; + } + + interface XHRPort extends Browser.Runtime.Port { + onMessage: Browser.Events.Event< + (message: TypeContentScripts.XHRMessage, port: XHRPort) => void + >; + postMessage: (message: XHRMessage) => void; + } +} + +declare namespace TypeContentScripts { + interface XHRMessage { + name: string; + } + + interface XHRPort extends Browser.Runtime.Port { + onMessage: Browser.Events.Event< + (message: TypeBackground.XHRMessage, port: XHRPort) => void + >; + postMessage(message: XHRMessage): void; + } +} diff --git a/src/ext/types.d.ts b/src/ext/types.d.ts index b194b29f..1ad82689 100644 --- a/src/ext/types.d.ts +++ b/src/ext/types.d.ts @@ -43,10 +43,24 @@ declare namespace TypeExtMessages { "onloadend", ]; + type XHRUploadHandlers = [ + "onabort", + "onerror", + "onload", + "onloadend", + "onloadstart", + "onprogress", + "ontimeout", + ]; + type XHRHandlersObj = { [handler in XHRHandlers[number]]?: (response: XHRResponse) => void; }; + type XHRUploadHandlersObj = { + [handler in XHRUploadHandlers[number]]?: (response: XHRProgress) => void; + }; + interface XHRTransportableDetails { binary: boolean; data: XHRProcessedData; @@ -59,6 +73,7 @@ declare namespace TypeExtMessages { url: string; user: string; hasHandlers: { [handler in XHRHandlers[number]]?: boolean }; + hasUploadHandlers: { [handler in XHRUploadHandlers[number]]?: boolean }; } interface XHRTransportableResponse { @@ -78,4 +93,10 @@ declare namespace TypeExtMessages { responseText?: string; responseXML?: Document; } + + interface XHRProgress { + lengthComputable: boolean; + loaded: number; + total: number; + } }