Explorar el Código

Add option 'Misc > pass "Referer" header after a cross-origin request error' (fix #548)

Gildas hace 5 años
padre
commit
efd3facaf8

+ 5 - 1
_locales/de/messages.json

@@ -447,6 +447,10 @@
 		"message": "Maximale Größe (MB)",
 		"description": "Options page label: 'maximum size (MB)'"
 	},
+	"optionPassReferrerOnError": {
+		"message": "Den \"Referer\"-Header nach einem Fehler bei einer Cross-Origin-Anfrage übergeben",
+		"description": "Options page label: 'pass \"Referer\" header after a cross-origin request error'"
+	},
 	"optionSaveRawPage": {
 		"message": "Originalseite sichern",
 		"description": "Options page label: 'save raw page'"
@@ -646,7 +650,7 @@
 	"editorPrintPage": {
 		"message": "Drucken der Webseite",
 		"description": "Title of the button 'Print the page' in the editor"
-	},	
+	},
 	"pendingsTitle": {
 		"message": "Pending saves",
 		"description": "Title of the pending save page 'Pending saves' in the editor"

+ 12 - 8
_locales/en/messages.json

@@ -3,14 +3,14 @@
 		"message": "Save a complete page into a single HTML file",
 		"description": "Description of the extension."
 	},
-    "commandSaveSelectedTabs": {
-        "message": "Save the selected tabs or their selected contents",
-        "description": "Command (Ctrl+Shift+Y): 'Save the selected tabs or their selected contents'"
-    },
-    "commandSaveAllTabs": {
-        "message": "Save all tabs",
-        "description": "Command (Ctrl+Shift+U): 'Save all tabs'"
-    },
+	"commandSaveSelectedTabs": {
+		"message": "Save the selected tabs or their selected contents",
+		"description": "Command (Ctrl+Shift+Y): 'Save the selected tabs or their selected contents'"
+	},
+	"commandSaveAllTabs": {
+		"message": "Save all tabs",
+		"description": "Command (Ctrl+Shift+U): 'Save all tabs'"
+	},
 	"menuSavePage": {
 		"message": "Save page with SingleFile",
 		"description": "Menu entry: 'Save page with SingleFile'"
@@ -447,6 +447,10 @@
 		"message": "maximum size (MB)",
 		"description": "Options page label: 'maximum size (MB)'"
 	},
+	"optionPassReferrerOnError": {
+		"message": "pass \"Referer\" header after a cross-origin request error",
+		"description": "Options page label: 'pass \"Referer\" header after a cross-origin request error'"
+	},
 	"optionSaveRawPage": {
 		"message": "save raw page",
 		"description": "Options page label: 'save raw page'"

+ 12 - 8
_locales/es/messages.json

@@ -3,14 +3,14 @@
 		"message": "Guarda una página completa en un único archivo HTML",
 		"description": "Description of the extension."
 	},
-    "commandSaveSelectedTabs": {
-        "message": "Guardar las pestañas seleccionadas o su contenidos seleccionados",
-        "description": "Command (Ctrl+Shift+Y): 'Save the selected tabs or their selected contents'"
-    },
-    "commandSaveAllTabs": {
-        "message": "Guardar todas las pestañas",
-        "description": "Command (Ctrl+Shift+U): 'Save all tabs'"
-    },
+	"commandSaveSelectedTabs": {
+		"message": "Guardar las pestañas seleccionadas o su contenidos seleccionados",
+		"description": "Command (Ctrl+Shift+Y): 'Save the selected tabs or their selected contents'"
+	},
+	"commandSaveAllTabs": {
+		"message": "Guardar todas las pestañas",
+		"description": "Command (Ctrl+Shift+U): 'Save all tabs'"
+	},
 	"menuSavePage": {
 		"message": "Guardar página con SingleFile",
 		"description": "Menu entry: 'Save page with SingleFile'"
@@ -447,6 +447,10 @@
 		"message": "tamaño máximo (MB)",
 		"description": "Options page label: 'maximum size (MB)'"
 	},
+	"optionPassReferrerOnError": {
+		"message": "pasar el encabezado \"Referer\" después de un error de solicitud de origen cruzado",
+		"description": "Options page label: 'pass \"Referer\" header after a cross-origin request error'"
+	},
 	"optionSaveRawPage": {
 		"message": "guardar página en crudo",
 		"description": "Options page label: 'save raw page'"

+ 4 - 0
_locales/fr/messages.json

@@ -447,6 +447,10 @@
 		"message": "taille maximale (Mo)",
 		"description": "Options page label: 'maximum size (MB)'"
 	},
+	"optionPassReferrerOnError": {
+		"message": "passer l'en-tête \"Referer\" après une erreur de requête multi-origine",
+		"description": "Options page label: 'pass \"Referer\" header after a cross-origin request error'"
+	},
 	"optionSaveRawPage": {
 		"message": "sauvegarder la page brute",
 		"description": "Options page label: 'save raw page'"

+ 4 - 0
_locales/ja/messages.json

@@ -447,6 +447,10 @@
 		"message": "最大サイズ(MB)",
 		"description": "Options page label: 'maximum size (MB)'"
 	},
+	"optionPassReferrerOnError": {
+		"message": "pass \"Referer\" header after a cross-origin request error",
+		"description": "Options page label: 'pass \"Referer\" header after a cross-origin request error'"
+	},
 	"optionSaveRawPage": {
 		"message": "生のページを保存",
 		"description": "Options page label: 'save raw page'"

+ 4 - 0
_locales/pl/messages.json

@@ -447,6 +447,10 @@
 		"message": "maksymalny rozmiar (MB)",
 		"description": "Options page label: 'maximum size (MB)'"
 	},
+	"optionPassReferrerOnError": {
+		"message": "pass \"Referer\" header after a cross-origin request error",
+		"description": "Options page label: 'pass \"Referer\" header after a cross-origin request error'"
+	},
 	"optionSaveRawPage": {
 		"message": "zapisuj surową stronę",
 		"description": "Options page label: 'save raw page'"

+ 4 - 0
_locales/ru/messages.json

@@ -447,6 +447,10 @@
 		"message": "максимальный размер (МБ)",
 		"description": "Options page label: 'maximum size (MB)'"
 	},
+	"optionPassReferrerOnError": {
+		"message": "pass \"Referer\" header after a cross-origin request error",
+		"description": "Options page label: 'pass \"Referer\" header after a cross-origin request error'"
+	},
 	"optionSaveRawPage": {
 		"message": "сохранить исходную страницу",
 		"description": "Options page label: 'save raw page'"

+ 4 - 0
_locales/uk/messages.json

@@ -447,6 +447,10 @@
 		"message": "максимальний розмір (МБ)",
 		"description": "Options page label: 'maximum size (MB)'"
 	},
+	"optionPassReferrerOnError": {
+		"message": "pass \"Referer\" header after a cross-origin request error",
+		"description": "Options page label: 'pass \"Referer\" header after a cross-origin request error'"
+	},
 	"optionSaveRawPage": {
 		"message": "зберегти вихідну сторінку",
 		"description": "Options page label: 'save raw page'"

+ 4 - 0
_locales/zh_CN/messages.json

@@ -447,6 +447,10 @@
 		"message": "大小上限(兆字节)",
 		"description": "Options page label: 'maximum size (MB)'"
 	},
+	"optionPassReferrerOnError": {
+		"message": "pass \"Referer\" header after a cross-origin request error",
+		"description": "Options page label: 'pass \"Referer\" header after a cross-origin request error'"
+	},
 	"optionSaveRawPage": {
 		"message": "保存原始页面",
 		"description": "Options page label: 'save raw page'"

+ 4 - 0
_locales/zh_TW/messages.json

@@ -447,6 +447,10 @@
 		"message": "大小上限(兆字節)",
 		"description": "Options page label: 'maximum size (MB)'"
 	},
+	"optionPassReferrerOnError": {
+		"message": "pass \"Referer\" header after a cross-origin request error",
+		"description": "Options page label: 'pass \"Referer\" header after a cross-origin request error'"
+	},
 	"optionSaveRawPage": {
 		"message": "保存原始頁面",
 		"description": "Options page label: 'save raw page'"

+ 3 - 0
extension/core/bg/business.js

@@ -121,6 +121,9 @@ singlefile.extension.core.bg.business = (() => {
 			tabOptions.tabId = tabId;
 			tabOptions.tabIndex = tab.index;
 			tabOptions.extensionScriptFiles = extensionScriptFiles;
+			if (tabOptions.passReferrerOnError) {
+				await singlefile.extension.core.bg.requests.enableReferrerOnError();
+			}
 			if (options.autoSave) {
 				if (autosave.isEnabled(tab)) {
 					const taskInfo = addTask({

+ 2 - 1
extension/core/bg/config.js

@@ -96,7 +96,8 @@ singlefile.extension.core.bg.config = (() => {
 		includeBOM: false,
 		warnUnsavedPage: true,
 		autoSaveExternalSave: false,
-		insertMetaNoIndex: false
+		insertMetaNoIndex: false,
+		passReferrerOnError: false
 	};
 
 	let configStorage;

+ 3 - 0
extension/core/bg/messages.js

@@ -56,6 +56,9 @@ singlefile.extension.core.bg.messages = (() => {
 		if (message.method.startsWith("companion.")) {
 			return singlefile.extension.core.bg.companion.onMessage(message, sender);
 		}
+		if (message.method.startsWith("requests.")) {
+			return singlefile.extension.core.bg.requests.onMessage(message, sender);
+		}
 	});
 	if (browser.runtime.onMessageExternal) {
 		browser.runtime.onMessageExternal.addListener(async (message, sender) => {

+ 90 - 0
extension/core/bg/requests.js

@@ -0,0 +1,90 @@
+/*
+ * Copyright 2010-2020 Gildas Lormeau
+ * contact : gildas.lormeau <at> gmail.com
+ * 
+ * This file is part of SingleFile.
+ *
+ *   The code in this file is free software: you can redistribute it and/or 
+ *   modify it under the terms of the GNU Affero General Public License 
+ *   (GNU AGPL) as published by the Free Software Foundation, either version 3
+ *   of the License, or (at your option) any later version.
+ * 
+ *   The code in this file is distributed in the hope that it will be useful, 
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of 
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 
+ *   General Public License for more details.
+ *
+ *   As additional permission under GNU AGPL version 3 section 7, you may 
+ *   distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 
+ *   AGPL normally required by section 4, provided you include this license 
+ *   notice and a URL through which recipients can access the Corresponding 
+ *   Source.
+ */
+
+/* global browser, singlefile, */
+
+singlefile.extension.core.bg.requests = (() => {
+
+	const REQUEST_ID_HEADER_NAME = "x-single-file-request-id";
+	const referrers = new Map();
+	let referrerOnErrorEnabled = false;
+
+	return {
+		REQUEST_ID_HEADER_NAME,
+		onMessage(message) {
+			if (message.method.endsWith(".enableReferrerOnError")) {
+				enableReferrerOnError();
+				return {};
+			}
+			if (message.method.endsWith(".disableReferrerOnError")) {
+				disableReferrerOnError();
+				return {};
+			}
+		},
+		setReferrer(requestId, referrer) {
+			referrers.set(requestId, referrer);
+		},
+		enableReferrerOnError
+	};
+
+	function injectRefererHeader(details) {
+		if (referrerOnErrorEnabled) {
+			let requestIdHeader = details.requestHeaders.find(header => header.name === REQUEST_ID_HEADER_NAME);
+			if (requestIdHeader) {
+				details.requestHeaders = details.requestHeaders.filter(header => header.name !== REQUEST_ID_HEADER_NAME);
+				const referrer = referrers.get(requestIdHeader.value);
+				if (referrer) {
+					referrers.delete(requestIdHeader.value);
+					let header = details.requestHeaders.find(header => header.name.toLowerCase() === "referer");
+					if (!header) {
+						header = { name: "Referer" };
+						details.requestHeaders.push(header);
+						header.value = referrer;
+					}
+					return { requestHeaders: details.requestHeaders };
+				}
+			}
+		}
+	}
+
+	function enableReferrerOnError() {
+		if (!referrerOnErrorEnabled) {
+			try {
+				browser.webRequest.onBeforeSendHeaders.addListener(injectRefererHeader, { urls: ["<all_urls>"] }, ["blocking", "requestHeaders", "extraHeaders"]);
+			} catch (error) {
+				browser.webRequest.onBeforeSendHeaders.addListener(injectRefererHeader, { urls: ["<all_urls>"] }, ["blocking", "requestHeaders"]);
+			}
+			referrerOnErrorEnabled = true;
+		}
+	}
+
+	function disableReferrerOnError() {
+		try {
+			browser.webRequest.onBeforeSendHeaders.removeListener(injectRefererHeader);
+		} catch (error) {
+			// ignored
+		}
+		referrerOnErrorEnabled = false;
+	}
+
+})();

+ 15 - 0
extension/lib/single-file/browser-polyfill/chrome-browser-polyfill.js

@@ -253,6 +253,15 @@
 							resolve(result);
 						}
 					});
+				}),
+				remove: permissions => new Promise((resolve, reject) => {
+					nativeAPI.permissions.remove(permissions, result => {
+						if (nativeAPI.runtime.lastError) {
+							reject(nativeAPI.runtime.lastError);
+						} else {
+							resolve(result);
+						}
+					});
 				})
 			},
 			runtime: {
@@ -481,6 +490,12 @@
 						return nativeAPI.devtools.inspectedWindow.tabId;
 					}
 				}
+			},
+			webRequest: {
+				onBeforeSendHeaders: {
+					addListener: (listener, filters, extraInfoSpec) => nativeAPI.webRequest.onBeforeSendHeaders.addListener(listener, filters, extraInfoSpec),
+					removeListener: listener => nativeAPI.webRequest.onBeforeSendHeaders.removeListener(listener)
+				}
 			}
 		}));
 	}

+ 23 - 8
extension/lib/single-file/fetch/bg/fetch.js

@@ -21,7 +21,7 @@
  *   Source.
  */
 
-/* global browser, XMLHttpRequest */
+/* global singlefile, browser, XMLHttpRequest */
 
 (() => {
 
@@ -37,13 +37,13 @@
 
 	function onRequest(message, sender) {
 		if (message.method == "singlefile.fetch") {
-			return fetchResource(message.url);
+			return fetchResource(message.url, { referrer: message.referrer });
 		} else if (message.method == "singlefile.fetchFrame") {
 			return browser.tabs.sendMessage(sender.tab.id, message);
 		}
 	}
 
-	function fetchResource(url) {
+	function fetchResource(url, options, includeRequestId) {
 		return new Promise((resolve, reject) => {
 			const xhrRequest = new XMLHttpRequest();
 			xhrRequest.withCredentials = true;
@@ -51,14 +51,29 @@
 			xhrRequest.onerror = event => reject(new Error(event.detail));
 			xhrRequest.onreadystatechange = () => {
 				if (xhrRequest.readyState == XMLHttpRequest.DONE) {
-					resolve({
-						array: Array.from(new Uint8Array(xhrRequest.response)),
-						headers: { "content-type": xhrRequest.getResponseHeader("Content-Type") },
-						status: xhrRequest.status
-					});
+					if (xhrRequest.status || xhrRequest.response.byteLength) {
+						if ((xhrRequest.status == 401 || xhrRequest.status == 403 || xhrRequest.status == 404) && !includeRequestId) {
+							fetchResource(url, options, true)
+								.then(resolve)
+								.catch(reject);
+						} else {
+							resolve({
+								array: Array.from(new Uint8Array(xhrRequest.response)),
+								headers: { "content-type": xhrRequest.getResponseHeader("Content-Type") },
+								status: xhrRequest.status
+							});
+						}
+					} else {
+						reject();
+					}
 				}
 			};
 			xhrRequest.open("GET", url, true);
+			if (includeRequestId) {
+				const randomId = String(Math.random()).substring(2);
+				singlefile.extension.core.bg.requests.setReferrer(randomId, options.referrer);
+				xhrRequest.setRequestHeader(singlefile.extension.core.bg.requests.REQUEST_ID_HEADER_NAME, randomId);
+			}
 			xhrRequest.send();
 		});
 	}

+ 4 - 4
extension/lib/single-file/fetch/content/content-fetch.js

@@ -62,7 +62,7 @@ this.singlefile.extension.lib.fetch.content.resources = this.singlefile.extensio
 	}
 
 	return {
-		fetch: async url => {
+		fetch: async (url, options) => {
 			try {
 				let response = await fetch(url, { cache: "force-cache" });
 				if (response.status == 401 || response.status == 403 || response.status == 404) {
@@ -71,7 +71,7 @@ this.singlefile.extension.lib.fetch.content.resources = this.singlefile.extensio
 				return response;
 			}
 			catch (error) {
-				const response = await sendMessage({ method: "singlefile.fetch", url });
+				const response = await sendMessage({ method: "singlefile.fetch", url, referrer: options.referrer });
 				return {
 					status: response.status,
 					headers: { get: headerName => response.headers && response.headers[headerName] },
@@ -79,8 +79,8 @@ this.singlefile.extension.lib.fetch.content.resources = this.singlefile.extensio
 				};
 			}
 		},
-		frameFetch: async (url, frameId) => {
-			const response = await sendMessage({ method: "singlefile.fetchFrame", url, frameId });
+		frameFetch: async (url, options) => {
+			const response = await sendMessage({ method: "singlefile.fetchFrame", url, frameId: options.frameId, referrer: options.referrer });
 			return {
 				status: response.status,
 				headers: new Map(response.headers),

+ 36 - 1
extension/ui/bg/ui-options.js

@@ -71,6 +71,7 @@
 	const removeAlternativeImagesLabel = document.getElementById("removeAlternativeImagesLabel");
 	const removeAlternativeMediasLabel = document.getElementById("removeAlternativeMediasLabel");
 	const saveCreatedBookmarksLabel = document.getElementById("saveCreatedBookmarksLabel");
+	const passReferrerOnErrorLabel = document.getElementById("passReferrerOnErrorLabel");
 	const replaceBookmarkURLLabel = document.getElementById("replaceBookmarkURLLabel");
 	const titleLabel = document.getElementById("titleLabel");
 	const userInterfaceLabel = document.getElementById("userInterfaceLabel");
@@ -148,6 +149,7 @@
 	const removeAlternativeImagesInput = document.getElementById("removeAlternativeImagesInput");
 	const removeAlternativeMediasInput = document.getElementById("removeAlternativeMediasInput");
 	const saveCreatedBookmarksInput = document.getElementById("saveCreatedBookmarksInput");
+	const passReferrerOnErrorInput = document.getElementById("passReferrerOnErrorInput");
 	const replaceBookmarkURLInput = document.getElementById("replaceBookmarkURLInput");
 	const groupDuplicateImagesInput = document.getElementById("groupDuplicateImagesInput");
 	const infobarTemplateInput = document.getElementById("infobarTemplateInput");
@@ -378,6 +380,7 @@
 		}
 	}, false);
 	saveCreatedBookmarksInput.addEventListener("click", saveCreatedBookmarks, false);
+	passReferrerOnErrorInput.addEventListener("click", passReferrerOnError, false);
 	autoSaveExternalSaveInput.addEventListener("click", enableExternalSave, false);
 	saveToGDriveInput.addEventListener("click", async () => {
 		if (!saveToGDriveInput.checked) {
@@ -412,7 +415,8 @@
 			target != ruleEditProfileInput &&
 			target != ruleEditAutoSaveProfileInput &&
 			target != showAutoSaveProfileInput &&
-			target != saveCreatedBookmarksInput) {
+			target != saveCreatedBookmarksInput &&
+			target != passReferrerOnErrorInput) {
 			if (target != profileNamesInput && target != showAllProfilesInput) {
 				await update();
 			}
@@ -481,6 +485,7 @@
 	removeAlternativeImagesLabel.textContent = browser.i18n.getMessage("optionRemoveAlternativeImages");
 	removeAlternativeMediasLabel.textContent = browser.i18n.getMessage("optionRemoveAlternativeMedias");
 	saveCreatedBookmarksLabel.textContent = browser.i18n.getMessage("optionSaveCreatedBookmarks");
+	passReferrerOnErrorLabel.textContent = browser.i18n.getMessage("optionPassReferrerOnError");
 	replaceBookmarkURLLabel.textContent = browser.i18n.getMessage("optionReplaceBookmarkURL");
 	groupDuplicateImagesLabel.textContent = browser.i18n.getMessage("optionGroupDuplicateImages");
 	titleLabel.textContent = browser.i18n.getMessage("optionsTitle");
@@ -687,6 +692,7 @@
 		groupDuplicateImagesInput.checked = profileOptions.groupDuplicateImages;
 		removeAlternativeMediasInput.checked = profileOptions.removeAlternativeMedias;
 		saveCreatedBookmarksInput.checked = profileOptions.saveCreatedBookmarks;
+		passReferrerOnErrorInput.checked = profileOptions.passReferrerOnError;
 		replaceBookmarkURLInput.checked = profileOptions.saveCreatedBookmarks && profileOptions.backgroundSave && profileOptions.replaceBookmarkURL;
 		replaceBookmarkURLInput.disabled = !profileOptions.saveCreatedBookmarks || !profileOptions.backgroundSave || profileOptions.saveToClipboard || profileOptions.saveToGDrive;
 		infobarTemplateInput.value = profileOptions.infobarTemplate;
@@ -755,6 +761,7 @@
 				removeAlternativeImages: removeAlternativeImagesInput.checked,
 				removeAlternativeMedias: removeAlternativeMediasInput.checked,
 				saveCreatedBookmarks: saveCreatedBookmarksInput.checked,
+				passReferrerOnError: passReferrerOnErrorInput.checked,
 				replaceBookmarkURL: replaceBookmarkURLInput.checked,
 				groupDuplicateImages: groupDuplicateImagesInput.checked,
 				infobarTemplate: infobarTemplateInput.value,
@@ -811,6 +818,34 @@
 		}
 	}
 
+	async function passReferrerOnError() {
+		if (passReferrerOnErrorInput.checked) {
+			passReferrerOnErrorInput.checked = false;
+			try {
+				const permissionGranted = await browser.permissions.request({ permissions: ["webRequest", "webRequestBlocking"] });
+				if (permissionGranted) {
+					passReferrerOnErrorInput.checked = true;
+					await update();
+					await refresh();
+					await browser.runtime.sendMessage({ method: "requests.enableReferrerOnError" });
+				} else {
+					await disableOption();
+				}
+			} catch (error) {
+				await disableOption();
+			}
+		} else {
+			await disableOption();
+		}
+
+		async function disableOption() {
+			await update();
+			await refresh();
+			await browser.runtime.sendMessage({ method: "requests.disableReferrerOnError" });
+			await browser.permissions.remove({ permissions: ["webRequest", "webRequestBlocking"] });
+		}
+	}
+
 	async function enableExternalSave() {
 		if (autoSaveExternalSaveInput.checked) {
 			autoSaveExternalSaveInput.checked = false;

+ 7 - 0
extension/ui/pages/help.html

@@ -410,6 +410,13 @@
 					<li data-options-label="maxResourceSizeLabel"> <span class="option">Option: maximum size (MB)</span>
 						<p>Specify the maximum size of embedded resources in megabytes.</p>
 					</li>
+					<li data-options-label="passReferrerOnErrorLabel"> <span class="option">Option: pass \"Referer\" header on
+							cross-origin errors</span>
+						<p>Check this option to pass the HTTP header "Referer" with the "origin" policy after an 401,
+							403, or 404 HTTP error when downloading a cross-origin resource. You should enable this
+							option if you cannot download resources blocked by a hotlink protection.</p>
+						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
+					</li>
 					<li data-options-label="saveRawPageLabel"> <span class="option">Option: save raw page</span>
 						<p>Check this option to save the page without interpreting JavaScript. Checking this option may
 							alter the document, will force the options "remove frames", "remove hidden elements" to be

+ 4 - 0
extension/ui/pages/options.html

@@ -240,6 +240,10 @@
 				<label for="maxResourceSizeInput" id="maxResourceSizeLabel"></label>
 				<input type="number" id="maxResourceSizeInput" min="1">
 			</div>
+			<div class="option">
+				<label for="passReferrerOnErrorInput" id="passReferrerOnErrorLabel"></label>
+				<input type="checkbox" id="passReferrerOnErrorInput">
+			</div>
 			<div class="option">
 				<label for="saveRawPageInput" id="saveRawPageLabel"></label>
 				<input type="checkbox" id="saveRawPageInput">

+ 15 - 6
lib/single-file/single-file-core.js

@@ -154,6 +154,8 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 			this.root = root;
 			this.options = options;
 			this.options.url = this.options.url || (rootDocDefined && this.options.doc.location.href);
+			const matchResourceReferrer = this.options.url.match(/^.*\//);
+			this.options.resourceReferrer = this.options.passReferrerOnError && matchResourceReferrer && matchResourceReferrer[0];
 			this.options.baseURI = rootDocDefined && this.options.doc.baseURI;
 			this.options.rootDocument = root;
 			this.options.updatedResources = this.options.updatedResources || {};
@@ -349,7 +351,8 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 						expectedType,
 						maxResourceSize: options.maxResourceSize,
 						maxResourceSizeEnabled: options.maxResourceSizeEnabled,
-						frameId: options.windowId
+						frameId: options.windowId,
+						resourceReferrer: options.resourceReferrer
 					});
 					onloadListener({ url: resourceURL });
 					if (!this.cancelled) {
@@ -427,7 +430,8 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 					maxResourceSize: this.options.maxResourceSize,
 					maxResourceSizeEnabled: this.options.maxResourceSizeEnabled,
 					charset,
-					frameId: this.options.windowId
+					frameId: this.options.windowId,
+					resourceReferrer: this.options.resourceReferrer
 				});
 				pageContent = content.data;
 			}
@@ -903,7 +907,8 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 				compressCSS: this.options.compressCSS,
 				updatedResources: this.options.updatedResources,
 				rootDocument: this.options.rootDocument,
-				frameId: this.options.windowId
+				frameId: this.options.windowId,
+				resourceReferrer: this.options.resourceReferrer
 			};
 			await Promise.all(Array.from(this.doc.querySelectorAll("style, link[rel*=stylesheet]"))
 				.map(async element => {
@@ -1199,7 +1204,8 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 						charset: this.charset != UTF8_CHARSET && this.charset,
 						maxResourceSize: this.options.maxResourceSize,
 						maxResourceSizeEnabled: this.options.maxResourceSizeEnabled,
-						frameId: this.options.windowId
+						frameId: this.options.windowId,
+						resourceReferrer: this.options.resourceReferrer
 					});
 					content.data = getUpdatedResourceContent(resourceURL, content, this.options);
 					if (element.tagName == "SCRIPT") {
@@ -1565,7 +1571,8 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 								maxResourceSize: options.maxResourceSize,
 								maxResourceSizeEnabled: options.maxResourceSizeEnabled,
 								validateTextContentType: true,
-								frameId: options.frameId
+								frameId: options.frameId,
+								resourceReferrer: options.resourceReferrer
 							});
 							resourceURL = content.resourceURL;
 							content.data = getUpdatedResourceContent(resourceURL, content, options);
@@ -1640,6 +1647,7 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 					maxResourceSizeEnabled: options.maxResourceSizeEnabled,
 					charset: options.charset,
 					frameId: options.frameId,
+					resourceReferrer: options.resourceReferrer,
 					validateTextContentType: true
 				});
 				resourceURL = content.resourceURL;
@@ -1790,7 +1798,8 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 												expectedType: "image",
 												maxResourceSize: options.maxResourceSize,
 												maxResourceSizeEnabled: options.maxResourceSizeEnabled,
-												frameId: options.windowId
+												frameId: options.windowId,
+												resourceReferrer: options.resourceReferrer
 											})).data;
 										} catch (error) {
 											// ignored

+ 3 - 3
lib/single-file/single-file-util.js

@@ -35,7 +35,7 @@ this.singlefile.lib.util = this.singlefile.lib.util || (() => {
 	const DOMParser = window.DOMParser;
 	const Blob = window.Blob;
 	const FileReader = window.FileReader;
-	const fetch = window.fetch;
+	const fetch = url => window.fetch(url);
 	const crypto = window.crypto;
 	const TextDecoder = window.TextDecoder;
 	const TextEncoder = window.TextEncoder;
@@ -219,12 +219,12 @@ this.singlefile.lib.util = this.singlefile.lib.util || (() => {
 				try {
 					if (options.frameId) {
 						try {
-							response = await fetchFrameResource(resourceURL, options.frameId);
+							response = await fetchFrameResource(resourceURL, { frameId: options.frameId, referrer: options.resourceReferrer });
 						} catch (error) {
 							response = await fetchResource(resourceURL);
 						}
 					} else {
-						response = await fetchResource(resourceURL);
+						response = await fetchResource(resourceURL, { referrer: options.resourceReferrer });
 					}
 				} catch (error) {
 					return { data: options.asBinary ? "data:null;base64," : "", resourceURL };

+ 4 - 1
manifest.json

@@ -97,6 +97,7 @@
 			"extension/core/bg/editor.js",
 			"extension/core/bg/bookmarks.js",
 			"extension/core/bg/companion.js",
+			"extension/core/bg/requests.js",
 			"extension/ui/bg/ui-main.js",
 			"extension/ui/bg/ui-menus.js",
 			"extension/ui/bg/ui-commands.js",
@@ -174,7 +175,9 @@
 	"optional_permissions": [
 		"identity",
 		"nativeMessaging",
-		"bookmarks"
+		"bookmarks",
+		"webRequest",
+		"webRequestBlocking"
 	],
 	"browser_specific_settings": {
 		"gecko": {