Conditionally loading javascript script - javascript

I have a chrome extension that have a settings page. In the settings page I want to have preview and save buttons. I want to be able to pass temporary options object to my APPLICATION. How can I do it so that i don't have to rewrite APPLICATION twice? How to pass information to html page that this time it should access LocalStorage['permanent_settings'] and other LocalStorage['temporary_settings'], and render content using that object as a settings object. Also I want my code to execute locally, so I don't want any PHP etc.

You can add your conditions to this method, which allows you to dynamically add an external javascript file:
loadExternalScriptFile = function(filename) {
var fileref = document.createElement("script");
if (fileref){
fileref.setAttribute("type","text/javascript");
fileref.setAttribute("src", filename);
if (typeof fileref != "undefined")
document.getElementsByTagName("head")[0].appendChild(fileref);
}
}
//dynamically load and add this .js file
loadExternalScriptFile("myscript.js");

You should try holding the read source in local storage as well, in a similar manner to:
// At startup, defaulting to permanent_settings
function onLoad() {
LocalStorage['read_from'] = 'permanent_settings';
// Other initialization work
// ...
}
// When pressing Save
function onSaveClick() {
LocalStorage['read_from'] = 'permanent_settings';
}
// When pressing Preview
function onPreviewClick() {
LocalStorage['read_from'] = 'temporary_settings';
}
// When accessing the settings, read their source
function getSettingForKey(var key) {
var source = LocalStorage['read_from'];
// It can be either permanent_settings, or temporary_settings
var settingsArray = LocalStorage[source];
return settingsArray[key];
}

You can use jQuery to load the script and even execute code when it's loaded. You don't even have to wrap the code in a $(function(){ ... }) block:
$.getScript(chrome.extension.getURL("myscript.js"), function() {
alert("myscript.js has finished loading");
});

Related

Accessing background script object from content script

How to access background script objects form a content script inside chrome extension?
In content script I have:
// this will store settings
var settings = {};
// load settings from background
chrome.extension.sendMessage({
name: "get-settings"
}, function(response) {
debugger;
settings = response.data.settings;
});
Inside the background script I have:
var Settings = function() {
var me = this;
// internal, default
var settingList = {
serverUrl : "http://automatyka-pl.p4",
isRecordingEnabled : true,
isScanEnabled : true
};
this.get = function( key ) {
return settingList[key];
};
this.set = function( key , value ) {
if (settingList[key] != value) {
var setting = {};
setting[key] = value;
chrome.storage.sync.set(setting, function() {
settingList[key] = value;
});
}
return true;
};
chrome.extension.onMessage.addListener(function(request, sender, sendResponse) {
if (request.name == 'get-settings') {
sendResponse({
data : {
settings : settings
}
});
return true;
}
});
var settings = new Settings();
Messaging works, i mean response is send but returned object is empty. Do you know how to solve that?
EDIT
Based on your comments and answer will try to add different light to my question.
The actual problem is:
How to access background "model" from content script.
Lets assume that content script continuously responds to page DOM changes. Any time changes are detected some processing is made inside content script. But this processing is depended on extension setting. Those setting can be set via page action popup script which informs background model what those settings are.
So, any time page is processed with content script it should be aware of current settings stored inside background script.
As already described pulling settings from background is an asynchronous process, so i need a callback for further processing inside content script. Further processing must wait for settings (so this should be handled synchronously?).
It's hard for my to imagine what program flow should look like in this case.
background loads (setting initialized)
page loads -> content script loads
content script requests settings -> further processing is done inside callback function.
user changes setting, background settings are changed
page change is triggered and content script responds
content script requests settings -> further processing is done inside callback function - but it cannot be the same function like in pt. 3 (content "model" does not have to be initialized)?
sendMessage doesn't transfer the object itself, but only its JSON-ifiable representation,
effectively objReceived = JSON.parse(JSON.stringify(objSent)), so since your object's settingList is invisible outside function context it's lost during serialization.
You can make it public by exposing a stringifiable property
this.settingList = { foo: 'bar' };
that would be transferred to your content script successfully.
Since messaging is asynchronous, to use the response in the content script you should do it inside the response callback:
// this will store the settings
var settings = {};
// load settings from background
chrome.runtime.sendMessage({
name: "get-settings"
}, function(response) {
settings = response.data.settings;
onSettingsReady();
});
function onSettingsReady() {
// put your logic here, settings are set at this point
}
To know if settings changed outside your content-script, in settings setter in background.js send messages to your tab's content-script:
this.set = function( key , value ) {
...
// notify active tab if settings changed
chrome.tabs.query({"windowType":"normal"}, function(tabs){
for( id in tabs ){
if("id" in tabs[id]){
chrome.tabs.sendMessage(tabs[id].id,{"action":"update-settings","settings":settings});
}
}
});
return true;
};
And in content-script listen and process this message:
chrome.runtime.onMessage.addListener(function(msg){
if("action" in msg && msg.action == 'update-settings'){
// You are setting global settings variable, so on it will be visible in another functions too
settings = msg.settings;
}
});
More details: https://developer.chrome.com/extensions/runtime#method-sendMessage.
P.S. Use chrome.runtime.sendMessage instead of chrome.extension.sendMessage as the latter is deprecated in Chrome API and totally unsupported in WebExtensions API (Firefox/Edge).
It would probably make more sense to have another instance of Settings in your content script.
After all, chrome.storage API is available in content scripts.
Of course, you need to watch for changes made in other parts of the extension - but you should be doing so anyway, since you're using chrome.storage.sync and its value can change independently by Chrome Sync.
So, proposed solution:
Add a listener to chrome.storage.onChanged and process changes to update your settingList as needed.
Move the Storage logic to a separate JS "library", e.g. storage.js
Load storage.js in your content script and use it normally.
You may also want to adjust your storage logic so that saved data is actually taken into account - right now it's always the default. You can do something like this:
var defaultSettingList = {
serverUrl : "http://automatyka-pl.p4",
isRecordingEnabled : true,
isScanEnabled : true
};
var settingList = Object.assign({}, defaultSettingList);
chrome.storage.sync.get(defaultSettingList, function(data) {
settingList = Object.assign(settingList, data);
// At this point you probably should call the "ready" callback - initial
// load has to be async, no way around it
});

How to detect if any of css/js files failed to load and alert user to refresh browser [duplicate]

I have an HTML page where several JavaScript, CSS and images files are referenced. These references are dynamically injected and user can manually copy the HTML page and the support files to another machine.
If some JS or CSS are missing, the browser complains in the console. For example:
Error GET file:///E:/SSC_Temp/html_005/temp/Support/jquery.js
I need somehow these errors reported back to me on the inline JavaScript of the HTML page so I can ask user to first verify that support files are copied correctly.
There's the window.onerror event which just inform me that there's a JS error on the page such as an Unexpected Syntax error, but this doesn't fire in the event of a 404 Not Found error. I want to check for this condition in case of any resource type, including CSS, JS, and images.
I do not like to use jQuery AJAX to verify that file physically exists - the I/O overhead is expensive for every page load.
The error report has to contain the name of the file missing so I can check if the file is core or optional.
Any Ideas?
To capture all error events on the page, you can use addEventListener with the useCapture argument set to true. The reason window.onerror will not do this is because it uses the bubble event phase, and the error events you want to capture do not bubble.
If you add the following script to your HTML before you load any external content, you should be able to capture all the error events, even when loading offline.
<script type="text/javascript">
window.addEventListener('error', function(e) {
console.log(e);
}, true);
</script>
You can access the element that caused the error through e.target. For example, if you want to know what file did not load on an img tag, you can use e.target.src to get the URL that failed to load.
NOTE: This technically will not detect the error code, it detects if the image failed to load, as it technically behaves the same regardless of the status code. Depending on your setup this would probably be enough, but for example if a 404 is returned with a valid image it will not trigger an error event.
you can use the onload and onerror attributes to detect the error
for example upon loading the following html it gives alert error1 and error2 you can call your own function e.g onerror(logError(this);) and record them in an Array and once the page is fully loaded post is with single Ajax call.
<html>
<head>
<script src="file:///SSC_Temp/html_005/temp/Support/jquery.js" onerror="alert('error1');" onload="alert('load');" type="text/javascript" ></script>
</head>
<body>
<script src="file:///SSC_Temp/html_005/temp/Support/jquery.js" onerror="alert('error2');" onload="alert('load');" type="text/javascript" ></script>
</body>
</html>
I've put together the code below in pure JavaScript, tested, and it works.
All the source code (html, css, and Javascript) + images and example font is here: on github.
The first code block is an object with methods for specific file extensions: html and css.
The second is explained below, but here is a short description.
It does the following:
the function check_file takes 2 arguments: a string path and a callback function.
gets the contents of given path
gets the file extension (ext) of the given path
calls the srcFrom [ext] object method that returns an array of relative paths that was referenced in the string context by src, href, etc.
makes a synchronous call to each of these paths in the paths array
halts on error, and returns the HTTP error message and the path that had a problem, so you can use it for other issues as well, like 403 (forbidden), etc.
For convenience, it resolves to relative path names and does not care about which protocol is used (http or https, either is fine).
It also cleans up the DOM after parsing the CSS.
var srcFrom = // object
{
html:function(str)
{
var prs = new DOMParser();
var obj = prs.parseFromString(str, 'text/html');
var rsl = [], nds;
['data', 'href', 'src'].forEach(function(atr)
{
nds = [].slice.call(obj.querySelectorAll('['+atr+']'));
nds.forEach(function(nde)
{ rsl[rsl.length] = nde.getAttribute(atr); });
});
return rsl;
},
css:function(str)
{
var css = document.createElement('style');
var rsl = [], nds, tmp;
css.id = 'cssTest';
css.innerHTML = str;
document.head.appendChild(css);
css = [].slice.call(document.styleSheets);
for (var idx in css)
{
if (css[idx].ownerNode.id == 'cssTest')
{
[].slice.call(css[idx].cssRules).forEach(function(ssn)
{
['src', 'backgroundImage'].forEach(function(pty)
{
if (ssn.style[pty].length > 0)
{
tmp = ssn.style[pty].slice(4, -1);
tmp = tmp.split(window.location.pathname).join('');
tmp = tmp.split(window.location.origin).join('');
tmp = ((tmp[0] == '/') ? tmp.substr(1) : tmp);
rsl[rsl.length] = tmp;
}
});
});
break;
}
}
css = document.getElementById('cssTest');
css.parentNode.removeChild(css);
return rsl;
}
};
And here is the function that gets the file contents and calls the above object method according to the file extension:
function check_file(url, cbf)
{
var xhr = new XMLHttpRequest();
var uri = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function()
{
var ext = url.split('.').pop();
var lst = srcFrom[ext](this.response);
var rsl = [null, null], nds;
var Break = {};
try
{
lst.forEach(function(tgt)
{
uri.open('GET', tgt, false);
uri.send(null);
if (uri.statusText != 'OK')
{
rsl = [uri.statusText, tgt];
throw Break;
}
});
}
catch(e){}
cbf(rsl[0], rsl[1]);
};
xhr.send(null);
}
To use it, simply call it like this:
var uri = 'htm/stuff.html'; // html example
check_file(uri, function(err, pth)
{
if (err)
{ document.write('Aw Snap! "'+pth+'" is missing !'); }
});
Please feel free to comment and edit as you wish, i did this is a hurry, so it may not be so pretty :)
#alexander-omara gave the solution.
You can even add it in many files but the window handler can/should be added once.
I use the singleton pattern to achieve this:
some_global_object = {
error: (function(){
var activate = false;
return function(enable){
if(!activate){
activate = true;
window.addEventListener('error', function(e){
// maybe extra code here...
// if(e.target.custom_property)
// ...
}, true);
}
return activate;
};
}());
Now, from any context call it as many times you want as the handler is attached only once:
some_global_object.error();

Is PhantomJS supposed to load local js for a local html file

I'm using phantomjs to load a local html, which is loading a local js file, so the paths are like this:
/Users/me/html/page.html
/Users/me/html/page.js
and page.html includes the page.js file, and I can verify that by loading file:///Users/me/html/page.html and see the console.loging in the console for that page.
Now this js file is also merely adding a attribute to the body for testing this issue, which works normally. When I load this html file with PhantomJS however the js file does not change the DOM (ie does not add the attribute to the body).
The js file is loaded last on the html file, so it is at the bottom of the page:
<html>
<head></head>
<body>
<script src="page.js"></script>
</body>
</html>
Again loading this page normally it works, but with a phantom.js script it does not:
var page = require("webpage").create();
var system = require("system");
var args = system.args;
var pageURL = args[1];
page.open(pageURL, function(status) {
if (status !== 'success')
{
console.log(status);
}
else
{
var result = page.evaluate(function()
{
return document.body.getAttribute("data-changed") || "not found";
});
console.log(result);
}
});
the page.js looks like this:
document.body.setAttribute("data-changed", "true");
console.log("changed the page with js!")
So, is PhantomJS supposed to run js from the page being opened? or not? and if it is then what am I doing wrong here?
PhantomJS pages are not directly aware of the filesystem, and the page.js script is practically invisible to your page even though they are in the same directory. Instead of including the script directly in your HTML, you can use Phantom's injectJs to dynamically inject the script once the page is created, and before you evaluate it. injectJs will accept an absolute local path to the script, a path relative to the Phantom script itself, or a remote script, but not one that is relative to the page.
Modify your page.open callback like so:
page.open(pageURL, function(status) {
...
else {
// try to inject page.js
if ( page.injectJs('/Users/me/html/page.js') ) {
// page.js was injected, so evaluate:
var result = page.evaluate(function() {
return document.body.getAttribute("data-changed") || "not found";
});
console.log(result);
}
}
});

Chrome extension regarding injected script + localstorage

I am puzzling my way through my first 'putting it all together' Chrome extension, I'll describe what I am trying to do and then how I have been going about it with some script excerpts:
I have an options.html page and an options.js script that lets the user set a url in a textfield -- this gets stored using localStorage.
function load_options() {
var repl_adurl = localStorage["repl_adurl"];
default_img.src = repl_adurl;
tf_default_ad.value = repl_adurl;
}
function save_options() {
var tf_ad = document.getElementById("tf_default_ad");
localStorage["repl_adurl"] = tf_ad.value;
}
document.addEventListener('DOMContentLoaded', function () {
document.querySelector('button').addEventListener('click', save_options);
});
document.addEventListener('DOMContentLoaded', load_options );
My contentscript injects a script 'myscript' into the page ( so it can have access to the img elements from the page's html )
var s = document.createElement('script');
s.src = chrome.extension.getURL("myscript.js");
console.log( s.src );
(document.head||document.documentElement).appendChild(s);
s.parentNode.removeChild(s);
myscript.js is supposed to somehow grab the local storage data and that determines how the image elements are manipulated.
I don't have any trouble grabbing the images from the html source, but I cannot seem to access the localStorage data. I realize it must have to do with the two scripts having different environments but I am unsure of how to overcome this issue -- as far as I know I need to have myscript.js injected from contentscript.js because contentscript.js doesn't have access to the html source.
Hopefully somebody here can suggest something I am missing.
Thank you, I appreciate any help you can offer!
-Andy
First of all: You do not need an injected script to access the page's DOM (<img> elements). The DOM is already available to the content script.
Content scripts cannot directly access the localStorage of the extension's process, you need to implement a communication channel between the background page and the content script in order to achieve this. Fortunately, Chrome offers a simple message passing API for this purpose.
I suggest to use the chrome.storage API instead of localStorage. The advantage of chrome.storage is that it's available to content scripts, which allows you to read/set values without a background page. Currently, your code looks quite manageable, so switching from the synchronous localStorage to the asynchronous chrome.storage API is doable.
Regardless of your choice, the content script's code has to read/write the preferences asynchronously:
// Example of preference name, used in the following two content script examples
var key = 'adurl';
// Example using message passing:
chrome.extension.sendMessage({type:'getPref',key:key}, function(result) {
// Do something with result
});
// Example using chrome.storage:
chrome.storage.local.get(key, function(items) {
var result = items[key];
// Do something with result
});
As you can see, there's hardly any difference between the two. However, to get the first to work, you also have to add more logic to the background page:
// Background page
chrome.extension.onMessage.addListener(function(message, sender, sendResponse) {
if (message.type === 'getPref') {
var result = localStorage.getItem(message.key);
sendResponse(result);
}
});
On the other hand, if you want to switch to chrome.storage, the logic in your options page has to be slightly rewritten, because the current code (using localStorage) is synchronous, while chrome.storage is asynchronous:
// Options page
function load_options() {
chrome.storage.local.get('repl_adurl', function(items) {
var repl_adurl = items.repl_adurl;
default_img.src = repl_adurl;
tf_default_ad.value = repl_adurl;
});
}
function save_options() {
var tf_ad = document.getElementById('tf_default_ad');
chrome.storage.local.set({
repl_adurl: tf_ad.value
});
}
Documentation
chrome.storage (method get, method set)
Message passing (note: this page uses chrome.runtime instead chrome.extension. For backwards-compatibility with Chrome 25-, use chrome.extension (example using both))
A simple and practical explanation of synchronous vs asynchronous ft. Chrome extensions

Access NPAPI from pages DOM

I'm attempting to override the default functionality for webkitNotifications.createNotification and via a Chrome extension I'm able to inject a script in the pages DOM that does so. Problem I'm having now is I need access to chrome.extension.sendRequest from the pages DOM in order to push my request to the NPAPI I have embedded in the background page. I previously had the embed object rendered on each page during the execution of the content-script - but believe it's more effective (and safe) if the NPAPI is embedded within the extension not injected on every page.
if (window.webkitNotifications)
{
(function()
{
window.webkitNotifications.originalCreateNotification = window.webkitNotifications.createNotification;
window.webkitNotifications.createNotification = function (iconUrl, title, body) {
var n = window.webkitNotifications.originalCreateNotification(iconUrl, title, body);
n.original_show = n.show;
n.show = function ()
{
console.log("Chrome object", chrome);
console.log("Chrome.extension object", chrome.extension);
chrome.extension.sendRequest({'title' : title, 'body' : body, 'icon' : iconUrl});
}
return n;
}
})();
}
That is what is injected in the DOM as a script element. The background page is as follows:
<embed type="application/x-npapiplugz" id="plugz">
<script>
var osd = document.getElementById('plugz');
function processReq(req, sender, callback)
{
osd.notify(req.title, req.body, req.image);
console.log("NOTIFY!", req.title, req.body, req.image);
};
chrome.extension.onRequest.addListener(processReq);
</script>
Once your extension includes a NPAPI plugin, it is no longer safe :) But your correct, instead of allowing every single page have access to the plugin, it is better to let your extension have it. I assume you know about the "public" property which specifies whether your plugin can be accessed by regular web pages, the default is false.
Below, I will explain whats your problem, it isn't a accessing NPAPI from DOM pages problem, it is basically why can't your notifications access your content script or extension pages.
As you noticed, access to the content scripts and the pages DOM are isolated from each other. The only thing they share, is the DOM. If you want your notifications override to communicate to your content script, you must do so within a shared DOM. This is explained in Communication with the embedding page in the Content Scripts documentation.
You could do it the event way, where your content script listens on such event for data coming from your DOM, something like the following:
var exportEvent = document.createEvent('Event');
exportEvent.initEvent('notificationCallback', true, true);
window.webkitNotifications.createNotification = function (iconUrl, title, body) {
var n = window.webkitNotifications.createNotification(iconUrl, title, body);
n.show = function() {
var data = JSON.stringify({title: title, body: body, icon: iconUrl});
document.getElementById('transfer-dom-area').innerText = data;
window.dispatchEvent(exportEvent);
};
return n;
}
window.webkitNotifications.createHTMLNotification = function (url) {
var n = window.webkitNotifications.createHTMLNotification(url);
n.show = function() {
var data = JSON.stringify({'url' : url});
document.getElementById('transfer-dom-area').innerText = data;
window.dispatchEvent(exportEvent);
};
return n;
};
Then your event listener can send that to the background page:
// Listen for that notification callback from your content script.
window.addEventListener('notificationCallback', function(e) {
var transferObject = JSON.parse(transferDOM.innerText);
chrome.extension.sendRequest({NotificationCallback: transferObject});
});
I added that to my gist on GitHub for the whole extension (https://gist.github.com/771033), Within your background page, you can call your NPAPI plugin.
I hope that helps you out, I smell a neat idea from this :)

Categories