Chrome extension regarding injected script + localstorage - javascript

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

Related

Accessing a pages object using a Chrome Extension

I simply have to access an object that is a variable on the page that I am running my content script on from my Chrome Extension.
I know about the environments and their isolated worlds in which the content scripts and injected scripts run and that it's possible to get some variables using the injected scripts and then send them back.
I have searched for other answers regarding this question and most work for other type of variables and are the basic way of doing it but none currently work for accessing objects.
Any current solutions or workarounds?
EDIT: The solution that I used:
Content script:
//Sends an object from the page to the background page as a string
window.addEventListener("message", function(message) {
if (message.data.from == "myCS") {
chrome.runtime.sendMessage({
siteObject: message.data.prop
});
}
});
var myScript = document.createElement("script");
myScript.innerHTML = 'window.postMessage({from: "myCS", prop: JSON.stringify(OBJECT)},"*");';
document.body.appendChild(myScript);
Background.js:
//Info receiver
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
//When the content script sends the sites object to extract the needed data
if (message.siteObject !== undefined) {
console.log(message.siteObject);
//Process the data
}
});
You can try to inject a script tag in the page to access the object. If needed, you could use messaging to communicate with your extension. For example, assuming the object you want to access in your page is called pageObject:
content1.js
//this code will add a new property to the page's object
var myOwnData = "createdFromContentScript";
var myScript = document.createElement("script");
myScript.innerHTML = "pageObject.myOwnData = " + myOwnData;
document.body.appendChild(myScript);
content2.js
//this code will read a property from the existing object and send it to background page
window.addEventListener("message", function(message) {
if (message.data.from == "myCS") {
chrome.runtime.sendMessage({theProperty: message.data.prop});
}
});
var myScript = document.createElement("script");
myScript.innerHTML = 'window.postMessage({from: "myCS", prop: pageObject.existingProperty},"*");';
document.body.appendChild(myScript);
No, there is no way. There is no point having the isolated worlds for security and then there being a workaround whereby an extension can hack the content script and variables if it really needs to.
Presumably the object on the page interacts with the page or has some effect on the page or something on the page affects the state of the variable. You can trigger actions on the page (via the DOM) that might change the state of that variable but you should stop looking for ways to access variables directly.
Of course if the page author is cooperative then it's a different ball game - a mechanism could be provided in the author's script, a getter and setter mechanism. But somehow I doubt that's what you're after.

WebExtension how to send data from popup.html to content scripts [duplicate]

I have a text area and a button in my Chrome extension popup. I want users to input desired text in the text area. Then, once they click the button, it will inject a content script to change the text of the fields on the current page that have <textarea class="comments"> to the text that user entered in the <textarea> in the Chrome extension popup.
My question is, how can I get the text from the <textarea> in my popup.html and pass it from the popup.js to the content script?
This is what I have currently:
popup.html:
<!doctype html>
<html>
<head><title>activity</title></head>
<body>
<button id="clickactivity3">N/A</button>
<textarea rows="4" cols="10" id="comments" placeholder="N/A Reason..."></textarea>
<script src="popup.js"></script>
</body>
</html>
popup.js:
function injectTheScript3() {
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
// query the active tab, which will be only one tab
//and inject the script in it
chrome.tabs.executeScript(tabs[0].id, {file: "content_script3.js"});
});
}
document.getElementById('clickactivity3').addEventListener('click', injectTheScript3);
content_script3:
//returns a node list which is good
var objSelectComments = document.querySelectorAll('.comments');
//desired user input how?
function setCommentsValue(objSelectComments,){
//This will be for loop to iterate among all the text fields on the page, and apply
// the text to each instance.
for (var i=0; i<objSelectComments.length; i++) {
objSelectComments[i]= //user's desired text
}
There are three general ways to do this:
Use chrome.storage.local (MDN) to pass the data (set prior to injecting your script).
Inject code prior to your script which sets a variable with the data (see detailed discussion for possible security issue).
Use message passing (MDN) to pass the data after your script is injected.
Use chrome.storage.local (set prior to executing your script)
Using this method maintains the execution paradigm you are using of injecting a script that performs a function and then exits. It also does not have the potential security issue of using a dynamic value to build executing code, which is done in the second option below.
From your popup script:
Store the data using chrome.storage.local.set() (MDN)
In the callback for chrome.storage.local.set(), call tabs.executeScript() (MDN)
var updateTextTo = document.getElementById('comments').value;
chrome.storage.local.set({
updateTextTo: updateTextTo
}, function () {
chrome.tabs.executeScript({
file: "content_script3.js"
});
});
From your content script:
Read the data from chrome.storage.local.get() (MDN)
Make the changes to the DOM
Invalidate the data in storage.local (e.g. remove the key: chrome.storage.local.remove() (MDN)).
chrome.storage.local.get('updateTextTo', function (items) {
assignTextToTextareas(items.updateTextTo);
chrome.storage.local.remove('updateTextTo');
});
function assignTextToTextareas(newText){
if (typeof newText === 'string') {
Array.from(document.querySelectorAll('textarea.comments')).forEach(el => {
el.value = newText;
});
}
}
See: Notes 1 & 2.
Inject code prior to your script to set a variable
Prior to executing your script, you can inject some code that sets a variable in the content script context which your primary script can then use:
Security issue:
The following uses "'" + JSON.stringify().replace(/\\/g,'\\\\').replace(/'/g,"\\'") + "'" to encode the data into text which will be proper JSON when interpreted as code, prior to putting it in the code string. The .replace() methods are needed to A) have the text correctly interpreted as a string when used as code, and B) quote any ' which exist in the data. It then uses JSON.parse() to return the data to a string in your content script. While this encoding is not strictly required, it is a good idea as you don't know the content of the value which you are going to send to the content script. This value could easily be something that would corrupt the code you are injecting (i.e. The user may be using ' and/or " in the text they entered). If you do not, in some way, escape the value, there is a security hole which could result in arbitrary code being executed.
From your popup script:
Inject a simple piece of code that sets a variable to contain the data.
In the callback for chrome.tabs.executeScript() (MDN), call tabs.executeScript() to inject your script (Note: tabs.executeScript() will execute scripts in the order in which you call tabs.executeScript(), as long as they have the same value for runAt. Thus, waiting for the callback of the small code is not strictly required).
var updateTextTo = document.getElementById('comments').value;
chrome.tabs.executeScript({
code: "var newText = JSON.parse('"
+ JSON.stringify(updateTextTo).replace(/\\/g,'\\\\').replace(/'/g,"\\'") + "';"
}, function () {
chrome.tabs.executeScript({
file: "content_script3.js"
});
});
From your content script:
Make the changes to the DOM using the data stored in the variable
if (typeof newText === 'string') {
Array.from(document.querySelectorAll('textarea.comments')).forEach(el => {
el.value = newText;
});
}
See: Notes 1, 2, & 3.
Use message passing (MDN) (send data after content script is injected)
This requires your content script code to install a listener for a message sent by the popup, or perhaps the background script (if the interaction with the UI causes the popup to close). It is a bit more complex.
From your popup script:
Determine the active tab using tabs.query() (MDN).
Call tabs.executeScript() (MDN)
In the callback for tabs.executeScript(), use tabs.sendMessage() (MDN) (which requires knowing the tabId), to send the data as a message.
var updateTextTo = document.getElementById('comments').value;
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.executeScript(tabs[0].id, {
file: "content_script3.js"
}, function(){
chrome.tabs.sendMessage(tabs[0].id,{
updateTextTo: updateTextTo
});
});
});
From your content script:
Add a listener using chrome.runtime.onMessage.addListener() (MDN)
Exit your primary code, leaving the listener active. You could return a success indicator, if you choose.
Upon receiving a message with the data:
Make the changes to the DOM
Remove your runtime.onMessage listener
#3.2 is optional. You could keep your code active waiting for another message, but that would change the paradigm you are using to one where you load your code and it stays resident waiting for messages to initiate actions.
chrome.runtime.onMessage.addListener(assignTextToTextareas);
function assignTextToTextareas(message){
newText = message.updateTextTo;
if (typeof newText === 'string') {
Array.from(document.querySelectorAll('textarea.comments')).forEach(el => {
el.value = newText;
});
}
chrome.runtime.onMessage.removeListener(assignTextToTextareas); //optional
}
See: Notes 1 & 2.
Note 1: Using Array.from() is fine if you are not doing it many times and are using a browser version which has it (Chrome >= version 45, Firefox >= 32). In Chrome and Firefox, Array.from() is slow compared to other methods of getting an array from a NodeList. For a faster, more compatible conversion to an Array, you could use the asArray() code in this answer. The second version of asArray() provided in that answer is also more robust.
Note 2: If you are willing to limit your code to Chrome version >= 51 or Firefox version >= 50, Chrome has a forEach() method for NodeLists as of v51. Thus, you don't need to convert to an array. Obviously, you don't need to convert to an Array if you use a different type of loop.
Note 3: While I have previously used this method (injecting a script with the variable value) in my own code, I was reminded that I should have included it here when reading this answer.

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
});

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 :)

How to use javascript to get information from the content of another page (same domain)?

Let's say I have a web page (/index.html) that contains the following
<li>
<div>item1</div>
details
</li>
and I would like to have some javascript on /index.html to load that
/details/item1.html page and extract some information from that page.
The page /details/item1.html might contain things like
<div id="some_id">
picture
map
</div>
My task is to write a greasemonkey script, so changing anything serverside is not an option.
To summarize, javascript is running on /index.html and I would
like to have the javascript code to add some information on /index.html
extracted from both /index.html and /details/item1.html.
My question is how to fetch information from /details/item1.html.
I currently have written code to extract the link (e.g. /details/item1.html)
and pass this on to a method that should extract the wanted information (at first
just .innerHTML from the some_id div is ok, I can process futher later).
The following is my current attempt, but it does not work. Any suggestions?
function get_information(link)
{
var obj = document.createElement('object');
obj.data = link;
document.getElementsByTagName('body')[0].appendChild(obj)
var some_id = document.getElementById('some_id');
if (! some_id) {
alert("some_id == NULL");
return "";
}
return some_id.innerHTML;
}
First:
function get_information(link, callback) {
var xhr = new XMLHttpRequest();
xhr.open("GET", link, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
callback(xhr.responseText);
}
};
xhr.send(null);
}
then
get_information("/details/item1.html", function(text) {
var div = document.createElement("div");
div.innerHTML = text;
// Do something with the div here, like inserting it into the page
});
I have not tested any of this - off the top of my head. YMMV
As only one page exists in the client (browser) at a time and all other (virtual/possible) pages are on the server, how will you get information from another page using JavaScript as you will have to interact with the server at some point to retrieve the second page?
If you can, integrate some AJAX-request to load the second page (and parse it), but if that's not an option, I'd say you'll have to load all pages that you want to extract information from at the same time, hide the bits you don't want to show (in hidden DIVs?) and then get your index (or whoever controls the view) to retrieve the needed information from there ... even though that sounds pretty creepy ;)
You can load the page in a hidden iframe and use normal DOM manipulation to extract the results, or get the text of the page via AJAX, grab the part between <body...>...</body>ยจ and temporarily inject it into a div. (The second might fail for some exotic elements like ins.) I would expect Greasemonkey to have more powerful functions than normal Javascript for stuff like that, though - it might be worth to thumb through the documentation.

Categories