chrome.webRequest.onBeforeRequest acting unpredictably - javascript

I'm trying to make a web filtering chrome extension that will block certain sites and replace their html with a block page included in the extension.
let bans = ["*://*.amazon.com/*", "*://*.youtube.com/*", "*://*.netflix.com/*","*://*.facebook.com/*", "*://*.twitter.com/*"];
chrome.webRequest.onBeforeRequest.addListener(
function(details){
try{
chrome.tabs.executeScript(null, {file: "content.js"});
}
catch(error){
console.error(error);
}
return {cancel: true};
},
{urls: bans},
["blocking"]
);
This should mean that if I try to visit any site on that banned list the content script should replace the page with my own block page. However for some reason some sites never load the block page, other sites don't get blocked at all and some sites seem to work perfectly. Even stranger the listener seems to be triggered on sites not listed in the bans array at all. I can't figure out any sort of pattern between these behaviors.
I don't believe they are the source of the problem but here are the permissions in my manifest (manifest v2)
"web_accessible_resources": [
"certBlockPage.html",
"blockPageLight.html"
],
"incognito": "split",
"permissions": [
"webNavigation",
"webRequest",
"webRequestBlocking",
"tabs",
"windows",
"identity",
"http://*/*",
"https://*/*",
"<all_urls>"
]
and here is the content.js file
window.onload = function(){
fetch(chrome.runtime.getURL('certBlockPage.html'))
.then(r => r.text())
.then(html => {
document.open();
document.write(html);
document.close;
});
}

There are several problems.
You didn't specify runAt: 'document_start' in executeScript's options so it will wait in case you navigated to a banned site while the old page in this tab was still loading.
executeScript is asynchronous so it can easily run after load event was already fired in the tab so your window.onload will never run. Don't use onload, just run the code immediately.
You always run executeScript in the currently focused tab (the term is active) but the tab may be non-focused (inactive). You need to use details.tabId and details.frameId.
The user may have opened a new tab and typed the blocked url or clicked its link, which is blocked by your {cancel: true}, but then executeScript will fail because the newtab page, which is currently shown in this tab, can't run content scripts. Same for any other chrome:// or chrome-extension:// tab or when a network error is displayed inside the tab.
If you call onBeforeRequest.addListener another time without removing the previous registration, both will be active.
document.write will fail on sites with strict CSP
Solution 1: webRequest + executeScript
background script:
updateListener(['amazon.com', 'youtube.com', 'netflix.com']);
function updateListener(hosts) {
chrome.webRequest.onBeforeRequest.removeListener(onBeforeRequest);
chrome.webRequest.onBeforeRequest.addListener(
onBeforeRequest, {
types: ['main_frame', 'sub_frame'],
urls: hosts.map(h => `*://*.${h}/*`),
}, [
'blocking',
]);
}
function onBeforeRequest(details) {
const {tabId, frameId} = details;
chrome.tabs.executeScript(tabId, {
file: 'content.js',
runAt: 'document_start',
matchAboutBlank: true,
frameId,
}, () => chrome.runtime.lastError && redirectTab(tabId));
// Cancel the navigation without showing that it was canceled
return {redirectUrl: 'javascript:void 0'};
}
function redirectTab(tabId) {
chrome.tabs.update(tabId, {url: 'certBlockPage.html'});
}
content.js:
fetch(chrome.runtime.getURL('certBlockPage.html'))
.then(r => r.text())
.then(html => {
try {
document.open();
document.write(html);
document.close();
} catch (e) {
location.href = chrome.runtime.getURL('certBlockPage.html');
}
});
Solution 2: webRequest + redirection
No need for content scripts.
const bannedHosts = ['amazon.com', 'youtube.com', 'netflix.com'];
chrome.webRequest.onBeforeRequest.addListener(
details => ({
redirectUrl: chrome.runtime.getURL('certBlockPage.html'),
}), {
types: ['main_frame', 'sub_frame'],
urls: bannedHosts.map(h => `*://*.${h}/*`),
}, [
'blocking',
]);
Solution 3: declarativeNetRequest + redirection
This is the fastest method but it's limited to a very simple predefined set of actions. See the documentation and don't forget to add "declarativeNetRequest" to "permissions" in manifest.json, and "<all_urls>" to host_permissions (ManifestV3 still doesn't support optional permissions).
const bannedHosts = ['amazon.com', 'youtube.com', 'netflix.com'];
chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: bannedHosts.map((h, i) => i + 1),
addRules: bannedHosts.map((h, i) => ({
id: i + 1,
action: {type: 'redirect', redirect: {extensionPath: '/certBlockPage.html'}},
condition: {urlFilter: `||${h}/`, resourceTypes: ['main_frame', 'sub_frame']},
})),
});

Related

Execute Chrome Developer Console Commands though Chrome Extension [duplicate]

I am trying to create an extension that will have a side panel. This side panel will have buttons that will perform actions based on the host page state.
I followed this example to inject the side panel and I am able to wire up a button onClick listener. However, I am unable to access the global js variable. In developer console, in the scope of the host page I am able to see the variable (name of variable - config) that I am after. but when I which to the context of the sidepanel (popup.html) I get the following error -
VM523:1 Uncaught ReferenceError: config is not defined. It seems like popup.html also runs in a separate thread.
How can I access the global js variable for the onClick handler of my button?
My code:
manifest.json
{
"manifest_version": 2,
"name": "Hello World",
"description": "This extension to test html injection",
"version": "1.0",
"content_scripts": [{
"run_at": "document_end",
"matches": [
"https://*/*",
"http://*/*"
],
"js": ["content-script.js"]
}],
"browser_action": {
"default_icon": "icon.png"
},
"background": {
"scripts":["background.js"]
},
"permissions": [
"activeTab"
],
"web_accessible_resources": [
"popup.html",
"popup.js"
]
}
background.js
chrome.browserAction.onClicked.addListener(function(){
chrome.tabs.query({active: true, currentWindow: true}, function(tabs){
chrome.tabs.sendMessage(tabs[0].id,"toggle");
})
});
content-script.js
chrome.runtime.onMessage.addListener(function(msg, sender){
if(msg == "toggle"){
toggle();
}
})
var iframe = document.createElement('iframe');
iframe.style.background = "green";
iframe.style.height = "100%";
iframe.style.width = "0px";
iframe.style.position = "fixed";
iframe.style.top = "0px";
iframe.style.right = "0px";
iframe.style.zIndex = "9000000000000000000";
iframe.frameBorder = "none";
iframe.src = chrome.extension.getURL("popup.html")
document.body.appendChild(iframe);
function toggle(){
if(iframe.style.width == "0px"){
iframe.style.width="400px";
}
else{
iframe.style.width="0px";
}
}
popup.html
<head>
<script src="popup.js"> </script>
</head>
<body>
<h1>Hello World</h1>
<button name="toggle" id="toggle" >on</button>
</body>
popup.js
document.addEventListener('DOMContentLoaded', function() {
document.getElementById("toggle").addEventListener("click", handler);
});
function handler() {
console.log("Hello");
console.log(config);
}
Since content scripts run in an "isolated world" the JS variables of the page cannot be directly accessed from an extension, you need to run code in page's main world.
WARNING! DOM element cannot be extracted as an element so just send its innerHTML or another attribute. Only JSON-compatible data types can be extracted (string, number, boolean, null, and arrays/objects of these types), no circular references.
1. ManifestV3 in modern Chrome 95 or newer
This is the entire code in your extension popup/background script:
async function getPageVar(name, tabId) {
const [{result}] = await chrome.scripting.executeScript({
func: name => window[name],
args: [name],
target: {
tabId: tabId ??
(await chrome.tabs.query({active: true, currentWindow: true}))[0].id
},
world: 'MAIN',
});
return result;
}
Usage:
(async () => {
const v = await getPageVar('foo');
console.log(v);
})();
See also how to open correct devtools console.
2. ManifestV3 in old Chrome and ManifestV2
We'll extract the variable and send it into the content script via DOM messaging. Then the content script can relay the message to the extension script in iframe or popup/background pages.
ManifestV3 for Chrome 94 or older needs two separate files
content script:
const evtToPage = chrome.runtime.id;
const evtFromPage = chrome.runtime.id + '-response';
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg === 'getConfig') {
// DOM messaging is synchronous so we don't need `return true` in onMessage
addEventListener(evtFromPage, e => {
sendResponse(JSON.parse(e.detail));
}, {once: true});
dispatchEvent(new Event(evtToPage));
}
});
// Run the script in page context and pass event names
const script = document.createElement('script');
script.src = chrome.runtime.getURL('page-context.js');
script.dataset.args = JSON.stringify({evtToPage, evtFromPage});
document.documentElement.appendChild(script);
page-context.js should be exposed in manifest.json's web_accessible_resources, example.
// This script runs in page context and registers a listener.
// Note that the page may override/hook things like addEventListener...
(() => {
const el = document.currentScript;
const {evtToPage, evtFromPage} = JSON.parse(el.dataset.args);
el.remove();
addEventListener(evtToPage, () => {
dispatchEvent(new CustomEvent(evtFromPage, {
// stringifying strips nontranferable things like functions or DOM elements
detail: JSON.stringify(window.config),
}));
});
})();
ManifestV2 content script:
const evtToPage = chrome.runtime.id;
const evtFromPage = chrome.runtime.id + '-response';
// this creates a script element with the function's code and passes event names
const script = document.createElement('script');
script.textContent = `(${inPageContext})("${evtToPage}", "${evtFromPage}")`;
document.documentElement.appendChild(script);
script.remove();
// this function runs in page context and registers a listener
function inPageContext(listenTo, respondWith) {
addEventListener(listenTo, () => {
dispatchEvent(new CustomEvent(respondWith, {
detail: window.config,
}));
});
}
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg === 'getConfig') {
// DOM messaging is synchronous so we don't need `return true` in onMessage
addEventListener(evtFromPage, e => sendResponse(e.detail), {once: true});
dispatchEvent(new Event(evtToPage));
}
});
usage example for extension iframe script in the same tab:
function handler() {
chrome.tabs.getCurrent(tab => {
chrome.tabs.sendMessage(tab.id, 'getConfig', config => {
console.log(config);
// do something with config
});
});
}
usage example for popup script or background script:
function handler() {
chrome.tabs.query({active: true, currentWindow: true}, tabs => {
chrome.tabs.sendMessage(tabs[0].id, 'getConfig', config => {
console.log(config);
// do something with config
});
});
}
So, basically:
the iframe script gets its own tab id (or the popup/background script gets the active tab id) and sends a message to the content script
the content script sends a DOM message to a previously inserted page script
the page script listens to that DOM message and sends another DOM message back to the content script
the content script sends it in a response back to the extension script.

browser.downloads.download - images disappearing after download

So I was tinkering with a firefox extension and came across something I can't explain. This extension downloads images from a certain site when a browser action (button) is clicked. Can confirm that the rest of the extension works perfectly and the code below has proper access to the response object.
const downloading = browser.downloads.download({
filename:response.fileName + '.jpg',
url:response.src,
headers:[{name:"Content-Type", value:"image/jpeg"}],
saveAs:true,
conflictAction:'uniquify'
});
const onStart = (id) => {console.log('started: '+id)};
const onError = (error) => {console.log(error)};
downloading.then(onStart, onError);
So the saveAs dialog pops up (filename with file extension populated), I click save, and then it downloads. As soon as the file finishes downloading it disappears from the folder it was saved in. I have no idea how this is happening.
Is this something wrong with my code, Firefox, or maybe a OS security action? Any help would be greatly appreciated.
Extra Information:
Firefox - 95.0.2 (64-bit)
macOS - 11.4 (20F71)
I had the same issue. You have to put download in background, background.js.
Attached sample of Thunderbird addon creates new menu entry in the message list and save raw message to the file on click.
If you look to the manifest.json, "background.js" script is defined in the "background" section. The background.js script is automatically loaded when the add-on is enabled during Thunderbird start or after the add-on has been manually enabled or installed.
See: onClicked event from the browserAction (John Bieling)
manifest.json:
{
"description": "buttons",
"manifest_version": 2,
"name": "button",
"version": "1.0",
"background": {
"scripts": ["background.js"]
},
"permissions": [
"menus","messagesRead","downloads"
],
"browser_action": {
"default_icon": {
"16": "icons/page-16.png",
"32": "icons/page-32.png"
}
}
}
background.js:
async function main() {
// create a new context menu entry in the message list
// the function defined in onclick will get passed a OnClickData obj
// https://thunderbird-webextensions.readthedocs.io/en/latest/menus.html#menus-onclickdata
await messenger.menus.create({
contexts: ["all"],
id: "edit_email_subject_entry",
onclick: (onClickData) => {
saveMsg(onClickData.selectedMessages?.messages);
},
title: "iktatEml"
});
messenger.browserAction.onClicked.addListener(async (tab) => {
let msgs = await messenger.messageDisplay.getDisplayedMessages(tab.id);
saveMsg(msgs);
})
}
async function saveMsg(MessageHeaders) {
if (MessageHeaders && MessageHeaders.length > 0) {
// get MessageHeader of first selected messages
// https://thunderbird-webextensions.readthedocs.io/en/latest/messages.html#messageheader
let MessageHeader = MessageHeaders[0];
let raw = await messenger.messages.getRaw(MessageHeader.id);
let blob = new Blob([raw], { type: "text;charset=utf-8" })
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads
await browser.downloads.download({
'url': URL.createObjectURL(blob),
'filename': "xiktatx.eml",
'conflictAction': "overwrite",
'saveAs': false
});
} else {
console.log("No message selected");
}
}
main();

How To Call Chrome Extension Function After Page Redirect?

I am working on building a Javascript (in-browser) Instagram bot. However, I ran into a problem.
If you run this script, the first function will be called and the page will be redirected to "https://www.instagram.com/explore/tags/samplehashtag/" and the second function will be called immediately after (on the previous URL before the page changes to the new URL). Is there a way to make the second function be called after this second URL has been loaded completely?
I have tried setting it to a Window setInterval() Method for an extended time period, window.onload and a couple of other methods. However, I can't seem to get anything to work. Any chance someone has a solution?
This is my first chrome extension and my first real project, so I may be missing something simple..
manifest.json
{
"name": "Inject Me",
"version": "1.0",
"manifest_version": 2,
"description": "Injecting stuff",
"homepage_url": "http://danharper.me",
"background": {
"scripts": [
"background.js"
],
"persistent": true
},
"browser_action": {
"default_title": "Inject!"
},
"permissions": [
"https://*/*",
"http://*/*",
"tabs"
]
}
inject.js
(function() {
let findUrl = () => {
let hashtag = "explore/tags/samplehashtag/";
location.replace("https://www.instagram.com/" + hashtag);
}
findUrl();
})();
background.js
// this is the background code...
// listen for our browerAction to be clicked
chrome.browserAction.onClicked.addListener(function(tab) {
// for the current tab, inject the "inject.js" file & execute it
chrome.tabs.executeScript(tab.ib, {
file: 'inject.js'
});
});
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
chrome.tabs.executeScript(tab.ib, {
file: 'inject2.js'
});
});
inject2.js
(function() {
if (window.location.href.indexOf("https://www.instagram.com/explore/tags/samplehashtag/") != -1){
let likeAndRepeat = () => {
let counter = 0;
let grabPhoto = document.querySelector('._9AhH0');
grabPhoto.click();
let likeAndSkip = function() {
let heart = document.querySelector('.glyphsSpriteHeart__outline__24__grey_9.u-__7');
let arrow = document.querySelector('a.coreSpriteRightPaginationArrow');
if (heart) {
heart.click();
counter++;
console.log(`You have liked ${counter} photographs`)
}
arrow.click();
}
setInterval(likeAndSkip, 3000);
//alert('likeAndRepeat Inserted');
};
likeAndRepeat();
}
})();
It is not clear from the question and the example, when you want to run your function. But in chrome extension there is something called Message Passing
https://developer.chrome.com/extensions/messaging
With message passing you can pass messages from one file to another, and similarly listen for messages.
So as it looks from your use case, you can listen for a particular message and then fire your method.
For example
background.js
chrome.runtime.sendMessage({message: "FIRE_SOME_METHOD"})
popup.js
chrome.runtime.onMessage.addListener(
function(request) {
if (request.message == "FIRE_SOME_METHOD")
someMethod();
});
EDIT
Also if you want to listen for the URL changes, you can simply put a listener provided as in the documentation.
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
console.log('updated tab');
});

javascript - Chrome extension: Communication between content.js and background.js on load

Edit: Modified code using https://developer.chrome.com/extensions/devtools#evaluated-scripts-to-devtools as reference. Still no luck.
I'm trying to code a chrome-extension which uses chrome.* API call and save portions of the result in a file. I want to automate everything from the loading of the page to the text file download and hence, I don't want to use the browser.onclick() event.
My current attempt has no effect.
What changes would I need to make?
https://stackoverflow.com/a/16720024
Using the above answer as reference, I attempted the following:
manifest.json
{
"name":"Test Extension",
"version":"0.0.1",
"manifest_version": 2,
"description":"Description",
"permissions":["tabs"],
"background": {
"scripts": ["background.js"]
},
"devtools_page": "devtools.html"
}
background.js
// Background page -- background.js
chrome.runtime.onConnect.addListener(function(devToolsConnection) {
// assign the listener function to a variable so we can remove it later
var devToolsListener = function(message, sender, sendResponse) {
// Inject a content script into the identified tab
chrome.tabs.executeScript(message.tabId,
{ file: message.scriptToInject });
}
// add the listener
devToolsConnection.onMessage.addListener(devToolsListener);
devToolsConnection.onDisconnect.addListener(function() {
devToolsConnection.onMessage.removeListener(devToolsListener);
});
}
devtools.js
var backgroundPageConnection = chrome.runtime.connect({
name: "devtools-page"
});
backgroundPageConnection.onMessage.addListener(function (message) {
// Handle responses from the background page, if any
});
chrome.devtools.network.onRequestFinished.addListener(
function(request) {
chrome.runtime.sendMessage({
string: "Hi",
tabId: chrome.devtools.inspectedWindow.tabId,
scriptToInject: "content.js"
});
}
);
chrome.runtime.sendMessage({
string: "Hi",
tabId: chrome.devtools.inspectedWindow.tabId,
scriptToInject: "content.js"
});
content.js
alert("Hello");

messaging between content script and background page in a chrome extension is not working as it is supposed to be

I post the code below:
manifest.json
{
"manifest_version": 2,
"name": "Demo",
"description": "all_frames test",
"version": "1.0",
"background": {
"scripts": ["background.js"]
},
"content_scripts": [{
"matches": ["*://*/*"],
"js": ["content.js"],
"all_frames": true
}],
"permissions": [
"tabs",
"*://*/*"
]
}
background.js
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
var tabStatus = changeInfo.status;
if (tabStatus == 'complete') {
function return_msg_callback() {
console.log('Got a msg from cs...')
}
chrome.tabs.sendMessage(tabId, {
text: 'hey_cs'
}, return_msg_callback);
}
});
content.js
/* Listen for messages */
chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
/* If the received message has the expected format... */
if (msg.text && (msg.text == 'hey_cs')) {
console.log('Received a msg from bp...')
sendResponse('hey_bp');
}
});
Then, if I go to a site that includes multiples cross-origin iFrames, e.g., http://www.sport.es/ you would see that all the iFrames within the page receive the message from the background page but only one of them is able to response back. Is this a normal behavior?
Thanks in advance for your answer.
You send just one message with a direct callback so naturally Chrome can use this response callback just one time (it's a one-time connection to one entity, be it a page or an iframe).
Solution 1: send multiple messages to each iframe explicitly:
manifest.json, additional permissions:
"permissions": [
"webNavigation"
],
background.js
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
.............
// before Chrome 49 it was chrome.webNavigation.getAllFrames(tabId, .....
// starting with Chrome 49 tabId is passed inside an object
chrome.webNavigation.getAllFrames({tabId: tabId}, function(details) {
details.forEach(function(frame) {
chrome.tabs.sendMessage(
tabId,
{text: 'hey_cs'},
{frameId: frame.frameId},
function(response) { console.log(response) }
);
});
});
});
Solution 2: rework your background script logic so that the content script is the lead in communication and let it send the message once it's loaded.
content.js
chrome.runtime.sendMessage({text: "hey"}, function(response) {
console.log("Response: ", response);
});
background.js
chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
console.log("Received %o from %o, frame", msg, sender.tab, sender.frameId);
sendResponse("Gotcha!");
});
Communicating between a content script and the background page in a Chrome extension
Content script to background page
Send info to background page
chrome.extension.sendRequest({message: contentScriptMessage});
Receive info from content script
chrome.extension.onRequest.addListener(function(request, sender) {
console.log(request.message);
});
Background page to content script
Send info to content script
chrome.tabs.getSelected(null, function(tab) {
chrome.tabs.sendMessage(tab.id, { message: "TEST" });
});
Receive info from background page
chrome.runtime.onMessage.addListener(function(request, sender) {
console.log(request.message);
});
Instead of messaging, you can use executeScript for your purposes. While the callback's argument is rarely used (and I don't think many know how it works), it's perfect here:
chrome.tabs.executeScript(tabId, {file: "script.js"}, function(results) {
// Whichever is returned by the last executed statement of script.js
// is considered a result.
// "results" is an Array of all results - collected from all frames
})
You can make sure, for instance, that the last executed statement is something like
// script.js
/* ... */
result = { someFrameIdentifier: ..., data: ...};
// Note: you shouldn't do a "return" statement - it'll be an error,
// since it's not a function call. It just needs to evaluate to what you want.
Make sure you make script.js able to execute more than once on the same context.
For a frame identifier, you can devise your own algorithm. Perhaps a URL is enough, perhaps you can use the frame's position in the hierarchy.

Categories

Resources