Dynamic reloading of page with Chrome extension re-injects content script - javascript

I have an extension that injects content scripts when the tab is updated.
Main script:
chrome.browserAction.onClicked.addListener(function(tab) {
"use strict";
const sendScriptToPage = function(tabId, changeInfo, tab) {
if (changeInfo.status === "complete" && tab && tab.url && tab.url.indexOf("http") === 0) {
console.log ("executeScript ");
chrome.tabs.executeScript(tabId, {
file: "content.js", allFrames: true
});
}
};
chrome.tabs.onUpdated.addListener(sendScriptToPage);
});
Content script:
const Content = (function() {
"use strict";
console.log("Content");
const makeRandomColor = function(){
let c = '';
while (c.length < 6) {
c += (Math.random()).toString(16).substr(-6).substr(-1)
}
return '#'+c;
};
document.body.style.backgroundColor = makeRandomColor();
}
)();
It works fine when I reload a tab. However, when a tab is reloaded dynamically, the content script gets reloaded although it is already loaded in the tab. This shows in the log since const can't be re-declared.
Should not content scripts be unloaded when an update occurs? How can I know if a content script is already loaded or not?
A URL that shows this behaviour:
http://www.prisjakt.nu/produkt.php?p=391945#rparams=ss=android
Typing something in the search field triggers the onUpdated event handler, but the content script is already in the page.
A test extension:
https://github.com/hawk-lord/chrome-test

A navigation would wipe the content scripts, but onUpdated can be triggered by many things.
The first that comes to mind is an iframe being loaded. The main page doesn't navigate but you do inject indiscriminately.
Many approaches are possible:
Make the content script more robust by checking for Content being defined before you do anything.
Listen to a different event, for example, various webNavigation API events. They will uniquely identify the frame they refer to.
Don't listen to events - always inject, and change your triggering logic (for instance, keep a flag in chrome.storage).

Related

How to execute a content script every time a page loads?

I'm writing a Chrome extension that provides a content script to any page on GitHub (i.e. any URL matching https://github.com/*).
I'm simply trying to log something to the console each time a page on GitHub loads, like so:
window.onload = function() {
console.log("LOAD");
};
This listener function is executed the very first time a GitHub page is loaded, but if the user navigates to other pages on GitHub from there (by clicking on links or through other means), it doesn't fire. Why? :(
Steps to reproduce:
Open any repository's page on GitHub (example). You should see the message logged to the console.
Click on any link on that page. When the new page is loaded, no message is logged. :(
How do I solve this?
It seems that GitHub uses AJAX (along with history.pushState) to load some parts of the site, so onload will fire only when the page truly loads, but not when content is loaded via AJAX.
Since GitHub uses pushState to change the URL when AJAX content is done loading, you can detect when that happens, and execute your code.
There isn't actually a native event right now that fires when pushState is used, but there's this little hack:
(function(history){
var pushState = history.pushState;
history.pushState = function(state) {
if (typeof history.onpushstate == "function") {
history.onpushstate({state: state});
}
return pushState.apply(history, arguments);
}
})(window.history);
So, run that, and then, instead of window.onload, you can do:
history.onpushstate = function () {
console.log("LOAD");
};
Not ALL of GitHub page's load this way (AJAX + pushState), so you'd have to use both, window.onload and history.onpushstate.
Also, you should use window.addEventListener('load', fn); instead of window.onload, since you don't know if GitHub's code could be overwriting window.onload.

Chrome content script run in all pages in a given tab

I have a Chrome extension (content script) with a popup window. When the user clicks a "Start" button in the popup window, I'd like a new tab to open to a url (say www.test.com), and for the content script to be injected into that tab. Not just executed once, but injected so that it will work on (www.test.com/*) on that same tab. Not in other tabs - just that one.
Here's what I have now:
chrome.tabs.create({
'url': 'http://test.com/shop/new'
}, function(tab) {
chrome.tabs.executeScript(tab.id, {
'file': 'script.js'
});
});
But, chrome.tabs.executeScript is being used, which only executes the script once. The script redirects the page to 'http://test.com/shop/new/xxx', but since the script is only executed once, it stops working when the page changes. Again - how can I make it so that the script is injected into all 'http://test.com/shop/*' pages in that tab?
A good idea is to make a script that is always injected into http://test.com/shop/* (via manifest):
"content_scripts" : [
{
matches: ["http://test.com/shop/*"],
js: ["script.js"]
}
],
Then, in the script, ask the background page if it should be active for this ID:
// script.js
chrome.runtime.sendMessage({shouldIRun : true}, function(response){
if(response) {
// Actually do stuff
}
});
And in the background script, keep a record of tabs that you want it to apply to:
// Background script
var activeTabs = {}; // Slightly more difficult with event pages
// At some point when you enable it, e.g. in a browserAction.onClicked listener
activeTabs[tabId] = true;
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if(message.shouldIRun) {
// Double negation to ensure true/false
sendResponse(!!activeTabs[sender.tab.id]);
}
});
// It's a good idea to clear the stray entries
chrome.tabs.onRemoved.addListener(function(tabId, removeInfo) {
delete activeTabs[tabId];
});
// Sometimes this can also happen
chrome.tabs.onReplaced.addListener(function(addedTabId, removedTabId) {
if(!!activeTabs[removedTabId]) activeTabs[addedTabId] = true;
delete activeTabs[removedTabId];
});

How do I make it so my Chrome extension will only inject a script once?

I'm using programmatic injection to inject my extension's code into a page only when the browser action is clicked.
This is what I have on my extension's event page (per the example in the documentation):
chrome.browserAction.onClicked.addListener(function callback(tab){
chrome.tabs.executeScript(null, {file: "content-script.js"});
});
However, the way this works, the script is injected every time the button is clicked.
How can I change it so that the script is not injected on subsequent button presses - so that it is inserted only the first time the button is clicked on that page?
Put a global variable in your contentscript to judge if the contentscript has been executed.
if (something) { return; }
One way I can think of right now (easy and simple) is to use html5webstorage. Since you are running this code from your background or popup page it will be ok.
if(!localStorage.getItem("isAlreadyInjected")){
localStorage['isAlreadyInjected'] = "true";
chrome.browserAction.onClicked.addListener(function callback(tab){chrome.tabs.executeScript(null, {file: "content-script.js"});});}
So, the very first time when storage value "isAlreadyInjected" does not exist, the listener will be added. Afterwards, even when the browser closes and opens again this value will remain stored and so the listener will not be added to your extension.
UPDATE
As your background page loads only once at the beginning, it can keep variable that is not re-initialized with the browser action click. So you can use that variable to do your job!
background.js
var isAlreadyInjected =false;
function isInjected(){
if(!isAlreadyInjected ){
isAlreadyInjected=true;
return false;
}
else
return true;
}
popup.js
var bgpage=chrome.extension.getBackgroundPage();
if(!bgpage.isInjected()){
chrome.browserAction.onClicked.addListener(function callback(tab) {chrome.tabs.executeScript(null, {file: "content-script.js"});});
}
or
var bgpage=chrome.extension.getBackgroundPage();
chrome.browserAction.onClicked.addListener(function callback(tab) {
if(!bgpage.isInjected()){
chrome.tabs.executeScript(null, {file: "content-script.js"});
}});
I know this is an older question but I encountered the issue now that Manifest V3 is out and persistent background pages have been replaced with service workers. I figured I'd give what I used as a solution in case anyone else needs it. Code must be executed within the global context of the content script. Anytime it tries to inject it again, the relevant code will only be executed if the global variable is not defined.
if (typeof hasBeenExecuted === 'undefined') {
// Code that needs to execute only once goes here
}
var hasBeenExecuted = true;
Hopefully this is helpful for someone else who comes across the question.

how to resume script when new window loads

I am writing an page action extension for Google Chrome. The extension injects the following script into a search page after it loads. After the script finds all the occurrences of class "f_foto" (typically 10 items), it finds the first link in each of them, puts these hrefs in an array and then iterates thru the array opening a new window for each link and examining the result. That's what it is supposed to do.
Everything works ok in this code except the last part. The new window opens in a new tab (I have tabs permission) but it only finishes loading after the script finishes. Each new window overwrites the previous one in the same tab which would be ok if I had a chance to examine the contents first. So if I run it without using the debugger when the script finishes the new tab contains the last item in the array and focus is on the new tab. As far as I can see, handleResponse is never called.
If I run it in the DOM inspector and stop it at window.open, I can see that the new tab opens with "About Blank" in the title and the tab shows a spinning thingy showing that it is loading. Stepping thru the code, detailWin remains undefined even after detailWin=window.open(profileLinks[i], "Detail Window"); is executed. I've tried replacing window.onload = handleResponse; with detailWin.onload =handleResponse; but in this case detailWin is undefined.
It seems to me I need to add an event listener that fires when the new window is loaded and executes handleResponse. Yes? No?
//PEEK.JS//
var req;
var detailWin;
var profileLinks = new Array();
function handleResponse()
{
// var contentDetail = document.getElementsByClassName("content");
alert("Examine Detail Page Here");
};
//drag off the f_foto class
var searchResult = document.getElementsByClassName("f_foto");
alert("Found Class f_foto "+searchResult.length+" times.");
//collect profile links
for (var i = 0; i<searchResult.length; ++i)
{
var profileLink=searchResult[i].getElementsByTagName("a");
profileLinks[i]=profileLink[0].href;
// alert(i+1+" of "+searchResult.length+" "+profileLinks[i]+" length of "+profileLinks[i].length);
}
for (var i = 0; i<searchResult.length; ++i)
{
//DYSFUNCTIONAL CODE: New window finishes loading only after script completes, how to execute handleResponse?
detailWin=window.open(profileLinks[i], "Detail Window");
window.onload = handleResponse;
}
Option #1: make two separated content scripts - one for the search page only, one for the profile page only. Search script would only open profile link, profile script would only process it (contains code inside your handleResponse())
Option #2 If for some reasons you don't want to inject profile script to all profile pages, only to those you opened yourself from the search page, then instead of opening windows from a content script you should send a message to a background page asking it to open a profile link in a new tab and inject your profile script.
You still will have two content scripts.
search.js (injected to search pages only):
//PEEK.JS//
var req;
var detailWin;
//drag off the f_foto class
var searchResult = document.getElementsByClassName("f_foto");
alert("Found Class f_foto "+searchResult.length+" times.");
//collect profile links
for (var i = 0; i<searchResult.length; ++i)
{
var profileLink=searchResult[i].getElementsByTagName("a");
profileLinks[i]=profileLink[0].href;
// alert(i+1+" of "+searchResult.length+" "+profileLinks[i]+" length of "+profileLinks[i].length);
}
for (var i = 0; i<searchResult.length; ++i)
{
//tell bkgd page to open link
chrome.extension.sendRequest({cmd: "openProfile", url: profileLinks[i]});
}
profile.js (will be injected to profile pages you opened)
var contentDetail = document.getElementsByClassName("content");
alert("Examine Detail Page Here");
background.html:
chrome.extension.onRequest.addListener(function(request, sender, sendResponse) {
if(request.cmd == "openProfile") {
chrome.tabs.create({url: request.url}, function(tab){
//profile tab is created, inject profile script
chrome.tabs.executeScript(tab.id, {file: "profile.js"});
});
}
});
Option #3: Maybe you don't need to create profile window at all? If all you need is to find something in the page source, then you can just load that page through ajax and parse it (you would need to do it in a background page).

.onload called multiple times from Firefox extension

I'm developing a Firefox extension and have the following code:
function initialize() {
// For accessing browser window from sidebar code.
var mainWindow = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIWebNavigation)
.QueryInterface(Components.interfaces.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindow);
var gBrowser = mainWindow.gBrowser;
gBrowser.onload = function() {
alert('loaded');
};
}
When I open the extension (a sidebar) and proceed to open a new tab within the Firefox window, there are three alert boxes.
When I refresh a page, there are two alert boxes.
When a page finishes loading, there is only one alert box.
When I change tabs, an alert is fired.
I use .onload rather than DOMContentLoaded or readystatechange as I need to wait until all other javascript has finished loading on a page before I run mine.
Any ideas as to why multiple events are being triggered (and for things that the event shouldn't be triggered for)?
SOLUTION
Following from MatrixFrog's suggestion, here is the solution I came to:
function initialize() {
// For accessing browser window from sidebar code.
var mainWindow = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIWebNavigation)
.QueryInterface(Components.interfaces.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindow);
var gBrowser = mainWindow.gBrowser;
if (gBrowser.addEventListener) {
gBrowser.addEventListener("load",pageLoaded,true);
}
}
function pageLoaded(aEvent) {
if ((aEvent.originalTarget.nodeName == '#document') &&
(aEvent.originalTarget.defaultView.location.href == gBrowser.currentURI.spec))
{
alert('loaded');
}
}
aEvent.originalTarget.nodeName == '#document' checks that the page is loaded and not favicons.
(aEvent.originalTarget.defaultView.location.href == gBrowser.currentURI.spec)) checks that the element that fired the event is the page in the tab, and not one of its IFRAMEs
gBrowser.onload would only fire for xul-image and not for #document so it was replaced with gBrowser.addEventListener("load",pageLoaded,true);
If you want to avoid firing the event for new blank tabs, make sure gBrowser.currentURI.spec != "about:blank"
From https://developer.mozilla.org/en/Code_snippets/On_page_load
Current Firefox trunk nightlies will fire the onPageLoad function for not only documents, but xul:images (favicons in tabbrowser). If you only want to handle documents, ensure aEvent.originalTarget.nodeName == "#document"
If you're still seeing extraneous 'load' events firing, you may want to inspect the event target to figure out what's being loaded, and use similar logic to avoid calling your extension's logic in certain specific cases.

Categories