chrome.downloads.download ignoring parameters after 'url' - google-chrome-extension

I am trying to write a simple extension that will automatically download an image from our company website to a directory. The problem is that chrome.downloads.download seems to ignore every parameter after 'url'.
It downloads the file. But keeps the original name, and ignores sub-directories. It will append a (#) if the file already exits instead of overwriting as specified in the code.
I have tried various ways of implementing the object passed to chrome.downloads.download, including creating a object with all the parameters and passing that object. I've tried using quotes even where it didn't make sense.
I have tried reformatting the filename parameter, even giving it a fixed value of "foo.jpg". I have tried doing this in both the content.js and bs.js.
I have confirmed that the filename is passed to the background script and the value is accessible by displaying it with an alert() in the background script
//Manifest.js (important parts)
"permissions": ["downloads","webNavigation"],
"background": {
"scripts": ["bs.js"],
"Persistent": false
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["jquery-3.4.1.min.js"]
},
{
"matches": ["https://mycompany.com/*"],
"js": ["content.js"]
}
]
//Content.js (important parts)
var $imgs = $('#spec_jpg img');
var url = "https:" + $imgs.attr('src');
var filename ="specificationArchive/" + url.substring(url.lastIndexOf('/')+1); //edited
chrome.runtime.sendMessage({url: url, filename: filename});
//bs.js (whole thing)
chrome.runtime.onMessage.addListener(
function(request, sender){
chrome.downloads.download ( { url: request.url,
filename: request.filename,
conflictAction: "overwrite",
saveAs: false }
)
}
);
The extension does download the file, but I expected it to save it into a sub-directorycalled specificationArchive. I also expected it to overwrite an existing file instead of appending a (#) to the end of the name. Even when passing an absolute string for the filename, I get the file's original name

The solution provided by wOxxOm is to add an additional listener. Here's the code that was added to bs.js
//bs.js
chrome.downloads.onDeterminingFilename.addListener(function(item, suggest) {
suggest({filename: "specificationArchive/" + item.filename, conflictAction: 'overwrite'});
});
Once this was included it saved to the directory listed.
Thanks again.

Related

Content script is not yet loaded while application is waiting for extension to get installed [duplicate]

After the Chrome extension I'm working on is installed, or upgraded, the content scripts (specified in the manifest) are not re-injected so a page refresh is required to make the extension work. Is there a way to force the scripts to be injected again?
I believe I could inject them again programmatically by removing them from the manifest and then handling which pages to inject in the background page, but this is not a good solution.
I don't want to automatically refresh the user's tabs because that could lose some of their data. Safari automatically refreshes all pages when you install or upgrade an extension.
There's a way to allow a content script heavy extension to continue functioning after an upgrade, and to make it work immediately upon installation.
Install/upgrade
The install method is to simply iterate through all tabs in all windows, and inject some scripts programmatically into tabs with matching URLs.
ManifestV3
manifest.json:
"background": {"service_worker": "background.js"},
"permissions": ["scripting"],
"host_permissions": ["<all_urls>"],
These host_permissions should be the same as the content script's matches.
background.js:
chrome.runtime.onInstalled.addListener(async () => {
for (const cs of chrome.runtime.getManifest().content_scripts) {
for (const tab of await chrome.tabs.query({url: cs.matches})) {
chrome.scripting.executeScript({
target: {tabId: tab.id},
files: cs.js,
});
}
}
});
This is a simplified example that doesn't handle frames. You can use getAllFrames API and match the URLs yourself, see the documentation for matching patterns.
ManifestV2
Obviously, you have to do it in a background page or event page script declared in manifest.json:
"background": {
"scripts": ["background.js"]
},
background.js:
// Add a `manifest` property to the `chrome` object.
chrome.manifest = chrome.runtime.getManifest();
var injectIntoTab = function (tab) {
// You could iterate through the content scripts here
var scripts = chrome.manifest.content_scripts[0].js;
var i = 0, s = scripts.length;
for( ; i < s; i++ ) {
chrome.tabs.executeScript(tab.id, {
file: scripts[i]
});
}
}
// Get all windows
chrome.windows.getAll({
populate: true
}, function (windows) {
var i = 0, w = windows.length, currentWindow;
for( ; i < w; i++ ) {
currentWindow = windows[i];
var j = 0, t = currentWindow.tabs.length, currentTab;
for( ; j < t; j++ ) {
currentTab = currentWindow.tabs[j];
// Skip chrome:// and https:// pages
if( ! currentTab.url.match(/(chrome|https):\/\//gi) ) {
injectIntoTab(currentTab);
}
}
}
});
Historical trivia
In ancient Chrome 26 and earlier content scripts could restore connection to the background script. It was fixed http://crbug.com/168263 in 2013. You can see an example of this trick in the earlier revisions of this answer.
The only way to force a content script to be injected without refreshing the page is via programatic injection.
You can get all tabs and inject code into them using the chrome tabs API.
For example you can store a manifest version in local storage and every time check if the manifest version is old one (in background page), if so you can get all active tabs and inject your code programmatically, or any other solution that will make you sure that the extension is updated.
Get all tabs using:
chrome.tabs.query
and inject your code into all pages
chrome.tabs.executeScript(tabId, {file: "content_script.js"});
Try this in your background script. Many of the old methods have been deprecated now, so I have refactored the code. For my use I'm only installing single content_script file. If need you can iterate over
chrome.runtime.getManifest().content_scripts array to get all .js files.
chrome.runtime.onInstalled.addListener(installScript);
function installScript(details){
// console.log('Installing content script in all tabs.');
let params = {
currentWindow: true
};
chrome.tabs.query(params, function gotTabs(tabs){
let contentjsFile = chrome.runtime.getManifest().content_scripts[0].js[0];
for (let index = 0; index < tabs.length; index++) {
chrome.tabs.executeScript(tabs[index].id, {
file: contentjsFile
},
result => {
const lastErr = chrome.runtime.lastError;
if (lastErr) {
console.error('tab: ' + tabs[index].id + ' lastError: ' + JSON.stringify(lastErr));
}
})
}
});
}
Chrome has added a method to listen for the install or upgrade event of the extension. One can re-inject the content script when such an event occur.
https://developers.chrome.com/extensions/runtime#event-onInstalled
Due to https://bugs.chromium.org/p/chromium/issues/detail?id=168263, the connection between your content script and background script is severed. As others have mentioned, one way to get around this issue is by reinjecting a content script. A rough overview is detailed in this StackOverflow answer.
The main tricky part is that it's necessary to "destruct" your current content script before injecting a new content script. Destructing can be really tricky, so one way to reduce the amount of state you must destruct is by making a small reinjectable script, that talks to your main content script over the DOM.
can't you add ?ver=2.10 at the end of css or js you upgraded?
"content_scripts": [ {
"css": [ "css/cs.css?ver=2.10" ],
"js": [ "js/contentScript.js?ver=2.10" ],
"matches": [ "http://*/*", "https://*/*" ],
"run_at": "document_end"
} ],

How do I send a message from background script to content script?

I want my background script in my chrome extension to send a message to the content script and then wait for the response. I tried a couple of different solutions I found but I always get the same error:
_generated_background_page.html:1 Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.
_generated_background_page.html:1 Error handling response: TypeError: Cannot read property 'getSelected' of undefined
at chrome-extension://pkolpfkgeblgjiiaklpppfmeomlkbhop/background_script.js:11:25
I also already tried disabling all the other chrome extensions.
All the code that might be important:
//background_script.js
chrome.contextMenus.onClicked.addListener(function(info, tab){
chrome.tabs.sendMessage(tab.id, {
content: "getSelected"
}, function(response) {
console.log(response.getSelected());
});
});
//content_script.js
chrome.runtime.onMessage.addListener(function(message, sender, callback) {
if (message.content == "getSelected") {
callback({getSelected: getSelected()});
}
});
//manifest.json
{
"manifest_version": 2,
"content_scripts":[ {
"matches": ["<all_urls>"],
"js": ["content_script.js"]
}],
"background":{
"scripts": ["background_script.js"]
},
"permissions": [
"*://*/*",
"activeTab",
"declarativeContent",
"storage",
"contextMenus",
"tabs"
]
}
Thanks in advance for the help :)
You need to define getSelected() in the content script, it's not a built-in function. You probably meant getSelection() which is a built-in function.
When the response is sent it's not a function so it can't be invoked: you need to remove () in response.getSelected()
Receiving end does not exist usually means the tab doesn't run the content script:
happens if the tab is not a web page e.g. it's an empty new tab page or a chrome:// page or an extension page
or the tab wasn't reloaded after you reloaded or re-enabled the extension, see content script re-injection after upgrade or install
Apparently you want to get the currently selected text, in which case you don't need a declared content script at all so you can remove content_scripts section and instead use programmatic injection:
chrome.contextMenus.onClicked.addListener((info, tab) => {
chrome.tabs.executeScript(tab.id, {
frameId: info.frameId,
runAt: 'document_start',
code: 'getSelection().toString()',
}, ([sel] = []) => {
if (!chrome.runtime.lastError) {
console.log(sel);
}
})
});
Notes:
getSelection() returns a Selection object which is a complex DOM object so it cannot be transferred, hence we explicitly extract the selection as a string.
Only simple types like strings, numbers, boolean, null, and arrays/objects of such simple types can be transferred.
We're using the frameId where the user invoked the menu
Using runAt: 'document_start' ensures the code runs immediately even if the page is still loading its initial HTML
For this particular task you don't need "*://*/*" or tabs in permissions.
Where to read console messages from background.js in a Chrome extension?
P.S. For more complex data extraction, use file: 'content.js', instead of code: .... and transfer the last expression from content.js like this:
function foo() {
let results;
// .......
return results;
}
foo(); // this line should be the last executed expression in content.js

Is there an easy way to limit my content script to a specific array of matches?

I want my Chrome extension to only work only on news websites, so I have a long list of many URLs I'd like to limit it to.
Is there an easier way to restrict my extension than manually adding the long list in the "matches" field of the manifest.json? Thanks!
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["http://www.newswebsite.com/*",
"...long long array with urls..."],
...
}
],
...
}
In the extension's manifest
I'll just start by saying that well, the "simplest" way to inject your content scripts in all the sites of your list is to actually just add them in the array of "matches" in the extension's manifest.
If you are worrying about having to insert each site in the array manually then that's no big deal, you can easily paste it inside a text editor and use find and replace to do what you want, for example replacing \n with /*",\n"*:// and then editing the first and last manually:
site1.com -> site1.com/*", -> "*://site1.com/*",
site2.net -> "*://site2.net/*", -> "*://site2.net/*",
site3.org -> "*://site3.org/*", -> "*://site3.org/*"
"*//
Once you've got this you can just copy and paste it inside your "matches": [ ... array
Through the background script with the tabs API
If you don't really want to add them inside your manifest, you can put them in a text file inside your extension's directory, then load it in your background page and add a listener to chrome.tabs.onUpdated to check when the URL changes and inject the script if the new URL matches one of the sites. This IMHO is more complicated than simply adding them in your manifest.
Working example
Your list.txt:
site1.com
site2.net
site3.org
Your background.js script:
// Function to extract the hostname from an URL.
function getHostname(url) {
return url.match(/^(.*:)\/\/([A-Za-z0-9\-\.]+)/)[2];
}
// XHR to get the list.txt
var xhr = new XMLHttpRequest();
xhr.addEventListener('readystatechange', function() {
if (xhr.readyState == 4 && xhr.status == 200) {
// Parse the list and create a set from it:
var list = new Set(xhr.responseText.split('\n'));
// Listen for tabs.onUpdated:
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
// If the tab changed URL:
if (changeInfo.url)
// If the new URL is one of the sites in the list:
if (list.has(getHostname(changeInfo.url)))
// Inject the script:
chrome.tabs.executeScript(tabId, {file: 'yourScript.js'});
});
}
});
xhr.open('GET', 'list.txt', true);
xhr.send();
Don't forget to also add permissions for <all_urls> (since you're gonna inject scripts in sites not listed directly), and the chrome.tabs API in your manifest:
...
"permissions": ["<all_urls>", "tabs"],
...
Through the background script with the declarativeContent API
Similar to the previous one, but simpler: parse the list.txt and create a new rule to be processed by the chrome.declarativeContent API. You will not have to worry about matching the URLs manually, and Chrome will run the matches and optimize the rule for you.
chrome.runtime.onInstalled.addListener(function() {
chrome.declarativeContent.onPageChanged.removeRules(undefined, function() {
var xhr = new XMLHttpRequest();
xhr.addEventListener('readystatechange', function() {
if (xhr.readyState == 4 && xhr.status == 200) {
var list = xhr.responseText.split('\n');
// Create a new rule with an array of conditions to match all the sites:
var rule = {
conditions: list.map(site => new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostEquals: site, schemes: ['http', 'https'] }
})),
// To inject the script when the conditions are met:
actions: [new chrome.declarativeContent.RequestContentScript({js: ['yourScript.js']})]
}
// And register it:
chrome.declarativeContent.onPageChanged.addRules([rule]);
}
});
});
xhr.open('GET', 'list.txt', true);
xhr.send();
});
Note that the above code is all wrapped inside a chrome.runtime.onInstalled listener, since that declarativeContent rules are persistent, and you don't need to add them each time your extension starts.
You'll need permission for both "<all_urls>" and "declarativeContent" in your manifest in this case:
...
"permissions": ["<all_urls>", "declarativeContent"],
...
Now, with this said, I actually think that the easiest way to do what you want is to just simply add the sites in your "matches" field of the extension's manifest, but you're free to do what you think is best. I do actually use the second approach often, however overall the third method is the most efficent if you don't want to manually add the matching rules in your manifest.json.
Take a look at the documentation for the methods and types I used in the examples if you want to know more:
chrome.tabs.onUpdated
chrome.tabs.executeScript
chrome.declarativeContent.PageStateMatcher
chrome.declarativeContent.RequestContentScript
I was searching for an answer that worked with the framework, but came to creating straight Javascript within the Listener and for the active tab. I also wanted to use a defined function, rather than loading a separate Javascript file.
My end goal was to have the extension respond to a hotkey, but only for specific hosts. This is how I did it:
In manifest.json:
...
"commands": {
"some-command": {
"suggested_key": {
"default": "Ctrl+Shift+L"
},
"description": "Tell me something special."
}
},
...
In background.js:
chrome.commands.onCommand.addListener(function(command) {
chrome.tabs.getSelected(null, function(tab) {
var url = new URL(tab.url);
if (url.hostname != 'somedomain.com') return;
if (command == 'some-command') {
custom_function();
}
});
});
function custom_function {}
The end result is that the extension only works on tabs that have the particular domain name, regardless if triggering a hotkey command or through the address bar popup. I didn't include the address bar popup code, as that is right on the "build a Chrome Extension" guide - something I'd expect we have all already completed.

Chrome extension content script re-injection after upgrade or install

After the Chrome extension I'm working on is installed, or upgraded, the content scripts (specified in the manifest) are not re-injected so a page refresh is required to make the extension work. Is there a way to force the scripts to be injected again?
I believe I could inject them again programmatically by removing them from the manifest and then handling which pages to inject in the background page, but this is not a good solution.
I don't want to automatically refresh the user's tabs because that could lose some of their data. Safari automatically refreshes all pages when you install or upgrade an extension.
There's a way to allow a content script heavy extension to continue functioning after an upgrade, and to make it work immediately upon installation.
Install/upgrade
The install method is to simply iterate through all tabs in all windows, and inject some scripts programmatically into tabs with matching URLs.
ManifestV3
manifest.json:
"background": {"service_worker": "background.js"},
"permissions": ["scripting"],
"host_permissions": ["<all_urls>"],
These host_permissions should be the same as the content script's matches.
background.js:
chrome.runtime.onInstalled.addListener(async () => {
for (const cs of chrome.runtime.getManifest().content_scripts) {
for (const tab of await chrome.tabs.query({url: cs.matches})) {
chrome.scripting.executeScript({
target: {tabId: tab.id},
files: cs.js,
});
}
}
});
This is a simplified example that doesn't handle frames. You can use getAllFrames API and match the URLs yourself, see the documentation for matching patterns.
ManifestV2
Obviously, you have to do it in a background page or event page script declared in manifest.json:
"background": {
"scripts": ["background.js"]
},
background.js:
// Add a `manifest` property to the `chrome` object.
chrome.manifest = chrome.runtime.getManifest();
var injectIntoTab = function (tab) {
// You could iterate through the content scripts here
var scripts = chrome.manifest.content_scripts[0].js;
var i = 0, s = scripts.length;
for( ; i < s; i++ ) {
chrome.tabs.executeScript(tab.id, {
file: scripts[i]
});
}
}
// Get all windows
chrome.windows.getAll({
populate: true
}, function (windows) {
var i = 0, w = windows.length, currentWindow;
for( ; i < w; i++ ) {
currentWindow = windows[i];
var j = 0, t = currentWindow.tabs.length, currentTab;
for( ; j < t; j++ ) {
currentTab = currentWindow.tabs[j];
// Skip chrome:// and https:// pages
if( ! currentTab.url.match(/(chrome|https):\/\//gi) ) {
injectIntoTab(currentTab);
}
}
}
});
Historical trivia
In ancient Chrome 26 and earlier content scripts could restore connection to the background script. It was fixed http://crbug.com/168263 in 2013. You can see an example of this trick in the earlier revisions of this answer.
The only way to force a content script to be injected without refreshing the page is via programatic injection.
You can get all tabs and inject code into them using the chrome tabs API.
For example you can store a manifest version in local storage and every time check if the manifest version is old one (in background page), if so you can get all active tabs and inject your code programmatically, or any other solution that will make you sure that the extension is updated.
Get all tabs using:
chrome.tabs.query
and inject your code into all pages
chrome.tabs.executeScript(tabId, {file: "content_script.js"});
Try this in your background script. Many of the old methods have been deprecated now, so I have refactored the code. For my use I'm only installing single content_script file. If need you can iterate over
chrome.runtime.getManifest().content_scripts array to get all .js files.
chrome.runtime.onInstalled.addListener(installScript);
function installScript(details){
// console.log('Installing content script in all tabs.');
let params = {
currentWindow: true
};
chrome.tabs.query(params, function gotTabs(tabs){
let contentjsFile = chrome.runtime.getManifest().content_scripts[0].js[0];
for (let index = 0; index < tabs.length; index++) {
chrome.tabs.executeScript(tabs[index].id, {
file: contentjsFile
},
result => {
const lastErr = chrome.runtime.lastError;
if (lastErr) {
console.error('tab: ' + tabs[index].id + ' lastError: ' + JSON.stringify(lastErr));
}
})
}
});
}
Chrome has added a method to listen for the install or upgrade event of the extension. One can re-inject the content script when such an event occur.
https://developers.chrome.com/extensions/runtime#event-onInstalled
Due to https://bugs.chromium.org/p/chromium/issues/detail?id=168263, the connection between your content script and background script is severed. As others have mentioned, one way to get around this issue is by reinjecting a content script. A rough overview is detailed in this StackOverflow answer.
The main tricky part is that it's necessary to "destruct" your current content script before injecting a new content script. Destructing can be really tricky, so one way to reduce the amount of state you must destruct is by making a small reinjectable script, that talks to your main content script over the DOM.
can't you add ?ver=2.10 at the end of css or js you upgraded?
"content_scripts": [ {
"css": [ "css/cs.css?ver=2.10" ],
"js": [ "js/contentScript.js?ver=2.10" ],
"matches": [ "http://*/*", "https://*/*" ],
"run_at": "document_end"
} ],

chrome extension : How to get key events

Is there any way to get key events in a google chrome extension file - background.html - ?
document.onkeydown = function() {
alert('test)
};
Previous code doesn't work.
Not sure if this is still active, but an update might help someone like me who is just now playing around with Chrome extensions. The new commands api allows you to receive the same functionality without using a content script.
Use your manifest.json file to register the keyboard commands. For example:
...
"commands": {
"save" : {
"suggested_key": {
"default": "Alt+Shift+S"
},
"description": "Save a link"
},
"random": {
"suggested_key": {
"default": "Alt+Shift+L"
},
"description": "Load a random link"
}
}
...
and then you can catch it in your background page
chrome.commands.onCommand.addListener(function (command) {
if (command === "save") {
alert("save");
} else if (command === "random") {
alert("random");
}
});
Hopefully that helps!
I assume you want to implement hotkeys for your extension. Your code should in fact work, except it works on the background page, which is usually not open to catch key presses.
To catch keypresses globally, or at least on web pages, you will have to use a content script that sends messages to the background page. The content script is injected to the open web page and insert methods for catching keypresses, and then send a message to the background page with information on which keys are pressed.
Firstly, you'd need to have a background JavaScript file, which in this case I'll call popup.js. And that'll include the code you gave:
document.onkeydown = function() {
// what you want to on key press.
};
Then you want to include this as a background Script file in your manifest.json:
"background": {
"scripts": [
"popup.js"
],
"persistent": false
},
"content_scripts": [
{
"matches": [ "<all_urls>" ],
"js": [
"popup.js"
]
}
]
You can get the key strokes from contentScript.js and then pass it as variable using chrome.runtime.SendMessage().
For example:
Add below piece of code inside contentScript.js file
window.addEventListener('keypress',function(key){
console.log(key.key)
let keyvalue = key.key
chrome.runtime.sendMessage(null,keyvalue,(response)=>{
console.log("Sent key value"+response)
})
})
Inside background.js file place below piece of code,
chrome.runtime.onMessage.addListener((message,sender,sendResponse)=>{
console.log(message)
console.log(sender)
sendResponse("Received message in background!!")
})
Now you will get the key strokes as you type. In background console you can view them like below.

Resources