WebExtension failing silently in Firefox but not Chrome - javascript

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.

Related

How to use chrome extension to automatically inject "content.js" script on the current page without refresh the page? [duplicate]

After the Chrome extension I'm working on is installed, or upgraded, the content scripts (specified in the manifest) are not re-injected so a page refresh is required to make the extension work. Is there a way to force the scripts to be injected again?
I believe I could inject them again programmatically by removing them from the manifest and then handling which pages to inject in the background page, but this is not a good solution.
I don't want to automatically refresh the user's tabs because that could lose some of their data. Safari automatically refreshes all pages when you install or upgrade an extension.
There's a way to allow a content script heavy extension to continue functioning after an upgrade, and to make it work immediately upon installation.
Install/upgrade
The install method is to simply iterate through all tabs in all windows, and inject some scripts programmatically into tabs with matching URLs.
ManifestV3
manifest.json:
"background": {"service_worker": "background.js"},
"permissions": ["scripting"],
"host_permissions": ["<all_urls>"],
These host_permissions should be the same as the content script's matches.
background.js:
chrome.runtime.onInstalled.addListener(async () => {
for (const cs of chrome.runtime.getManifest().content_scripts) {
for (const tab of await chrome.tabs.query({url: cs.matches})) {
chrome.scripting.executeScript({
target: {tabId: tab.id},
files: cs.js,
});
}
}
});
This is a simplified example that doesn't handle frames. You can use getAllFrames API and match the URLs yourself, see the documentation for matching patterns.
ManifestV2
Obviously, you have to do it in a background page or event page script declared in manifest.json:
"background": {
"scripts": ["background.js"]
},
background.js:
// Add a `manifest` property to the `chrome` object.
chrome.manifest = chrome.runtime.getManifest();
var injectIntoTab = function (tab) {
// You could iterate through the content scripts here
var scripts = chrome.manifest.content_scripts[0].js;
var i = 0, s = scripts.length;
for( ; i < s; i++ ) {
chrome.tabs.executeScript(tab.id, {
file: scripts[i]
});
}
}
// Get all windows
chrome.windows.getAll({
populate: true
}, function (windows) {
var i = 0, w = windows.length, currentWindow;
for( ; i < w; i++ ) {
currentWindow = windows[i];
var j = 0, t = currentWindow.tabs.length, currentTab;
for( ; j < t; j++ ) {
currentTab = currentWindow.tabs[j];
// Skip chrome:// and https:// pages
if( ! currentTab.url.match(/(chrome|https):\/\//gi) ) {
injectIntoTab(currentTab);
}
}
}
});
Historical trivia
In ancient Chrome 26 and earlier content scripts could restore connection to the background script. It was fixed http://crbug.com/168263 in 2013. You can see an example of this trick in the earlier revisions of this answer.
The only way to force a content script to be injected without refreshing the page is via programatic injection.
You can get all tabs and inject code into them using the chrome tabs API.
For example you can store a manifest version in local storage and every time check if the manifest version is old one (in background page), if so you can get all active tabs and inject your code programmatically, or any other solution that will make you sure that the extension is updated.
Get all tabs using:
chrome.tabs.query
and inject your code into all pages
chrome.tabs.executeScript(tabId, {file: "content_script.js"});
Try this in your background script. Many of the old methods have been deprecated now, so I have refactored the code. For my use I'm only installing single content_script file. If need you can iterate over
chrome.runtime.getManifest().content_scripts array to get all .js files.
chrome.runtime.onInstalled.addListener(installScript);
function installScript(details){
// console.log('Installing content script in all tabs.');
let params = {
currentWindow: true
};
chrome.tabs.query(params, function gotTabs(tabs){
let contentjsFile = chrome.runtime.getManifest().content_scripts[0].js[0];
for (let index = 0; index < tabs.length; index++) {
chrome.tabs.executeScript(tabs[index].id, {
file: contentjsFile
},
result => {
const lastErr = chrome.runtime.lastError;
if (lastErr) {
console.error('tab: ' + tabs[index].id + ' lastError: ' + JSON.stringify(lastErr));
}
})
}
});
}
Chrome has added a method to listen for the install or upgrade event of the extension. One can re-inject the content script when such an event occur.
https://developers.chrome.com/extensions/runtime#event-onInstalled
Due to https://bugs.chromium.org/p/chromium/issues/detail?id=168263, the connection between your content script and background script is severed. As others have mentioned, one way to get around this issue is by reinjecting a content script. A rough overview is detailed in this StackOverflow answer.
The main tricky part is that it's necessary to "destruct" your current content script before injecting a new content script. Destructing can be really tricky, so one way to reduce the amount of state you must destruct is by making a small reinjectable script, that talks to your main content script over the DOM.
can't you add ?ver=2.10 at the end of css or js you upgraded?
"content_scripts": [ {
"css": [ "css/cs.css?ver=2.10" ],
"js": [ "js/contentScript.js?ver=2.10" ],
"matches": [ "http://*/*", "https://*/*" ],
"run_at": "document_end"
} ],

Chrome Extension javascript injection

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.

Injecting a JavaScript into a webpage automatically?

I am a very novice when it comes to writing code, this is my first attempt. I have done a fair amount of research and learning, but there is so much to learn. So i'm looking for some help or advice. At work we have to click a button to get us a work order. I am trying to automate the process, I have some code written that clicks the button for me. Unfortunately when it returns with "No Matches" it automatically reloads the page and my code is gone in the DOM. Is there a way to automatically inject my code every time the webpage reloads?
My Current Code:
var button = document.getElementById('GetMeAWorkOrder')
setInterval
button.click()
,1500
You can look into creating your own Chrome extension that injects the code through a content script. To make it easier, I've included the starter files for your specific project. Simply entering your specific URL into the marked lines should be enough for the code you posted.
Manifest.json
{
"manifest_version": 2,
"name": "My Extension",
"description": "Injects custom code to [website]",
"version": "1",
"permissions": [
"*://URL HERE*"
],
"content_scripts": [
{
"matches": [
"*://URL HERE*"
],
"js": ["content.js"]
}
]
}
content.js
//code you want to inject into the website
var button = document.getElementById('GetMeAWorkOrder');
setInterval( function() { button.click() },1500 );
Remember to replace the URLS with the website you want to inject the code into; here's a reference on match patterns.
Good luck and have fun coding! :)

What is causing my chrome extension's background.js to freeze up and not respond to messages

Every once in a while my chrome extension's background.js page freezes, i have no idea what is causing it.
When the background.js file has frozen, it no longer responds to messages from the content script, and when I try to open the background page via the extensions manager to inspect it, the window pops up but it stays blank, and no interface appears.
The only things im doing in the background page are message passing and retrieving localstorage variables.
I cant figure out what is causing this, the bug only seems to have happened since i transitioned to the new chrome.runtime api, from the chrome.extension api
Can anyone tell me what is going wrong here? or help me figure it out? Thanks!
Heres the background.js file's code in its entirety
if (!chrome.runtime) {
// Chrome 20-21
chrome.runtime = chrome.extension;
} else if(!chrome.runtime.onMessage) {
// Chrome 22-25
chrome.runtime.onMessage = chrome.extension.onMessage;
chrome.runtime.sendMessage = chrome.extension.sendMessage;
}
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.method == "getLocalStorage")
sendResponse({data: localStorage[request.key]}); // decodeURIComponent
else if (request.method == "setLocalStorage")
sendResponse({data: localStorage[request.key]=request.value});
else
sendResponse({}); // send empty response
});
Is it possible a deadlock situation is occurring that is freezing the page? It doesnt cause the CPU to go mad, so im guessing its not an endless loop.
Update
here is the manifest.json as requested
{
"manifest_version": 2,
"content_scripts": [ {
"exclude_globs": [ "http://*.facebook.com/ajax/*", "https://*.facebook.com/ajax/*" , "http://www.facebook.com/ai.php?*", "https://www.facebook.com/ai.php?*", "http://www.facebook.com/ajax/*", "https://www.facebook.com/ajax/*"],
"include_globs": [ "http://*.facebook.com/*", "https://*.facebook.com/*" ],
"js": [ "script.js" ],
"matches": [ "http://*.facebook.com/*", "https://*.facebook.com/*" ],
"run_at": "document_start"
} ],
"converted_from_user_script": true,
"background": {"scripts": ["background.js"],
"persistent": false},
"icons": {
"128": "ET-128x128.png",
"48": "ET-48x48.png"
},
"key": "xxxxxxxxxxxxxxxxxxxx",
"name": "Extension Test",
"short_name": "ET",
"description": "ET Does this and that, but doesnt phone home",
"version": "999",
"homepage_url": "http://www.etphonehome.com"
}
Only disabling and re-enabling the extension get it to start working again, once the background page has frozen
Below is a screenshot of the frozen background page inspection window:
The localStorage API is problematic because in chrome it is a synchronous API to an inherently asynchronous operation (called from a renderer process, which must then communicate with the browser process that reads from / writes to a backing store in the filesystem and possibly replies back to the renderer process). While it should not in theory be possible to cause deadlocks from webpage or extension code, it's possible there are bugs in chrome's implementation.
One thing you might try is switching from localStorage to chrome.storage.local. It is a little more work to use since it has an asynchronous API, but does not suffer from the same implementation complexity as localStorage.
E.g.
sendResponse({data: localStorage[request.key]});
becomes
chrome.storage.local.get(request.key, function(storageResult) {
sendResponse(storageResult[request.key]);
});

How to log fetched network resources in JavaScript?

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.

Categories