I am developing a small extension for chromium and I will do a script injection when the user loads a specific page.
manifest.json
{
"name" : "e-CalendarL3",
"description": "Extension pour les L3 informatique étudiant à l'université d'Angers",
"version": "0.0.1",
"background" : {
"scripts" : ["background.js"],
"persistent": true
},
"permissions":["webNavigation","storage","activeTab"],
"browser_action": {
"default_popup": "popup.html"
},
"content_scripts": [
{
"matches": ["https://edt.univ-angers.fr/edt/*"],
"js": ["injection.js"],
"run_at": "document_end"
}
],
"manifest_version": 2
}
background.js
chrome.runtime.onInstalled.addListener(function() {
alert('Merci d\'avoir installé l\'extension ');
})
injection.js
var aGrid = document.getElementsByClassName('fc-time-grid-event');
console.log(aGrid); // works
console.log(aGrid[0]) // don't works
console.log(aGrid.length); // don't works
console.log(aGrid); // works
injection.js result in the console
the html collection is not empty and it is correctly displayed but the other two lines of the injection.js script cause me problems (line 3 and 4)
for example line 4 should return to me 24
There are several problems here.
Content scripts run after DOMContentLoaded (by default) but this will never guarantee that a page won't run its scripts dynamically afterwards. A lot of modern pages do that while building their UI based on callbacks for requestAnimationFrame and setTimeout or in response to a network request they make to fetch the fresh data.
getElementsByClassName returns a live collection:
it was empty when the script ran as evidenced by console.log(aGrid[0])
then your script finished
then a page script ran and added the elements dynamically
then you expanded the collection in devtools console
then devtools looked for elements and found them at that exact moment
Solution.
A possible solution is to use MutationObserver to wait for the elements:
let aGrid = document.getElementsByClassName('fc-time-grid-event');
if (aGrid[0]) {
onGridAdded();
} else {
new MutationObserver((mutations, observer) => {
if (aGrid[0]) {
observer.disconnect();
onGridAdded();
}
}).observe(document, {childList: true, subtree: true});
}
function onGridAdded() {
console.log([...aGrid]);
// use aGrid here
}
More examples: link.
Related
When migrating a Chrome Extension to Manifest v3 we are getting rid of the background script and are instead using service workers.
The problem is that we previously sent messages from multiple content scripts to another content script through the background script, and this is no longer possible because in Manifest v3 the background script will become inactive after a while.
Is it possible to send messages between multiple content scripts without using the background script?
This is an example of how the content scripts are setup, sender.js is available in multiple iframes while receiver.js only is present in the top document.
"content_scripts": [
{
"js": ["receiver.js"],
"all_frames": false,
"matches": ["<all_urls>"]
},
{
"js": ["sender.js"],
"all_frames": true,
"matches": ["<all_urls>"],
"run_at": "document_start"
}
]
You can send messages from contents scripts to other content scripts with chrome.storage.local, no service worker required.
Proof of concept:
The counter has two purposes:
Allows content scripts to distinguish their own messages from other content scripts' messages.
Guarantees that chrome.storage.onChanged always fires, even if the data hasn't changed.
You need to execute the entire code inside the async function every time you send a message.
You could also use random numbers instead of a counter.
Advantage: You don't have to read the old counter value from storage.
Disadvantage: You need to make sure there are no collisions.
I don't know if the "event doesn't wake up the service worker" bug can occur in content scripts.
Instead of chrome.storage.local, you could use chrome.storage.session, but you'd need to set the access level: Storage areas > storage.session
manifest.json
{
"manifest_version": 3,
"name": "Content Script Messaging with Storage",
"version": "1.0",
"action": {
},
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["content_script.js"]
}
],
"permissions": [
"storage"
]
}
content_script.js
let counter = -1;
function storage_on_changed(changes, areaName) {
if (changes?.message?.newValue?.counter == counter) {
console.log("The message came from this content script", changes);
}
else {
console.log("The message came from another content script", changes);
}
}
chrome.storage.onChanged.addListener(storage_on_changed);
(async () => {
let { message } = await chrome.storage.local.get("message");
if (message === undefined) {
counter = 0;
}
else {
counter = message.counter + 1;
}
await chrome.storage.local.set({ message: {counter, data: document.URL} });
})();
I have a chrome extension that fires on a button click, the extension still can't access DOM elements, even though the page has clearly loaded.
I've seen other posts say it is because the elements are being created dynamically but not in my case.
The only thing that works is to wrap my debugTools.js in a timeout and fire it after 800 or so milliseconds.
The code inside debugTools.js is the part not working. I don't understand why is says "theWrapper" is undefined even if I wait 10 minutes before clicking my button which executes the code.
wrapper definitely exists on the page I navigate to before clicking my button. It is there in regular HMTL code. Not dynamically generated. I feel posting this page code would just confuse the question.
// manifest.json
{
"name": "Jams Dev Tools",
"version": "1.0",
"description": "Provides dev tools",
"background": {
"scripts": ["background.js"],
"persistent": true
},
... // icons are included here
"manifest_version": 2,
"web_accessible_resources": [
"debugTools.css",
"debugTools.js"
],
"permissions": [
"https://*/*",
"http://*/*",
"tabs",
"activeTab"
]
}
// debugTools.js
var theWrapper = document.getElementById("wrapper");
console.log(theWrapper.style.width);
// injectFiles.js
if(document.getElementById("debugJs") == undefined) {
// Inject Css
var debugJs = document.createElement('script');
var theSource = chrome.extension.getURL("debugTools.js");
debugJs.setAttribute( 'src', theSource );
debugJs.id = "debugJs";
document.body.appendChild(debugJs);
}
// background.js
chrome.browserAction.onClicked.addListener(function (tab) {
// for the current tab, inject the "inject.js" file & execute it
chrome.tabs.executeScript(tab.id, {
file: 'injectFiles.js'
});
});
Ok, so this was one of those times when you just can't see the wood for the trees. Thank you all for any help you have given. It turns out I'm an idiot. The problem can be summed up with the below snippet.
debug();
var theWrapper = document.getElementById("wrapper");
function debug() {
console.log(theWrapper.width);
}
I'm making a WebExtension for Chrome and Firefox that adds more information to GitHub. It's supposed to be faster than existing extensions.
I have my manifest set up like Mozilla's documentation recommends.
{
"manifest_version": 2,
"name": "GitHub Extended",
"version": "0.0.1",
"description": "Adds information to GitHub normally accessible only by the API.",
"permissions": [
"https://github.com/*"
],
"content_scripts": [
{
"all_frames": true,
"run_at": "document_start",
"matches": [
"https://github.com/*"
],
"js": [
"source/github.js",
"source/repository.js"
]
}
]
}
When the page is loaded, the content scripts are injected. The file github.js is a light wrapper around GitHub's API, and repository.js is the code to modfy the DOM of the main repository root page.
The most important code in there is this the preloader, which makes an API request while the page is loading and waits for both events to complete before adding to the DOM.
While this current code works fine in Chrome, in Firefox it simply does nothing. I tried testing it by putting console.log("I'm loaded!"); in repository.js. Nothing is printed. Why is this code not working in Firefox?
function beginPreload() {
console.log("Test from preload scope!");
let urlMatch = window.location.pathname.match(/\/([\w-]+)\/([\w-]+)/);
console.log(urlMatch);
Promise.all([
getSortedReleases(urlMatch[1], urlMatch[2]),
documentReady()
]).then((values) => {
let releaseJson = values[0];
let actionsEl = document.getElementsByClassName("pagehead-actions")[0];
let dlCount = 0;
for (release of releaseJson)
for (asset of release.assets)
dlCount += asset.download_count;
let buttonEl = createDownloadButton(
releaseJson[0].html_url,
window.location.pathname + "/releases",
formatNum(dlCount)
);
actionsEl.appendChild(buttonEl);
});
}
beginPreload();
console.log("Test from global scope!");
This was the solution.
"permissions": [
"https://api.github.com/*"
]
All that needed to happen was add permission for the extension to use GitHub's API. AFAIK, this is only required for content scripts using XHR.
You need to go step by step and first ask yourself if script is really injected in the FF github page: remove everything thing from your contentScript, reload extension and check your FF console. If you see the log then start adding code progressively until it breaks, if not you have a problem with your build content.
I would like my Google Extension to start execution as soon as Google Chrome window is opened. I have the following code in my background.js :
if (window.Notification) {
setInterval( function() { callAutomate(); }, 60000 );
}
function callAutomate() {
// Code to automate hello-hello.com
}
The manifest file is as follows -
{
"name" : "Hello.co Extension",
"version" : "1.1",
"description" : "Say Hello",
"background" :
{
"scripts": ["background.js"],
"persistent": false
},
"page_action" :
{
"default_icon" : "hello-19.png",
"default_title": "Hello World",
"default_popup": "popup.html"
},
"content_scripts": [
{
"matches": ["https://www.hellohello.com/*"],
"js": [
"content.js",
"webDB.js"
]
}
],
"permissions": [
"tabs",
"storage",
"unlimitedStorage",
"webNavigation",
"notifications",
"https://www.hellohello.com/"
],
"options_page": "options.html",
"icons" : {
"48" : "hello-48.png",
"128" : "hello-128.png"
},
"manifest_version": 2,
"web_accessible_resources": [
"hello-48.png"
]
}
Here is my problem the callAutomate(); function seems to called only while I'm observing the console logs for background.js. However the expected behavior of the extension is to call the callAutomate(); function every one minute from the time Google Chrome window is opened.
Any help with explanatory code would be highly appreciated.
As explained in the Chrome extension documentation, there are 2 types of background pages:
persistent background page : always "opened"
event background page : "open and closed" as needed
You are using the second one, as specified with the "persistent": false in your manifest.json file,
so the background code will not execute itself when you normally load your page.
And I am pretty sure that when you are using the developer tool ("observing the console") on your background page, the page is "opened" and does not close while the console remains open.
By removing the "persistent": false your code will be executed.
But as suggested in the documentation, you should use event pages as much as possible so have a look at the lifetime documentation to see the different ways you can communicate with your background page and thus execute your desired code.
That is because of "persistent": false in the manifest.
This describes an Event page, that is, a page Chrome can unload at will if it is idle for more than a couple of seconds, only keeping track of registered event handlers.
Notes for Event pages explicitly mention:
If your extension uses window.setTimeout() or window.setInterval(), switch to using the alarms API instead. DOM-based timers won't be honored if the event page shuts down.
And if you open a Dev Tools window for the background page, it will NOT be unloaded, leading to your code executing properly.
You can switch to using chrome.alarms API, but before you do, carefully read the Event page documentation. You need to understand all the limitations: for instance, since the page is unloaded, all local state in variables is lost. If you need to persist state, you'll need to employ storage APIs.
If that is too complicated for your purposes, remove "persistent": false to revert to a normal Background page.
Is there a way to access the list of resources that the browser requested (the ones found in this Chrome inspector's network panel)?
I would like to be able to iterate through these fetched resources to show the domains that have been accessed, something like:
for (var i = 0; i < window.navigator.resources.length; i++) {
var resource = window.navigator.resources[i];
console.log(resource); //=> e.g. `{domain: "www.google-analytics.com", name: "ga.js"}`
}
Or, maybe there is some event to write a handler for, such as:
window.navigator.onrequest = function(resource) {
console.log(resource); //=> e.g. `{domain: "www.google-analytics.com", name: "ga.js"}`
}
It doesn't need to work cross browser, or even be possible using client-side JavaScript. Just being able to access this information in any way would work (maybe there's some way to do this using phantomjs or watching network traffic from a shell/node script). Any ideas?
You can do this, but you will need to use Chrome extensions.
Chrome extensions have a lot of sandbox-style security. Communication between the Chrome extension and the web page is a multi-step process. Here's the most concise explanation I can offer with a full working example at the end:
A Chrome extension has full access to the chrome.* APIs, but a Chrome extension cannot communicate directly with the web page JS nor can the web page JS communicate directly with the Chrome extension.
To bridge the gap between the Chrome extension and the web page, you need to use a content script . A content script is essentially JavaScript that is injected at the window scope of the targeted web page. The content script cannot invoke functions nor access variables that are created by the web page JS, but they do share access to the same DOM and therefore events as well.
Because directly accessing variables and invoking functions is not allowed, the only way the web page and the content script can communicate is through firing custom events.
For example, if I wanted to pass a message from the Chrome extension to the page I could do this:
content_script.js
document.getElementById("theButton").addEventListener("click", function() {
window.postMessage({ type: "TO_PAGE", text: "Hello from the extension!" }, "*");
}, false);
web_page.js
window.addEventListener("message", function(event) {
// We only accept messages from ourselves
if (event.source != window)
return;
if (event.data.type && (event.data.type == "TO_PAGE")) {
alert("Received from the content script: " + event.data.text);
}
}, false);
`4. Now that you can send a message from the content script to the web page, you now need the Chrome extension gather up all the network info you want. You can accomplish this through a couple different modules, but the most simple option is the webRequest module (see background.js below).
`5. Use message passing to relay the info on the web requests to the content script and then on to the web page JavaScript.
Visually, you can think of it like this:
Full working example:
The first three files comprise your Google Chrome Extension and the last file is the HTML file you should upload to http:// web space somewhere.
icon.png
Use any 16x16 PNG file.
manifest.json
{
"name": "webRequest Logging",
"description": "Displays the network log on the web page",
"version": "0.1",
"permissions": [
"tabs",
"debugger",
"webRequest",
"http://*/*"
],
"background": {
"scripts": ["background.js"]
},
"browser_action": {
"default_icon": "icon.png",
"default_title": "webRequest Logging"
},
"content_scripts": [
{
"matches": ["http://*/*"],
"js": ["content_script.js"]
}
],
"manifest_version": 2
}
background.js
var aNetworkLog = [];
chrome.webRequest.onCompleted.addListener(function(oCompleted) {
var sCompleted = JSON.stringify(oCompleted);
aNetworkLog.push(sCompleted);
}
,{urls: ["http://*/*"]}
);
chrome.extension.onConnect.addListener(function (port) {
port.onMessage.addListener(function (message) {
if (message.action == "getNetworkLog") {
port.postMessage(aNetworkLog);
}
});
});
content_script.js
var port = chrome.extension.connect({name:'test'});
document.getElementById("theButton").addEventListener("click", function() {
port.postMessage({action:"getNetworkLog"});
}, false);
port.onMessage.addListener(function(msg) {
document.getElementById('outputDiv').innerHTML = JSON.stringify(msg);
});
And use the following for the web page (named whatever you want):
<!doctype html>
<html>
<head>
<title>webRequest Log</title>
</head>
<body>
<input type="button" value="Retrieve webRequest Log" id="theButton">
<div id="outputDiv"></div>
</head>
</html>
Big shoutout to #Elliot B.
I essentially used what he did but I wanted events to trigger in the content script rather than listeners triggering in the background. For whatever reason, I was unable to connect to the port from the background script so this is what I came up with.
PS: you need jquery.js in the extension folder to make this work.
manifest.json
{
"manifest_version": 2,
"name": "MNC",
"version": "0.0.1",
"description": "Monitor Network Comms",
"permissions":["webRequest","*://*/"],
"content_scripts": [{
"matches": ["<all_urls>"],
"run_at": "document_start",
"js": ["content.js",
"jquery.js"]
}],
"background": {
"scripts": ["background.js"]
}
}
background.js
var aNetworkLog = [];
chrome.webRequest.onResponseStarted.addListener(
function(oCompleted) {
var sCompleted = JSON.stringify(oCompleted);
aNetworkLog.push(sCompleted);
},{urls: ["https://*/*"]}
);
chrome.extension.onConnect.addListener(function (port) {
chrome.webRequest.onResponseStarted.addListener(
function(){
port.postMessage({networkLog:JSON.stringify(aNetworkLog)});
},{urls: ["https://*/*"]}
);
port.onMessage.addListener(function (message) {
if (message.disconnect==true) {
port.disconnect();
}
});
});
content.js
div = $('<div id="outputDiv" style="float:left;max-width:fit-content;position:fixed;display:none;"></div>').appendTo(document.body);
var port = chrome.extension.connect({name:'networkLogging'});
port.onMessage.addListener(function (message) {
if (message.networkLog) {
div[0].innerHTML = message.networkLog;
}
});
observer = new WebKitMutationObserver(function(mutation,observer){
JSON.parse(mutation[0]['target'].innerHTML).forEach(function(item){
JSON.parse(item);
})
});
observer.observe(div[0],{childList:true});
This is definitely not the most efficient way of doing things but it works for me. Thought that I would add it in here just in case someone is needing it.