Use asyncrhonous messages to deliver SyncMessage payloads on Firefox.
This commit is contained in:
parent
534ab54c28
commit
d196982cd5
|
@ -7,14 +7,33 @@
|
||||||
if (typeof browser.runtime.onSyncMessage !== "object") {
|
if (typeof browser.runtime.onSyncMessage !== "object") {
|
||||||
// Background Script side
|
// Background Script side
|
||||||
|
|
||||||
// cache of senders from early async messages to track tab ids in Firefox
|
|
||||||
let pending = new Map();
|
let pending = new Map();
|
||||||
if (MOZILLA) {
|
if (MOZILLA) {
|
||||||
// we don't care this is async, as long as it get called before the
|
// we don't care this is async, as long as it get called before the
|
||||||
// sync XHR (we are not interested in the response on the content side)
|
// sync XHR (we are not interested in the response on the content side)
|
||||||
browser.runtime.onMessage.addListener((m, sender) => {
|
browser.runtime.onMessage.addListener((m, sender) => {
|
||||||
if (!m.___syncMessageId) return;
|
let wrapper = m.__syncMessage__;
|
||||||
pending.set(m.___syncMessageId, sender);
|
if (!wrapper) return;
|
||||||
|
let {id} = wrapper;
|
||||||
|
pending.set(id, wrapper);
|
||||||
|
let result;
|
||||||
|
let unsuspend = result => {
|
||||||
|
pending.delete(id);
|
||||||
|
if (wrapper.unsuspend) {
|
||||||
|
setTimeout(wrapper.unsuspend, 0);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
result = notifyListeners(JSON.stringify(wrapper.payload), sender);
|
||||||
|
} catch(e) {
|
||||||
|
unsuspend();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
console.debug("sendSyncMessage: returning", result);
|
||||||
|
return (result instanceof Promise ? result
|
||||||
|
: new Promise(resolve => resolve(result))
|
||||||
|
).then(result => unsuspend(result));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +45,7 @@
|
||||||
|
|
||||||
|
|
||||||
let obrListener = request => {
|
let obrListener = request => {
|
||||||
|
try {
|
||||||
let {url, tabId} = request;
|
let {url, tabId} = request;
|
||||||
let params = new URLSearchParams(url.split("?")[1]);
|
let params = new URLSearchParams(url.split("?")[1]);
|
||||||
let msgId = params.get("id");
|
let msgId = params.get("id");
|
||||||
|
@ -33,91 +53,73 @@
|
||||||
return asyncRet(msgId);
|
return asyncRet(msgId);
|
||||||
}
|
}
|
||||||
let msg = params.get("msg");
|
let msg = params.get("msg");
|
||||||
|
|
||||||
|
if (MOZILLA || tabId === TAB_ID_NONE) {
|
||||||
|
// this shoud be a mozilla suspension request
|
||||||
|
return params.get("suspend") ? new Promise(resolve => {
|
||||||
|
if (pending.has(msgId)) {
|
||||||
|
let wrapper = pending.get(msgId);
|
||||||
|
if (!wrapper.unsuspend) {
|
||||||
|
wrapper.unsuspend = resolve;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
}).then(() => ret("go on"))
|
||||||
|
: CANCEL; // otherwise, bail
|
||||||
|
}
|
||||||
|
// CHROME from now on
|
||||||
let documentUrl = params.get("url");
|
let documentUrl = params.get("url");
|
||||||
let suspension = !!params.get("suspend");
|
let {frameAncestors, frameId} = request;
|
||||||
let sender;
|
let isTop = frameId === 0 || !!params.get("top");
|
||||||
if (tabId === TAB_ID_NONE || suspension) {
|
let tabUrl = frameAncestors && frameAncestors.length
|
||||||
// Firefox sends privileged content script XHR without valid tab ids
|
&& frameAncestors[frameAncestors.length - 1].url;
|
||||||
// so we cache sender info from unprivileged XHR correlated by msgId
|
|
||||||
if (pending.has(msgId)) {
|
if (!tabUrl) {
|
||||||
sender = pending.get(msgId);
|
if (isTop) {
|
||||||
if (suspension) { // we hold any script execution / DOM modification on this promise
|
tabUrlCache.set(tabId, tabUrl = documentUrl);
|
||||||
return new Promise(resolve => {
|
if (!tabRemovalListener) {
|
||||||
sender.unsuspend = resolve;
|
browser.tabs.onRemoved.addListener(tabRemovalListener = tab => {
|
||||||
|
tabUrlCache.delete(tab.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (sender.unsuspend) {
|
|
||||||
let {unsuspend} = sender;
|
|
||||||
delete sender.unsuspend;
|
|
||||||
setTimeout(unsuspend(ret("unsuspend")), 0);
|
|
||||||
}
|
|
||||||
pending.delete(msgId);
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`sendSyncMessage: cannot correlate sender info for ${msgId}.`);
|
tabUrl = tabUrlCache.get(tabId);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
let {frameAncestors, frameId} = request;
|
|
||||||
let isTop = frameId === 0 || !!params.get("top");
|
|
||||||
let tabUrl = frameAncestors && frameAncestors.length
|
|
||||||
&& frameAncestors[frameAncestors.length - 1].url;
|
|
||||||
|
|
||||||
if (!tabUrl) {
|
|
||||||
if (isTop) {
|
|
||||||
tabUrlCache.set(tabId, tabUrl = documentUrl);
|
|
||||||
if (!tabRemovalListener) {
|
|
||||||
browser.tabs.onRemoved.addListener(tabRemovalListener = tab => {
|
|
||||||
tabUrlCache.delete(tab.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tabUrl = tabUrlCache.get(tabId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sender = {
|
|
||||||
tab: {
|
|
||||||
id: tabId,
|
|
||||||
url: tabUrl
|
|
||||||
},
|
|
||||||
frameId,
|
|
||||||
url: documentUrl,
|
|
||||||
timeStamp: Date.now()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
let sender = {
|
||||||
|
tab: {
|
||||||
|
id: tabId,
|
||||||
|
url: tabUrl
|
||||||
|
},
|
||||||
|
frameId,
|
||||||
|
url: documentUrl,
|
||||||
|
timeStamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
if (!(msg !== null && sender)) {
|
if (!(msg !== null && sender)) {
|
||||||
return CANCEL;
|
return CANCEL;
|
||||||
}
|
}
|
||||||
// Just like in the async runtime.sendMessage() API,
|
let result = notifyListeners(msg, sender);
|
||||||
// we process the listeners in order until we find a not undefined
|
|
||||||
// result, then we return it (or undefined if none returns anything).
|
|
||||||
let result;
|
|
||||||
for (let l of listeners) {
|
|
||||||
try {
|
|
||||||
if ((result = l(JSON.parse(msg), sender)) !== undefined) break;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("%o processing message %o from %o", e, msg, sender);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (result instanceof Promise) {
|
if (result instanceof Promise) {
|
||||||
if (MOZILLA) {
|
|
||||||
// Firefox supports asynchronous webRequest listeners, so we can
|
// On Chromium, if the promise is not resolved yet,
|
||||||
// just defer the return
|
// we redirect the XHR to the same URL (hence same msgId)
|
||||||
return (async () => ret(await result))();
|
// while the result get cached for asynchronous retrieval
|
||||||
} else {
|
result.then(r => {
|
||||||
// On Chromium, if the promise is not resolved yet,
|
asyncResults.set(msgId, result = r);
|
||||||
// we redirect the XHR to the same URL (hence same msgId)
|
});
|
||||||
// while the result get cached for asynchronous retrieval
|
return asyncResults.has(msgId)
|
||||||
result.then(r => {
|
? asyncRet(msgId) // promise was already resolved
|
||||||
asyncResults.set(msgId, result = r);
|
: {redirectUrl: url.replace(
|
||||||
});
|
/&redirects=(\d+)|$/, // redirects count to avoid loop detection
|
||||||
return asyncResults.has(msgId)
|
(all, count) => `&redirects=${parseInt(count) + 1 || 1}`)};
|
||||||
? asyncRet(msgId) // promise was already resolved
|
|
||||||
: {redirectUrl: url.replace(
|
|
||||||
/&redirects=(\d+)|$/, // redirects count to avoid loop detection
|
|
||||||
(all, count) => `&redirects=${parseInt(count) + 1 || 1}`)};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ret(result);
|
return ret(result);
|
||||||
};
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
return CANCEL;
|
||||||
|
} };
|
||||||
|
|
||||||
let ret = r => ({redirectUrl: `data:application/json,${JSON.stringify(r)}`})
|
let ret = r => ({redirectUrl: `data:application/json,${JSON.stringify(r)}`})
|
||||||
let asyncRet = msgId => {
|
let asyncRet = msgId => {
|
||||||
|
@ -127,6 +129,19 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let listeners = new Set();
|
let listeners = new Set();
|
||||||
|
function notifyListeners(msg, sender) {
|
||||||
|
// Just like in the async runtime.sendMessage() API,
|
||||||
|
// we process the listeners in order until we find a not undefined
|
||||||
|
// result, then we return it (or undefined if none returns anything).
|
||||||
|
for (let l of listeners) {
|
||||||
|
try {
|
||||||
|
let result = l(JSON.parse(msg), sender);
|
||||||
|
if (result !== undefined) return result;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("%o processing message %o from %o", e, msg, sender);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
browser.runtime.onSyncMessage = {
|
browser.runtime.onSyncMessage = {
|
||||||
ENDPOINT_PREFIX,
|
ENDPOINT_PREFIX,
|
||||||
addListener(l) {
|
addListener(l) {
|
||||||
|
@ -163,30 +178,46 @@
|
||||||
// about frameAncestors
|
// about frameAncestors
|
||||||
url += "&top=true";
|
url += "&top=true";
|
||||||
}
|
}
|
||||||
let finalizers = [];
|
|
||||||
if (MOZILLA) {
|
if (MOZILLA) {
|
||||||
// on Firefox we first need to send an async message telling the
|
// on Firefox we first need to send an async message telling the
|
||||||
// background script about the tab ID, which does not get sent
|
// background script about the tab ID, which does not get sent
|
||||||
// with "privileged" XHR
|
// with "privileged" XHR
|
||||||
browser.runtime.sendMessage({___syncMessageId: msgId});
|
let result;
|
||||||
|
browser.runtime.sendMessage(
|
||||||
|
{__syncMessage__: {id: msgId, payload: msg}}
|
||||||
|
).then(r => {
|
||||||
|
result = r;
|
||||||
|
}).catch(e => {
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
// In order to cope with inconsistencies in XHR synchronicity,
|
// In order to cope with inconsistencies in XHR synchronicity,
|
||||||
// allowing DOM element to be inserted and script to be executed
|
// allowing DOM element to be inserted and script to be executed
|
||||||
// (seen with file:// and ftp:// loads) we additionally suspend on
|
// (seen with file:// and ftp:// loads) we additionally suspend on
|
||||||
// Mutation notifications and beforescriptexecute events
|
// Mutation notifications and beforescriptexecute events
|
||||||
let suspendURL = url + "&suspend";
|
let suspendURL = url + "&suspend=true";
|
||||||
|
let suspended = false;
|
||||||
let suspend = () => {
|
let suspend = () => {
|
||||||
let r = new XMLHttpRequest();
|
if (result || suspended) return;
|
||||||
r.open("GET", url, false);
|
suspended = true;
|
||||||
r.send(null);
|
try {
|
||||||
|
let r = new XMLHttpRequest();
|
||||||
|
r.open("GET", suspendURL, false);
|
||||||
|
r.send(null);
|
||||||
|
} finally {
|
||||||
|
suspended = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let domSuspender = new MutationObserver(suspend);
|
let domSuspender = new MutationObserver(suspend);
|
||||||
domSuspender.observe(document.documentElement, {childList: true});
|
domSuspender.observe(document.documentElement, {childList: true});
|
||||||
addEventListener("beforescriptexecute", suspend, true);
|
addEventListener("beforescriptexecute", suspend, true);
|
||||||
finalizers.push(() => {
|
try {
|
||||||
|
suspend();
|
||||||
|
} finally {
|
||||||
removeEventListener("beforescriptexecute", suspend, true);
|
removeEventListener("beforescriptexecute", suspend, true);
|
||||||
domSuspender.disconnect();
|
domSuspender.disconnect();
|
||||||
});
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
// then we send the payload using a privileged XHR, which is not subject
|
// then we send the payload using a privileged XHR, which is not subject
|
||||||
// to CORS but unfortunately doesn't carry any tab id except on Chromium
|
// to CORS but unfortunately doesn't carry any tab id except on Chromium
|
||||||
|
@ -199,8 +230,6 @@
|
||||||
return JSON.parse(r.responseText);
|
return JSON.parse(r.responseText);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(`syncMessage error in ${document.URL}: ${e.message} (response ${r.responseText})`);
|
console.error(`syncMessage error in ${document.URL}: ${e.message} (response ${r.responseText})`);
|
||||||
} finally {
|
|
||||||
for (let f of finalizers) try { f(); } catch(e) { console.error(e); }
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue