I have been working on developing an extension that replicates the Ctrl+Click to save image functionality from Opera 12. In my extension, I am utilizing chrome.downloads.download() in the background script to trigger downloads, while a content script is responsible for detecting user actions and sending a message containing the image URL to download.
Most of the functionality works smoothly, but I have encountered issues on certain websites like pixiv.net where the downloads are getting interrupted and failing. After inspecting using the webRequest API, I noticed that while the cookie from the active tab is sent with the download request, no referer header is included. My assumption is that these sites block download requests from external sources. Unfortunately, I have not been able to confirm this due to the webRequest.onError event not firing on failed downloads.
The challenge I'm facing is that I am unable to set the referer header manually since it cannot be done through chrome.downloads, and webRequest.onBeforeSendHeaders cannot be applied directly to a download request, making it impossible to add headers later on. Is there a way to initiate a download within the context of a tab to mimic the behavior of right-click > save as...?
To provide more clarity on how I'm initiating the downloads, here is a simplified version of my TypeScript code:
Injected script:
window.addEventListener('click', (e) => {
if (suspended) {
return;
}
if (e.ctrlKey && (<Element>e.target).nodeName === 'IMG') {
chrome.runtime.sendMessage({
url: (<HTMLImageElement>e.target).src,
saveAs: true,
});
e.preventDefault();
e.stopImmediatePropagation();
suspended = true;
window.setTimeout(() => suspended = false, 100);
}
}, true);
Background script:
interface DownloadMessage {
url: string;
saveAs: boolean;
}
chrome.runtime.onMessage.addListener((message: DownloadMessage, sender, sendResponse) => {
chrome.downloads.download({
url: message.url,
saveAs: message.saveAs,
});
});
Update
Building upon ExpertSystem's solution below, I've come up with an approach that mostly resolves the issue. Now, when the background script receives a download request for an image, I check the host name of the URL against a list of specified sites that require a workaround. If a workaround is necessary, I send a message back to the tab to handle the download using an XMLHttpRequest with responseType = 'blob'. Then, I use URL.createObjectURL() on the blob and transmit that URL back to the background script for downloading. This method avoids any size limitations associated with data URIs. Additionally, in case the XHR request fails, I ensure to retry using the standard method to display a failed download prompt to the user.
The updated code structure looks like this:
Injected Script:
// ...Original code here...
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
switch (message.action) {
case 'xhr-download':
var xhr = new XMLHttpRequest();
xhr.responseType = 'blob';
xhr.addEventListener('load', (e) => {
chrome.runtime.sendMessage({
url: URL.createObjectURL(xhr.response),
filename: message.url.substr(message.url.lastIndexOf('/') + 1),
saveAs: message.saveAs,
});
});
xhr.addEventListener('error', (e) => {
chrome.runtime.sendMessage({
url: message.url,
saveAs: message.saveAs,
forceDownload: true,
});
});
xhr.open('get', message.url, true);
xhr.send();
break;
}
});
Background Script:
interface DownloadMessage {
url: string;
saveAs: boolean;
filename?: string;
forceDownload?: boolean;
}
chrome.runtime.onMessage.addListener((message: DownloadMessage, sender, sendResponse) => {
var downloadOptions = {
url: message.url,
saveAs: message.saveAs,
};
if (message.filename) {
options.filename = message.filename;
}
var a = document.createElement('a');
a.href = message.url;
if (message.forceDownload || a.protocol === 'blob:' || !needsReferralWorkaround(a.hostname)) {
chrome.downloads.download(downloadOptions);
if (a.protocol === 'blob:') {
URL.revokeObjectUrl(message.url);
}
} else {
chrome.tabs.sendMessage(sender.tab.id, {
action: 'xhr-download',
url: message.url,
saveAs: message.saveAs
});
}
});
This solution may encounter challenges if the image is hosted on a different domain than the page and the server does not send CORS headers. However, I believe there won't be many sites restricting image downloads based on referrer and serving images from a distinct domain.