Skip to content

Commit 4c2aebb

Browse files
committed
fix: add missing web request interception functionality
1 parent f7935c8 commit 4c2aebb

3 files changed

Lines changed: 246 additions & 1 deletion

File tree

manifest.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
"tabs",
6868
"storage",
6969
"nativeMessaging",
70-
"offscreen"
70+
"offscreen",
71+
"webRequest"
7172
],
7273

7374
"web_accessible_resources": [
@@ -97,6 +98,7 @@
9798
"sources/zotero-translate/src/translation/translate_item.js",
9899
"sources/zotero-translate/src/http.js",
99100
"sources/http.js",
101+
"sources/webRequestIntercept.js",
100102
"sources/translateWeb.js",
101103
"translators/zotero/*.js"
102104
]

sources/setupZotero.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,10 @@ import "./zotero-utilities/cachedTypes.js";
1616
import "./zotero-translate/src/utilities_translate.js";
1717
import "./zotero-translate/src/http.js";
1818
import "./http.js";
19+
import "./webRequestIntercept.js";
1920

2021
Zotero.setTypeSchema(ZOTERO_TYPE_SCHEMA);
22+
if (browser.webRequest) {
23+
// webRequest is not available some contexts, so check before initializing
24+
Zotero.WebRequestIntercept.init();
25+
}

sources/webRequestIntercept.js

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
// Taken from https://github.com/zotero/zotero-connectors/blob/92d2c56de53c82526bc68bab378b014dcc3b007a/src/browserExt/webRequestIntercept.js
2+
3+
/*
4+
***** BEGIN LICENSE BLOCK *****
5+
6+
Copyright © 2016 Center for History and New Media
7+
George Mason University, Fairfax, Virginia, USA
8+
http://zotero.org
9+
10+
This file is part of Zotero.
11+
12+
Zotero is free software: you can redistribute it and/or modify
13+
it under the terms of the GNU Affero General Public License as published by
14+
the Free Software Foundation, either version 3 of the License, or
15+
(at your option) any later version.
16+
17+
Zotero is distributed in the hope that it will be useful,
18+
but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
GNU Affero General Public License for more details.
21+
22+
You should have received a copy of the GNU Affero General Public License
23+
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
24+
25+
***** END LICENSE BLOCK *****
26+
*/
27+
28+
/**
29+
* Handles web request interception and header parsing
30+
*/
31+
32+
(function () {
33+
"use strict";
34+
35+
Zotero.WebRequestIntercept = {
36+
listeners: {
37+
beforeSendHeaders: [],
38+
headersReceived: [],
39+
errorOccurred: [],
40+
},
41+
42+
reqIDToReqMeta: {},
43+
44+
init: function () {
45+
const types = ["main_frame", "sub_frame"];
46+
let extraInfoSpec = ["requestHeaders"].concat(!Zotero.isManifestV3 ? ["blocking"] : []);
47+
browser.webRequest.onBeforeSendHeaders.addListener(
48+
Zotero.WebRequestIntercept.handleRequest("beforeSendHeaders"),
49+
{ urls: ["<all_urls>"], types },
50+
extraInfoSpec,
51+
);
52+
browser.webRequest.onErrorOccurred.addListener(Zotero.WebRequestIntercept.removeRequestMeta, {
53+
urls: ["<all_urls>"],
54+
types,
55+
});
56+
browser.webRequest.onCompleted.addListener(Zotero.WebRequestIntercept.removeRequestMeta, {
57+
urls: ["<all_urls>"],
58+
types,
59+
});
60+
extraInfoSpec = ["responseHeaders"].concat(!Zotero.isManifestV3 ? ["blocking"] : []);
61+
browser.webRequest.onHeadersReceived.addListener(
62+
Zotero.WebRequestIntercept.handleRequest("headersReceived"),
63+
{ urls: ["<all_urls>"], types },
64+
extraInfoSpec,
65+
);
66+
67+
Zotero.WebRequestIntercept.addListener(
68+
"beforeSendHeaders",
69+
Zotero.WebRequestIntercept.storeRequestHeaders,
70+
);
71+
Zotero.WebRequestIntercept.addListener(
72+
"headersReceived",
73+
Zotero.WebRequestIntercept.offerSavingPDFInFrame,
74+
);
75+
},
76+
77+
storeRequestHeaders: function (details, meta) {
78+
meta.requestHeadersObject = details.requestHeadersObject;
79+
},
80+
81+
offerSavingPDFInFrame: function (details) {
82+
if (details.frameId === 0) return;
83+
if (!details.responseHeadersObject["content-type"]) return;
84+
const contentType = details.responseHeadersObject["content-type"].split(";")[0];
85+
86+
// If no translators are found for the top frame or the first child frame, and some frame
87+
// contains a pdf, saving that PDF will be offered.
88+
if (contentType == "application/pdf") {
89+
setTimeout(() =>
90+
Zotero.Connector_Browser.onPDFFrame(details.url, details.frameId, details.tabId),
91+
);
92+
}
93+
},
94+
95+
removeRequestMeta: function (details) {
96+
delete Zotero.WebRequestIntercept.reqIDToReqMeta[details.requestId];
97+
},
98+
99+
/**
100+
* Convert from webRequest.HttpHeaders array to a lowercased object.
101+
*
102+
* headers = _processHeaders(details.requestHeaders)
103+
* console.log(headers['accept-charset']) // utf-8
104+
*
105+
* @param {Array} headerArray
106+
* @return {Object} headers
107+
*/
108+
processHeaders: function (headerArray) {
109+
if (!Array.isArray(headerArray)) return headerArray;
110+
111+
let headers = {};
112+
for (let header of headerArray) {
113+
headers[header.name.toLowerCase()] = header.value;
114+
}
115+
return headers;
116+
},
117+
118+
handleRequest: function (requestType) {
119+
return function (details) {
120+
if (!Zotero.WebRequestIntercept.listeners[requestType].length) return;
121+
122+
let meta = Zotero.WebRequestIntercept.reqIDToReqMeta[details.requestId];
123+
if (!meta) {
124+
meta = {};
125+
Zotero.WebRequestIntercept.reqIDToReqMeta[details.requestId] = meta;
126+
}
127+
128+
if (meta.requestHeadersObject) {
129+
details.requestHeadersObject = meta.requestHeadersObject;
130+
} else if (details.requestHeaders) {
131+
details.requestHeadersObject = Zotero.WebRequestIntercept.processHeaders(
132+
details.requestHeaders,
133+
);
134+
}
135+
136+
if (details.responseHeaders) {
137+
details.responseHeadersObject = Zotero.WebRequestIntercept.processHeaders(
138+
details.responseHeaders,
139+
);
140+
}
141+
142+
var returnValue = null;
143+
for (let listener of Zotero.WebRequestIntercept.listeners[requestType]) {
144+
let retVal = listener(details, meta);
145+
if (typeof retVal == "object") {
146+
returnValue = Object.assign(returnValue || {}, retVal);
147+
}
148+
}
149+
if (returnValue !== null && !Zotero.isManifestV3) {
150+
return returnValue;
151+
}
152+
};
153+
},
154+
155+
addListener: function (requestType, listener) {
156+
if (Zotero.WebRequestIntercept.listeners[requestType] === undefined) {
157+
throw new Error(`Web request listener for '${requestType}' not allowed`);
158+
}
159+
if (typeof listener != "function") {
160+
throw new Error(`Web request listener of type ${typeof listener} is not allowed`);
161+
}
162+
Zotero.WebRequestIntercept.listeners[requestType].push(listener);
163+
},
164+
165+
removeListener: function (requestType, listener) {
166+
if (Zotero.WebRequestIntercept.listeners[requestType] === undefined) {
167+
throw new Error(`Web request listener for '${requestType}' not allowed`);
168+
}
169+
if (typeof listener != "function") {
170+
throw new Error(`Web request listener of type ${typeof listener} is not allowed`);
171+
}
172+
let idx = Zotero.WebRequestIntercept.listeners[requestType].indexOf(listener);
173+
if (idx != -1) {
174+
Zotero.WebRequestIntercept.listeners[requestType].splice(idx, 1);
175+
}
176+
},
177+
178+
replaceHeaders: async function (url, headers) {
179+
if (!Zotero.isBrowserExt) return;
180+
return this.replaceHeadersDNR(url, headers);
181+
},
182+
183+
replaceHeadersDNR: async function (url, headers) {
184+
const requestHeaders = headers.map((headerObj) => {
185+
if (headerObj.name.toLowerCase() === "cookie") {
186+
// Chrome bug: 'append' op is checked against an allow-list and fails if header name is not lowercase
187+
return {
188+
header: headerObj.name.toLowerCase(),
189+
value: headerObj.value,
190+
operation: "append",
191+
};
192+
}
193+
return { header: headerObj.name, value: headerObj.value, operation: "set" };
194+
});
195+
const ruleID = Math.floor(Math.random() * 100000);
196+
const rules = [
197+
{
198+
id: ruleID,
199+
action: {
200+
type: "modifyHeaders",
201+
requestHeaders,
202+
},
203+
condition: {
204+
resourceTypes: ["xmlhttprequest"],
205+
initiatorDomains: [new URL(browser.runtime.getURL("")).hostname],
206+
},
207+
},
208+
];
209+
// Keep the service worker alive while the rule is active
210+
Zotero.Connector_Browser.setKeepServiceWorkerAlive(true);
211+
try {
212+
await browser.declarativeNetRequest.updateSessionRules({
213+
removeRuleIds: rules.map((r) => r.id),
214+
addRules: rules,
215+
});
216+
// Automatically clean up the rule after 60 seconds in case the caller does not
217+
setTimeout(async () => {
218+
try {
219+
await Zotero.WebRequestIntercept.removeRuleDNR(ruleID);
220+
} catch (e) {
221+
Zotero.logError(e);
222+
}
223+
}, 60000);
224+
Zotero.debug(
225+
`HTTP: Added a DNR rule to change headers for ${url} to ${JSON.stringify(headers)}`,
226+
);
227+
} catch (e) {
228+
Zotero.logError(e);
229+
}
230+
return ruleID;
231+
},
232+
233+
removeRuleDNR: async function (ruleId) {
234+
Zotero.Connector_Browser.setKeepServiceWorkerAlive(false);
235+
return browser.declarativeNetRequest.updateSessionRules({ removeRuleIds: [ruleId] });
236+
},
237+
};
238+
})();

0 commit comments

Comments
 (0)