Prevent popup if current tab url is not permitted in manifest v3 - google-chrome-extension

I'm writing a chrome extension and I want to either disable the popup entirely, or present a message if the url of the current tab is not permitted via the v3 manifest's host_permissions property.
This is for a dev support tool and we don't want it enabled for the production url. So if I set:
"host_permissions": [
"http://localhost:3000/",
"https://*.dev.some-site.com/",
"https://www.uat.some-site.com/"
]
...Then if the user is on www.some-site.com (or anywhere else), I want the popup to be disabled.
I can obtain the relevant url easily enough:
let [currentTab] = await chrome.tabs.query({ active: true, currentWindow: true });
const { url } = currentTab;
const cookieUrl = url.match(/https?:\/\/[^/]+\//)?.[0];
...and I can obtain the array of permitted file patterns with
chrome.runtime.getManifest().host_permissions
But how can I use this or anything else to prevent the popup? Also, converting that wild-card into a genuine regex will be a bit of a pain. Isn't there some out-of-the-box method to do all this??

Use chrome.declarativeContent API to enable the action popup for sites in host_permissions and disable it everywhere else by default.
When disabled, the icon becomes grayed out. Clicking it will show the built-in context menu, chrome.action.onClicked won't be triggered. The name of the extension in the puzzle-piece menu is also grayed out.
manifest.json:
"action": {"default_icon": "icon.png"},
"permissions": ["declarativeContent"],
background script:
chrome.action.disable();
chrome.runtime.onInstalled.addListener(() => {
chrome.declarativeContent.onPageChanged.removeRules(() => {
chrome.declarativeContent.onPageChanged.addRules([{
conditions: chrome.runtime.getManifest().host_permissions.map(h => {
const [, sub, host] = h.match(/:\/\/(\*\.)?([^/]+)/);
return new chrome.declarativeContent.PageStateMatcher({
pageUrl: sub ? {hostSuffix: '.' + host} : {hostEquals: host},
});
}),
actions: [new chrome.declarativeContent.ShowAction()],
}]);
});
});
For simplicity, I don't limit PageStateMatcher's schemes.

Related

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 DevTools Protocol: control new tabs

Need to be done: I need to simulate user interactions (journeys) across a chain of sites.
Question: Do you have any tips how to programatically controll a tab opened as a result of a simulated click?
My experience:
I'm using the chrome-remote-interface npm package.
I'm able to simulate a click with a custom ChromeController class which initializes the chrome-remote-interface and these methods:
async simulateClick(selector) {
return await this.evaluate(function (selector) {
document.querySelector(selector).click()
}, selector);
}
/**
* Shamelessly stolen from simple-headless-browser
*/
async evaluate (fn, ...args) {
const exp = args && args.length > 0 ? `(${String(fn)}).apply(null, ${JSON.stringify(args)})` : `(${String(fn)}).apply(null)`
const result = await this.client.Runtime.evaluate({
expression: exp,
returnByValue: true
})
return result
}
Now I would like to interact with the recently opened tab. I can get the targetId of the new tab with the experimenetal Target Domain (prototyping in node cli):
var targets;
chromeController.client.Target.getTargets().then(t => targets = t);
Which results in:
{ targetInfos:
[ { targetId: '97556479-cdb6-415c-97a1-6efa4e00b281',
type: 'page',
title: 'xxx/preview/239402/',
url: 'xxx/preview/239402/' },
{ targetId: 'bbfe11d5-8e4a-4879-9081-10bb7234209c',
type: 'page',
title: 'Document',
url: 'xxx/preview/239402/teaser/0/' } ] }
I am able to switch between the tabs with:
chromeController.client.Target.activateTarget({targetId:'xxx'})
However I'm not able to get any interaction with this, I can't find the connection, how to load it into the Page and Runtime objects.
I've searched in the docs and also tried googling: 'site:chromedevtools.github.io targetId' which only lead me to
> chromeController.client.Browser.getWindowForTarget({targetId: '97556479-cdb6-415c-97a1-6efa4e00b281'}).catch(e => console.log(e.message));
Promise { <pending> }
> 'Browser.getWindowForTarget' wasn't found
I've also tried to Target.setDiscoverTargets({discover: true}) and to close the original tab.
Thanks for any help!
Recently faced this same issue and in short I had to create a new dev tools protocol client for each new target I wanted control over.
My experience is with dev tools protocol using direct communication with websocket but the api is the same so it should be similar. So here is a summary of what I had to do.
Initially looking at the docs I would have assumed Target.attachToTarget should give us control of the new tab but I found that it didn't work.
My workaround was to create a listener that listened for the Target.targetCreated event which provides a targetInfos just like you found with Target.getTargets but for every new target created like a new tab, page, or iframe. Note: you need to enable Target.setDiscoverTargets in order to receive these events over the protocol.
[ { targetId: '97556479-cdb6-415c-97a1-6efa4e00b281',
type: 'page',
title: 'xxx/preview/239402/',
url: 'xxx/preview/239402/' },
{ targetId: 'bbfe11d5-8e4a-4879-9081-10bb7234209c',
type: 'page',
title: 'Document',
url: 'xxx/preview/239402/teaser/0/' } ] }
With that listener I looked for targets that were of type page, you could filter on a specific url if you know what the page will be. With the targetId in hand I requested available websocket targets following the HTTPEndpoints section near the bottom of the devtools home page.
GET /json or /json/list
A list of all available websocket targets.
[ {
"description": "",
"devtoolsFrontendUrl": "/devtools/inspector.html?ws=localhost:9222/devtools/page/DAB7FB6187B554E10B0BD18821265734",
"id": "DAB7FB6187B554E10B0BD18821265734",
"title": "Yahoo",
"type": "page",
"url": "https://www.yahoo.com/",
"webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/DAB7FB6187B554E10B0BD18821265734"
} ]
I could then launch a new dev tools protocol client using the webSocketDebuggerUrl and have full control over the tab.
I know this is a pretty round about way but, its the only way I was able to make if work.
Although these days it's probably easier to use something like puppeteer to interface with multiple tabs in chrome if you can. Here is the source code to a puppeteer module that follows new tabs that could be good reference for trying to replicate it pageVideoStreamCollector.ts
This is a very late answer but just putting this here if anyone else has the same issue as help on chrome dev tools is very hard to come by. Hope it helps someone out.
I also am getting "Browser.getWindowForTarget wasn't found" on debian, google-chrome-unstable version 61

wrong current url in google chrome extension

I have a google chrome extension that is shown onclick in a popup. There is a context menu with some options that need the current / active tab url. The extension has a problem described below.
Old code:
function menuCallback(info, tab) {
var currentUrl = tab.url;
With old code: If you right-click inside the popup, the current url returned is "chrome-extension..." and so on.
New Code: (i tried to fix the issue with)
chrome.tabs.query({'active': true, 'windowId': chrome.windows.WINDOW_ID_CURRENT},
function(tabs){
currentUrl = tabs[0].url;
}
);
This works, as it returns the tab url even if the click comes from inside the popup. But if i have like 10 open tabs and switch between two, always the old one is returned. For example i'm at google.de first, url returned is google.de. Now i switch to an already open tab like "heise.de" and right-click, it's still google.de. Next attempt / try the url is correct.
You probably want to use the onUpdated listener.
chrome.tabs.onUpdated.addListener(doSomething);
Which gives you the tabId, changeInfo and tab metadata. You then can do as you please:
var doSomething = function (tabId, changeInfo, tab) {
var match = /http:\/\/www.google.com/.exec(tab.url);
if(match && changeInfo.status == 'complete') {
... do something ...
}
}
EDIT: After reading again your question, what you probably want is the onHighlighted event listener. It returns an object with an array of tabsIds and the window as soon as you select a tab.
chrome.tabs.onHighlighted.addListener(function(o) { tabId = o.tabIds[0]; })
You can then use get in order to obtain more information about that specific tab.
chrome.tabs.get(tabId, function(tab) { ... })
I'm leaving the onUpdated code in case anyone wants the tab information whenever a page changes.
Using the query parameters the simplest solution to finding the URL of the active tab in the currently focused window would be the following.
chrome.tabs.query({active: true, currentWindow: true}, function(tabs){
currentUrl = tabs[0].url;
});
chrome.tabs.getSelected(null, function(tab) {
var currentURL= tab.url;
alert(currentURL); // this should give you current url.
}

CKeditor in Chrome Extension popup issued: "Refused to load frame from 'about:blank' because of Content-Security-Policy."

CKeditor in Chrome Extension popup issued: "Refused to load frame from 'about:blank' because of Content-Security-Policy."
I am trying to get a chrome extension updated to work with the manifest 2 and new Chrome API's that have currently broken the plugin with the latest version release 18.0.1025.142.
I am using CKEditor in a popup window that is issued by a background page in the extension.
But I get the Refused to load frame from 'about:blank' because of Content-Security-Policy.2 error -- is there a way to get around this?
Update
This could be related: http://code.google.com/p/chromium/issues/detail?id=77947&q=Error%20during%20tabs.executeScript%3A&colspec=ID%20Pri%20Mstone%20ReleaseBlock%20Area%20Feature%20Status%20Owner%20Summary
In Chrome extensions, you cannot access or modify content from protocols other than file:, http:, ftp: and chrome-extension: (data:, blob: and filesystem: when the page created the resource themselves).
I considered four approaches to solve the problem:
Bind a beforeload event to the document, and change the URL when it matches about:blank.
Does not work: The CSP is activated before this event is fired.
Bind a DOMNodeInserted event to the document, and check for about:blank iframes.
Does not work: The CSP is activated before this event is fired.
Use the webRequest API to intercept the request.
Does not work: The strict match patterns do not allow the about: protocol.
Change the src property before inserting the IFRAME in the document:
Either manually (best option), or
Modify document.createElement for IFrames, or
Modify the appendChild, insertBefore and replaceChild methods for Iframes.
For all methods, you have to create a dummy page, say blank.html within your extension, and allow access via web_accessible_resources.
Manifest file example (last four lines are important):
{
"name": "Name",
"version": "1.0",
"background": {"scripts": ["aboutblank.js"]},
"manifest_version": 2,
"content_security_policy": "default-src 'self'",
"web_accessible_resources": ["blank.html"]
}
aboutblank.js
var BLANK_PAGE = chrome.extension.getURL("blank.html");
(function(BLANK_PAGE, createElement, appendChild, insertBefore, replaceChild) {
HTMLDocument.prototype.createElement = function(str) {
var elem = createElement.call(this, str);
if (str.toUpperCase() == 'IFRAME') {
elem.src = BLANK_PAGE;
}
return elem;
};
Element.prototype.appendChild = function(newChild) {
iframe_check(newChild);
return appendChild.call(this, newChild);
};
Element.prototype.insertBefore = function(newChild, refChild) {
iframe_check(newChild);
return insertBefore.call(this, newChild, refChild);
};
Element.prototype.replaceChild = function(newChild, refChild) {
iframe_check(newChild);
return replaceChild.call(this, newChild, refChild);
};
function iframe_check(elem) {
if (elem instanceof HTMLIFrameElement && (elem.src == '' || elem.src.slice(0,11) == 'about:blank')) {
elem.src = BLANK_PAGE;
}
}
})(BLANK_PAGE,
HTMLDocument.prototype.createElement,
Element.prototype.appendChild,
Element.prototype.insertBefore,
Element.prototype.replaceChild);
Note: Option 1 is recommended over modifying these prototype methods. Also, the prototype method does not work for <iframes> injected using .innerHTML.

Resources