Sfoglia il codice sorgente

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

Gildas 5 anni fa
parent
commit
efd3facaf8

+ 5 - 1
_locales/de/messages.json

@@ -447,6 +447,10 @@
 		"message": "Maximale Größe (MB)",
 		"message": "Maximale Größe (MB)",
 		"description": "Options page label: 'maximum size (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": {
 	"optionSaveRawPage": {
 		"message": "Originalseite sichern",
 		"message": "Originalseite sichern",
 		"description": "Options page label: 'save raw page'"
 		"description": "Options page label: 'save raw page'"
@@ -646,7 +650,7 @@
 	"editorPrintPage": {
 	"editorPrintPage": {
 		"message": "Drucken der Webseite",
 		"message": "Drucken der Webseite",
 		"description": "Title of the button 'Print the page' in the editor"
 		"description": "Title of the button 'Print the page' in the editor"
-	},	
+	},
 	"pendingsTitle": {
 	"pendingsTitle": {
 		"message": "Pending saves",
 		"message": "Pending saves",
 		"description": "Title of the pending save page 'Pending saves' in the editor"
 		"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",
 		"message": "Save a complete page into a single HTML file",
 		"description": "Description of the extension."
 		"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": {
 	"menuSavePage": {
 		"message": "Save page with SingleFile",
 		"message": "Save page with SingleFile",
 		"description": "Menu entry: 'Save page with SingleFile'"
 		"description": "Menu entry: 'Save page with SingleFile'"
@@ -447,6 +447,10 @@
 		"message": "maximum size (MB)",
 		"message": "maximum size (MB)",
 		"description": "Options page label: '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": {
 	"optionSaveRawPage": {
 		"message": "save raw page",
 		"message": "save raw page",
 		"description": "Options page label: '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",
 		"message": "Guarda una página completa en un único archivo HTML",
 		"description": "Description of the extension."
 		"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": {
 	"menuSavePage": {
 		"message": "Guardar página con SingleFile",
 		"message": "Guardar página con SingleFile",
 		"description": "Menu entry: 'Save page with SingleFile'"
 		"description": "Menu entry: 'Save page with SingleFile'"
@@ -447,6 +447,10 @@
 		"message": "tamaño máximo (MB)",
 		"message": "tamaño máximo (MB)",
 		"description": "Options page label: 'maximum size (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": {
 	"optionSaveRawPage": {
 		"message": "guardar página en crudo",
 		"message": "guardar página en crudo",
 		"description": "Options page label: 'save raw page'"
 		"description": "Options page label: 'save raw page'"

+ 4 - 0
_locales/fr/messages.json

@@ -447,6 +447,10 @@
 		"message": "taille maximale (Mo)",
 		"message": "taille maximale (Mo)",
 		"description": "Options page label: 'maximum size (MB)'"
 		"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": {
 	"optionSaveRawPage": {
 		"message": "sauvegarder la page brute",
 		"message": "sauvegarder la page brute",
 		"description": "Options page label: 'save raw page'"
 		"description": "Options page label: 'save raw page'"

+ 4 - 0
_locales/ja/messages.json

@@ -447,6 +447,10 @@
 		"message": "最大サイズ(MB)",
 		"message": "最大サイズ(MB)",
 		"description": "Options page label: '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": {
 	"optionSaveRawPage": {
 		"message": "生のページを保存",
 		"message": "生のページを保存",
 		"description": "Options page label: 'save raw page'"
 		"description": "Options page label: 'save raw page'"

+ 4 - 0
_locales/pl/messages.json

@@ -447,6 +447,10 @@
 		"message": "maksymalny rozmiar (MB)",
 		"message": "maksymalny rozmiar (MB)",
 		"description": "Options page label: '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": {
 	"optionSaveRawPage": {
 		"message": "zapisuj surową stronę",
 		"message": "zapisuj surową stronę",
 		"description": "Options page label: 'save raw page'"
 		"description": "Options page label: 'save raw page'"

+ 4 - 0
_locales/ru/messages.json

@@ -447,6 +447,10 @@
 		"message": "максимальный размер (МБ)",
 		"message": "максимальный размер (МБ)",
 		"description": "Options page label: '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": {
 	"optionSaveRawPage": {
 		"message": "сохранить исходную страницу",
 		"message": "сохранить исходную страницу",
 		"description": "Options page label: 'save raw page'"
 		"description": "Options page label: 'save raw page'"

+ 4 - 0
_locales/uk/messages.json

@@ -447,6 +447,10 @@
 		"message": "максимальний розмір (МБ)",
 		"message": "максимальний розмір (МБ)",
 		"description": "Options page label: '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": {
 	"optionSaveRawPage": {
 		"message": "зберегти вихідну сторінку",
 		"message": "зберегти вихідну сторінку",
 		"description": "Options page label: 'save raw page'"
 		"description": "Options page label: 'save raw page'"

+ 4 - 0
_locales/zh_CN/messages.json

@@ -447,6 +447,10 @@
 		"message": "大小上限(兆字节)",
 		"message": "大小上限(兆字节)",
 		"description": "Options page label: '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": {
 	"optionSaveRawPage": {
 		"message": "保存原始页面",
 		"message": "保存原始页面",
 		"description": "Options page label: 'save raw page'"
 		"description": "Options page label: 'save raw page'"

+ 4 - 0
_locales/zh_TW/messages.json

@@ -447,6 +447,10 @@
 		"message": "大小上限(兆字節)",
 		"message": "大小上限(兆字節)",
 		"description": "Options page label: '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": {
 	"optionSaveRawPage": {
 		"message": "保存原始頁面",
 		"message": "保存原始頁面",
 		"description": "Options page label: 'save raw page'"
 		"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.tabId = tabId;
 			tabOptions.tabIndex = tab.index;
 			tabOptions.tabIndex = tab.index;
 			tabOptions.extensionScriptFiles = extensionScriptFiles;
 			tabOptions.extensionScriptFiles = extensionScriptFiles;
+			if (tabOptions.passReferrerOnError) {
+				await singlefile.extension.core.bg.requests.enableReferrerOnError();
+			}
 			if (options.autoSave) {
 			if (options.autoSave) {
 				if (autosave.isEnabled(tab)) {
 				if (autosave.isEnabled(tab)) {
 					const taskInfo = addTask({
 					const taskInfo = addTask({

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

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

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

@@ -56,6 +56,9 @@ singlefile.extension.core.bg.messages = (() => {
 		if (message.method.startsWith("companion.")) {
 		if (message.method.startsWith("companion.")) {
 			return singlefile.extension.core.bg.companion.onMessage(message, sender);
 			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) {
 	if (browser.runtime.onMessageExternal) {
 		browser.runtime.onMessageExternal.addListener(async (message, sender) => {
 		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);
 							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: {
 			runtime: {
@@ -481,6 +490,12 @@
 						return nativeAPI.devtools.inspectedWindow.tabId;
 						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.
  *   Source.
  */
  */
 
 
-/* global browser, XMLHttpRequest */
+/* global singlefile, browser, XMLHttpRequest */
 
 
 (() => {
 (() => {
 
 
@@ -37,13 +37,13 @@
 
 
 	function onRequest(message, sender) {
 	function onRequest(message, sender) {
 		if (message.method == "singlefile.fetch") {
 		if (message.method == "singlefile.fetch") {
-			return fetchResource(message.url);
+			return fetchResource(message.url, { referrer: message.referrer });
 		} else if (message.method == "singlefile.fetchFrame") {
 		} else if (message.method == "singlefile.fetchFrame") {
 			return browser.tabs.sendMessage(sender.tab.id, message);
 			return browser.tabs.sendMessage(sender.tab.id, message);
 		}
 		}
 	}
 	}
 
 
-	function fetchResource(url) {
+	function fetchResource(url, options, includeRequestId) {
 		return new Promise((resolve, reject) => {
 		return new Promise((resolve, reject) => {
 			const xhrRequest = new XMLHttpRequest();
 			const xhrRequest = new XMLHttpRequest();
 			xhrRequest.withCredentials = true;
 			xhrRequest.withCredentials = true;
@@ -51,14 +51,29 @@
 			xhrRequest.onerror = event => reject(new Error(event.detail));
 			xhrRequest.onerror = event => reject(new Error(event.detail));
 			xhrRequest.onreadystatechange = () => {
 			xhrRequest.onreadystatechange = () => {
 				if (xhrRequest.readyState == XMLHttpRequest.DONE) {
 				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);
 			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();
 			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 {
 	return {
-		fetch: async url => {
+		fetch: async (url, options) => {
 			try {
 			try {
 				let response = await fetch(url, { cache: "force-cache" });
 				let response = await fetch(url, { cache: "force-cache" });
 				if (response.status == 401 || response.status == 403 || response.status == 404) {
 				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;
 				return response;
 			}
 			}
 			catch (error) {
 			catch (error) {
-				const response = await sendMessage({ method: "singlefile.fetch", url });
+				const response = await sendMessage({ method: "singlefile.fetch", url, referrer: options.referrer });
 				return {
 				return {
 					status: response.status,
 					status: response.status,
 					headers: { get: headerName => response.headers && response.headers[headerName] },
 					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 {
 			return {
 				status: response.status,
 				status: response.status,
 				headers: new Map(response.headers),
 				headers: new Map(response.headers),

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

@@ -71,6 +71,7 @@
 	const removeAlternativeImagesLabel = document.getElementById("removeAlternativeImagesLabel");
 	const removeAlternativeImagesLabel = document.getElementById("removeAlternativeImagesLabel");
 	const removeAlternativeMediasLabel = document.getElementById("removeAlternativeMediasLabel");
 	const removeAlternativeMediasLabel = document.getElementById("removeAlternativeMediasLabel");
 	const saveCreatedBookmarksLabel = document.getElementById("saveCreatedBookmarksLabel");
 	const saveCreatedBookmarksLabel = document.getElementById("saveCreatedBookmarksLabel");
+	const passReferrerOnErrorLabel = document.getElementById("passReferrerOnErrorLabel");
 	const replaceBookmarkURLLabel = document.getElementById("replaceBookmarkURLLabel");
 	const replaceBookmarkURLLabel = document.getElementById("replaceBookmarkURLLabel");
 	const titleLabel = document.getElementById("titleLabel");
 	const titleLabel = document.getElementById("titleLabel");
 	const userInterfaceLabel = document.getElementById("userInterfaceLabel");
 	const userInterfaceLabel = document.getElementById("userInterfaceLabel");
@@ -148,6 +149,7 @@
 	const removeAlternativeImagesInput = document.getElementById("removeAlternativeImagesInput");
 	const removeAlternativeImagesInput = document.getElementById("removeAlternativeImagesInput");
 	const removeAlternativeMediasInput = document.getElementById("removeAlternativeMediasInput");
 	const removeAlternativeMediasInput = document.getElementById("removeAlternativeMediasInput");
 	const saveCreatedBookmarksInput = document.getElementById("saveCreatedBookmarksInput");
 	const saveCreatedBookmarksInput = document.getElementById("saveCreatedBookmarksInput");
+	const passReferrerOnErrorInput = document.getElementById("passReferrerOnErrorInput");
 	const replaceBookmarkURLInput = document.getElementById("replaceBookmarkURLInput");
 	const replaceBookmarkURLInput = document.getElementById("replaceBookmarkURLInput");
 	const groupDuplicateImagesInput = document.getElementById("groupDuplicateImagesInput");
 	const groupDuplicateImagesInput = document.getElementById("groupDuplicateImagesInput");
 	const infobarTemplateInput = document.getElementById("infobarTemplateInput");
 	const infobarTemplateInput = document.getElementById("infobarTemplateInput");
@@ -378,6 +380,7 @@
 		}
 		}
 	}, false);
 	}, false);
 	saveCreatedBookmarksInput.addEventListener("click", saveCreatedBookmarks, false);
 	saveCreatedBookmarksInput.addEventListener("click", saveCreatedBookmarks, false);
+	passReferrerOnErrorInput.addEventListener("click", passReferrerOnError, false);
 	autoSaveExternalSaveInput.addEventListener("click", enableExternalSave, false);
 	autoSaveExternalSaveInput.addEventListener("click", enableExternalSave, false);
 	saveToGDriveInput.addEventListener("click", async () => {
 	saveToGDriveInput.addEventListener("click", async () => {
 		if (!saveToGDriveInput.checked) {
 		if (!saveToGDriveInput.checked) {
@@ -412,7 +415,8 @@
 			target != ruleEditProfileInput &&
 			target != ruleEditProfileInput &&
 			target != ruleEditAutoSaveProfileInput &&
 			target != ruleEditAutoSaveProfileInput &&
 			target != showAutoSaveProfileInput &&
 			target != showAutoSaveProfileInput &&
-			target != saveCreatedBookmarksInput) {
+			target != saveCreatedBookmarksInput &&
+			target != passReferrerOnErrorInput) {
 			if (target != profileNamesInput && target != showAllProfilesInput) {
 			if (target != profileNamesInput && target != showAllProfilesInput) {
 				await update();
 				await update();
 			}
 			}
@@ -481,6 +485,7 @@
 	removeAlternativeImagesLabel.textContent = browser.i18n.getMessage("optionRemoveAlternativeImages");
 	removeAlternativeImagesLabel.textContent = browser.i18n.getMessage("optionRemoveAlternativeImages");
 	removeAlternativeMediasLabel.textContent = browser.i18n.getMessage("optionRemoveAlternativeMedias");
 	removeAlternativeMediasLabel.textContent = browser.i18n.getMessage("optionRemoveAlternativeMedias");
 	saveCreatedBookmarksLabel.textContent = browser.i18n.getMessage("optionSaveCreatedBookmarks");
 	saveCreatedBookmarksLabel.textContent = browser.i18n.getMessage("optionSaveCreatedBookmarks");
+	passReferrerOnErrorLabel.textContent = browser.i18n.getMessage("optionPassReferrerOnError");
 	replaceBookmarkURLLabel.textContent = browser.i18n.getMessage("optionReplaceBookmarkURL");
 	replaceBookmarkURLLabel.textContent = browser.i18n.getMessage("optionReplaceBookmarkURL");
 	groupDuplicateImagesLabel.textContent = browser.i18n.getMessage("optionGroupDuplicateImages");
 	groupDuplicateImagesLabel.textContent = browser.i18n.getMessage("optionGroupDuplicateImages");
 	titleLabel.textContent = browser.i18n.getMessage("optionsTitle");
 	titleLabel.textContent = browser.i18n.getMessage("optionsTitle");
@@ -687,6 +692,7 @@
 		groupDuplicateImagesInput.checked = profileOptions.groupDuplicateImages;
 		groupDuplicateImagesInput.checked = profileOptions.groupDuplicateImages;
 		removeAlternativeMediasInput.checked = profileOptions.removeAlternativeMedias;
 		removeAlternativeMediasInput.checked = profileOptions.removeAlternativeMedias;
 		saveCreatedBookmarksInput.checked = profileOptions.saveCreatedBookmarks;
 		saveCreatedBookmarksInput.checked = profileOptions.saveCreatedBookmarks;
+		passReferrerOnErrorInput.checked = profileOptions.passReferrerOnError;
 		replaceBookmarkURLInput.checked = profileOptions.saveCreatedBookmarks && profileOptions.backgroundSave && profileOptions.replaceBookmarkURL;
 		replaceBookmarkURLInput.checked = profileOptions.saveCreatedBookmarks && profileOptions.backgroundSave && profileOptions.replaceBookmarkURL;
 		replaceBookmarkURLInput.disabled = !profileOptions.saveCreatedBookmarks || !profileOptions.backgroundSave || profileOptions.saveToClipboard || profileOptions.saveToGDrive;
 		replaceBookmarkURLInput.disabled = !profileOptions.saveCreatedBookmarks || !profileOptions.backgroundSave || profileOptions.saveToClipboard || profileOptions.saveToGDrive;
 		infobarTemplateInput.value = profileOptions.infobarTemplate;
 		infobarTemplateInput.value = profileOptions.infobarTemplate;
@@ -755,6 +761,7 @@
 				removeAlternativeImages: removeAlternativeImagesInput.checked,
 				removeAlternativeImages: removeAlternativeImagesInput.checked,
 				removeAlternativeMedias: removeAlternativeMediasInput.checked,
 				removeAlternativeMedias: removeAlternativeMediasInput.checked,
 				saveCreatedBookmarks: saveCreatedBookmarksInput.checked,
 				saveCreatedBookmarks: saveCreatedBookmarksInput.checked,
+				passReferrerOnError: passReferrerOnErrorInput.checked,
 				replaceBookmarkURL: replaceBookmarkURLInput.checked,
 				replaceBookmarkURL: replaceBookmarkURLInput.checked,
 				groupDuplicateImages: groupDuplicateImagesInput.checked,
 				groupDuplicateImages: groupDuplicateImagesInput.checked,
 				infobarTemplate: infobarTemplateInput.value,
 				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() {
 	async function enableExternalSave() {
 		if (autoSaveExternalSaveInput.checked) {
 		if (autoSaveExternalSaveInput.checked) {
 			autoSaveExternalSaveInput.checked = false;
 			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>
 					<li data-options-label="maxResourceSizeLabel"> <span class="option">Option: maximum size (MB)</span>
 						<p>Specify the maximum size of embedded resources in megabytes.</p>
 						<p>Specify the maximum size of embedded resources in megabytes.</p>
 					</li>
 					</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>
 					<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
 						<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
 							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>
 				<label for="maxResourceSizeInput" id="maxResourceSizeLabel"></label>
 				<input type="number" id="maxResourceSizeInput" min="1">
 				<input type="number" id="maxResourceSizeInput" min="1">
 			</div>
 			</div>
+			<div class="option">
+				<label for="passReferrerOnErrorInput" id="passReferrerOnErrorLabel"></label>
+				<input type="checkbox" id="passReferrerOnErrorInput">
+			</div>
 			<div class="option">
 			<div class="option">
 				<label for="saveRawPageInput" id="saveRawPageLabel"></label>
 				<label for="saveRawPageInput" id="saveRawPageLabel"></label>
 				<input type="checkbox" id="saveRawPageInput">
 				<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.root = root;
 			this.options = options;
 			this.options = options;
 			this.options.url = this.options.url || (rootDocDefined && this.options.doc.location.href);
 			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.baseURI = rootDocDefined && this.options.doc.baseURI;
 			this.options.rootDocument = root;
 			this.options.rootDocument = root;
 			this.options.updatedResources = this.options.updatedResources || {};
 			this.options.updatedResources = this.options.updatedResources || {};
@@ -349,7 +351,8 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 						expectedType,
 						expectedType,
 						maxResourceSize: options.maxResourceSize,
 						maxResourceSize: options.maxResourceSize,
 						maxResourceSizeEnabled: options.maxResourceSizeEnabled,
 						maxResourceSizeEnabled: options.maxResourceSizeEnabled,
-						frameId: options.windowId
+						frameId: options.windowId,
+						resourceReferrer: options.resourceReferrer
 					});
 					});
 					onloadListener({ url: resourceURL });
 					onloadListener({ url: resourceURL });
 					if (!this.cancelled) {
 					if (!this.cancelled) {
@@ -427,7 +430,8 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 					maxResourceSize: this.options.maxResourceSize,
 					maxResourceSize: this.options.maxResourceSize,
 					maxResourceSizeEnabled: this.options.maxResourceSizeEnabled,
 					maxResourceSizeEnabled: this.options.maxResourceSizeEnabled,
 					charset,
 					charset,
-					frameId: this.options.windowId
+					frameId: this.options.windowId,
+					resourceReferrer: this.options.resourceReferrer
 				});
 				});
 				pageContent = content.data;
 				pageContent = content.data;
 			}
 			}
@@ -903,7 +907,8 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 				compressCSS: this.options.compressCSS,
 				compressCSS: this.options.compressCSS,
 				updatedResources: this.options.updatedResources,
 				updatedResources: this.options.updatedResources,
 				rootDocument: this.options.rootDocument,
 				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]"))
 			await Promise.all(Array.from(this.doc.querySelectorAll("style, link[rel*=stylesheet]"))
 				.map(async element => {
 				.map(async element => {
@@ -1199,7 +1204,8 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 						charset: this.charset != UTF8_CHARSET && this.charset,
 						charset: this.charset != UTF8_CHARSET && this.charset,
 						maxResourceSize: this.options.maxResourceSize,
 						maxResourceSize: this.options.maxResourceSize,
 						maxResourceSizeEnabled: this.options.maxResourceSizeEnabled,
 						maxResourceSizeEnabled: this.options.maxResourceSizeEnabled,
-						frameId: this.options.windowId
+						frameId: this.options.windowId,
+						resourceReferrer: this.options.resourceReferrer
 					});
 					});
 					content.data = getUpdatedResourceContent(resourceURL, content, this.options);
 					content.data = getUpdatedResourceContent(resourceURL, content, this.options);
 					if (element.tagName == "SCRIPT") {
 					if (element.tagName == "SCRIPT") {
@@ -1565,7 +1571,8 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 								maxResourceSize: options.maxResourceSize,
 								maxResourceSize: options.maxResourceSize,
 								maxResourceSizeEnabled: options.maxResourceSizeEnabled,
 								maxResourceSizeEnabled: options.maxResourceSizeEnabled,
 								validateTextContentType: true,
 								validateTextContentType: true,
-								frameId: options.frameId
+								frameId: options.frameId,
+								resourceReferrer: options.resourceReferrer
 							});
 							});
 							resourceURL = content.resourceURL;
 							resourceURL = content.resourceURL;
 							content.data = getUpdatedResourceContent(resourceURL, content, options);
 							content.data = getUpdatedResourceContent(resourceURL, content, options);
@@ -1640,6 +1647,7 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 					maxResourceSizeEnabled: options.maxResourceSizeEnabled,
 					maxResourceSizeEnabled: options.maxResourceSizeEnabled,
 					charset: options.charset,
 					charset: options.charset,
 					frameId: options.frameId,
 					frameId: options.frameId,
+					resourceReferrer: options.resourceReferrer,
 					validateTextContentType: true
 					validateTextContentType: true
 				});
 				});
 				resourceURL = content.resourceURL;
 				resourceURL = content.resourceURL;
@@ -1790,7 +1798,8 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 												expectedType: "image",
 												expectedType: "image",
 												maxResourceSize: options.maxResourceSize,
 												maxResourceSize: options.maxResourceSize,
 												maxResourceSizeEnabled: options.maxResourceSizeEnabled,
 												maxResourceSizeEnabled: options.maxResourceSizeEnabled,
-												frameId: options.windowId
+												frameId: options.windowId,
+												resourceReferrer: options.resourceReferrer
 											})).data;
 											})).data;
 										} catch (error) {
 										} catch (error) {
 											// ignored
 											// 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 DOMParser = window.DOMParser;
 	const Blob = window.Blob;
 	const Blob = window.Blob;
 	const FileReader = window.FileReader;
 	const FileReader = window.FileReader;
-	const fetch = window.fetch;
+	const fetch = url => window.fetch(url);
 	const crypto = window.crypto;
 	const crypto = window.crypto;
 	const TextDecoder = window.TextDecoder;
 	const TextDecoder = window.TextDecoder;
 	const TextEncoder = window.TextEncoder;
 	const TextEncoder = window.TextEncoder;
@@ -219,12 +219,12 @@ this.singlefile.lib.util = this.singlefile.lib.util || (() => {
 				try {
 				try {
 					if (options.frameId) {
 					if (options.frameId) {
 						try {
 						try {
-							response = await fetchFrameResource(resourceURL, options.frameId);
+							response = await fetchFrameResource(resourceURL, { frameId: options.frameId, referrer: options.resourceReferrer });
 						} catch (error) {
 						} catch (error) {
 							response = await fetchResource(resourceURL);
 							response = await fetchResource(resourceURL);
 						}
 						}
 					} else {
 					} else {
-						response = await fetchResource(resourceURL);
+						response = await fetchResource(resourceURL, { referrer: options.resourceReferrer });
 					}
 					}
 				} catch (error) {
 				} catch (error) {
 					return { data: options.asBinary ? "data:null;base64," : "", resourceURL };
 					return { data: options.asBinary ? "data:null;base64," : "", resourceURL };

+ 4 - 1
manifest.json

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