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") {
|
||||
// Background Script side
|
||||
|
||||
// cache of senders from early async messages to track tab ids in Firefox
|
||||
let pending = new Map();
|
||||
if (MOZILLA) {
|
||||
// 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)
|
||||
browser.runtime.onMessage.addListener((m, sender) => {
|
||||
if (!m.___syncMessageId) return;
|
||||
pending.set(m.___syncMessageId, sender);
|
||||
let wrapper = m.__syncMessage__;
|
||||
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 => {
|
||||
try {
|
||||
let {url, tabId} = request;
|
||||
let params = new URLSearchParams(url.split("?")[1]);
|
||||
let msgId = params.get("id");
|
||||
|
@ -33,91 +53,73 @@
|
|||
return asyncRet(msgId);
|
||||
}
|
||||
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 suspension = !!params.get("suspend");
|
||||
let sender;
|
||||
if (tabId === TAB_ID_NONE || suspension) {
|
||||
// Firefox sends privileged content script XHR without valid tab ids
|
||||
// so we cache sender info from unprivileged XHR correlated by msgId
|
||||
if (pending.has(msgId)) {
|
||||
sender = pending.get(msgId);
|
||||
if (suspension) { // we hold any script execution / DOM modification on this promise
|
||||
return new Promise(resolve => {
|
||||
sender.unsuspend = resolve;
|
||||
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);
|
||||
});
|
||||
}
|
||||
if (sender.unsuspend) {
|
||||
let {unsuspend} = sender;
|
||||
delete sender.unsuspend;
|
||||
setTimeout(unsuspend(ret("unsuspend")), 0);
|
||||
}
|
||||
pending.delete(msgId);
|
||||
} 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)) {
|
||||
return CANCEL;
|
||||
}
|
||||
// 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).
|
||||
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);
|
||||
}
|
||||
}
|
||||
let result = notifyListeners(msg, sender);
|
||||
if (result instanceof Promise) {
|
||||
if (MOZILLA) {
|
||||
// Firefox supports asynchronous webRequest listeners, so we can
|
||||
// just defer the return
|
||||
return (async () => ret(await result))();
|
||||
} else {
|
||||
// On Chromium, if the promise is not resolved yet,
|
||||
// we redirect the XHR to the same URL (hence same msgId)
|
||||
// while the result get cached for asynchronous retrieval
|
||||
result.then(r => {
|
||||
asyncResults.set(msgId, result = r);
|
||||
});
|
||||
return asyncResults.has(msgId)
|
||||
? asyncRet(msgId) // promise was already resolved
|
||||
: {redirectUrl: url.replace(
|
||||
/&redirects=(\d+)|$/, // redirects count to avoid loop detection
|
||||
(all, count) => `&redirects=${parseInt(count) + 1 || 1}`)};
|
||||
}
|
||||
|
||||
// On Chromium, if the promise is not resolved yet,
|
||||
// we redirect the XHR to the same URL (hence same msgId)
|
||||
// while the result get cached for asynchronous retrieval
|
||||
result.then(r => {
|
||||
asyncResults.set(msgId, result = r);
|
||||
});
|
||||
return asyncResults.has(msgId)
|
||||
? 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);
|
||||
};
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
return CANCEL;
|
||||
} };
|
||||
|
||||
let ret = r => ({redirectUrl: `data:application/json,${JSON.stringify(r)}`})
|
||||
let asyncRet = msgId => {
|
||||
|
@ -127,6 +129,19 @@
|
|||
}
|
||||
|
||||
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 = {
|
||||
ENDPOINT_PREFIX,
|
||||
addListener(l) {
|
||||
|
@ -163,30 +178,46 @@
|
|||
// about frameAncestors
|
||||
url += "&top=true";
|
||||
}
|
||||
let finalizers = [];
|
||||
if (MOZILLA) {
|
||||
// on Firefox we first need to send an async message telling the
|
||||
// background script about the tab ID, which does not get sent
|
||||
// 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,
|
||||
// allowing DOM element to be inserted and script to be executed
|
||||
// (seen with file:// and ftp:// loads) we additionally suspend on
|
||||
// Mutation notifications and beforescriptexecute events
|
||||
let suspendURL = url + "&suspend";
|
||||
let suspendURL = url + "&suspend=true";
|
||||
let suspended = false;
|
||||
let suspend = () => {
|
||||
let r = new XMLHttpRequest();
|
||||
r.open("GET", url, false);
|
||||
r.send(null);
|
||||
if (result || suspended) return;
|
||||
suspended = true;
|
||||
try {
|
||||
let r = new XMLHttpRequest();
|
||||
r.open("GET", suspendURL, false);
|
||||
r.send(null);
|
||||
} finally {
|
||||
suspended = false;
|
||||
}
|
||||
};
|
||||
let domSuspender = new MutationObserver(suspend);
|
||||
domSuspender.observe(document.documentElement, {childList: true});
|
||||
addEventListener("beforescriptexecute", suspend, true);
|
||||
finalizers.push(() => {
|
||||
try {
|
||||
suspend();
|
||||
} finally {
|
||||
removeEventListener("beforescriptexecute", suspend, true);
|
||||
domSuspender.disconnect();
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// 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
|
||||
|
@ -199,8 +230,6 @@
|
|||
return JSON.parse(r.responseText);
|
||||
} catch(e) {
|
||||
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;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue