wiki:I2P_Browser_develop_n_hacks

Version 21 (modified by Meeh, 5 months ago) (diff)

I2P Browser Development And Hacking

Howto get started with the build system

Howto get started with firefox

The firefox codebase is no doubt massive, for an overview, take a look at Directory Overview.

It's worth having a look at Tor's trac wiki as well, as they also have browser hacking pages - but with more or less no duplicity of info between here and there.

Is it possible to edit javascript in firefox without recompile?

YES!, in most cases. What you have to do is to find your installation's omni.ja, and unzip it. Do your changes, afterwards from inside the directory you extracted omni.ja to, do:

zip -qr9XD ../omni.ja *

In the parent directoy it's a omni.ja which you can now replace with the one you found in your installation. If what you do is experimental and you don't know if it will work or not, maybe backup your installation's omni.ja first. :)

How to keep the code clean?

Many things can be done, one thing I would like hackers of the browsers to be aware of fast is the possibility to use Javascript Modules (ES6 stuff), please check out mozilla's wiki on this subject.

Plugins, Extensions, Add-ons, wtf?

Yes this can be quite confusing to begin with. In reality it's only the webextension api inherited from Google Chrome that browser third-party developers can use. However since we're so awesome we ship our own, we bypass this limit of possibilities. The reason for this is that Firefox still heavily uses the old API internally in Firefox.

We have three main categories of "Firefox API" before they introduced WebExtensions on top of it all:

  1. Classic extensions (not restartless): these will typically overlay the browser window and run code from this overlay. Since there is one overlay per window, there will be as many code instances as browser windows. However, classic extensions can also register an XPCOM component (via chrome.manifest as of Gecko 2.0). This component will be loaded on first use and stay around for the entire browsing session. You probably want your component to load when the browser starts, for this you should register it in the profile-after-change category and implement nsIObserver.
  1. Restartless extensions, also called bootstrapped extensions: these cannot register overlays which makes working with the browser UI somewhat more complicated. Instead they have a bootstrap.js script that will load when the extension is activated, this context will stay around in background until the browser is shut down or the extension is disabled. You can have XPCOM components in restartless extensions as well but you will have to register them manually (via nsIComponentRegistrar.registerFactory() and nsICategoryManager.addCategoryEntry()). You will also have to take care of unregistering the component if the extension is shut down. This is unnecessary if you merely need to add an observer, nsIObserverService will take any object implementing nsIObserver, not only one that has been registered as an XPCOM component. The big downside is: most MDN examples are about classic extensions and don't explain how you would do things in a restartless extension.
  1. Extensions based on the Add-on SDK: these are based on a framework that produces restartless extensions. The Add-on SDK has its own API which is very different from what you usually do in Firefox extension - but it is simple, and it mostly takes care of shutting down the extension so that you don't have to do it manually. Extensions here consist of a number of modules, with main.js loading automatically and being able to load additional modules as necessary. Once loaded, each module stays around for as long as the extension is active. They run sandboxed but you can still leave the sandbox and access XPCOM directly. However, you would probably use the internal observer-service module instead.

We mainly uses the classic one, but for launching the I2P router in the future we maybe need to use restartless internally - or just make firefox (without a extension) run it. People who wish to develop on our extensions are somewhat free to use the Add-on SDK API if they are afraid the ugly XPCOM monster will come and bite them, but currently we do not have any plans of using the API ourself as Meeh is now somewhat already known with XPCOM.

Privileged Javascript, wtf?

The fact is that most of Firefox's source is probably Javascript, without me fact checking now. Yes, Firefox contains quite a lot of C++ and some Rust etc.

However the glue to make all those different components together to a usable browser is privileged Javascript. It's about the same as regular, but totally different environment and APIs.

You can launch firefox/i2pbrowser with the —jsconsole argument to spawn the browser console at launch. If not, ctrl+shift+J and cmd+shift+J for OSX users.

let console = (Cu.import("resource://gre/modules/Console.jsm", {})).console;
console.log("Hello from Firefox code");

Often all you need would be imported/required in this statement:

const {classes: Cc, interfaces: Ci, manager: Cm, results: Cr, utils: Cu, Constructor: CC} = Components;

Here are some other nice snippets:

Components.utils.import("resource://gre/modules/Log.jsm");
function getStackDump() {
  var lines = [];
  for (var frame = Components.stack; frame; frame = frame.caller) {
    lines.push(frame.filename + " (" + frame.lineNumber + ")");
  }
  return lines.join("\n");
}

This one prints the extensions that's registered in firefox:

Components.utils.import("resource://gre/modules/AddonManager.jsm", null).AddonManager.getAllAddons(addons => {addons.filter(addon => addon.type == "extension").forEach(addon => console.log(addon.name, addon.getResourceURI().spec))});

This code produces a standard notification, on OSX at least, the native notification system. Not tested myself on Linux and Windows but I guess they, at least Windows, provide something similar which Firefox uses under the hood.

var myServices = {};
Cu.import('resource://gre/modules/XPCOMUtils.jsm');

//set it up
XPCOMUtils.defineLazyGetter(myServices, 'as', function () { return Cc['@mozilla.org/alerts-service;1'].getService(Ci.nsIAlertsService) });

//when you need to use it
myServices.as.showAlertNotification('chrome://branding/content/icon64.png', 'this was lazyloaded', 'this is a notification from myServices.as', null, null);

This code can load and execute files from file://, resource:// and chrome://.

let context = {};
Services.scriptloader.loadSubScript("chrome://my-package/content/foo-script.js",
                                    context, "UTF-8" /* The script's encoding */);

How can I query the I2P health service?

Just a note, the API was written quite quickly and is not pretty at all, it should change in near future so remember to check if the example bellow is up to date :) (i2pbutton with the chrome/content/i2pbutton.js should contain our live example)

let checkSvc = Cc["@geti2p.net/i2pbutton-i2pCheckService;1"].getService(Ci.nsISupports).wrappedJSObject;
  let req = checkSvc.createCheckConsoleRequest(true);
  req.onreadystatechange = function(event) {
    if (req.readyState === 4) {
      // Done
      let result = checkSvc.parseCheckConsoleResponse(req)
      i2pbutton_log(3, "I2P Console check done. Result: " + result)
      callback(result)
    }
  }
  req.send(null)

Common methods to solve problems

In firefox they use a lot of the concept "observer". We use it ourself to enable the I2P Health check, and in the future to launch I2P itself.

Here is a example on a object registering for "firefox global" events. "quit-application" is self-explained, but for "profile-after-change" it is the first event we as a plugin and not base firefox can hook ourself into.

If you want more information on why "profile-after-change" is the correct one to use, and how, take a look at Receiving startup notifications at the Mozilla wiki.

function myExt() {}
myExt.prototype = {
  observe: function(aSubject, aTopic, aData) {
    switch (aTopic) {
      case "quit-application":
        stopServer();
        obs.removeObserver(this, "quit-application");
        break;
      case "profile-after-change":
        startServer();
        obs.addObserver(this, "quit-application", false);
        break;
    }
  }
};

But to fully register you also need to edit the chrome.manifest or a equivalent file if it's in the firefox codebase. Bellow are the line we would need in chrome.manifest when it's about plugins;

category profile-after-change MyComponent @foobar/mycomponent;1

Remember consepts like the observer is kind of like interfaces, if you don't implement all functions the object is said to have, your code would throw a exception and not work as expected.

To read more about all the different notifications a observer can receive please check out Observer Notifications at Mozilla wiki.

Another common problem I want to document and bring up is preferences. Since we use them heavily, most likely you would need to deal with them as you're hacking on the browser. Here is a quick and clean read example:

Cu.import("resource://gre/modules/Services.jsm", this);
let accepted = Services.prefs.getBoolPref("toolkit.asyncshutdown.testing", false);

Recommended development preferences

Note: Not all preferences are defined by default, and are therefore not listed by default. You will have to create new (boolean) entries for them.

devtools.chrome.enabled = true
  - This is quite awesome, enable this and you get a REPL for the high privileged Javascript code that makes firefox. 

browser.dom.window.dump.enabled = true
  - Enables the use of the dump() statement to print to the standard console. See window.dump for more info.
     You can use nsIConsoleService instead of dump() from a privileged script.

devtools.chrome.enabled = true
  - This enables to run JavaScript code snippets in the chrome context of the Scratchpad from the Tools menu.
     Don't forget to switch from content to browser as context.

javascript.options.showInConsole = true

dom.report_all_js_exceptions = true

devtools.errorconsole.enabled = true
  - Logs errors in chrome files to the Error Console.

nglayout.debug.disable_xul_fastload = true

nglayout.debug.disable_xul_cache = true
  - Disables the XUL cache so that changes to windows and dialogs do not require a restart. This assumes you're using directories rather than JARs.
     Changes to XUL overlays will still require reloading of the document overlaid.

extensions.logging.enabled = true
  - This will send more detailed information about installation and update problems to the Error Console. (Note that the extension manager 
     automatically restarts the application at startup sometimes, which may mean you won't have time to see the messages logged before 
     the automatic restart happens. To see them, prevent the automatic restart by setting the environment NO_EM_RESTART to 1 before 
     starting the application.)

dom.report_all_js_exceptions = true

Howto get started with i2pbutton

Much of our features lives here. And a great thing to notice with the i2pbutton is that you do not need to recompile firefox and so on to test your development build.

Clone the i2pbutton source, do your changes and then from the root of the repository do something like:

./makexpi.sh
cp pkg/i2pbutton-0.2.xpi  ~/I2PBrowser-Data/Browser/914o5i1s.default/extensions/i2pbutton@geti2p.net.xpi

But of course remember to correct paths and the version number might also have changed. Use your brain on this one, not just copy&paste :)