Accessing XUL Elements using Firefox Add-on SDK - javascript

I'm trying to manipulate the XUL elements in the Firefox add-on page using the Add-on SDK. I wouldn't mind using lower-level modules. I used DOM inspector to see the structure for the add-on page. It looks like this for the add-on page:
#document
--page (id='addons-page', windowtype='Addons:Manager', etc.)
----...
----hbox
----hbox
----etc.
So I tried this bit of code in exports.main:
let delegate = {
onTrack: function(window) {
console.log('window is being tracked: ' + window); // outputs [object ChromeWindow
let doc = window.document;
var addOnPage = doc.getElementById('addons-page');
console.log(window.document.page); // outputs undefined
console.log(addOnPage); // outputs null
var xulElements = window.document.getElementsByClassName('addon-control');
console.log('our elements: ' + xulElements); // outputs [object HTMLCollection]
console.log('our elements length: ' + xulElements.length); // outputs length of 0
}
};
var tracker = new winUtils.WindowTracker(delegate);
The first problem is that the window tracker only open when Firefox is first started. How can I get it to listen and wait for the add-on page to be opened?
The second problem (probably related to the first) is that getting the elements doesn't seem to be working (xulElements.length is 0).
Any ideas?

Two issues here:
Add-on Manager doesn't usually open as a separate window so using WindowTracker is pointless. It is a page loaded into the browser.
You access the window before it has a chance to load to it isn't surprising that you don't see any elements.
Given that page-mod module doesn't seem to work for this page, listening to the chrome-document-global-created notification is probably the best solution. This code works for me:
var observers = require("observer-service");
observers.add("chrome-document-global-created", function(wnd)
{
if (wnd.location.href == "about:addons")
{
// Wait for the window to load before accessing it
wnd.addEventListener("load", function()
{
console.log(wnd.document.getElementsByClassName('addon-control').length);
}, false);
}
});

Related

Accessing the console log commands via chrome extension

I'm looking to override the existing console commands via my Chrome extension - the reason for this is I wish to record the console logs for a specific site.
Unfortunately I cannot seem to update the DOM, this is what i've tried so far:
// Run functions on page change
chrome.tabs.onUpdated.addListener( function (tabId, changeInfo, tab) {
var s = document.createElement('script');
// TODO: add "script.js" to web_accessible_resources in manifest.json
s.src = chrome.runtime.getURL('core/js/app/console.js');
s.onload = function() {
this.remove();
};
(document.head || document.documentElement).appendChild(s);
});
console.js
// Replace functionality of console log
console.defaultLog = console.log.bind(console);
console.logs = [];
console.log = function(){
console.defaultLog.apply(console, arguments);
console.logs.push(Array.from(arguments));
};
// Replace functionality of console error
console.defaultError = console.error.bind(console);
console.errors = [];
console.error = function(){
console.defaultError.apply(console, arguments);
console.errors.push(Array.from(arguments));
};
// Replace functionality of console warn
console.defaultWarn = console.warn.bind(console);
console.warns = [];
console.warn = function(){
console.defaultWarn.apply(console, arguments);
console.warns.push(Array.from(arguments));
};
// Replace functionality of console debug
console.defaultDebug = console.debug.bind(console);
console.debugs = [];
console.debug = function(){
console.defaultDebug.apply(console, arguments);
console.debugs.push(Array.from(arguments));
};
The script runs successfully with an alert().
The goal for me is to access console.logs - but its undefined which means I haven't gotten access to the DOM, despite injecting a script.
If not possible, even a third party integration would be helpful i.e. Java or C?
Any thoughts would be greatly appreciated :)
I found this post and I think Tampermonkey injects a script with the immediate function that you add in the Tampermonkey Chrome extension page, I found something similar in extensions like Wappalyzer, and looks good and safe, you could use WebRequest to inject to your website the new "polyfill" before the page is fully loaded as the post says.
Here the example of Wappalyzer that I mentioned before, this is the JS load in StackOverflow with Wappalyzer using the code injection, I didn't test it with Tampermonkey yet
EDIT
Checking Wappalyzer, how to inject the code is the easy part, you can use (Wappalyzer github example):
const script = document.createElement('script')
script.setAttribute('src', chrome.extension.getURL('js/inject.js'))
This probably will not fix your problem, this code is executed after all the content was loaded in the DOM. But, you can find how to fix that problem in this post
I'll suggest to use onCommitted event (doc1/doc2)
Using the mozilla.org example you will have something like
const filter = {
url: //website to track logs
[
{hostContains: "example.com"},
{hostPrefix: "developer"}
]
}
function logOnCommitted(details) {
//Inject Script on webpage
}
browser.webNavigation.onCommitted.addListener(logOnCommitted, filter);
It might be worth trying to redefine the entire console object:
const saved = window.console
window.console = {...saved, log: function(...args){ saved.log("Hello", ...args) }}
But it's probably impossible, because content scripts live in an isolated world:
Isolated worlds do not allow for content scripts, the extension, and the web page to access any variables or functions created by the others. This also gives content scripts the ability to enable functionality that should not be accessible to the web page.
Although in Tampermonkey this script works.
I believe Tampermonkey handles this by knowing the subtleties and tracking changes in the extensions host's protection mechanism.
BTW, for small tasks, there is a decent alternative to chrome extensions in the form of code snippets.

Mozilla extension: How to get a window object in Background script

I want to use different XPCOM interfaces in Firefox Extension, here is an example taken from their API:
var domWindowUtils = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindowUtils);
But I can't use it in my background script because object Window is undefined:
window.QueryInterface is not a function
Please guide me in right direction.
P.S. What is actually available in a global scope for background scripts? I can't seem to find a single clue in documentation.
The issue is that Firefox extensions do not run in the context of any particular window. As such, they often do not have the window object defined, or it is defined as something which you are not expecting if you are not familiar with writing extension code. This is particularly true if you are approaching this from the point of view of writing JavaScript for use within an HTML page. Extensions operate in a significantly larger context which includes the entire browser and all windows and tabs. Thus, there is no automatically appropriate window to use as the window object. In the context of an extension, each HTML page is just a part of the whole.
You can obtain each primary browser window through the use of nsIWindowMediator. The following function, from MDN, will run the function you pass to it once for each open window:
Components.utils.import("resource://gre/modules/Services.jsm");
function forEachOpenWindow(todo) // Apply a function to all open browser windows
{
var windows = Services.wm.getEnumerator("navigator:browser");
while (windows.hasMoreElements())
todo(windows.getNext().QueryInterface(Components.interfaces.nsIDOMWindow));
}
You will often want to find the window for the most recent browser/tab which was accessed by the user. The following code will define and set the window variable to the most recently used browser/tab. It will work either in the Add-on SDK, or in overlay/bootstrap extensions depending on which portion you un-comment.
For more information about using windows in a Firefox extension, you should see Working with windows in chrome code and perhaps Tabbed browser.
if (window === null || typeof window !== "object") {
//If you do not already have a window reference, you need to obtain one:
// Add a "/" to un-comment the code appropriate for your add-on type.
/* Add-on SDK:
var window = require('sdk/window/utils').getMostRecentBrowserWindow();
//*/
/* Overlay and bootstrap (from almost any context/scope):
var window=Components.classes["#mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator)
.getMostRecentWindow("navigator:browser");
//*/
}
Alternately using Services.jsm to access nsIWindowMediator:
/* Overlay and bootstrap:
Components.utils.import("resource://gre/modules/Services.jsm");
//*/
if (window === null || typeof window !== "object") {
//If you do not already have a window reference, you need to obtain one:
// Add a "/" to un-comment the code appropriate for your add-on type.
/* Add-on SDK:
var window = require('sdk/window/utils').getMostRecentBrowserWindow();
//*/
/* Overlay and bootstrap (from almost any context/scope):
var window = Services.wm.getMostRecentWindow("navigator:browser");
//*/
}
Here are some additional variables which might be useful to have available, depending on what you are doing:
if (typeof document === "undefined") {
//If there is no document defined, get it
var document = window.content.document;
}
if (typeof gBrowser === "undefined") {
//If there is no gBrowser defined, get it
var gBrowser = window.gBrowser;
}
var tab = gBrowser.selectedTab;
var browserForTab = gBrowser.getBrowserForTab( tab );
var notificationBox = gBrowser.getNotificationBox( browserForTab );
var ownerDocument = gBrowser.ownerDocument;
NOTE: The content of this answer was primarily taken from my answers here and here, but is based on code from MDN. For me, the code to traverse all open browser windows has its origin in the original article the MDN article How to convert an overlay extension to restartless is based on.
A quick and dirty way for testing, and sometimes real application is Services.wm.getMostRecentWindow('navigator:browser'), or you can use null or any other windowType.

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

Controlling a Firefox Extension via Javascript

Is it possible, using javascript, to control an overlay firefox extension? I've extracted the contents of the extension and have identified what functions/methods I need to run, but they are not accessible within the scope of the console.
Thanks in advance for any ideas.
Yes it possible to interact with other add-ons, given the right circumstances.
My test case here will be com.googlecode.sqlitemanager.openInOwnWindow(), which is part of the SqliteManager addon.
In newer builds (I'm using Nightly), there is the Browser Toolbox. With it is is as simple as opening a toolbox and executing com.googlecode.sqlitemanager.openInOwnWindow() in the Console.
You may instead use the Browser Console (or any chrome enabled WebDev Console for that matter, e.g. the Console of "about:newtab"). But you need some boilerplate code to first find the browser window. So here is the code you can execute there: var bwin = Services.wm.getMostRecentWindow("navigator:browser"); bwin.com.googlecode.sqlitemanager.openInOwnWindow()
Again, enable chrome debugging. Then open a Scratchpad and switch to Chrome in the Environment menu. Now executing com.googlecode.sqlitemanager.openInOwnWindow() in our Scratchpad will work.
You may of course write your own overlay add-on.
As a last resort, patch the add-on itself.
Bootstrapped/SDK add-ons: you can load XPIProvider.jsm (which changed location recently) and get to the bootstrapped scope (run environment of bootstrap.js) via XPIProvider.bootstrapScopes[addonID], and take it from there (use whatever is in the bootstrap scope, e.g. the SDK loader).
Now about the right circumstances: If and how you can interact with a certain add-on depends on the add-on. Add-ons may have global symbols in their overlay and hence browser window, such as in the example I used. Or may use (to some extend) JS code modules. Or have their own custom loader stuff (e.g. AdBlock Plus has their own require()-like stuff and SDK add-ons have their own loader, which isn't exactly easy to infiltate)...
Since your question is rather unspecific, I'll leave it at this.
Edit by question asker: This is correct, however I figured I'd add an example of the code I ended up using in the end, which was in fact taken directly from mozilla's developer network website:
In my chrome js:
var myExtension = {
myListener: function(evt) {
IprPreferences.setFreshIpStatus(true); // replace with whatever you want to 'fire' in the extension
}
}
document.addEventListener("MyExtensionEvent", function(e) { myExtension.myListener(e); }, false, true);
// The last value is a Mozilla-specific value to indicate untrusted content is allowed to trigger the event.
In the web content:
var element = document.createElement("MyExtensionDataElement");
element.setAttribute("attribute1", "foobar");
element.setAttribute("attribute2", "hello world");
document.documentElement.appendChild(element);
var evt = document.createEvent("Events");
evt.initEvent("MyExtensionEvent", true, false);
element.dispatchEvent(evt);
Update for Firefox 47 and up
Things changed drastically in Firefox 47. This is the new way to access it.
var XPIScope = Cu.import('resource://gre/modules/addons/XPIProvider.jsm');
var addonid = 'Profilist#jetpack';
var scope = XPIScope.XPIProvider.activeAddons.get(addonid).bootstrapScope
Old way for < Firefox 47
Update for methods of today
Typically you will do so like this:
If i wanted to get into AdBlocks scope, I check AdBlock id, it is {d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d} so I would go:
var XPIScope = Cu.import('resource://gre/modules/addons/XPIProvider.jsm');
var adblockScope = XPIScope.XPIProvider.bootstrapScopes['{d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d}'];
You can now tap into anything there.
Another example, I have an addon installed with id NativeShot#jetpack
I would tap into it like this:
var XPIScope = Cu.import('resource://gre/modules/addons/XPIProvider.jsm');
var nativeshotScope = XPIScope.XPIProvider.bootstrapScopes['NativeShot#jetpack'];
if you do console.log(nativeshotScope) you will see all that is inside.

Simplest way to launch Firefox, drive 3rd party site using privileged nsI* APIs

What's the simplest way to launch Firefox, load a 3rd party website (which I'm authorised to "automate"), and run some "privileged" APIs against that site? (e.g: nsIProgressListener, nsIWindowMediator, etc).
I've tried a two approaches:
Create a tabbed browser using XULrunner, "plumbing" all the appropriate APIs required for the 3rd party site to open new windows, follow 302 redirects, etc. Doing it this way, it's an aweful lot of code, and requires (afaict) that the user installs the app, or runs Firefox with -app. It's also extremely fragile. :-/
Launch Firefox passing URL of the 3rd party site, with MozRepl already listening. Then shortly after startup, telnet from the "launch" script to MozRepl, use mozIJSSubScriptLoader::loadSubScript to load my code, then execute my code from MozRepl in the context of the 3rd party site -- this is the way I'm currently doing it.
With the first approach, I'm getting lots of security issues (obviously) to work around, and it seems like I'm writing 10x more browser "plumbing" code then automation code.
With the second approach, I'm seeing lots of "timing issues", i.e:
the 3rd party site is somehow prevented from loading by MozRepl (or the execution of the privileged code I supply)???, or
the 3rd party site loads, but code executed by MozRepl doesn't see it load, or
the 3rd party site loads, and MozRepl isn't ready to take requests (despite other JavaScript running in the page, and port 4242 being bound by the Firefox process),
etc.
I thought about maybe doing something like this:
Modify the MozRepl source in some way to load privileged JavaScript from a predictable place in the filesystem at start-up (or interact with Firefox command-line arguments) and execute it in the context of the 3rd party website.
... or even write another similar add-on which is more dedicated to the task.
Any simpler ideas?
Update:
After a lot of trial-and-error, answered my own question (below).
I found the easiest way was to write a purpose-built Firefox extension!
Step 1. I didn't want to do a bunch of unnecessary XUL/addon related stuff that wasn't necessary; A "Bootstrapped" (or re-startless) extension needs only an install.rdf file to identify the addon, and a bootstrap.js file to implement the bootstrap interface.
Bootstrapped Extension: https://developer.mozilla.org/en-US/docs/Extensions/Bootstrapped_extensions
Good example: http://blog.fpmurphy.com/2011/02/firefox-4-restartless-add-ons.html
The bootstrap interface can be implemented very simply:
const path = '/PATH/TO/EXTERNAL/CODE.js';
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
var loaderSvc = Cc["#mozilla.org/moz/jssubscript-loader;1"];
.getService(Ci.mozIJSSubScriptLoader);
function install() {}
function uninstall() {}
function shutdown(data, reason) {}
function startup(data, reason) { loaderSvc.loadSubScript("file://"+path); }
You compile the extension by putting install.rdf and bootstrap.js into the top-level of a new zip file, and rename the zip file extension to .xpi.
Step 2. To have a repeatable environment for production & testing, I found the easiest way was to launch Firefox with a profile dedicated to the automation task:
Launch the Firefox profile manager: firefox -ProfileManager
Create a new profile, specifying the location for easy re-use (I called mine testing-profile) and then exit the profile manager.
Remove the new profile from profiles.ini in your user's mozilla config (so that it won't interfere with normal browsing).
Launch Firefox with that profile: firefox -profile /path/to/testing-profile
Install the extension from the file-system (rather than addons.mozilla.org).
Do anything else needed to prepare the profile. (e.g: I needed to add 3rd party certificates and allow pop-up windows for the relevant domain.)
Leave a single about:blank tab open, then exit Firefox.
Snapshot the profile: tar cvf testing-profile-snapshot.tar /path/to/testing-profile
From that point onward, every time I run the automation, I unpack testing-profile-snapshot.tar over the existing testing-profile folder and run firefox -profile /path/to/testing-profile about:blank to use the "pristine" profile.
Step 3. So now when I launch Firefox with the testing-profile it will "include" the external code at /PATH/TO/EXTERNAL/CODE.js on each start-up.
NOTE: I found that I had to move the /PATH/TO/EXTERNAL/ folder elsewhere during step 2 above, as the external JavaScript code would be cached (!!! - undesirable during development) inside the profile (i.e: changes to the external code wouldn't be seen on next launch).
The external code is privileged and can use any of the Mozilla platform APIs. There is however an issue of timing. The moment-in-time at which the external code is included (and hence executed) is one at which no Chrome window objects (and so no DOMWindow objects) yet exist.
So then we need to wait around until there's a useful DOMWindow object:
// useful services.
Cu.import("resource://gre/modules/Services.jsm");
var loader = Cc["#mozilla.org/moz/jssubscript-loader;1"]
.getService(Ci.mozIJSSubScriptLoader);
var wmSvc = Cc["#mozilla.org/appshell/window-mediator;1"]
.getService(Ci.nsIWindowMediator);
var logSvc = Cc["#mozilla.org/consoleservice;1"]
.getService(Ci.nsIConsoleService);
// "user" code entry point.
function user_code() {
// your code here!
// window, gBrowser, etc work as per MozRepl!
}
// get the gBrowser, first (about:blank) domWindow,
// and set up common globals.
var done_startup = 0;
var windowListener;
function do_startup(win) {
if (done_startup) return;
done_startup = 1;
wm.removeListener(windowListener);
var browserEnum = wm.getEnumerator("navigator:browser");
var browserWin = browserEnum.getNext();
var tabbrowser = browserWin.gBrowser;
var currentBrowser = tabbrowser.getBrowserAtIndex(0);
var domWindow = currentBrowser.contentWindow;
window = domWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
gBrowser = window.gBrowser;
setTimeout = window.setTimeout;
setInterval = window.setInterval;
alert = function(message) {
Services.prompt.alert(null, "alert", message);
};
console = {
log: function(message) {
logSvc.logStringMessage(message);
}
};
// the first domWindow will finish loading a little later than gBrowser...
gBrowser.addEventListener('load', function() {
gBrowser.removeEventListener('load', arguments.callee, true);
user_code();
}, true);
}
// window listener implementation
windowListener = {
onWindowTitleChange: function(aWindow, aTitle) {},
onCloseWindow: function(aWindow) {},
onOpenWindow: function(aWindow) {
var win = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
win.addEventListener("load", function(aEvent) {
win.removeEventListener("load", arguments.callee, false);
if (aEvent.originalTarget.nodeName != "#document") return;
do_startup();
}
};
// CODE ENTRY POINT!
wm.addListener(windowListener);
Step 4. All of that code executes in the "global" scope. If you later need to load other JavaScript files (e.g: jQuery), call loadSubscript explicitly within the null (global!) scope
function some_user_code() {
loader.loadSubScript.call(null,"file:///PATH/TO/SOME/CODE.js");
loader.loadSubScript.call(null,"http://HOST/PATH/TO/jquery.js");
$ = jQuery = window.$;
}
Now we can use jQuery on any DOMWindow by passing <DOMWindow>.document as the second parameter to the selector call!

Categories

Resources