Problem
I am making a Chrome extension that downloads files and adds links to these downloaded files to a webpage. When these links are clicked, I would like to relay the click as a "user gesture" to my background script so that the file opens without prompting. Looking at the docs on the relevant method, chrome.downloads.open, there is no discussion of user gestures.
Essentially, I want to get rid of this =>
using the idea in this comment.
Background
It seems like this is possible because
This post on what constitutes a user gesture lists click as one of the types of user gestures
The spec, which says clicks will generate a user gesture
In the code below, logging the event results in a MouseEvent, with type click and isTrusted set to true.
[downloads.open] can only run in a code initiated by a user action, like a click on a button. It cannot be executed from non-user events. - Xan, comment for How to open a downloaded file?
Code below aims to be an MCVE.
Content Script
// Add an event listener for every download link
function addDownloadListeners() {
const pathElems = document.getElementsByClassName('pathClass');
for (path of pathElems) {
path.addEventListener('click', openDownload);
}
}
// Send a message with the download ID to the background script
function openDownload(event) {
const selector = '#' + event.currentTarget.id;
const downloadId = parseInt($(selector).attr('download_id'));
chrome.runtime.sendMessage({
'downloadId': downloadId,
}, function(response) {
if (response !== undefined) {
resolve(response.response);
} else {
reject(new Error(response.response));
}
});
}
manifest.json
{
"background": {
"scripts": ["js/background.js"]
},
"content_scripts": [
{
"js": [
"js/content_script.js"
],
"matches": ["*://*.website.com/*/urlpath*"],
"run_at": "document_end"
}
],
"permissions": [
"downloads",
"downloads.open"
],
"manifest_version": 2,
}
Background Script
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
try {
chrome.downloads.open(request.downloadId);
} catch (e) {
sendResponse({response: 'Error opening file with download id ' +
request.downloadId + ' getting error ' + e.toString()
});
}
}
)
Question
How can I get a click to open a download without creating an additional prompt?
This is not possible
It is not possible to prevent/use an alternative to prompts for chrome methods that require user consent. Based on this discussion in the Chromium Extension google group,
Some Chrome methods (like chrome.downloads.open) need to get user consent via prompt.
These are automatically generated via Chrome itself - they cannot be overridden or modified.
User gestures outside of prompts are not relevant to methods that require user consent.
This current behavior is circa 2014 for chrome.downloads.open.
Special thanks to #wOxxOm and Decklin Johnston for making this answer possible.
Related
I want to know whether the current tab is an old tab which was opened before installing the extension or is that a special tab (browser UI, extension page, chrome.google.com) where I can not inject the content script.
There's a partial solution where I try to send message to the content script and if it throws an error (i.e. content script is not loaded on the page) then it's either an old tab or a special page. I need a way to know which one is it so that I can inform via popup page.
Detecting if browser extension popup is running on a tab that has content script
There is possibly one more way: try to execute script on page, if it succeeds then it was an old tab but this would need one more permission in manifest i.e. scripting which I feel is a bit excessive just to detect an old tab. any other possible solutions?
This is for chrome extension development.
If you're only interested in distinguishing new tabs from tabs that are old and/or non-injectable, you can let the content scripts add the IDs of their tabs to session storage. Later on, you can look up the ID of any tab in session storage.
old tab = tab that was opened before installing the extension
new tab = tab that was opened after installing the extension
non-injectable tab = tab that you can't inject content scripts into, see Chrome Extension postMessage from background script to content script every time in change crome tabs
Tab IDs are only valid during the current session. When you store tab IDs in sessions storage, they are gone when you start a new session, which is what you want.
Content scripts don't know the ID of the tab they're running in, so they can't store it by calling chrome.storage.session.set() directly. However, content scripts can send a message to the service worker. The service worker receives information about the sender's tab along with the message.
The proof of concept below doesn't try to determine if a tab is injectable or not.
You can either do this by checking the tab's URL, e.g. if it starts with "chrome://" or "chrome-extension://". But I don't know if you can determine all non-injectable tabs like this, e.g. those whose URL is forbidden by the runtime_blocked_hosts policy.
Or you can inject an empty content script into the tab and check for errors. This requires the "scripting" permission, plus "activeTab" or the right host permissions, but lets you determine all non-injectable tabs.
When you click the action, the extension shows a notification. It tells you if the active tab is old, or if it's new and/or non-injectable.
manifest.json
{
"manifest_version": 3,
"name": "Tabs with vs without Content Scripts",
"version": "1.0",
"action": {
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["content_script.js"]
}
],
"permissions": [
"notifications",
"storage"
]
}
background.js
async function action_on_clicked(tab) {
let { [tab.id.toString()]: tab_id_stored } = await chrome.storage.session.get(tab.id.toString());
let message;
if (tab_id_stored === undefined) {
/* Old or non-injectable tab */
message = "Old or non-injectable tab";
}
else {
message = "New tab";
}
chrome.notifications.create(
{
iconUrl: "/icon_128.png",
message,
title: "Tabs with vs without Content Scripts",
type: "basic",
},
notificationId => {
if (chrome.runtime.lastError === undefined) {
console.log(notificationId);
}
else {
console.error(chrome.runtime.lastError);
}
}
);
}
function runtime_on_message(message, sender, sendResponse) {
if (message == "store_tab_id") {
if (sender.tab) {
chrome.storage.session.set({ [sender.tab.id.toString()]: true })
.then(() => {
sendResponse("tab id stored");
})
.catch(error => {
sendResponse(error);
});
return true;
}
else {
sendResponse("sender.tab is undefined");
}
}
else {
sendResponse("unknown message");
}
}
chrome.action.onClicked.addListener(action_on_clicked);
chrome.runtime.onMessage.addListener(runtime_on_message);
content_script.js
(async () => {
let response = await chrome.runtime.sendMessage("store_tab_id");
console.log("response", response);
})();
I am currently trying to make a chrome extension that lists all of the open tabs in its popup window. With more functionality to be added later, such as closing a specific tab through the popup, opening up a new tab with a specific URL etc.
manifest.json
{
"manifest_version": 2,
"name": "List your tabs!",
"version": "1.0.0",
"description": "This extension only lists all of your tabs, for now.",
"background": {
"persistent": true,
"scripts": [
"js/background.js"
]
},
"permissions": [
"contextMenus",
"activeTab",
"tabs"
],
"browser_action": {
"default_popup": "popup.html"
}
}
background.js
const tabStorage = {};
(function() {
getTabs();
chrome.tabs.onRemoved.addListener((tab) => {
getTabs();
});
chrome.tabs.onUpdated.addListener((tab) => {
getTabs();
});
}());
function getTabs() {
console.clear();
chrome.windows.getAll({populate:true},function(windows){
windows.forEach(function(window){
window.tabs.forEach(function(tab){
//collect all of the urls here, I will just log them instead
tabStorage.tabUrl = tab.url;
console.log(tabStorage.tabUrl);
});
});
});
chrome.runtime.sendMessage({
msg: "current_tabs",
data: {
subject: "Tabs",
content: tabStorage
}
});
}
popup.js
(function() {
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if (request.msg === "current_tabs") {
// To do something
console.log(request.data.subject)
console.log(request.data.content)
}
}
);
}());
From my understanding, since you're supposed to have listeners in background.js for any changes to your tabs. Then when those occur, you can send a message to popup.js
As you can see, for now I'm simply trying to log my tabs in the console to make sure it works, before appending it to a div or something in my popup.html. This does not work, however, because in my popup.html I'm getting the following error in the console:
popup.js:3 Uncaught TypeError: Cannot read property 'sendMessage' of undefined
so I'm... kind of understanding that I can't use onMessage in popup.js due to certain restrictions, but I also have no clue, then, on how to achieve what I'm trying to do.
Any help would be appreciated.
The Google's documentation about the background script is a bit vague. The important thing for your use case is that the popup runs only when it's shown, it doesn't run when hidden, so you don't need background.js at all, just put everything in popup.js which will run every time your popup is shown, here's your popup.html:
<script src="popup.js"></script>
The error message implies you were opening the html file directly from disk as a file:// page, but it should be opened by clicking the extension icon or via its own URL chrome-extension://id/popup.html where id is your extension's id. This happens automatically when you click the extension icon - the popup is a separate page with that URL, with its own DOM, document, window, and so on.
The popup has its own devtools, see this answer that shows how to invoke it (in Chrome it's by right-clicking inside the popup, then clicking "inspect").
Extension API is asynchronous so the callbacks run at a later point in the future, after the outer code has already completed, which is why you can't use tabStorage outside the callback like you do currently. More info: Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference.
There should be no need to enumerate all the tabs in onRemoved and onUpdated because it may be really slow when there's a hundred of tabs open. Instead you can modify your tabStorage using the parameters provided to the listeners of these events, see the documentation for details. That requires tabStorage to hold the id of each tab so it would make sense to simply keep the entire response from the API as is. Here's a simplified example:
let allTabs = [];
chrome.tabs.query({}, tabs => {
allTabs = tabs;
displayTabs();
});
function displayTabs() {
document.body.appendChild(document.createElement('ul'))
.append(...allTabs.map(createTabElement));
}
function createTabElement(tab) {
const el = document.createElement('li');
el.textContent = tab.id + ': ' + tab.url;
return el;
}
I am trying to trigger a block of code every time the user selects/highlights some text on any of the tabs.
I am able to run the javascript when I highlight some text and then click on my extension.
I have already read some of the chrome apis but none of them seem to work.
https://developer.chrome.com/extensions/api_index
chrome.browserAction.onClicked.addListener(function() {
alert(window.getSelection().toString());
});
I am not able to run the code as soon as I highlight some text. Is there an API that already handles that?
You should just add a buffer:
Store selection
As you are talking about selections from any tab, You should use the storage api and refer the tab id for further use.
document.onmouseup = function() {
chrome.tabs.getCurrent(function(_tabId){
if(_tabId){
var _SELECTION = {};
_SELECTION[tabId] = window.getSelection().toString();
chrome.storage.local.set(_SELECTION, function() {
console.log('Selection saved: ', _SELECTION[tabId]);
});
}
});
}
Use it when you click on your extension
chrome.browserAction.onClicked.addListener(function() {
chrome.tabs.getCurrent(function(_tabId){
if(_tabId){
chrome.storage.local.get(_tabId, function(result) {
alert('Selection restored: ' + result[tabId].txt);
});
}
});
});
Manifest
Don't forget to update your manifest.json to set the according permissions
{
...
"permissions": [
"storage",
"tabs"
],
...
}
Note
I used storage.local as the clipboard should be kept on the local machine, but if you want to share it cross-machines you can use storage.sync. More to read in the docs.
I'd use contextMenus. It makes more sense if you highlight text to right-click and perform an action.
I am trying to change the background of about:newtab in Firefox using the new WebExtensions API. At the moment, I don't understand why I receive the following error message in the debug console:
Unchecked lastError value: Error: No window matching {"matchesHost":[]}
The code for my add-on is as follows:
manifest.json
{
"background": {
"scripts": ["background.js"]
},
"description": "Yourdomain description.",
"homepage_url": "https://yourdomain.com",
"icons": {
"64": "icons/icon-64.png"
},
"manifest_version": 2,
"name": "yourDomain",
"permissions": [
"tabs",
"activeTab"
],
"version": "2.0"
}
background.js:
var tabId;
function changeBackground() {
chrome.tabs.insertCSS( tabId, {
code: "body { border: 20px dotted pink; }"
});
}
function handleCreated(tab) {
if (tab.url == "about:newtab") {
console.log(tab);
tabId = tab.id;
changeBackground();
}
}
chrome.tabs.onCreated.addListener(handleCreated);
Currently no way for WebExtensions add-ons to change about:newtab
That is the error you receive when the page into which you are attempting to inject does not match the match pattern, or can not be expressed as a match pattern (i.e. the URL must be able to be expressed as a match pattern, even if you didn't supply a match pattern). Match patterns must have a scheme that is one of http, https, file, ftp, or app. The error you are seeing is a result of not checking the runtime.lastError value in the callback function in tabs.insertCSS() when the error is the one expected when you attempt to inject code or CSS into an about:* page.
The MDN documentation is quite specific about not being able to use tabs.insertCSS() with any of the about:* pages (emphasis mine):
You can only inject CSS into pages whose URL can be expressed using a match pattern: meaning, its scheme must be one of "http", "https", "file", "ftp". This means that you can't inject CSS into any of the browser's built-in pages, such as about:debugging, about:addons, or the page that opens when you open a new empty tab [about:newtab].
Future:
Chrome has the manifest.json key chrome_url_overrides which allows overriding any of: bookmarks, history, or newtab. It appears that Firefox support for this manifest.json key is a "maybe".
Alternatives:
It is possible to override the newtab page with other types of Firefox add-ons.
You can use WebExtensions to detect that a tab has changed its URL to about:newtab and redirect the tab to a page of your choice.
Using tabs.insertCSS() or tabs.executeScript() for tabs, generally
If you are using tabs.insertCSS() or tabs.executeScript() in a situation where the tab normally contains a regular URL, but might be an about*: (or chrome:* scheme on Chrome) (e.g. a browser_action button), you could handle the error with the following code (tested and working in both Firefox WebExtensions and Chrome):
function handleExecuteScriptAndInsertCSSErrors(tabId){
if(chrome.runtime.lastError){
let message = chrome.runtime.lastError.message;
let isFirefox = window.InstallTrigger?true:false;
let extraMessage = tabId ? 'in tab ' + tabId + '.' : 'on this page.';
if((!isFirefox && message.indexOf('Cannot access a chrome:') > -1) //Chrome
||(isFirefox && message.indexOf('No window matching') > -1) //Firefox
){
//The current tab is one into which we are not allowed to inject scripts.
// You should consider alternatives for informing the user that your extension
// does not work on a particular page/URL/tab.
// For example: For browser_actions, you should listen to chrome.tabs events
// and use use browserAction.disable()/enable() when the URL of the
// active tab is, or is not, one for which your add-on can operate.
// Alternately, you could use a page_action button.
//In other words, using a console.log() here in a released add-on is not a good
// idea, but works for examples/testing.
console.log('This extension, ' + chrome.runtime.id
+ ', does not work ' + extraMessage);
} else {
// Report the error
if(isFirefox){
//In Firefox, runtime.lastError is an Error Object including a stack trace.
console.error(chrome.runtime.lastError);
}else{
console.error(message);
}
}
}
}
You could then change your changeBackground() code to:
function changeBackground(tabId) {
chrome.tabs.insertCSS( tabId, {
code: "body { border: 20px dotted pink; }"
}, handleExecuteScriptAndInsertCSSErrors.bind(null,tabId));
}
Obviously, changing your code like that won't actually allow you to change the background on the about:newtab page.
I'm toying around with Chrome trying to create my first extension. Basically, I want to create a script that does some DOM manipulation on a particular domain. Furthermore, I want the user to be able to toggle the script through an icon displayed in the address bar, when visiting that particular domain.
So far, I've got this manifest.json:
{
"manifest_version": 2,
"name": "Ekstrafri",
"description": "Removes annoying boxes for paid articles.",
"version": "1.0",
"page_action": {
"default_title": "Foobar"
},
"content_scripts": [
{
"matches": ["http://ekstrabladet.dk/*"],
"js": ["jquery.min.js", "cleaner.js"],
"run_at": "document_end"
}
]
}
cleaner.js contains a couple of jQuery DOM selectors that removes some stuff.
The current setup works, but the context script is injected all the time. I want the user to be able to toggle, which should trigger a confirmation prompt in which the user accepts or rejects a page reload.
Anyway, page_action doesn't seem to display any icon. According to the documentation, it should display an icon in the address bar.
I have two questions:
How do I display this page_action icon on the matched content?
How do I bind an event to that icon?
One thing you could do here is get and set a variable in a background.js page using the message passing framework. Essentially when the content-script runs you can contact the background script to check the state of a boolean variable. You can then determine whether or not to execute the rest of the script.
chrome.extension.sendMessage({ cmd: "runScript" }, function (response) {
if (response == true) {
//Run the rest of your script inside here...
}
});
You would also use this initial call to bind some UI on the page so you can toggle this state (i.e. switch the content script on/off). In this example I'm using a checkbox.
$("chkOnOff").click(function(){
var scriptOn = ($(this).is(":checked");
chrome.extension.sendMessage({ cmd: "updateRunScriptState", data: { "scriptOn" : scriptOn } }, function (response) {
//Update confirmed
});
});
The background.js page would look something like this:
//Global variable in background.js
var scriptOn = true;
chrome.extension.onMessage.addListener(
function (request, sender, sendResponse) {
if (request.cmd == "runScript") {
sendResponse(scriptOn);
}
if (request.cmd == "updateRunScriptState") {
//here we can update the state
scriptOn = request.data.scriptOn;
}
});
And don't forget to register the background.js page in the manifest.
..
"background": {
"scripts" : ["background.js"]
},
..