"Chrome Apps" webview.executeScript access guest global varibles - javascript

I can access a Chrome App Webview HTML with:
webview.executeScript(
{code: 'document.documentElement.innerHTML'},
function(results) {
// results[0] would have the webview's innerHTML.
});
But I would like to get the value of global variables in the Guest like so:
webview.executeScript(
{code: 'window.globalVar'},
function(results) {
// results[0] should have the webview's value of "globalVar".
});
How can I do this?

An answer to summarize the steps required.
1) You inject a content script with webview.executeScript() into the embedded page.
2) Since the page's real window is isolated, you need a page-level script to access it. You inject it with a <script> tag as discussed here.
3) The page-level script can access the window object, but cannot talk to the app script. However, it can fire a custom DOM event, that the content script can catch. Discussed here.
4) Finally, from the content script you need to send a message to your app script. The content script calls chrome.runtime.sendMessage, while the app script listens with chrome.runtime.onMessage. chrome.runtime.sendMessage does not seem to be available to webview content scripts injected with webview.executeScript(). A workaround is to use postMessage as described here.
It's a bit of an onion structure, that's why you need 2 steps "in" and 2 steps "out". You can't really do it in the return value that's passed to the executeScript callback, since at least one of the "out" steps will be asynchronous.

You can inject a script that inserts a DOM node with the global variable's value. Then you return that node's innerHTML and you have your value right away without using a callback:
var code = "script = document.createElement('script'); script.text=\"var n=document.createElement('span');n.style.display='none';n.id='my-id';n.innerHTML=window.globalVar;document.body.appendChild(n)\"; document.head.appendChild(script);document.getElementById('my-id').innerHTML"
webview.executeScript(
{code: code},
function(results) {
console.log(results[0]);
});
Just use an ID for the DOM node that is not used and you should be fine. It works for me.

Related

How to communicate with a webpage via browser plugin

How can I communicate from a JavaScript code of a webpage to the main code of the add-on?
For example, something like this: If some element is clicked, in the corresponding event handler of the page script, which is the syntax that can be used to send some message to the main code?
Specifically, something like this, where the frame now must be replaced by a generic webpage. Is it possible?
Edit: I have tried the suggested code, but how I had said, the application returns this error:
console.error: sherlock:
Message: ReferenceError: document is not defined
Stack:
A coding exception was thrown in a Promise resolution callback.
See https://developer.mozilla.org/Mozilla/JavaScript_code_modules/Promise.jsm/Promise
Full message: ReferenceError: document is not defined
Previously my question, I had infact tried something similar without any effect.
Yes it is possible.
document.onload = function() {
var elementYouWant = document.getElementById("someID");
elementYouWant.onclick = console.log("Yup.. It was clicked..");
};
Reference.
The answer to the question is not as trivial as it may seem at first sight. I had also thought of a logic of the type described in the Pogrindis' response.
But here, in the case of interaction between the main script (i.e. that of the add-on) and generic script of arbitrary documents, the pattern is different.
In summary, the interaction takes place in this way:
It is required the API page-mod.
Through the property includes of the object PageMod you create a reference to the document, specifying the URI (wildcards are allowed).
Via the contentScriptFile property it is set the URL of the .js file that will act as a vehicle between the main code and that of the document.
Here's an example that refers to the specific needs of the context in which I am. We have:
an add-on code (the main code);
a Sidebar type html document (gui1.html) loaded in the file that I
use as a simple UI (I advise against the use of Frames, since it does
not support many typical HTML features - eg the click on a link,
etc.) containing a link to a second document (gui2.html) which will then
be loaded into the browser tab (I needed this trick because the
Sidebar does not support localStorage, while it is necessary for me);
a script in the document.
We must create an exchange of information between the two elements. In my case the exchange is unidirectional, from the page script to the main one.
Here's the code (main.js):
var pageMod = require("sdk/page-mod");
pageMod.PageMod({
include: "resource://path/to/document/gui2.html",
contentScriptFile: data.url("listen.js"),
onAttach: function(worker) {
worker.port.on("gotElement", function(elementContent) {
console.log(elementContent);
});
}
});
and in the html page script:
<script type="text/javascript">
[...]
SOWIN = (navigator.userAgent.toLowerCase().indexOf("win") > -1) ? "win" : "nix";
if (SOWIN == "win") {
window.postMessage("win","*");
} else {
window.postMessage("Linux","*");
}
[...]
</script>
Finally in the JS file (listen.js) to be attached to the page script:
window.addEventListener('message', function(event) {
self.port.emit("gotElement", event.data);
}, false);
This is just a small example, but logic I would say that it is clear. The uploaded content scripts are not accessible directly from main.js (i.e. the add-on), but you can create a bidirectional communication through the exchange of messages. To achieve this we have to put ourselves in listening the event Attach of the page-mod. Then, it is passed a worker object to the listener; that worker may be used by the add-on for the exchange of messages.
Here are the references to have an exhaustive picture:
Interacting with page scripts
Communicating with other scripts
page-mod
port
Communicating using "port"
postMessage
Communicating using postMessage

Chrome extension: sending data to window created with chrome.windows.create

I'm struggling to find the best way to communicate with my web app, which I'm opening with chrome.windows.create in my extension.
I've got the wiring between content script and background script right. I can right click an element and send it's value to the background script, and the background script creates a window containing my webapp. But from there I can't figure out how to access and use that value in my webapp (it needs to load the value into an editor).
I've tried setting fns and vars on the window and tab objects, but somehow they go missing from the window object once the web app is loaded.
With chrome.tabs.executeScript I can fiddle with the dom, but not set global variables or anything on 'window' either.
If there isn't a better way, I guess I'm forced to add to the DOM and pick that up once my web app is loaded, but it seems messy. I was hoping for a cleaner method, like setting an onLoadFromExtension fn which my web app can execute to get the value it needs.
I found a method that works after much trial and error, though it still seems error prone. And it also depends on the extension ID matching the installed one, so if that can't be hard-coded it'll be another message that needs passing through another channel (after reading up, looks like that can be hard-coded since it's a hash of the public key, so problem solved)... Starting to think manipulating the DOM is less messy...
background.js:
var selectedContent = null;
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
console.info("------------------------------- Got request", request);
if (request.getSelectedContent) {
sendResponse(selectedContent);
}
});
web app:
var extensionId = "naonkagfcedpnnhdhjahadkghagenjnc";
chrome.runtime.sendMessage(extensionId, {getSelectedContent: "true"},
response => {
console.info("----------------- Got response", response);
if(response) {
this.text = response;
}
});
manifest.json:
"externally_connectable": {
"ids": ["naonkagfcedpnnhdhjahadkghagenjnc"],
"matches": ["http://localhost:1338/*"]
},
Within the popup, do the following:
const parentWindow = window.opener
parentWindow.postMessage({ action: 'opened' })
window.onmessage = msg => {
alert(JSON.stringify(msg.data)) // Alerts you with {"your":"data"}
}
Within the script that will call chrome.windows.create, do the following:
window.onmessage = msg => {
if (msg.data.action == 'opened') {
msg.source.postMessage({ your: 'data' })
}
}
Set setSelfAsOpener: true when calling chrome.windows.create
How does this work?
Due to limitations of the Chrome extension windows API, the created window needs to post a message to its creator (aka window.opener) or else the creator won't have access to a WindowProxy (useful for posting messages to the created window).

Selenium: How to Inject/execute a Javascript in to a Page before loading/executing any other scripts of the page?

I'm using selenium python webdriver in order to browse some pages. I want to inject a javascript code in to a pages before any other Javascript codes get loaded and executed. On the other hand, I need my JS code to be executed as the first JS code of that page. Is there a way to do that by Selenium?
I googled it for a couple of hours, but I couldn't find any proper answer!
Selenium has now supported Chrome Devtools Protocol (CDP) API, so , it is really easy to execute a script on every page load. Here is an example code for that:
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {'source': 'alert("Hooray! I did it!")'})
And it will execute that script for EVERY page load. More information about this can be found at:
Selenium documentation: https://www.selenium.dev/documentation/en/support_packages/chrome_devtools/
Chrome Devtools Protocol documentation: https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-addScriptToEvaluateOnNewDocument
Since version 1.0.9, selenium-wire has gained the functionality to modify responses to requests. Below is an example of this functionality to inject a script into a page before it reaches a webbrowser.
import os
from seleniumwire import webdriver
from gzip import compress, decompress
from urllib.parse import urlparse
from lxml import html
from lxml.etree import ParserError
from lxml.html import builder
script_elem_to_inject = builder.SCRIPT('alert("injected")')
def inject(req, req_body, res, res_body):
# various checks to make sure we're only injecting the script on appropriate responses
# we check that the content type is HTML, that the status code is 200, and that the encoding is gzip
if res.headers.get_content_subtype() != 'html' or res.status != 200 or res.getheader('Content-Encoding') != 'gzip':
return None
try:
parsed_html = html.fromstring(decompress(res_body))
except ParserError:
return None
try:
parsed_html.head.insert(0, script_elem_to_inject)
except IndexError: # no head element
return None
return compress(html.tostring(parsed_html))
drv = webdriver.Firefox(seleniumwire_options={'custom_response_handler': inject})
drv.header_overrides = {'Accept-Encoding': 'gzip'} # ensure we only get gzip encoded responses
Another way in general to control a browser remotely and be able to inject a script before the pages content loads would be to use a library based on a separate protocol entirely, eg: Chrome DevTools Protocol. The most fully featured I know of is playwright
If you want to inject something into the html of a page before it gets parsed and executed by the browser I would suggest that you use a proxy such as Mitmproxy.
If you cannot modify the page content, you may use a proxy, or use a content script in an extension installed in your browser. Doing it within selenium you would write some code that injects the script as one of the children of an existing element, but you won't be able to have it run before the page is loaded (when your driver's get() call returns.)
String name = (String) ((JavascriptExecutor) driver).executeScript(
"(function () { ... })();" ...
The documentation leaves unspecified the moment at which the code would start executing. You would want it to before the DOM starts loading so that guarantee might only be satisfiable with the proxy or extension content script route.
If you can instrument your page with a minimal harness, you may detect the presence of a special url query parameter and load additional content, but you need to do so using an inline script. Pseudocode:
<html>
<head>
<script type="text/javascript">
(function () {
if (location && location.href && location.href.indexOf("SELENIUM_TEST") >= 0) {
var injectScript = document.createElement("script");
injectScript.setAttribute("type", "text/javascript");
//another option is to perform a synchronous XHR and inject via innerText.
injectScript.setAttribute("src", URL_OF_EXTRA_SCRIPT);
document.documentElement.appendChild(injectScript);
//optional. cleaner to remove. it has already been loaded at this point.
document.documentElement.removeChild(injectScript);
}
})();
</script>
...
so I know it's been a few years, but I've found a way to do this without modifying the webpage's content and without using a proxy! I'm using the nodejs version, but presumably the API is consistent for other languages as well. What you want to do is as follows
const {Builder, By, Key, until, Capabilities} = require('selenium-webdriver');
const capabilities = new Capabilities();
capabilities.setPageLoadStrategy('eager'); // Options are 'eager', 'none', 'normal'
let driver = await new Builder().forBrowser('firefox').setFirefoxOptions(capabilities).build();
await driver.get('http://example.com');
driver.executeScript(\`
console.log('hello'
\`)
That 'eager' option works for me. You may need to use the 'none' option.
Documentation: https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/capabilities_exports_PageLoadStrategy.html
EDIT: Note that the 'eager' option has not been implemented in Chrome yet...

Executing code at page-level from Background.js and returning the value

I've got a web page with its own scripts and variables that I need to execute and retrieve return values from my extension's Background.js.
I understand (I think!) that in order to interact with the web page, it must be done via chrome.tabs.executeScript or a ContentScript, but because the code must execute in the context of the original page (in order to have scope to the scripts and variables), it needs to be injected into the page first.
Following this great post by Rob W, I'm able to invoke the page-level script/variables, but I'm struggling to understand how to return values in this way.
Here's what I've got so far...
Web page code (that I want to interact with):
<html>
<head>
<script>
var favColor = "Blue";
function getURL() {
return window.location.href;
}
</script>
</head>
<body>
<p>Example web page with script content I want interact with...</p>
</body>
</html>
manifest.json:
{
// Extension ID: behakphdmjpjhhbilolgcfgpnpcoamaa
"name": "MyExtension",
"version": "1.0",
"manifest_version": 2,
"description": "My Desc Here",
"background": {
"scripts": ["background.js"]
},
"icons": {
"128": "icon-128px.png"
},
"permissions": [
"background",
"tabs",
"http://*/",
"https://*/",
"file://*/", //### (DEBUG ONLY)
"nativeMessaging"
]
}
background.js
codeToExec = ['var actualCode = "alert(favColor)";',
'var script = document.createElement("script");',
' script.textContent = actualCode;',
'(document.head||document.documentElement).appendChild(script);',
'script.parentNode.removeChild(script);'].join('\n');
chrome.tabs.executeScript( tab.id, {code:codeToExec}, function(result) {
console.log('Result = ' + result);
} );
I realise the code is currently just "alerting" the favColor variable (this was just a test to make sure I could see it working). However, if I ever try returning that variable (either by leaving it as the last statement or by saying "return favColor"), the executeScript callback never has the value.
So, there appear to be (at least) three levels here:
background.js
content scripts
actual web page (containing scripts/variables)
...and I would like to know what is the recommended way to talk from level 1 to level 3 (above) and return values?
Thanks in advance :o)
You are quite right in understanding the 3-layer context separation.
A background page is a separate page and therefore doesn't share JS or DOM with visible pages.
Content scripts are isolated from the webpage's JS context, but share DOM.
You can inject code into the page's context using the shared DOM. It has access to the JS context, but not to Chrome APIs.
To communicate, those layers use different methods:
Background <-> Content talk through Chrome API.
The most primitive is the callback of executeScript, but it's impractical for anything but one-liners.
The common way is to use Messaging.
Uncommon, but it's possible to communicate using chrome.storage and its onChanged event.
Page <-> Extension cannot use the same techniques.
Since injected page-context scripts do not technically differ from page's own scripts, you're looking for methods for a webpage to talk to an extension. There are 2 methods available:
While pages have very, very limited access to chrome.* APIs, they can nevertheless use Messaging to contact the extension. This is achieved through "externally_connectable" method.
I have recently described it in detail this answer. In short, if your extension declared that a domain is allowed to communicate with it, and the domain knows the extension's ID, it can send an external message to the extension.
The upside is directly talking to the extension, but the downside is the requirement to specifically whitelist domains you're using this from, and you need to keep track of your extension ID (but since you're injecting the code, you can supply the code with the ID). If you need to use it on any domain, this is unsuitable.
Another solution is to use DOM Events. Since the DOM is shared between the content script and the page script, an event generated by one will be visible to another.
The documentation demonstrates how to use window.postMessage for this effect; using Custom Events is conceptually more clear.
Again, I answered about this before.
The downside of this method is the requirement for a content script to act as a proxy. Something along these lines must be present in the content script:
window.addEventListener("PassToBackground", function(evt) {
chrome.runtime.sendMessage(evt.detail);
}, false);
while the background script processes this with a chrome.runtime.onMessage listener.
I encourage you to write a separate content script and invoke executeScript with a file attribute instead of code, and not rely on its callback. Messaging is cleaner and allows to return data to background script more than once.
The approach in Xan's answer (using events for communication) is the recommended approach. Implementing the concept (and in a secure way!) is however more difficult.
So I'll point out that it is possible to synchronously return a value from the page to the content script. When a <script> tag with an inline script is inserted in the page, the script is immediately and synchronously executed (before the .appendChild(script) method returns).
You can take advantage of this behavior by using the injected script to assign the result to a DOM object which can be accessed by the content script. For example, by overwriting the text content of the currently active <script> tag. The code in a <script> tag is executed only once, so you can assign any rubbish to the content of the <script> tag, because it won't be parsed as code any more. For example:
// background script
// The next code will run as a content script (via chrome.tabs.executeScript)
var codeToExec = [
// actualCode will run in the page's context
'var actualCode = "document.currentScript.textContent = favColor;";',
'var script = document.createElement("script");',
'script.textContent = actualCode;',
'(document.head||document.documentElement).appendChild(script);',
'script.remove();',
'script.textContent;'
].join('\n');
chrome.tabs.executeScript(tab.id, {
code: codeToExec
}, function(result) {
// NOTE: result is an array of results. It is usually an array with size 1,
// unless an error occurs (e.g. no permission to access page), or
// when you're executing in multiple frames (via allFrames:true).
console.log('Result = ' + result[0]);
});
This example is usable, but not perfect. Before you use this in your code, make sure that you implement proper error handling. Currently, when favColor is not defined, the script throws an error. Consequently the script text is not updated and the returned value is incorrect. After implementing proper error handling, this example will be quite solid.
And the example is barely readable because the script is constructed from a string. If the logic is quite big, but the content script in a separate file and use chrome.tabs.executeScript(tab.id, {file: ...}, ...);.
When actualCode becomes longer than a few lines, I suggest to wrap the code in a function literal and concatenate it with '(' and ')(); to allow you to more easily write code without having to add quotes and backslashes in actualCode (basically "Method 2b" of the answer that you've cited in the question).
chrome.browserAction.onClicked.addListener(function(tab) {
// No tabs or host permissions needed!
console.log('Turning ' + tab.url + ' red!');
chrome.tabs.executeScript({
file: 'index.js'
});
});
Here index.js is normal js file to inject in browser
#index.js
alert("Hello from api");

Permission denied to call method ChromeWindow.postMessage for iframe inside XUL page

I've an extension, and an XUL file inside it (let's call it A). XUL file contains an <iframe>, where is loaded some web page (let's call it B). B is loaded from the different domain.
A is parent to B. I want to send a message from within B to A using window.parent.postMessage().
I'm getting the following exception:
... permission denied to B to call method ChromeWindow.postMessage
How to fix that error? If there is no way to do that, how can I pass message from B to A?
I am using Firefox 16.0.1 under Windows 7.
I had a very similar problem,
it's just I had a html-popup (local) that couldn't send 'postMessage' to my xul-background-task.
I think I got it to work,
strangely enough by initiating a MessageEvent of my own (the very same thing postMessage does)
but with a (I believe obsolete) fallback.. in short: I brewed something together from MDN and other sites ;)
My script in the content:
var Communicator =
{
postMessage: function(data)
{
// We need some element to attach the event to, since "window" wont work
// let's just use a fallback JSON-stringified textnode
var request = document.createTextNode(JSON.stringify(data));
// and as always attach it to the current contentwindow
document.head.appendChild(request);
// Now let's make a MessageEvent of our own, just like window.postMessage would do
var event = document.createEvent("MessageEvent");
event.initMessageEvent ("own_message", true, false, data, window.location, 0, window, null);
// you might want to change "own_message" back to "message" or whatever you like
//and there we go
request.dispatchEvent(event);
}
}
And instead of window.postMessage(data) now use Communicator.postMessage(data)
that's all!
Now in my overlay there's nothing but our good old
addEventListener('own_message', someMessageFunc, false, true);
//or maybe even "message" just like originally
Hopefully this will work for you, too (didn't check that on iframes...)
You should check the type of iframe B
Edit:
Apparently you must flag your chrome as contentaccessible, and take into consideration the security.
Just posting in case someone faced the same problem.
Succeeded in posting message from within B to A using events as described here.
But it is not answer, because window.parent.postMessage() still doesn't work as intended.

Categories

Resources