Injecting Javascript in a page from a Chrome extension : concurrency issue? - javascript

I am taking part in the development of a Chrome extension.
At some point, we need to run some static non-generated code which has to run in the context of the page and not of the extension.
For simple scripts, there is no problem, using either $.getScript(chrome.extension.getURL(....)) either script = document.createElement('script'); ... document.body.appendChild(script);
For more complex scripts, we'd need sometimes to incluque jquery itself, or some other script definition (because of dependencies).
In this latter case, though, despite the fact that Javascript is supposedly single threaded, it seems that JQuery is not parsed entirely when the dependend script are run, leading to the following
Uncaught ReferenceError: $ is not defined
Am I wrong when assuming that JScript is single-threaded?
What is the correct way to inject scripts in a page when there are dependencies between those scripts? (e.g. script X uses a function defined in script Y)

You could use the onload event for the script....
function addScript(scriptURL, onload) {
var script = document.createElement('script');
script.setAttribute("type", "application/javascript");
script.setAttribute("src", scriptURL);
if (onload) script.onload = onload;
document.documentElement.appendChild(script);
}
function addSecondScript(){
addScript(chrome.extension.getURL("second.js"));
}
addScript(chrome.extension.getURL("jquery-1.7.1.min.js"), addSecondScript);

Appending a script element with src attribute in the DOM doesn't mean that the script is actually loaded and ready to be used. You will need to monitor when the script has actually loaded, after which you can start using jQuery as normal.
You could do something like this:
var script = doc.createElement("script");
script.src = url;
/* if you are just targeting Chrome, the below isn't necessary
script.onload = (function(script, func){
var intervalFunc;
if (script.onload === undefined) {
// IE lack of support for script onload
if( script.onreadystatechange !== undefined ) {
intervalFunc = function() {
if (script.readyState !== "loaded" && script.readyState !== "complete") {
window.setTimeout( intervalFunc, 250 );
} else {
// it is loaded
func();
}
};
window.setTimeout( intervalFunc, 250 );
}
} else {
return func;
}
})(script, function(){
// your onload event, whatever jQuery etc.
});
*/
script.onload = onreadyFunction; // your onload event, whatever jQuery etc.
document.body.appendChild( script );

Related

<script> onload fires when ...?

In the function below from Mozilla, JS is used to add a tag into the document. I'm confused when the onload event fires. Does onload fire when the script starts to download or has already downloaded?
function prefixScript(url, onloadFunction) {
var newScript = document.createElement("script");
newScript.onerror = loadError;
if (onloadFunction) { newScript.onload = onloadFunction; }
document.currentScript.parentNode.insertBefore(newScript, document.currentScript);
newScript.src = url;
}
https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement
Thank you
onload's callback is called when these have been completed:
The HTTP request to fetch the script file has been completed successfully
The script content has been parsed
The script has been executed, thus possibly exposing new global variables and/or triggering side effects like DOM manipulation or XHR
Here's a demo where a script for jQuery library is added to <head>, thus exposing the global variable $ created by the execution of the imported script:
const script = document.createElement('script');
script.onload = () => {
try {
$;
console.log('onload - $ is defined');
} catch(e) {
console.log('onload - $ is not defined yet');
}
}
script.src = 'https://code.jquery.com/jquery-3.3.1.js';
try {
$;
console.log('main - $ is defined');
} catch(e) {
console.log('main - $ is not defined yet');
}
document.head.appendChild(script);
Last precision - int this case, the load is triggered by appending the script to the DOM, not by script.src = ...! Try commenting out the last line - the script never loads.
There can also be other cases where the load is triggered by script.src = ..., for example when the script is appened to the DOM, the the .src is set.
From the MDN GlobalEventHandlers.onload docs:
The load event fires when a given resource has loaded.
There is also an event for onloadstart called when loading for that resource is started.

Object inside window is present but undefined when invoked

I dynamically added StripeCheckout script in the DOM like:
var script = document.createElement(`script`);
script.src = `https://checkout.stripe.com/checkout.js`;
document.getElementsByTagName('head')[0].appendChild(script);
When I do console.log(window) I get :
But when I do console.log(window.StripeCheckout) I freaking get an undefined.
Why?
P.S. window.StripeCheckout is present when I accessed it directly from the dev console.
Why?
Because it takes time to load and execute the script. When you log window, by the time you expand the object, the script has loaded and executed. Same for when you run window.StripeCheckout in the developer console.
When you hover over that little [i] in the console it will also tell you that the "content" of the object was just evaluated when you expanded that line.
Additionally, because JavaScript runs to completion, it is guaranteed that console.log(window.StripeCheckout) is executed before the script is evaluated. So the property cannot exist at that moment, even if the script was available immediately.
If you want to know when the script loaded have a look at
Dynamically load external javascript file, and wait for it to load - without using JQuery
Dynamically load a JavaScript file
Try this:
function loadScript(url, callback){
var script = document.createElement("script")
script.type = "text/javascript";
if (script.readyState){ //IE
script.onreadystatechange = function(){
if (script.readyState == "loaded" ||
script.readyState == "complete"){
script.onreadystatechange = null;
callback();
}
};
} else { //Others
script.onload = function(){
callback();
};
}
script.src = url;
document.getElementsByTagName("head")[0].appendChild(script);
}
loadScript("https://checkout.stripe.com/checkout.js", function(){
console.log(window.StripeCheckout)
});

How to call a function from injected script?

This is code from my contentScript.js:
function loadScript(script_url)
{
var head= document.getElementsByTagName('head')[0];
var script= document.createElement('script');
script.type= 'text/javascript';
script.src= chrome.extension.getURL('mySuperScript.js');
head.appendChild(script);
someFunctionFromMySuperScript(request.widgetFrame);// ReferenceError: someFunctionFromMySuperScript is not defined
}
but i got an error when calling a function from injected script:
ReferenceError: someFunctionFromMySuperScript is not defined
Is there is a way to call this function without modifying mySuperScript.js?
Your code suffers from multiple problems:
As you've noticed, the functions and variables from the injected script (mySuperScript.js) are not directly visible to the content script (contentScript.js). That is because the two scripts run in different execution environments.
Inserting a <script> element with a script referenced through a src attribute does not immediately cause the script to execute. Therefore, even if the scripts were to run in the same environment, then you can still not access it.
To solve the issue, first consider whether it is really necessary to run mySuperScript.js in the page. If you don't to access any JavaScript objects from the page itself, then you don't need to inject a script. You should try to minimize the amount of code that runs in the page itself to avoid conflicts.
If you don't have to run the code in the page, then run mySuperScript.js before contentScript.js, and then any functions and variables are immediately available (as usual, via the manifest or by programmatic injection).
If for some reason the script really needs to be loaded dynamically, then you could declare it in web_accessible_resources and use fetch or XMLHttpRequest to load the script, and then eval to run it in your content script's context.
For example:
function loadScript(scriptUrl, callback) {
var scriptUrl = chrome.runtime.getURL(scriptUrl);
fetch(scriptUrl).then(function(response) {
return response.text();
}).then(function(responseText) {
// Optional: Set sourceURL so that the debugger can correctly
// map the source code back to the original script URL.
responseText += '\n//# sourceURL=' + scriptUrl;
// eval is normally frowned upon, but we are executing static
// extension scripts, so that is safe.
window.eval(responseText);
callback();
});
}
// Usage:
loadScript('mySuperScript.js', function() {
someFunctionFromMySuperScript();
});
If you really have to call a function in the page from the script (i.e. mySuperScript.js must absolutely run in the context of the page), then you could inject another script (via any of the techniques from Building a Chrome Extension - Inject code in a page using a Content script) and then pass the message back to the content script (e.g. using custom events).
For example:
var script = document.createElement('script');
script.src = chrome.runtime.getURL('mySuperScript.js');
// We have to use .onload to wait until the script has loaded and executed.
script.onload = function() {
this.remove(); // Clean-up previous script tag
var s = document.createElement('script');
s.addEventListener('my-event-todo-rename', function(e) {
// TODO: Do something with e.detail
// (= result of someFunctionFromMySuperScript() in page)
console.log('Potentially untrusted result: ', e.detail);
// ^ Untrusted because anything in the page can spoof the event.
});
s.textContent = `(function() {
var currentScript = document.currentScript;
var result = someFunctionFromMySuperScript();
currentScript.dispatchEvent(new CustomEvent('my-event-todo-rename', {
detail: result,
}));
})()`;
// Inject to run above script in the page.
(document.head || document.documentElement).appendChild(s);
// Because we use .textContent, the script is synchronously executed.
// So now we can safely remove the script (to clean up).
s.remove();
};
(document.head || document.documentElement).appendChild(script);
(in the above example I'm using template literals, which are supported in Chrome 41+)
As long as the someFunctionFromMySuperScript function is global you can call it, however you need to wait for the code actually be loaded.
function loadScript(script_url)
{
var head= document.getElementsByTagName('head')[0];
var script= document.createElement('script');
script.type= 'text/javascript';
script.src= chrome.extension.getURL('mySuperScript.js');
script.onload = function () {
someFunctionFromMySuperScript(request.widgetFrame);
}
head.appendChild(script);
}
You can also use jQuery's getScript method.
This doesn't work, because your content script and the injected script live in different contexts: what you inject into the page is in the page context instead.
If you just want to load code dynamically into the content script context, you can't do it from the content script - you need to ask a background page to do executeScript on your behalf.
// Content script
chrome.runtime.sendMessage({injectScript: "mySuperScript.js"}, function(response) {
// You can use someFunctionFromMySuperScript here
});
// Background script
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if (message.injectScript) {
chrome.tabs.executeScript(
sender.tab.id,
{
frameId: sender.frameId},
file: message.injectScript
},
function() { sendResponse(true); }
);
return true; // Since sendResponse is called asynchronously
}
});
If you need to inject code in the page context, then your method is correct but you can't call it directly. Use other methods to communicate with it, such as custom DOM events.

Waiting on JS class load from dynamic script loading

I have a JS script which depends on jQuery.
I want to check for jQuery, and if it is not loaded/available add it myself, wait for it to load, and then define my script class.
The code I currently use:
// load jQuery if not loaded yet
if (typeof (jQuery) == 'undefined') {
var fileref = document.createElement('script');
fileref.setAttribute("type", "text/javascript");
fileref.setAttribute("src", 'http://code.jquery.com/jquery-1.4.4.min.js');
document.getElementsByTagName('body')[0].appendChild(fileref);
(ready = function() {
if ( typeof (jQuery) == 'undefined' || !jQuery) {
return setTimeout( ready, 1 );
} else {
// jQuery loaded and ready
jQuery.noConflict();
}
})();
}
// … class definition follows
var MView = function() …
Now, with FireFox 4 (I think it did work before, or execution was just too slow), it will continue the scripts execution even when I still want to wait on jQuery. The recursive setTimeout is non-blocking.
How can I fix this? Make setTimeout blocking? Use another approach? Is there a better way? A way at all?
The class should be global scope, so it can be used on the page that includes this script file.
I would recommend 2 things.
Use 'if (!jQuery)' since undefined is considered falsey
Use the script tag's onload event
if (!window.jQuery) {
var fileref = document.createElement('script');
fileref.setAttribute("type", "text/javascript");
fileref.setAttribute("src", 'http://code.jquery.com/jquery-1.4.4.min.js');
fileref.onload = function() {
// Callback code here
};
document.getElementsByTagName('body')[0].appendChild(fileref);
}

Load jQuery in a js, then execute a script that depends on it

I have a unique issue -
I am designing a web application that creates widgets that a user can then embed in their own page (blog posts, mostly). I want them to just have to embed one line, so I just had that line be an include statement, to pull a Javascript off my server.
The problem is, I am building the widget code using jQuery, and I need to load the jQuery plugin, since I obviously don't know whether or not my users will have it available. I thought 'this should be pretty simple'....
function includeJavaScript(jsFile) {
var script = document.createElement('script');
script.src = jsFile;
script.type = 'text/javascript';
document.getElementsByTagName('head')[0].appendChild(script);
}
includeJavaScript('http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js');
jQuery();
So, I am appending the jQuery file to the head, then afterwards, trying to run a jQuery function. Trouble is, this doesn't work! Everytime I run it, I get the error that variable jQuery is not defined. I have tried a few things. I tried putting the jQuery functions in an onLoad trigger, so that the whole page (including, presumably, the jQuery file) would load before it called my script. I tried putting the jQuery function in a seperate file, and loading it after loading the jQuery lib file. But I get the idea I'm missing something simple - I'm new to jQuery, so if I'm missing something obvious, I apologize...
EDIT
OK,I tried the suggestion offered by digitalFresh, as follows (using Safari 5, if that helps), but I still get the same error?
function test() {
jQuery()
}
var script = document.createElement("script");
script.type = "text/javascript";
script.src = 'http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js';
script.onload = test(); //execute
document.body.appendChild(script);
EDIT
OK, I FINALLY got it to work, in an offhand suggestion from Brendan, by putting the call ITSELF in an 'onload' handler, like so:
function addLoadEvent(func) {
var oldonload = window.onload;
if (typeof window.onload != 'function') {
window.onload = func;
} else {
window.onload = function() {
if (oldonload) {
oldonload();
}
func();
}
}
}
addLoadEvent( function() {
var script = document.createElement("script");
script.type = "text/javascript";
script.src = 'http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js';
document.body.appendChild(script);
jQuery();
});
At this point, as you can see, I don't even have to put it in an 'onload' - it just works. Though I have to admit, I still don't understand WHY it works, which bothers me...
The solution you end up using works but slow to start, and not 100% fail proof. If someone rewrites window.onload your code will not run. Also window.onload happens when all the content on the page is loaded (including images) which is not exactly what you want. You don't want your script to wait that long.
Fortunately <script> elements have their own onload (ready) event, which can be used to couple other scripts with them.
function include(file, callback) {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = file;
script.onload = script.onreadystatechange = function() {
// execute dependent code
if (callback) callback();
// prevent memory leak in IE
head.removeChild(script);
script.onload = null;
};
head.appendChild(script);
}
include('http://ajax.googleapis.com/.../jquery.min.js', myFunction);
In this case the function will be called exactly when jquery is available. Your code is fail proof and starts quickly.
The script is not loaded or executed when you call the jQuery function. Thats why you get the error that jQuery is not defined.
I answered this problem on Loading Scripts Dynamically. The script is simple for all browsers except IE. Just add an onload event listener
You should first check to make sure they do not already use jquery so something like:
if (jQuery) {
// jQuery is loaded
} else {
// jQuery is not loaded
}
Secondly, you should make sure you use jQuery in no conflict mode and do not use the $ operator as it may be claimed by another script library.
Then to embed, declare this function:
function load_script (url)
{
var xmlhttp;
try {
// Mozilla / Safari / IE7
xmlhttp = new XMLHttpRequest();
} catch (e) {
//Other IE
xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
}
xmlhttp.open('GET', url, false); x.send('');
eval(xmlhttp.responseText);
var s = xmlhttp.responseText.split(/\n/);
var r = /^function\s*([a-z_]+)/i;
for (var i = 0; i < s.length; i++)
{
var m = r.exec(s[i]);
if (m != null)
window[m[1]] = eval(m[1]);
}
}
Then call it:
load_script('http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js');
Hope this helps.
Use LABjs. Embedding <script> tags work because the browser loads and executes whatever script in it or referenced by it upon seeing any of them, but that also means the browser will block until it is done with the script.
If you're using jQuery, you can use getScript(). Facebook has a good example on how to employ this. This is a more manageable solution to the original one I posted below.
Alternatively:
Building off of galambalazs's answer you can wrap his include() function in an event like the jQuery $(document).ready() to create deferred scripts with a dependency line without having to rely on an extra JS plugin. I also added a callbackOnError, which is handy if you're fetching from Google CDN.
/* galambalazs's include() */
function include(file, callback, callbackOnError) {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = file;
script.onload = script.onreadystatechange = function() {
// execute dependent code
if (callback) callback();
// prevent memory leak in IE
head.removeChild(script);
script.onload = null;
script.onerror = null;
};
script.onerror = function() {
if(callbackOnError) callbackOnError();
// prevent memory leak in IE
head.removeChild(script);
script.onload = null;
script.onerror = null;
};
head.appendChild(script);
}
/* If you need a deferred inline script, this will work */
function init() {
// Insert Code
}
/* Example with callback */
$(document).ready(function() {
include('something.js', init);
});
/* Maybe I want something on window.load() */
$(window).load(function() {
// Without a callback
include('whatever.js');
});
/* Or use an event to load and initialize script when it's needed */
$(".nearAButton").hover(include('button.js',function() {initButtonJS()}));
function initButtonJS() {
$(".nearAButton").off("mouseenter mouseleave");
}
/* Attempt to load Google Code, on fail load local */
include(
'//ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js',
function () {
// Inline Callback
},
function () {
include('/local/jquery-ui-1.10.3.custom.min.js', function () {
// Inline Callback
});
}
);

Categories