A button on my web page upon clicking performs the following action i.e. Injects the script into the page
function InjectToolbar() {
var script = document.createElement('script');
scriptFarfalla.src = 'some_Path'
document.getElementsByTagName('head')[0].appendChild(script);
}
.
.
.
.
.
.
It successfully performs the action desired. But when I reload the page the script is lost
Is there any method/technique with which I can buffer the button's click
Like a toggle button
Toggle.....> script injected
Toggle.....> script detached
Everything that happens in javascript is reset when you leave a page (and return to it). So you need a way to store whether something is loaded or not. This depends on how long you want this to be "saved"/"remembered". There are a few options for you to save this information - Cookies, HTML5 localStorage, HTML5 sessionStorage, and any server session usage you have available (if applicable). So if you want to implement something like this, you now need code onload of your page that checks the specific storage to see if you have set it. If so, inject the script. Here's what I mean:
window.onload = function () {
if (checkIfInjected()) {
scriptInjection(true);
}
}
function toggleInjection() {
if (checkIfInjected()) {
scriptInjection(false);
} else {
scriptInjection(true);
}
}
function scriptInjection(inject) {
if (inject == true) {
var script = document.createElement('script');
script.src = 'some_Path';
script.id = 'injected_script_id';
document.getElementsByTagName('head')[0].appendChild(script);
// Set the storage to say that script is injected
} else {
var the_script = document.getElementById("injected_script_id");
the_script.parentNode.removeChild(the_script);
the_script = null;
// Set the storage to say that script has been removed (or remove from storage altogether)
}
}
function checkIfInjected() {
// The following syntax is wrong for anything - you need to use the correct getter for the storage type you use
return storage.contains("script_injected");
}
<input type="button" id="button1" onclick="toggleInjection();" />
Now it is up to you to determine what storage type you want because they all do different things, including how things are stored, what they are stored for, and how long they are stored for.
You can use a cookie to store the scripts you've injected, and then re-inject them on page load. Cookies and the newer local storage are the usual ways of storing state on the client.
Related
I am wondering if there is any way to run window.addEventListener('load' ...) only the first time the specific page is loaded.
I tried just setting a flag called loaded to false, and only run the code inside the eventListener if loaded === false. Then once it's run, I set loaded to true. But does not work, still runs every time.
Can I perhaprs remove the eventListener once its run?
Keep a localStorage item that contains an array corresponding to all pages that have been loaded so far. Only attach the listener if that page isn't stored in localStorage yet. For example:
const { href } = window.location;
const alreadyLoaded = JSON.parse(localStorage.loaded || '[]');
if (!alreadyLoaded.includes(href)) {
alreadyLoaded.push(href);
localStorage.loaded = JSON.stringify(alreadyLoaded);
window.addEventListener('load', () => {
// rest of your code
});
}
Set the once property to true and it will run only once (doesn't work with Internet explorer)
More information here
const once = {
once : true
};
window.addEventListener('load',callback, once);
Easy way: you can use web storage that is if it's supported. Something like:
if (!localStorage.getItem("listenerLoaded")) {
window.addEventListener('load'...)
localStorage.setItem("listenerLoaded", true);
}
A bit tedious work would be using:
2. cookie(still browser needs support etc).
3. ajax and hold session
No it is not possible a a new execution context will be created every time that page loads.
You can try something like localStorage to save the state.
LocalStorage API helps you to save data which can be accessed later.it is an Object which can be used to access the current origin's local storage space.
For more info visit:
https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
Simply set a value in local storage once listener gets loaded then read that value before adding it again.
if (!localStorage.getItem("isLoaded")) {
window.addEventListener('load' ...)
localStorage.setItem("isLoaded", true);
}
Using removeEventListener is a good option:
var callback = function(){
...
}
window.removeEventListener('load',callback);
window.addEventListener('load',callback);
I'm creating a Google Chrome extension that adds some extra functionality to Gmail by injecting a script that I've written. By 'injected script' please see this definition.
To do this, my content script matches on ["https://mail.google.com/mail/*"].
However, the problem with this is that this is the same URL for when Gmail is loading and has finished loading. As a result, my script appears to sometimes to get injected too early, while Gmail is still loading, leading to reference errors.
To try to circumvent this problem, I'm only running my script on load by doing:
window.addEventListener("load", runInjectedScript);
And in my content script (which does the injecting), it only injects when this conditional is true: if (window.self === window.top).
Yet, these measures do not seem to guarantee that my script will always get injected at the right time, once Gmail has finished loading and the inbox appears. How can I ensure that this happens? Are there any techniques that I haven't implemented?
To ensure injecting scripts when "inbox appears", take a look at MutationObserver, which provides a way to react to changes in the DOM, in particular for observing the input being inserted
You could potentially run a setInterval checking to see if the data you're intercepting in Gmail is available yet.
var gmailLoaded = setInterval(function () {
if (typeof GMAIL !== 'undefined') {
runInjectedScript();
clearInterval(gmailLoaded);
}
}, 100);
You'll need to replace GMAIL with whatever you're trying to reference from Gmail. You could also use the same approach above checking to see if the loading state is active, however that may add additional overhead.
I inspected the gmail page and once the inbox loads they add style="display: none" to the #loading element.
You could do some polling for that to change and then bootstrap your app.
function onInboxLoaded(cb) {
var pollInterval = setInterval(function() {
var elem = document.getElementById('loading');
if (elem && elem.style.display === 'none') {
clearInterval(pollInterval);
cb();
}
}, 100);
}
onInboxLoaded(function(){
// bootstrap app
})
I'm developing a Chrome extension. Instead of using manifest.json to match content script for all URLs, I lazily inject the content script by calling chrome.tabs.executeScript when user do click the extension icon.
What I'm trying is to avoid executing the script more than once. So I have following code in my content script:
if (!window.ALREADY_INJECTED_FLAG) {
window.ALREADY_INJECTED_FLAG = true
init() // <- All side effects go here
}
Question #1, is this safe enough to naively call chrome.tabs.executeScript every time the extension icon got clicked? In other words, is this idempotent?
Question #2, is there a similar method for chrome.tabs.insertCSS?
It seems impossible to check the content script inject status in the backgroud script since it can not access the DOM of web page. I've tried a ping/pong method for checking any content script instance is alive. But this introduces an overhead and complexity of designing the ping-timeout.
Question #3, any better method for background script to check the inject status of content script, so I can just prevent calling chrome.tabs.executeScript every time when user clicked the icon?
Thanks in advance!
is this safe enough to naively call chrome.tabs.executeScript every
time the extension icon got clicked? In other words, is this
idempotent?
Yes, unless your content script modifies the page's DOM AND the extension is reloaded (either by reloading it via the settings page, via an update, etc.). In this scenario, your old content script will no longer run in the extension's context, so it cannot use extension APIs, nor communicate directly with your extension.
is there a similar method for chrome.tabs.insertCSS?
No, there is no kind of inclusion guard for chrome.tabs.insertCSS. But inserting the same stylesheet again does not change the appearance of the page because all rules have the same CSS specificity, and the last stylesheet takes precedence in this case. But if the stylesheet is tightly coupled with your extension, then you can simply inject the script using executeScript, check whether it was injected for the first time, and if so, insert the stylesheet (see below for an example).
any better method for background script to check the inject status of
content script, so I can just prevent calling
chrome.tabs.executeScript every time when user clicked the icon?
You could send a message to the tab (chrome.tabs.sendMessage), and if you don't get a reply, assume that there was no content script in the tab and insert the content script.
Code sample for 2
In your popup / background script:
chrome.tabs.executeScript(tabId, {
file: 'contentscript.js',
}, function(results) {
if (chrome.runtime.lastError || !results || !results.length) {
return; // Permission error, tab closed, etc.
}
if (results[0] !== true) {
// Not already inserted before, do your thing, e.g. add your CSS:
chrome.tabs.insertCSS(tabId, { file: 'yourstylesheet.css' });
}
});
With contentScript.js you have two solutions:
Using windows directly: not recommended, cause everyone can change that variables and Is there a spec that the id of elements should be made global variable?
Using chrome.storage API: That you can share with other windows the state of the contentScript ( you can see as downside, which is not downside at all, is that you need to request permissions on the Manifest.json. But this is ok, because is the proper way to go.
Option 1: contentscript.js:
// Wrapping in a function to not leak/modify variables if the script
// was already inserted before.
(function() {
if (window.hasRun === true)
return true; // Will ultimately be passed back to executeScript
window.hasRun = true;
// rest of code ...
// No return value here, so the return value is "undefined" (without quotes).
})(); // <-- Invoke function. The return value is passed back to executeScript
Note, it's important to check window.hasRun for the value explicitly (true in the example above), otherwise it can be an auto-created global variable for a DOM element with id="hasRun" attribute, see Is there a spec that the id of elements should be made global variable?
Option 2: contentscript.js (using chrome.storage.sync you could use chrome.storage.local as well)
// Wrapping in a function to not leak/modify variables if the script
// was already inserted before.
(chrome.storage.sync.get(['hasRun'], (hasRun)=>{
const updatedHasRun = checkHasRun(hasRun); // returns boolean
chrome.storage.sync.set({'hasRun': updatedHasRun});
))()
function checkHasRun(hasRun) {
if (hasRun === true)
return true; // Will ultimately be passed back to executeScript
hasRun = true;
// rest of code ...
// No return value here, so the return value is "undefined" (without quotes).
}; // <-- Invoke function. The return value is passed back to executeScript
Rob W's option 3 worked great for me. Basically the background script pings the content script and if there's no response it will add all the necessary files. I only do this when a tab is activated to avoid complications of having to add to every single open tab in the background:
background.js
chrome.tabs.onActivated.addListener(function(activeInfo){
tabId = activeInfo.tabId
chrome.tabs.sendMessage(tabId, {text: "are_you_there_content_script?"}, function(msg) {
msg = msg || {};
if (msg.status != 'yes') {
chrome.tabs.insertCSS(tabId, {file: "css/mystyle.css"});
chrome.tabs.executeScript(tabId, {file: "js/content.js"});
}
});
});
content.js
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
if (msg.text === 'are_you_there_content_script?') {
sendResponse({status: "yes"});
}
});
Just a side note to the great answer from Rob.
I've found the Chrome extension from Pocket is using a similar method. In their dynamic injected script:
if (window.thePKT_BM)
window.thePKT_BM.save();
else {
var PKT_BM_OVERLAY = function(a) {
// ... tons of code
},
$(document).ready(function() {
if (!window.thePKT_BM) {
var a = new PKT_BM;
window.thePKT_BM = a,
a.init()
}
window.thePKT_BM.save()
}
)
}
For MV3 Chrome extension, I use this code, no chrome.runtime.lastError "leaking" as well:
In Background/Extension page (Popup for example)
private async injectIfNotAsync(tabId: number) {
let injected = false;
try {
injected = await new Promise((r, rej) => {
chrome.tabs.sendMessage(tabId, { op: "confirm" }, (res: boolean) => {
const err = chrome.runtime.lastError;
if (err) {
rej(err);
}
r(res);
});
});
} catch {
injected = false;
}
if (injected) { return tabId; }
await chrome.scripting.executeScript({
target: {
tabId
},
files: ["/js/InjectScript.js"]
});
return tabId;
}
NOTE that currently in Chrome/Edge 96, chrome.tabs.sendMessage does NOT return a Promise that waits for sendResponse although the documentation says so.
In content script:
const extId = chrome.runtime.id;
class InjectionScript{
init() {
chrome.runtime.onMessage.addListener((...params) => this.onMessage(...params));
}
onMessage(msg: any, sender: ChrSender, sendRes: SendRes) {
if (sender.id != extId || !msg?.op) { return; }
switch (msg.op) {
case "confirm":
console.debug("Already injected");
return void sendRes(true);
// Other ops
default:
console.error("Unknown OP: " + msg.op);
}
}
}
new InjectionScript().init();
What it does:
When user opens the extension popup for example, attempt to ask the current tab to "confirm".
If the script isn't injected yet, no response would be found and chrome.runtime.lastError would have value, rejecting the promise.
If the script was already injected, a true response would result in the background script not performing it again.
I'm not sure what happens when a page containing the asynchronous version of Google Analytics hasn't fully loaded but the user either closes the browser or navigates to another page.
If the analytics doesn't get recorded like I assume, then what methods are available to ensure that it does?
If it does, how does it work?
This is probably impossible. You can kind of reverse-engineer google analytics stuff by tracking the resources they add to the page, like this:
var googleAnalyticsDidTheThing = false;
// ga_src is the src to a script that Google dynamically adds to your page
// this is your asynchronous code
var ga_src = "something.google.com/ga.js";
var ga_script;
var id = setInterval(function () {
var scripts = document.getElementsByTagName("script");
for (var i=0, len=scripts.length; i<len; i++) {
var script = scripts[i];
if (script.src === ga_src) { ga_script = script; break; }
}
var cb = function () {
googleAnalyticsDidTheThing = true;
};
ga_script.onload = cb;
// This one's for IE
ga_script.onreadystatechange = function () {
if (this.readystate === "complete") cb();
}
}, 50);
But the problem is that only gets you halfway there. You can check to see if it's done by using window.onunload as #nidhin mentioned. However, since javascript is single-threaded, only one process can be taking place at a time. So, I see no way for you to block the user from exiting the page without also blocking ga_script's ability to run in the background. Hence, you can check to see if Google has finished doing their thing, but you can't actually make sure Google can finish.
You could, however, send some info to your own server (the page could leave, but the data would still get sent) and gather statistics on how many users actually do this, to get a feel for what your margin of error is. You could even attempt to do some of the tracking yourself, if you are really determined.
You can check Google analytics is loaded before window is closed like this
<script type="text/javascript">
window.onunload = checkIfAnalyticsLoaded;
function checkIfAnalyticsLoaded() {
if (window._gat && window._gat._getTrackerByName()) {
// Do tracking with new-style analytics
} else if (window.urchinTracker) {
// Do tracking with old-style analytics
} else {
// Probably want to cap the total number of times you call this.
setTimeout(500, checkIfAnalyticsLoaded();
}
}
</script>
I am looking for a quick way to grab some data off of one Web page and throw it into another. I don't have access to the query string in the URL of the second page, so passing the data that way is not an option. Right now, I am using a Greasemonkey user script in tandem with a JS bookmarklet trigger: javascript:doIt();
// ==UserScript==
// #include public_site
// #include internal_site
// ==/UserScript==
if (document.location.host.match(internal_site)) {
var datum1 = GM_getValue("d1");
var datum2 = GM_getValue("d2");
}
unsafeWindow.doIt = function() {
if(document.location.host.match(public_site)) {
var d1 = innerHTML of page element 1;
var d2 = innerHTML of page element 2;
//Next two lines use setTimeout to bypass GM_setValue restriction
window.setTimeout(function() {GM_setValue("d1", d1);}, 0);
window.setTimeout(function() {GM_setValue("d2", d2);}, 0);
}
else if(document.location.host.match(internal_site)) {
document.getElementById("field1").value = datum1;
document.getElementById("field2").value = datum2;
}
}
While I am open to another method, I would prefer to stay with this basic model if possible, as this is just a small fraction of the code in doIt() which is used on several other pages, mostly to automate date-based form fills; people really like their "magic button."
The above code works, but there's an interruption to the workflow: In order for the user to know which page on the public site to grab data from, the internal page has to be opened first. Then, once the GM cookie is set from the public page, the internal page has to be reloaded to get the proper information into the internal page variables. I'm wondering if there's any way to GM_getValue() at bookmarklet-clicktime to prevent the need for a refresh. Thanks!
Can you move the bookmarklet to a button or link -- that Greasemonkey will add to the page(s)?
Then you could set click-event handlers to fire GM_getValue().
It looks like the current method is exploiting a "security hole" -- one that may be closed in the future. You might consider doing everything in a Firefox extension, instead.
Possibly useful link: http://articles.sitepoint.com/article/ten-tips-firefox-extensions/1