I have an extension that, when the "browser action" (the icon next to the address bar) is clicked, executes a script on the current tab's page:
background.js
chrome.browserAction.onClicked.addListener(function(tab) {
chrome.tabs.executeScript({
file: 'controls.js'
});
});
The controls.js injects some stuff into the DOM so that the user can press a key to tell background.js to set up a WebRTC connection and do other business-logic stuff.
Usually this works just fine. But sometimes the extension stops working on a tab if it has been open but not used for a while, I think typically after the computer has gone to sleep and woken again. When this happens, in the console for background.js, I get the error "Unchecked runtime.lastError: Cannot access contents of the page. Extension manifest must request permission to access the respective host."
Any idea why this could happen, or what I can do to catch this error and handle it to get permissions back?
It took me a lot of futzing around to figure this out.
If the user has been idle for a while, upon refreshing the page, the website I'm injecting JS into will redirect to an authentication site on another domain, which will then redirect the user back. It happens fast enough to not notice typically, but Chrome removes the activeTab permission when the domain changes.
I'm handling the permission loss by notifying the user through the badge text, more or less like this:
chrome.tabs.executeScript(tab.id, {
code: 'alert("hello activeTab")'
}, (result) => {
if (result === undefined) { // this means that activeTab permission is lost
console.log('lost activeTab permission :(');
chrome.browserAction.setBadgeText({
text: 'off',
tabId: tab.id
});
}
});
Related
It appears that a message from content to background only begins firing after backgroundAction has been run at least once.
In the below code example, a click on browserAction turns the page red, and a click on the page body turns the page blue (via a message sent by the content script).
If I click the page body first, nothing happens. It only begins working after I've clicked browserAction once.
Why is this happening and how can I make it so the message listener fires without having browserAction run first?
Any help would be greatly appreciated!
content.js
$(function(){
$('body').on('click', function() {
// Send a message to background.js
chrome.runtime.sendMessage(true, function(response){
console.log(response);
});
});
});
background.js
// Make background red when browserAction is cliked
chrome.browserAction.onClicked.addListener(function(){
chrome.tabs.executeScript( {
code: 'document.body.style.backgroundColor="red"'
});
});
// Make background blue when any message is received
chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse){
chrome.tabs.executeScript( {
code: 'document.body.style.backgroundColor="blue"'
});
return true;
})
As always in such cases use the debugger. The error I'm seeing here in the extension's background page console which can be opened on chrome://extensions page:
Unchecked runtime.lastError while running tabs.executeScript: Cannot access contents of url "...". Extension manifest must request permission to access this host.
When runtime.onMessage is executed after a message from the content script Chrome doesn't know that executeScript was initiated by a user action so the code is blocked.
As for browserAction.onClicked, it is always invoked on user's interaction so "permissions": ["activeTab"] is sufficient for the code executed in the eventjob context of the issued click. And it creates temporary permission to modify the active tab, see the documentation:
The following user gestures enable activeTab:
Executing a browser action
Executing a page action
Executing a context menu item
Executing a keyboard shortcut from the commands API
Accepting a suggestion from the omnibox API
Solution #1 (the best) would be to avoid injecting any code from the background script and do everything in the content script based on messages from the background script.
Solution #2 would be to add the required permission to manifest.json:
"permissions": ["activeTab", "<all_urls>"]
I would like to allow users to click on a "use the extension for this website" button.
It would allow me to don't have matches for my content script at first and add them when the user needs it, is it possible? When a user clicks on the button, the extension will do the job every time the website is visited, not just once.
While possible (see the second pat of the question), it might be slightly hard to use.
A practical solution would be to declare a content script that is always injected (i.e. <all_urls> match pattern), but abort execution if the page doesn't match:
// content.js
// Maybe an async check to background / chrome.storage
if(someCondtion(window.location.host)) startWorking();
function startWorking() {
/* all your main code here */
}
This will cause a scarier permission warning at runtime, but is probably the easiest to implement.
Chrome has a mechanism called "optional permissions" that you can request after install time. You can request a blanket host permission as optional, and then only request origins you need.
Warning, the code below was not tested.
// background.js
function requestHostPermission(host){
chrome.permissions.request({
origins: ['*://' + host + '/']
}, function(granted) {
// The callback argument will be true if the user granted the permissions.
if (!granted) {
if(chrome.runtime.lastError) console.error(chrome.runtime.lastError);
throw Error("Permission denied for host "+host);
}
});
}
But then you will have to use some kind of programmatic injection, as you cannot use optional permissions with manifest-declared scripts.
It seems like the externally_connectable feature that allows a website to communicate with an extension is still in the dev channel and not yet stable. Are there any other ways to allow a specific website to communicate with my extension, while I wait for this feature to become stable? How have chrome extension developers traditionally done it?
Thanks Rob W for pointing me in the direction of HTML5 messaging. For the benefit of other chrome extension developers, I'm writing about the general problem I was trying to solve and the solution that worked in the end.
I am making a chrome extension that can control music playback on a tab via a popup player. When a user clicks on play/pause/etc on the popup player, the extension should be able to convey that message to the webpage and get back a response stating whether the action was accomplished.
My first approach was to inject a content script into the music player page. The problem is, though, that content scripts operate in a "sandbox" and cannot access native javascript on the page. Therefore, the content script was pretty useless (on its own), because while it could receive commands from the extension, it could not effect any change on the webpage itself.
One thing that worked in my favor was that the website where the music was playing belongs to me, so I could put whatever javascript I wanted there and have it be served from the server. That's exactly what I used to my advantage: I created another javascript file that would reside on the website and communicate with the content script mentioned above, via the window object of the page (i.e. HTML5 messaging). This only works because the content script and the javascript file both exist in the same webpage and can share the window object of the page. Thanks Rob W for pointing me to this capability. Here is an example of how the javascript file on the page can initiate a connection with the content script via the window object:
content_script.js (injected by extension into xyz.com):
window.addEventListener("message", function(event) {
if(event.data.secret_key &&
(event.data.secret_key === "my_secret_key") &&
event.data.source === "page"){
if(event.data.type){
switch(event.data.type) {
case 'init':
console.log("received connection request from page");
window.postMessage({source: "content_script", type: 'init',
secret_key: "my_secret_key"}, "*");
break;
}
}
}
}, false);
onpage.js (resides on server and served along with xyz.com):
window.postMessage({source: "page", type: 'init',
secret_key: "my_secret_key"}, "*");
window.addEventListener("message", function(event) {
if(event.data.secret_key &&
(event.data.secret_key === "my_secret_key") &&
event.data.source === "content_script"){
if(event.data.type){
switch(event.data.type) {
case 'init':
console.log("connection established");
break;
}
}
}
}, false);
I check the secret key just to make sure that the message originates from where I expect it to.
That's it! If anything is unclear, or if you have any questions, feel free to follow up!
You could have an extension inject a content script alongside a web page, and use that to pass messages back and forth between the website and the background page of the extension.
It's tedious, though, and externally connectable is a lot nicer.
I am trying to write an extension that adds functionality to the Chrome devtools.
According to the devtools documentation, it says that the pages in devtools support very limited apis. Any API that is not supported can be access by accessing it through the background page, just as what contentscripts does.
Here is the relevant documentation snippet:
The tabId property provides the tab identifier that you can use with the chrome.tabs.* API calls. However, please note that chrome.tabs.* API is not exposed to the Developer Tools extension pages due to security considerations — you will need to pass the tab ID to the background page and invoke the chrome.tabs.* API functions from there.
Here is the source url: http://developer.chrome.com/extensions/devtools.inspectedWindow.html
However, when I try to do that, I get the following error in the console:
uncaught Error: "getBackgroundPage" can only be used in extension processes. See the content scripts documentation for more details.
Here is my code in my devtools.js script:
chrome.extension.getBackgroundPage().getLocation();
What am I doing wrong?
EDIT
I should describe my scenario first, and show how I am implementing it.
What I want to do is to display extra data in a devtools panel related to a webpage. In order to get that data, I will need to send a HTTP request in the same session as the page being debugged, because it requires authentication.
Use Case:
User browses to a particular URL. He is authenticated to the site. He then invokes devtools. The devtools panel opens up and a new panel shows up that has extra data related to the page.
Implementation:
1) DevTools script finds out the url of the page being inspected. If the url matches the site base hostname, then it opens a panel. In the callback of the panel creation, it sends a message to a background page, asking it to download a JSON payload from a debug endpoint on the same site, and then sends it to the devtools extension, wh ich then displays it.
Problems:
1) The background page gets the request, and downloads the URL. However the download is not using the same session as the user, so the download request fails.
2) From devtools window, I got the tabId of the inspected window. I send this tabId to the background page so that it can parse some stuff out of the url. However, chrome.tabs.get(tabId) does not return the tab.
To summarize, I need to
1) Get the background page to download data in the same session as the user's tab that is being debugged.
2) I need to have the background page be able to get access to the user's tab.
The APIs available to extension pages within the Developer Tools window include all devtools modules listed above and chrome.extension API. Other extension APIs are not available to the Developer Tools pages, but you may invoke them by sending a request to the background page of your extension, similarly to how it's done in the content scripts.
I guess the documentation is little ambiguous, By chrome.extension API they mean the Supported API's for content scripts.
So, you can use long lived communication for communication between inspected page and background page
Demonstration:
The following code illustrate scenario where a devtools page need some information from background page, it uses messages for communication.
manifest.json
Ensured permissions are all available in manifest file
{
"name":"Inspected Windows Demo",
"description":"This demonstrates Inspected window API",
"devtools_page":"devtools.html",
"manifest_version":2,
"version":"2",
"permissions":["experimental"],
"background":{
"scripts" : ["background.js"]
}
}
devtools.html
A trivial HTML File
<html>
<head>
<script src="devtools.js"></script>
</head>
<body>
</body>
</html>
devtools.js
Used Long lived Communication API's
var port = chrome.extension.connect({
name: "Sample Communication"
});
port.postMessage("Request Tab Data");
port.onMessage.addListener(function (msg) {
console.log("Tab Data recieved is " + msg);
});
background.js
Responded to communication request and passed trivial information using tab API()'s
chrome.extension.onConnect.addListener(function (port) {
port.onMessage.addListener(function (message) {
chrome.tabs.query({
"status": "complete",
"currentWindow": true,
"active": true
}, function (tabs) {
port.postMessage(tabs[0].id);
});
console.log("Message recived is "+message);
});
});
Sample Output received for trivial devtools.js here
Let me know if you need more information
EDIT 1)
For your question 1)
Can you make you call(s) from browser extension HTML Page\Content Script so same session is shared, i have tried both the ways in a sample and it is working form me, instead of code in background page- make the code in content script or browser action HTML Page.
Let me know if you are still facing problems.
For your question 2)
The following code always fetches current window user is browsing
manifest.json
Ensure you have tabs permission in your manifest.
{
"name":"Inspected Windows Demo",
"description":"This demonstrates Inspected window API",
"manifest_version":2,
"version":"2",
"permissions":["tabs"],
"background":{
"scripts" : ["background.js"]
}
}
background.js
chrome.tabs.query({
"status": "complete", // Window load is completed
"currentWindow": true, // It is in current window
"active": true //Window user is browsing
}, function (tabs) {
for (tab in tabs) { // It returns array so used a loop to iterate over items
console.log(tabs[tab].id); // Catch tab id
}
});
Let me know if you are still unable to get tab id of current window.
I am trying to learn to use the chrome.tabs.executeScript commend. I've created a simple extension with a browser action. My background.html file currently looks like this:
<html>
<script>
chrome.browserAction.onClicked.addListener(function(tab) {
chrome.tabs.executeScript(null,{code:"document.body.bgColor='red'"});
chrome.tabs.executeScript(null, {file: "content_script.js"});
});
</script>
</html>
The "content_script.js" file contains document.body.bgColor='red'.
When pushing the browser action's button nothing happens. Obviously I'm missing something very basic.
I've checked with console.log that indeed control reaches the chrome.tabs.executeScript calls when the browser action is pressed. Otherwise I'm not sure how to even check if my content script's code is run (it seems not; console.log I put in the content script has no effect, but maybe it shouldn't have one even if the script is run successfully).
Make sure you have domain and tab permissions in the manifest:
"permissions": [
"tabs", "http://*/*", "https://*/*"
]
Then to change body color try:
chrome.tabs.executeScript(null,{code:"document.body.style.backgroundColor='red'"});
Also keep in mind that content scripts are not injected into any chrome:// or extension gallery pages.
For those of you still having issues, you need to make sure to reload the extension's permissions in Chrome.
Go to chrome://extensions , scroll to your extension, and click on "reload". Make sure that your permissions have been updated by clicking on the permissions link right next to your extension.
You actually don't need and don't want the 'tabs' permission for executeScript.
"permissions": [
"http://*/*",
"https://*/*"
]
Should be enough.
It's not recommended to use http://*/* and https://*/*. From the Google documentation:
To inject a programmatic content script, provide the activeTab permission in the manifest. This grants secure access to the active site's host and temporary access to the tabs permission, enabling the content script to run on the current active tab without specifying cross-origin permissions.
Instead, (as suggested in the page) just use activeTab permission.
Remark: more explanation for the security issue
Without activeTab, this extension would need to request full, persistent access to every web site, just so that it could do its work if it happened to be called upon by the user. This is a lot of power to entrust to such a simple extension. And if the extension is ever compromised, the attacker gets access to everything the extension had.
In contrast, an extension with the activeTab permission only obtains access to a tab in response to an explicit user gesture. If the extension is compromised the attacker would need to wait for the user to invoke the extension before obtaining access. And that access only lasts until the tab is navigated or is closed.
(emphasis mine)
In the example code posted by the OP, activeTab is sufficient.
However, if the extension is more complex and needs to work "automatically" (i.e. without the user clicking the button); then this method will not work and additional permission is required.
Most of the answers above seems to be working fine for manifest version 2 but when it comes manifest-3 their seems to be some workaround to make the content-script load in the latest manifest 3.We need to use the following steps to execute content script in manifest 3
First adding permission "scripting" in manifest
"permissions": [
"storage",
"tabs",
"activeTab",
"scripting"
]
Once the scripting perimission is provided, we can use the scripting api like below
In background.js,
chrome.tabs.query({}, (tabList) => {
if (!tabList.length) return;
tabList.forEach((tab) => {
chrome.scripting.executeScript(
{
files: ['contentScript.js'],
target: {
tabId: tab.id,
allFrames: true
}
}
);
});
});
In the above code we are executing the contentScript for all the available tabs in tab browser.