/*******************************************************************************

    uMatrix - a browser extension to block requests.
    Copyright (C) 2014-2016 The uMatrix/uBlock Origin authors

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see {http://www.gnu.org/licenses/}.

    Home: https://github.com/gorhill/uMatrix
*/

/* jshint bitwise: false, esnext: true */
/* global self, Components, punycode */

// For background page

/******************************************************************************/

(function() {

'use strict';

/******************************************************************************/

// Useful links
//
// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface
// https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Services.jsm

/******************************************************************************/

const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
const {Services} = Cu.import('resource://gre/modules/Services.jsm', null);

/******************************************************************************/

var vAPI = self.vAPI = self.vAPI || {};
vAPI.firefox = true;
vAPI.modernFirefox = Services.appinfo.ID === '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}' &&
                     Services.vc.compare(Services.appinfo.platformVersion, '44') > 0;

/******************************************************************************/

var deferUntil = function(testFn, mainFn, details) {
    if ( typeof details !== 'object' ) {
        details = {};
    }

    var now = 0;
    var next = details.next || 200;
    var until = details.until || 2000;

    var check = function() {
        if ( testFn() === true || now >= until ) {
            mainFn();
            return;
        }
        now += next;
        vAPI.setTimeout(check, next);
    };

    if ( 'sync' in details && details.sync === true ) {
        check();
    } else {
        vAPI.setTimeout(check, 1);
    }
};

/******************************************************************************/

vAPI.app = {
    name: 'uMatrix',
    version: location.hash.slice(1)
};

/******************************************************************************/

vAPI.app.start = function() {
};

/******************************************************************************/

vAPI.app.stop = function() {
};

/******************************************************************************/

vAPI.app.restart = function() {
    // Listening in bootstrap.js
    Cc['@mozilla.org/childprocessmessagemanager;1']
        .getService(Ci.nsIMessageSender)
        .sendAsyncMessage(location.host + '-restart');
};

/******************************************************************************/

// List of things that needs to be destroyed when disabling the extension
// Only functions should be added to it

var cleanupTasks = [];

// This must be updated manually, every time a new task is added/removed

// Fixed by github.com/AlexVallat:
//   https://github.com/AlexVallat/uBlock/commit/7b781248f00cbe3d61b1cc367c440db80fa06049
//   7 instances of cleanupTasks.push, but one is unique to fennec, and one to desktop.
var expectedNumberOfCleanups = 7;

window.addEventListener('unload', function() {
    if ( typeof vAPI.app.onShutdown === 'function' ) {
        vAPI.app.onShutdown();
    }

    // IMPORTANT: cleanup tasks must be executed using LIFO order.
    var i = cleanupTasks.length;
    while ( i-- ) {
        cleanupTasks[i]();
    }

    if ( cleanupTasks.length < expectedNumberOfCleanups ) {
        console.error(
            'uMatrix> Cleanup tasks performed: %s (out of %s)',
            cleanupTasks.length,
            expectedNumberOfCleanups
        );
    }

    // frameModule needs to be cleared too
    var frameModuleURL = vAPI.getURL('frameModule.js');
    var frameModule = {};
    Cu.import(frameModuleURL, frameModule);
    frameModule.contentObserver.unregister();
    Cu.unload(frameModuleURL);
});

/******************************************************************************/

// For now, only booleans.

vAPI.browserSettings = {
    originalValues: {},

    rememberOriginalValue: function(path, setting) {
        var key = path + '.' + setting;
        if ( this.originalValues.hasOwnProperty(key) ) {
            return;
        }
        var hasUserValue;
        var branch = Services.prefs.getBranch(path + '.');
        try {
            hasUserValue = branch.prefHasUserValue(setting);
        } catch (ex) {
        }
        if ( hasUserValue !== undefined ) {
            this.originalValues[key] = hasUserValue ? this.getValue(path, setting) : undefined;
        }
    },

    clear: function(path, setting) {
        var key = path + '.' + setting;

        // Value was not overriden -- nothing to restore
        if ( this.originalValues.hasOwnProperty(key) === false ) {
            return;
        }

        var value = this.originalValues[key];
        // https://github.com/gorhill/uBlock/issues/292#issuecomment-109621979
        // Forget the value immediately, it may change outside of
        // uBlock control.
        delete this.originalValues[key];

        // Original value was a default one
        if ( value === undefined ) {
            try {
                Services.prefs.getBranch(path + '.').clearUserPref(setting);
            } catch (ex) {
            }
            return;
        }

        // Reset to original value
        this.setValue(path, setting, value);
    },

    getValue: function(path, setting) {
        var branch = Services.prefs.getBranch(path + '.');
        var getMethod;

        // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPrefBranch#getPrefType%28%29
        switch ( branch.getPrefType(setting) ) {
        case  64: // PREF_INT
            getMethod = 'getIntPref';
            break;
        case 128: // PREF_BOOL
            getMethod = 'getBoolPref';
            break;
        default:  // not supported
            return;
        }

        try {
            return branch[getMethod](setting);
        } catch (ex) {
        }
    },

    setValue: function(path, setting, value) {
        var setMethod;
        switch ( typeof value ) {
        case  'number':
            setMethod = 'setIntPref';
            break;
        case 'boolean':
            setMethod = 'setBoolPref';
            break;
        default:  // not supported
            return;
        }

        try {
            Services.prefs.getBranch(path + '.')[setMethod](setting, value);
        } catch (ex) {
        }
    },

    setSetting: function(setting, value) {
        var prefName, prefVal;
        switch ( setting ) {
        case 'prefetching':
            this.rememberOriginalValue('network', 'prefetch-next');
            // http://betanews.com/2015/08/15/firefox-stealthily-loads-webpages-when-you-hover-over-links-heres-how-to-stop-it/
            // https://bugzilla.mozilla.org/show_bug.cgi?id=814169
            // Sigh.
            this.rememberOriginalValue('network.http', 'speculative-parallel-limit');
            // https://github.com/gorhill/uBlock/issues/292
            // "true" means "do not disable", i.e. leave entry alone
            if ( value ) {
                this.clear('network', 'prefetch-next');
                this.clear('network.http', 'speculative-parallel-limit');
            } else {
                this.setValue('network', 'prefetch-next', false);
                this.setValue('network.http', 'speculative-parallel-limit', 0);
            }
            break;

        case 'hyperlinkAuditing':
            this.rememberOriginalValue('browser', 'send_pings');
            this.rememberOriginalValue('beacon', 'enabled');
            // https://github.com/gorhill/uBlock/issues/292
            // "true" means "do not disable", i.e. leave entry alone
            if ( value ) {
                this.clear('browser', 'send_pings');
                this.clear('beacon', 'enabled');
            } else {
                this.setValue('browser', 'send_pings', false);
                this.setValue('beacon', 'enabled', false);
            }
            break;

        // https://github.com/gorhill/uBlock/issues/894
        // Do not disable completely WebRTC if it can be avoided. FF42+
        // has a `media.peerconnection.ice.default_address_only` pref which
        // purpose is to prevent local IP address leakage.
        case 'webrtcIPAddress':
            if ( this.getValue('media.peerconnection', 'ice.default_address_only') !== undefined ) {
                prefName = 'ice.default_address_only';
                prefVal = true;
            } else {
                prefName = 'enabled';
                prefVal = false;
            }

            this.rememberOriginalValue('media.peerconnection', prefName);
            if ( value ) {
                this.clear('media.peerconnection', prefName);
            } else {
                this.setValue('media.peerconnection', prefName, prefVal);
            }
            break;

        default:
            break;
        }
    },

    set: function(details) {
        for ( var setting in details ) {
            if ( details.hasOwnProperty(setting) === false ) {
                continue;
            }
            this.setSetting(setting, !!details[setting]);
        }
    },

    restoreAll: function() {
        var pos;
        for ( var key in this.originalValues ) {
            if ( this.originalValues.hasOwnProperty(key) === false ) {
                continue;
            }
            pos = key.lastIndexOf('.');
            this.clear(key.slice(0, pos), key.slice(pos + 1));
        }
    }
};

cleanupTasks.push(vAPI.browserSettings.restoreAll.bind(vAPI.browserSettings));

/******************************************************************************/

// API matches that of chrome.storage.local:
//   https://developer.chrome.com/extensions/storage

vAPI.storage = (function() {
    var db = null;
    var vacuumTimer = null;

    var close = function() {
        if ( vacuumTimer !== null ) {
            clearTimeout(vacuumTimer);
            vacuumTimer = null;
        }
        if ( db === null ) {
            return;
        }
        db.asyncClose();
        db = null;
    };

    var open = function() {
        if ( db !== null ) {
            return db;
        }

        // Create path
        var path = Services.dirsvc.get('ProfD', Ci.nsIFile);
        path.append('extension-data');
        if ( !path.exists() ) {
            path.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt('0774', 8));
        }
        if ( !path.isDirectory() ) {
            throw Error('Should be a directory...');
        }
        path.append(location.host + '.sqlite');

        // Open database
        try {
            db = Services.storage.openDatabase(path);
            if ( db.connectionReady === false ) {
                db.asyncClose();
                db = null;
            }
        } catch (ex) {
        }

        if ( db === null ) {
            return null;
        }

        // Database was opened, register cleanup task
        cleanupTasks.push(close);

        // Setup database
        db.createAsyncStatement('CREATE TABLE IF NOT EXISTS "settings" ("name" TEXT PRIMARY KEY NOT NULL, "value" TEXT);')
          .executeAsync();

        if ( vacuum !== null ) {
            vacuumTimer = vAPI.setTimeout(vacuum, 60000);
        }

        return db;
    };

    // https://developer.mozilla.org/en-US/docs/Storage/Performance#Vacuuming_and_zero-fill
    // Vacuum only once, and only while idle
    var vacuum = function() {
        vacuumTimer = null;
        if ( db === null ) {
            return;
        }
        var idleSvc = Cc['@mozilla.org/widget/idleservice;1']
                       .getService(Ci.nsIIdleService);
        if ( idleSvc.idleTime < 60000 ) {
            vacuumTimer = vAPI.setTimeout(vacuum, 60000);
            return;
        }
        db.createAsyncStatement('VACUUM').executeAsync();
        vacuum = null;
    };

    // Execute a query
    var runStatement = function(stmt, callback) {
        var result = {};

        stmt.executeAsync({
            handleResult: function(rows) {
                if ( !rows || typeof callback !== 'function' ) {
                    return;
                }

                var row;

                while ( (row = rows.getNextRow()) ) {
                    // we assume that there will be two columns, since we're
                    // using it only for preferences
                    result[row.getResultByIndex(0)] = row.getResultByIndex(1);
                }
            },
            handleCompletion: function(reason) {
                if ( typeof callback === 'function' && reason === 0 ) {
                    callback(result);
                }
            },
            handleError: function(error) {
                console.error('SQLite error ', error.result, error.message);
                // Caller expects an answer regardless of failure.
                if ( typeof callback === 'function' ) {
                    callback(null);
                }
            }
        });
    };

    var bindNames = function(stmt, names) {
        if ( Array.isArray(names) === false || names.length === 0 ) {
            return;
        }
        var params = stmt.newBindingParamsArray();
        var i = names.length, bp;
        while ( i-- ) {
            bp = params.newBindingParams();
            bp.bindByName('name', names[i]);
            params.addParams(bp);
        }
        stmt.bindParameters(params);
    };

    var clear = function(callback) {
        if ( open() === null ) {
            if ( typeof callback === 'function' ) {
                callback();
            }
            return;
        }
        runStatement(db.createAsyncStatement('DELETE FROM "settings";'), callback);
    };

    var getBytesInUse = function(keys, callback) {
        if ( typeof callback !== 'function' ) {
            return;
        }

        if ( open() === null ) {
            callback(0);
            return;
        }

        var stmt;
        if ( Array.isArray(keys) ) {
            stmt = db.createAsyncStatement('SELECT "size" AS "size", SUM(LENGTH("value")) FROM "settings" WHERE "name" = :name');
            bindNames(keys);
        } else {
            stmt = db.createAsyncStatement('SELECT "size" AS "size", SUM(LENGTH("value")) FROM "settings"');
        }

        runStatement(stmt, function(result) {
            callback(result.size);
        });
    };

    var read = function(details, callback) {
        if ( typeof callback !== 'function' ) {
            return;
        }

        var prepareResult = function(result) {
            var key;
            for ( key in result ) {
                if ( result.hasOwnProperty(key) === false ) {
                    continue;
                }
                result[key] = JSON.parse(result[key]);
            }
            if ( typeof details === 'object' && details !== null ) {
                for ( key in details ) {
                    if ( result.hasOwnProperty(key) === false ) {
                        result[key] = details[key];
                    }
                }
            }
            callback(result);
        };

        if ( open() === null ) {
            prepareResult({});
            return;
        }

        var names = [];
        if ( details !== null ) {
            if ( Array.isArray(details) ) {
                names = details;
            } else if ( typeof details === 'object' ) {
                names = Object.keys(details);
            } else {
                names = [details.toString()];
            }
        }

        var stmt;
        if ( names.length === 0 ) {
            stmt = db.createAsyncStatement('SELECT * FROM "settings"');
        } else {
            stmt = db.createAsyncStatement('SELECT * FROM "settings" WHERE "name" = :name');
            bindNames(stmt, names);
        }

        runStatement(stmt, prepareResult);
    };

    var remove = function(keys, callback) {
        if ( open() === null ) {
            if ( typeof callback === 'function' ) {
                callback();
            }
            return;
        }
        var stmt = db.createAsyncStatement('DELETE FROM "settings" WHERE "name" = :name');
        bindNames(stmt, typeof keys === 'string' ? [keys] : keys);
        runStatement(stmt, callback);
    };

    var write = function(details, callback) {
        if ( open() === null ) {
            if ( typeof callback === 'function' ) {
                callback();
            }
            return;
        }

        var stmt = db.createAsyncStatement('INSERT OR REPLACE INTO "settings" ("name", "value") VALUES(:name, :value)');
        var params = stmt.newBindingParamsArray(), bp;
        for ( var key in details ) {
            if ( details.hasOwnProperty(key) === false ) {
                continue;
            }
            bp = params.newBindingParams();
            bp.bindByName('name', key);
            bp.bindByName('value', JSON.stringify(details[key]));
            params.addParams(bp);
        }
        if ( params.length === 0 ) {
            return;
        }

        stmt.bindParameters(params);
        runStatement(stmt, callback);
    };

    // Export API
    var api = {
        QUOTA_BYTES: 100 * 1024 * 1024,
        clear: clear,
        get: read,
        getBytesInUse: getBytesInUse,
        remove: remove,
        set: write
    };
    return api;
})();

/******************************************************************************/

// This must be executed/setup early.

var winWatcher = (function() {
    var windowToIdMap = new Map();
    var windowIdGenerator = 1;
    var api = {
        onOpenWindow: null,
        onCloseWindow: null
    };

    // https://github.com/gorhill/uMatrix/issues/586
    // This is necessary hack because on SeaMonkey 2.40, for unknown reasons
    // private windows do not have the attribute `windowtype` set to
    // `navigator:browser`. As a fallback, the code here will also test whether
    // the id attribute is `main-window`.
    api.toBrowserWindow = function(win) {
        var docElement = win && win.document && win.document.documentElement;
        if ( !docElement ) {
            return null;
        }
        if ( vAPI.thunderbird ) {
            return docElement.getAttribute('windowtype') === 'mail:3pane' ? win : null;
        }
        return docElement.getAttribute('windowtype') === 'navigator:browser' ||
               docElement.getAttribute('id') === 'main-window' ?
               win : null;
    };

    api.getWindows = function() {
        return windowToIdMap.keys();
    };

    api.idFromWindow = function(win) {
        return windowToIdMap.get(win) || 0;
    };

    api.getCurrentWindow = function() {
        return this.toBrowserWindow(Services.wm.getMostRecentWindow(null));
    };

    var addWindow = function(win) {
        if ( !win || windowToIdMap.has(win) ) {
            return;
        }
        windowToIdMap.set(win, windowIdGenerator++);
        if ( typeof api.onOpenWindow === 'function' ) {
            api.onOpenWindow(win);
        }
    };

    var removeWindow = function(win) {
        if ( !win || windowToIdMap.delete(win) !== true ) {
            return;
        }
        if ( typeof api.onCloseWindow === 'function' ) {
            api.onCloseWindow(win);
        }
    };

    // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIWindowMediator
    // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIWindowWatcher
    // https://github.com/gorhill/uMatrix/issues/357
    // Use nsIWindowMediator for being notified of opened/closed windows.
    var listeners = {
        onOpenWindow: function(aWindow) {
            var win;
            try {
                win = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                             .getInterface(Ci.nsIDOMWindow);
            } catch (ex) {
            }
            addWindow(win);
        },

        onCloseWindow: function(aWindow) {
            var win;
            try {
                win = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                             .getInterface(Ci.nsIDOMWindow);
            } catch (ex) {
            }
            removeWindow(win);
        },

        observe: function(aSubject, topic) {
            // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIWindowWatcher#registerNotification%28%29
            //   "aSubject - the window being opened or closed, sent as an
            //   "nsISupports which can be ... QueryInterfaced to an
            //   "nsIDOMWindow."
            var win;
            try {
                win = aSubject.QueryInterface(Ci.nsIInterfaceRequestor)
                              .getInterface(Ci.nsIDOMWindow);
            } catch (ex) {
            }
            if ( !win ) { return; }
            if ( topic === 'domwindowopened' ) {
                addWindow(win);
                return;
            }
            if ( topic === 'domwindowclosed' ) {
                removeWindow(win);
                return;
            }
        }
    };

    (function() {
        var winumerator, win;

        // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIWindowMediator#getEnumerator%28%29
        winumerator = Services.wm.getEnumerator(null);
        while ( winumerator.hasMoreElements() ) {
            win = winumerator.getNext();
            if ( !win.closed ) {
                windowToIdMap.set(win, windowIdGenerator++);
            }
        }

        // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIWindowWatcher#getWindowEnumerator%28%29
        winumerator = Services.ww.getWindowEnumerator();
        while ( winumerator.hasMoreElements() ) {
            win = winumerator.getNext()
                             .QueryInterface(Ci.nsIInterfaceRequestor)
                             .getInterface(Ci.nsIDOMWindow);
            if ( !win.closed ) {
                windowToIdMap.set(win, windowIdGenerator++);
            }
        }

        Services.wm.addListener(listeners);
        Services.ww.registerNotification(listeners);
    })();

    cleanupTasks.push(function() {
        Services.wm.removeListener(listeners);
        Services.ww.unregisterNotification(listeners);
        windowToIdMap.clear();
    });

    return api;
})();

/******************************************************************************/

var getTabBrowser = function(win) {
    return win && win.gBrowser || null;
};

/******************************************************************************/

var getOwnerWindow = function(target) {
    if ( target.ownerDocument ) {
        return target.ownerDocument.defaultView;
    }
    return null;
};

/******************************************************************************/

vAPI.isBehindTheSceneTabId = function(tabId) {
    return tabId.toString() === '-1';
};

vAPI.noTabId = '-1';

/******************************************************************************/

vAPI.tabs = {};

/******************************************************************************/

vAPI.tabs.registerListeners = function() {
    tabWatcher.start();
};

/******************************************************************************/

// Firefox:
//   https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Tabbed_browser
//
// browser --> ownerDocument --> defaultView --> gBrowser --> browsers --+
//    ^                                                                  |
//    |                                                                  |
//    +-------------------------------------------------------------------
//
// browser (browser)
//   contentTitle
//   currentURI
//   ownerDocument (XULDocument)
//     defaultView (ChromeWindow)
//     gBrowser (tabbrowser OR browser)
//       browsers (browser)
//       selectedBrowser
//       selectedTab
//       tabs (tab.tabbrowser-tab)
//
// Fennec: (what I figured so far)
//
//   tab --> browser     windows --> window --> BrowserApp --> tabs --+
//    ^      window                                                   |
//    |                                                               |
//    +---------------------------------------------------------------+
//
// tab
//   browser
// [manual search to go back to tab from list of windows]

vAPI.tabs.get = function(tabId, callback) {
    var browser;

    if ( tabId === null ) {
        browser = tabWatcher.currentBrowser();
        tabId = tabWatcher.tabIdFromTarget(browser);
    } else {
        browser = tabWatcher.browserFromTabId(tabId);
    }

    // For internal use
    if ( typeof callback !== 'function' ) {
        return browser;
    }

    if ( !browser || !browser.currentURI ) {
        callback();
        return;
    }

    var win = getOwnerWindow(browser);
    var tabBrowser = getTabBrowser(win);

    // https://github.com/gorhill/uMatrix/issues/540
    // The `index` property is nowhere used by uMatrix at this point, so we
    // will refrain from returning this information for the time being.

    callback({
        id: tabId,
        index: undefined,
        windowId: winWatcher.idFromWindow(win),
        active: tabBrowser !== null && browser === tabBrowser.selectedBrowser,
        url: browser.currentURI.asciiSpec,
        title: browser.contentTitle
    });
};

/******************************************************************************/

vAPI.tabs.getAllSync = function(window) {
    var win, tab;
    var tabs = [];

    for ( win of winWatcher.getWindows() ) {
        if ( window && window !== win ) {
            continue;
        }

        var tabBrowser = getTabBrowser(win);
        if ( tabBrowser === null ) {
            continue;
        }

        // This can happens if a tab-less window is currently opened.
        // Example of a tab-less window: one opened from clicking
        //   "View Page Source".
        if ( !tabBrowser.tabs ) {
            continue;
        }

        for ( tab of tabBrowser.tabs ) {
            tabs.push(tab);
        }
    }

    return tabs;
};

/******************************************************************************/

vAPI.tabs.getAll = function(callback) {
    var tabs = [], tab;

    for ( var browser of tabWatcher.browsers() ) {
        tab = tabWatcher.tabFromBrowser(browser);
        if ( tab === null ) {
            continue;
        }
        if ( tab.hasAttribute('pending') ) {
            continue;
        }
        tabs.push({
            id: tabWatcher.tabIdFromTarget(browser),
            url: browser.currentURI.asciiSpec
        });
    }

    callback(tabs);
};

/******************************************************************************/

// properties of the details object:
//   url: 'URL', // the address that will be opened
//   tabId: 1, // the tab is used if set, instead of creating a new one
//   index: -1, // undefined: end of the list, -1: following tab, or after index
//   active: false, // opens the tab in background - true and undefined: foreground
//   select: true // if a tab is already opened with that url, then select it instead of opening a new one

vAPI.tabs.open = function(details) {
    if ( !details.url ) {
        return null;
    }
    // extension pages
    if ( /^[\w-]{2,}:/.test(details.url) === false ) {
        details.url = vAPI.getURL(details.url);
    }

    var tab;

    if ( details.select ) {
        var URI = Services.io.newURI(details.url, null, null);

        for ( tab of this.getAllSync() ) {
            var browser = tabWatcher.browserFromTarget(tab);

            // Or simply .equals if we care about the fragment
            if ( URI.equalsExceptRef(browser.currentURI) === false ) {
                continue;
            }

            this.select(tab);

            // Update URL if fragment is different
            if ( URI.equals(browser.currentURI) === false ) {
                browser.loadURI(URI.asciiSpec);
            }
            return;
        }
    }

    if ( details.active === undefined ) {
        details.active = true;
    }

    if ( details.tabId ) {
        tab = tabWatcher.browserFromTabId(details.tabId);
        if ( tab ) {
            tabWatcher.browserFromTarget(tab).loadURI(details.url);
            return;
        }
    }

    // Open in a standalone window
    if ( details.popup === true ) {
        Services.ww.openWindow(
            self,
            details.url,
            null,
            'location=1,menubar=1,personalbar=1,resizable=1,toolbar=1',
            null
        );
        return;
    }

    var win = winWatcher.getCurrentWindow();
    var tabBrowser = getTabBrowser(win);
    if ( tabBrowser === null ) {
        return;
    }

    if ( details.index === -1 ) {
        details.index = tabBrowser.browsers.indexOf(tabBrowser.selectedBrowser) + 1;
    }

    tab = tabBrowser.loadOneTab(details.url, { inBackground: !details.active });

    if ( details.index !== undefined ) {
        tabBrowser.moveTabTo(tab, details.index);
    }
};

/******************************************************************************/

// Replace the URL of a tab. Noop if the tab does not exist.

vAPI.tabs.replace = function(tabId, url) {
    var targetURL = url;

    // extension pages
    if ( /^[\w-]{2,}:/.test(targetURL) !== true ) {
        targetURL = vAPI.getURL(targetURL);
    }

    var browser = tabWatcher.browserFromTabId(tabId);
    if ( browser ) {
        browser.loadURI(targetURL);
    }
};

/******************************************************************************/

vAPI.tabs._remove = function(tab, tabBrowser) {
    if ( tabBrowser ) {
        tabBrowser.removeTab(tab);
    }
};

/******************************************************************************/

vAPI.tabs.remove = function(tabId) {
    var browser = tabWatcher.browserFromTabId(tabId);
    if ( !browser ) {
        return;
    }
    var tab = tabWatcher.tabFromBrowser(browser);
    if ( !tab ) {
        return;
    }
    this._remove(tab, getTabBrowser(getOwnerWindow(browser)));
};

/******************************************************************************/

vAPI.tabs.reload = function(tabId) {
    var browser = tabWatcher.browserFromTabId(tabId);
    if ( !browser ) {
        return;
    }

    browser.webNavigation.reload(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
};

/******************************************************************************/

vAPI.tabs.select = function(tab) {
    if ( typeof tab !== 'object' ) {
        tab = tabWatcher.tabFromBrowser(tabWatcher.browserFromTabId(tab));
    }
    if ( !tab ) {
        return;
    }

    // https://github.com/gorhill/uBlock/issues/470
    var win = getOwnerWindow(tab);
    win.focus();

    var tabBrowser = getTabBrowser(win);
    if ( tabBrowser ) {
        tabBrowser.selectedTab = tab;
    }
};

/******************************************************************************/

vAPI.tabs.injectScript = function(tabId, details, callback) {
    var browser = tabWatcher.browserFromTabId(tabId);
    if ( !browser ) {
        return;
    }

    if ( typeof details.file !== 'string' ) {
        return;
    }

    details.file = vAPI.getURL(details.file);
    browser.messageManager.sendAsyncMessage(
        location.host + ':broadcast',
        JSON.stringify({
            broadcast: true,
            channelName: 'vAPI',
            msg: {
                cmd: 'injectScript',
                details: details
            }
        })
    );

    if ( typeof callback === 'function' ) {
        vAPI.setTimeout(callback, 13);
    }
};

/******************************************************************************/

var tabWatcher = (function() {
    // TODO: find out whether we need a janitor to take care of stale entries.

    // https://github.com/gorhill/uMatrix/issues/540
    // Use only weak references to hold onto browser references.
    var browserToTabIdMap = new WeakMap();
    var tabIdToBrowserMap = new Map();
    var tabIdGenerator = 1;

    var indexFromBrowser = function(browser) {
        if ( !browser ) {
            return -1;
        }
        var win = getOwnerWindow(browser);
        if ( !win ) {
            return -1;
        }
        var tabBrowser = getTabBrowser(win);
        if ( tabBrowser === null ) {
            return -1;
        }
        // This can happen, for example, the `view-source:` window, there is
        // no tabbrowser object, the browser object sits directly in the
        // window.
        if ( tabBrowser === browser ) {
            return 0;
        }
        return tabBrowser.browsers.indexOf(browser);
    };

    var indexFromTarget = function(target) {
        return indexFromBrowser(browserFromTarget(target));
    };

    var tabFromBrowser = function(browser) {
        var i = indexFromBrowser(browser);
        if ( i === -1 ) {
            return null;
        }
        var win = getOwnerWindow(browser);
        if ( !win ) {
            return null;
        }
        var tabBrowser = getTabBrowser(win);
        if ( tabBrowser === null ) {
            return null;
        }
        if ( !tabBrowser.tabs || i >= tabBrowser.tabs.length ) {
            return null;
        }
        return tabBrowser.tabs[i];
    };

    var browserFromTarget = function(target) {
        if ( !target ) {
            return null;
        }
        if ( target.linkedPanel ) {  // target is a tab
            target = target.linkedBrowser;
        }
        if ( target.localName !== 'browser' ) {
            return null;
        }
        return target;
    };

    var tabIdFromTarget = function(target) {
        var browser = browserFromTarget(target);
        if ( browser === null ) {
            return vAPI.noTabId;
        }
        var tabId = browserToTabIdMap.get(browser);
        if ( tabId === undefined ) {
            tabId = '' + tabIdGenerator++;
            browserToTabIdMap.set(browser, tabId);
            tabIdToBrowserMap.set(tabId, Cu.getWeakReference(browser));
        }
        return tabId;
    };

    var browserFromTabId = function(tabId) {
        var weakref = tabIdToBrowserMap.get(tabId);
        var browser = weakref && weakref.get();
        return browser || null;
    };

    var currentBrowser = function() {
        var win = winWatcher.getCurrentWindow();
        // https://github.com/gorhill/uBlock/issues/399
        // getTabBrowser() can return null at browser launch time.
        var tabBrowser = getTabBrowser(win);
        if ( tabBrowser === null ) {
            return null;
        }
        return browserFromTarget(tabBrowser.selectedTab);
    };

    var removeBrowserEntry = function(tabId, browser) {
        if ( tabId && tabId !== vAPI.noTabId ) {
            vAPI.tabs.onClosed(tabId);
            delete vAPI.toolbarButton.tabs[tabId];
            tabIdToBrowserMap.delete(tabId);
        }
        if ( browser ) {
            browserToTabIdMap.delete(browser);
        }
    };

    var removeTarget = function(target) {
        onClose({ target: target });
    };

    var getAllBrowsers = function() {
        var browsers = [], browser;
        for ( var [tabId, weakref] of tabIdToBrowserMap ) {
            browser = weakref.get();
            // TODO:
            // Maybe call removeBrowserEntry() if the browser no longer exists?
            if ( browser ) {
                browsers.push(browser);
            }
        }
        return browsers;
    };

    // https://developer.mozilla.org/en-US/docs/Web/Events/TabOpen
    //var onOpen = function({target}) {
    //    var tabId = tabIdFromTarget(target);
    //    var browser = browserFromTabId(tabId);
    //    vAPI.tabs.onNavigation({
    //        frameId: 0,
    //        tabId: tabId,
    //        url: browser.currentURI.asciiSpec,
    //    });
    //};

    // https://developer.mozilla.org/en-US/docs/Web/Events/TabShow
    var onShow = function({target}) {
        tabIdFromTarget(target);
    };

    // https://developer.mozilla.org/en-US/docs/Web/Events/TabClose
    var onClose = function({target}) {
        // target is tab in Firefox, browser in Fennec
        var browser = browserFromTarget(target);
        var tabId = browserToTabIdMap.get(browser);
        removeBrowserEntry(tabId, browser);
    };

    // https://developer.mozilla.org/en-US/docs/Web/Events/TabSelect
    // This is an entry point: when creating a new tab, it is not always
    // reported through onLocationChanged... Sigh. It is "reported" here
    // however.
    var onSelect = function({target}) {
        var browser = browserFromTarget(target);
        var tabId = browserToTabIdMap.get(browser);
        if ( tabId === undefined ) {
            tabId = tabIdFromTarget(target);
            vAPI.tabs.onNavigation({
                frameId: 0,
                tabId: tabId,
                url: browser.currentURI.asciiSpec
            });
        }
        vAPI.setIcon(tabId, getOwnerWindow(target));
    };

    var locationChangedMessageName = location.host + ':locationChanged';

    var onLocationChanged = function(e) {
        var vapi = vAPI;
        var details = e.data;

        // Ignore notifications related to our popup
        if ( details.url.lastIndexOf(vapi.getURL('popup.html'), 0) === 0 ) {
            return;
        }

        var browser = e.target;
        var tabId = tabIdFromTarget(browser);
        if ( tabId === vapi.noTabId ) {
            return;
        }

        // LOCATION_CHANGE_SAME_DOCUMENT = "did not load a new document"
        if ( details.flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT ) {
            vapi.tabs.onUpdated(tabId, {url: details.url}, {
                frameId: 0,
                tabId: tabId,
                url: browser.currentURI.asciiSpec
            });
            return;
        }

        // https://github.com/chrisaljoudi/uBlock/issues/105
        // Allow any kind of pages
        vapi.tabs.onNavigation({
            frameId: 0,
            tabId: tabId,
            url: details.url
        });
    };

    var attachToTabBrowser = function(window) {
        if ( typeof vAPI.toolbarButton.attachToNewWindow === 'function' ) {
            vAPI.toolbarButton.attachToNewWindow(window);
        }

        var tabBrowser = getTabBrowser(window);
        if ( tabBrowser === null ) {
            return;
        }

        var tabContainer;
        if ( tabBrowser.deck ) {                    // Fennec
            tabContainer = tabBrowser.deck;
        } else if ( tabBrowser.tabContainer ) {     // Firefox
            tabContainer = tabBrowser.tabContainer;
            vAPI.contextMenu.register(document);
        }

        // https://github.com/gorhill/uBlock/issues/697
        // Ignore `TabShow` events: unfortunately the `pending` attribute is
        // not set when a tab is opened as a result of session restore -- it is
        // set *after* the event is fired in such case.
        if ( tabContainer ) {
            tabContainer.addEventListener('TabShow', onShow);
            tabContainer.addEventListener('TabClose', onClose);
            // when new window is opened TabSelect doesn't run on the selected tab?
            tabContainer.addEventListener('TabSelect', onSelect);
        }
    };

    // https://github.com/gorhill/uBlock/issues/906
    // Ensure the environment is ready before trying to attaching.
    var canAttachToTabBrowser = function(window) {
        var document = window && window.document;
        if ( !document || document.readyState !== 'complete' ) {
            return false;
        }

        // On some platforms, the tab browser isn't immediately available,
        // try waiting a bit if this happens.
        // https://github.com/gorhill/uBlock/issues/763
        // Not getting a tab browser should not prevent from attaching ourself
        // to the window.
        var tabBrowser = getTabBrowser(window);
        if ( tabBrowser === null ) {
            return false;
        }

        return winWatcher.toBrowserWindow(window) !== null;
    };

    var onWindowLoad = function(win) {
        deferUntil(
            canAttachToTabBrowser.bind(null, win),
            attachToTabBrowser.bind(null, win)
        );
    };

    var onWindowUnload = function(win) {
        vAPI.contextMenu.unregister(win.document);

        var tabBrowser = getTabBrowser(win);
        if ( tabBrowser === null ) {
            return;
        }

        var tabContainer = tabBrowser.tabContainer;
        if ( tabContainer ) {
            tabContainer.removeEventListener('TabShow', onShow);
            tabContainer.removeEventListener('TabClose', onClose);
            tabContainer.removeEventListener('TabSelect', onSelect);
        }

        // https://github.com/gorhill/uBlock/issues/574
        // To keep in mind: not all windows are tab containers,
        // sometimes the window IS the tab.
        var tabs;
        if ( tabBrowser.tabs ) {
            tabs = tabBrowser.tabs;
        } else if ( tabBrowser.localName === 'browser' ) {
            tabs = [tabBrowser];
        } else {
            tabs = [];
        }

        var browser, URI, tabId;
        var tabindex = tabs.length, tab;
        while ( tabindex-- ) {
            tab = tabs[tabindex];
            browser = browserFromTarget(tab);
            if ( browser === null ) {
                continue;
            }
            URI = browser.currentURI;
            // Close extension tabs
            if ( URI.schemeIs('chrome') && URI.host === location.host ) {
                vAPI.tabs._remove(tab, getTabBrowser(win));
            }
            tabId = browserToTabIdMap.get(browser);
            if ( tabId !== undefined ) {
                removeBrowserEntry(tabId, browser);
                tabIdToBrowserMap.delete(tabId);
            }
            browserToTabIdMap.delete(browser);
        }
    };

    // Initialize map with existing active tabs
    var start = function() {
        var tabBrowser, tabs, tab;
        for ( var win of winWatcher.getWindows() ) {
            onWindowLoad(win);
            tabBrowser = getTabBrowser(win);
            if ( tabBrowser === null ) {
                continue;
            }
            for ( tab of tabBrowser.tabs ) {
                if ( !tab.hasAttribute('pending') ) {
                    tabIdFromTarget(tab);
                }
            }
        }

        winWatcher.onOpenWindow = onWindowLoad;
        winWatcher.onCloseWindow = onWindowUnload;

        vAPI.messaging.globalMessageManager.addMessageListener(
            locationChangedMessageName,
            onLocationChanged
        );
    };

    var stop = function() {
        winWatcher.onOpenWindow = null;
        winWatcher.onCloseWindow = null;

        vAPI.messaging.globalMessageManager.removeMessageListener(
            locationChangedMessageName,
            onLocationChanged
        );

        for ( var win of winWatcher.getWindows() ) {
            onWindowUnload(win);
        }

        browserToTabIdMap = new WeakMap();
        tabIdToBrowserMap.clear();
    };

    cleanupTasks.push(stop);

    return {
        browsers: getAllBrowsers,
        browserFromTabId: browserFromTabId,
        browserFromTarget: browserFromTarget,
        currentBrowser: currentBrowser,
        indexFromTarget: indexFromTarget,
        removeTarget: removeTarget,
        start: start,
        tabFromBrowser: tabFromBrowser,
        tabIdFromTarget: tabIdFromTarget
    };
})();

/******************************************************************************/

vAPI.setIcon = function(tabId, iconId, badge) {
    // If badge is undefined, then setIcon was called from the TabSelect event
    var win;
    if ( badge === undefined ) {
        win = iconId;
    } else {
        win = winWatcher.getCurrentWindow();
    }
    var tabBrowser = getTabBrowser(win);
    if ( tabBrowser === null ) {
        return;
    }
    var curTabId = tabWatcher.tabIdFromTarget(tabBrowser.selectedTab);
    var tb = vAPI.toolbarButton;

    // from 'TabSelect' event
    if ( tabId === undefined ) {
        tabId = curTabId;
    } else if ( badge !== undefined ) {
        tb.tabs[tabId] = { badge: badge, img: iconId };
    }

    if ( tabId === curTabId ) {
        tb.updateState(win, tabId);
    }
};

/******************************************************************************/

vAPI.messaging = {
    get globalMessageManager() {
        return Cc['@mozilla.org/globalmessagemanager;1']
                .getService(Ci.nsIMessageListenerManager);
    },
    frameScript: vAPI.getURL('frameScript.js'),
    listeners: {},
    defaultHandler: null,
    NOOPFUNC: function(){},
    UNHANDLED: 'vAPI.messaging.notHandled'
};

/******************************************************************************/

vAPI.messaging.listen = function(listenerName, callback) {
    this.listeners[listenerName] = callback;
};

/******************************************************************************/

vAPI.messaging.onMessage = function({target, data}) {
    var messageManager = target.messageManager;

    if ( !messageManager ) {
        // Message came from a popup, and its message manager is not usable.
        // So instead we broadcast to the parent window.
        messageManager = getOwnerWindow(
            target.webNavigation.QueryInterface(Ci.nsIDocShell).chromeEventHandler
        ).messageManager;
    }

    var channelNameRaw = data.channelName;
    var pos = channelNameRaw.indexOf('|');
    var channelName = channelNameRaw.slice(pos + 1);

    var callback = vAPI.messaging.NOOPFUNC;
    if ( data.requestId !== undefined ) {
        callback = CallbackWrapper.factory(
            messageManager,
            channelName,
            channelNameRaw.slice(0, pos),
            data.requestId
        ).callback;
    }

    var sender = {
        tab: {
            id: tabWatcher.tabIdFromTarget(target)
        }
    };

    // Specific handler
    var r = vAPI.messaging.UNHANDLED;
    var listener = vAPI.messaging.listeners[channelName];
    if ( typeof listener === 'function' ) {
        r = listener(data.msg, sender, callback);
    }
    if ( r !== vAPI.messaging.UNHANDLED ) {
        return;
    }

    // Default handler
    r = vAPI.messaging.defaultHandler(data.msg, sender, callback);
    if ( r !== vAPI.messaging.UNHANDLED ) {
        return;
    }

    console.error('uMatrix> messaging > unknown request: %o', data);

    // Unhandled:
    // Need to callback anyways in case caller expected an answer, or
    // else there is a memory leak on caller's side
    callback();
};

/******************************************************************************/

vAPI.messaging.setup = function(defaultHandler) {
    // Already setup?
    if ( this.defaultHandler !== null ) {
        return;
    }

    if ( typeof defaultHandler !== 'function' ) {
        defaultHandler = function(){ return vAPI.messaging.UNHANDLED; };
    }
    this.defaultHandler = defaultHandler;

    this.globalMessageManager.addMessageListener(
        location.host + ':background',
        this.onMessage
    );

    this.globalMessageManager.loadFrameScript(this.frameScript, true);

    cleanupTasks.push(function() {
        var gmm = vAPI.messaging.globalMessageManager;

        gmm.removeDelayedFrameScript(vAPI.messaging.frameScript);
        gmm.removeMessageListener(
            location.host + ':background',
            vAPI.messaging.onMessage
        );
    });
};

/******************************************************************************/

vAPI.messaging.broadcast = function(message) {
    this.globalMessageManager.broadcastAsyncMessage(
        location.host + ':broadcast',
        JSON.stringify({broadcast: true, msg: message})
    );
};

/******************************************************************************/

// This allows to avoid creating a closure for every single message which
// expects an answer. Having a closure created each time a message is processed
// has been always bothering me. Another benefit of the implementation here
// is to reuse the callback proxy object, so less memory churning.
//
// https://developers.google.com/speed/articles/optimizing-javascript
// "Creating a closure is significantly slower then creating an inner
//  function without a closure, and much slower than reusing a static
//  function"
//
// http://hacksoflife.blogspot.ca/2015/01/the-four-horsemen-of-performance.html
// "the dreaded 'uniformly slow code' case where every function takes 1%
//  of CPU and you have to make one hundred separate performance optimizations
//  to improve performance at all"
//
// http://jsperf.com/closure-no-closure/2

var CallbackWrapper = function(messageManager, channelName, listenerId, requestId) {
    this.callback = this.proxy.bind(this); // bind once
    this.init(messageManager, channelName, listenerId, requestId);
};

CallbackWrapper.junkyard = [];

CallbackWrapper.factory = function(messageManager, channelName, listenerId, requestId) {
    var wrapper = CallbackWrapper.junkyard.pop();
    if ( wrapper ) {
        wrapper.init(messageManager, channelName, listenerId, requestId);
        return wrapper;
    }
    return new CallbackWrapper(messageManager, channelName, listenerId, requestId);
};

CallbackWrapper.prototype.init = function(messageManager, channelName, listenerId, requestId) {
    this.messageManager = messageManager;
    this.channelName = channelName;
    this.listenerId = listenerId;
    this.requestId = requestId;
};

CallbackWrapper.prototype.proxy = function(response) {
    var message = JSON.stringify({
        requestId: this.requestId,
        channelName: this.channelName,
        msg: response !== undefined ? response : null
    });

    if ( this.messageManager.sendAsyncMessage ) {
        this.messageManager.sendAsyncMessage(this.listenerId, message);
    } else {
        this.messageManager.broadcastAsyncMessage(this.listenerId, message);
    }

    // Mark for reuse
    this.messageManager =
    this.channelName =
    this.requestId =
    this.listenerId = null;
    CallbackWrapper.junkyard.push(this);
};

/******************************************************************************/

var httpRequestHeadersFactory = function(channel) {
    var entry = httpRequestHeadersFactory.junkyard.pop();
    if ( entry ) {
        return entry.init(channel);
    }
    return new HTTPRequestHeaders(channel);
};

httpRequestHeadersFactory.junkyard = [];

var HTTPRequestHeaders = function(channel) {
    this.init(channel);
};

HTTPRequestHeaders.prototype.init = function(channel) {
    this.channel = channel;
    return this;
};

HTTPRequestHeaders.prototype.dispose = function() {
    this.channel = null;
    httpRequestHeadersFactory.junkyard.push(this);
};

HTTPRequestHeaders.prototype.getHeader = function(name) {
    try {
        return this.channel.getRequestHeader(name);
    } catch (e) {
    }
    return '';
};

HTTPRequestHeaders.prototype.setHeader = function(name, newValue, create) {
    var oldValue = this.getHeader(name);
    if ( newValue === oldValue ) {
        return false;
    }
    if ( oldValue === '' && create !== true ) {
        return false;
    }
    this.channel.setRequestHeader(name, newValue, false);
    return true;
};

/******************************************************************************/

var httpObserver = {
    classDescription: 'net-channel-event-sinks for ' + location.host,
    classID: Components.ID('{5d2e2797-6d68-42e2-8aeb-81ce6ba16b95}'),
    contractID: '@' + location.host + '/net-channel-event-sinks;1',
    REQDATAKEY: location.host + 'reqdata',
    ABORT: Components.results.NS_BINDING_ABORTED,
    ACCEPT: Components.results.NS_SUCCEEDED,
    // Request types:
    // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIContentPolicy#Constants
    frameTypeMap: {
        6: 'main_frame',
        7: 'sub_frame'
    },
    typeMap: {
        1: 'other',
        2: 'script',
        3: 'image',
        4: 'stylesheet',
        5: 'object',
        6: 'main_frame',
        7: 'sub_frame',
        10: 'ping',
        11: 'xmlhttprequest',
        12: 'object',
        14: 'font',
        15: 'media',
        16: 'websocket',
        21: 'image'
    },
    mimeTypeMap: {
        'audio': 15,
        'video': 15
    },

    get componentRegistrar() {
        return Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
    },

    get categoryManager() {
        return Cc['@mozilla.org/categorymanager;1']
                .getService(Ci.nsICategoryManager);
    },

    QueryInterface: (function() {
        var {XPCOMUtils} = Cu.import('resource://gre/modules/XPCOMUtils.jsm', null);

        return XPCOMUtils.generateQI([
            Ci.nsIFactory,
            Ci.nsIObserver,
            Ci.nsIChannelEventSink,
            Ci.nsISupportsWeakReference
        ]);
    })(),

    createInstance: function(outer, iid) {
        if ( outer ) {
            throw Components.results.NS_ERROR_NO_AGGREGATION;
        }

        return this.QueryInterface(iid);
    },

    register: function() {
        this.pendingRingBufferInit();

        // https://developer.mozilla.org/en/docs/Observer_Notifications#HTTP_requests
        Services.obs.addObserver(this, 'http-on-modify-request', true);
        Services.obs.addObserver(this, 'http-on-examine-response', true);
        Services.obs.addObserver(this, 'http-on-examine-cached-response', true);

        // Guard against stale instances not having been unregistered
        if ( this.componentRegistrar.isCIDRegistered(this.classID) ) {
            try {
                this.componentRegistrar.unregisterFactory(this.classID, Components.manager.getClassObject(this.classID, Ci.nsIFactory));
            } catch (ex) {
                console.error('uMatrix> httpObserver > unable to unregister stale instance: ', ex);
            }
        }

        this.componentRegistrar.registerFactory(
            this.classID,
            this.classDescription,
            this.contractID,
            this
        );
        this.categoryManager.addCategoryEntry(
            'net-channel-event-sinks',
            this.contractID,
            this.contractID,
            false,
            true
        );
    },

    unregister: function() {
        Services.obs.removeObserver(this, 'http-on-modify-request');
        Services.obs.removeObserver(this, 'http-on-examine-response');
        Services.obs.removeObserver(this, 'http-on-examine-cached-response');

        this.componentRegistrar.unregisterFactory(this.classID, this);
        this.categoryManager.deleteCategoryEntry(
            'net-channel-event-sinks',
            this.contractID,
            false
        );
    },

    PendingRequest: function() {
        this.rawType = 0;
        this.tabId = 0;
        this._key = ''; // key is url, from URI.spec
    },

    // If all work fine, this map should not grow indefinitely. It can have
    // stale items in it, but these will be taken care of when entries in
    // the ring buffer are overwritten.
    pendingURLToIndex: new Map(),
    pendingWritePointer: 0,
    pendingRingBuffer: new Array(256),
    pendingRingBufferInit: function() {
        // Use and reuse pre-allocated PendingRequest objects = less memory
        // churning.
        var i = this.pendingRingBuffer.length;
        while ( i-- ) {
            this.pendingRingBuffer[i] = new this.PendingRequest();
        }
    },

    // Pending request ring buffer:
    // +-------+-------+-------+-------+-------+-------+-------
    // |0      |1      |2      |3      |4      |5      |...      
    // +-------+-------+-------+-------+-------+-------+-------
    //
    // URL to ring buffer index map:
    // { k = URL, s = ring buffer indices }
    //
    // s is a string which character codes map to ring buffer indices -- for
    // when the same URL is received multiple times by shouldLoadListener()
    // before the existing one is serviced by the network request observer.
    // I believe the use of a string in lieu of an array reduces memory
    // churning.

    createPendingRequest: function(url) {
        var bucket;
        var i = this.pendingWritePointer;
        this.pendingWritePointer = i + 1 & 255;
        var preq = this.pendingRingBuffer[i];
        var si = String.fromCharCode(i);
        // Cleanup unserviced pending request
        if ( preq._key !== '' ) {
            bucket = this.pendingURLToIndex.get(preq._key);
            if ( bucket.length === 1 ) {
                this.pendingURLToIndex.delete(preq._key);
            } else {
                var pos = bucket.indexOf(si);
                this.pendingURLToIndex.set(preq._key, bucket.slice(0, pos) + bucket.slice(pos + 1));
            }
        }
        bucket = this.pendingURLToIndex.get(url);
        this.pendingURLToIndex.set(url, bucket === undefined ? si : bucket + si);
        preq._key = url;
        return preq;
    },

    lookupPendingRequest: function(url) {
        var bucket = this.pendingURLToIndex.get(url);
        if ( bucket === undefined ) {
            return null;
        }
        var i = bucket.charCodeAt(0);
        if ( bucket.length === 1 ) {
            this.pendingURLToIndex.delete(url);
        } else {
            this.pendingURLToIndex.set(url, bucket.slice(1));
        }
        var preq = this.pendingRingBuffer[i];
        preq._key = ''; // mark as "serviced"
        return preq;
    },

    handleRequest: function(channel, URI, tabId, rawType) {
        var type = this.typeMap[rawType] || 'other';

        var onBeforeRequest = vAPI.net.onBeforeRequest;
        if ( onBeforeRequest.types === null || onBeforeRequest.types.has(type) ) {
            var result = onBeforeRequest.callback({
                hostname: URI.asciiHost,
                parentFrameId: type === 'main_frame' ? -1 : 0,
                tabId: tabId,
                type: type,
                url: URI.asciiSpec
            });
            if ( typeof result === 'object' ) {
                channel.cancel(this.ABORT);
                return true;
            }
        }

        var onBeforeSendHeaders = vAPI.net.onBeforeSendHeaders;
        if ( onBeforeSendHeaders.types === null || onBeforeSendHeaders.types.has(type) ) {
            var requestHeaders = httpRequestHeadersFactory(channel);
            onBeforeSendHeaders.callback({
                hostname: URI.asciiHost,
                parentFrameId: type === 'main_frame' ? -1 : 0,
                requestHeaders: requestHeaders,
                tabId: tabId,
                type: type,
                url: URI.asciiSpec
            });
            requestHeaders.dispose();
        }

        return false;
    },

    channelDataFromChannel: function(channel) {
        if ( channel instanceof Ci.nsIWritablePropertyBag ) {
            try {
                return channel.getProperty(this.REQDATAKEY) || null;
            } catch (ex) {
            }
        }
        return null;
    },

    // https://github.com/gorhill/uMatrix/issues/165
    // https://developer.mozilla.org/en-US/Firefox/Releases/3.5/Updating_extensions#Getting_a_load_context_from_a_request
    // Not sure `umatrix:shouldLoad` is still needed, uMatrix does not
    //   care about embedded frames topography.
    // Also:
    //   https://developer.mozilla.org/en-US/Firefox/Multiprocess_Firefox/Limitations_of_chrome_scripts
    tabIdFromChannel: function(channel) {
        var lc;
        try {
            lc = channel.notificationCallbacks.getInterface(Ci.nsILoadContext);
        } catch(ex) {
        }
        if ( !lc ) {
            try {
                lc = channel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
            } catch(ex) {
            }
            if ( !lc ) {
                return vAPI.noTabId;
            }
        }
        if ( lc.topFrameElement ) {
            return tabWatcher.tabIdFromTarget(lc.topFrameElement);
        }
        var win;
        try {
            win = lc.associatedWindow;
        } catch (ex) { }
        if ( !win ) {
            return vAPI.noTabId;
        }
        if ( win.top ) {
            win = win.top;
        }
        var tabBrowser;
        try {
            tabBrowser = getTabBrowser(
                win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
                   .QueryInterface(Ci.nsIDocShell).rootTreeItem
                   .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow)
            );
        } catch (ex) { }
        if ( !tabBrowser ) {
            return vAPI.noTabId;
        }
        if ( tabBrowser.getBrowserForContentWindow ) {
            return tabWatcher.tabIdFromTarget(tabBrowser.getBrowserForContentWindow(win));
        }
        // Falling back onto _getTabForContentWindow to ensure older versions
        // of Firefox work well.
        return tabBrowser._getTabForContentWindow ?
               tabWatcher.tabIdFromTarget(tabBrowser._getTabForContentWindow(win)) :
               vAPI.noTabId;
    },

    rawtypeFromContentType: function(channel) {
        var mime = channel.contentType;
        if ( !mime ) {
            return 0;
        }
        var pos = mime.indexOf('/');
        if ( pos === -1 ) {
            pos = mime.length;
        }
        return this.mimeTypeMap[mime.slice(0, pos)] || 0;
    },

    observe: function(channel, topic) {
        if ( channel instanceof Ci.nsIHttpChannel === false ) {
            return;
        }

        var URI = channel.URI;
        var channelData = this.channelDataFromChannel(channel);

        if ( topic.lastIndexOf('http-on-examine-', 0) === 0 ) {
            if ( channelData === null ) {
                return;
            }

            var type = this.frameTypeMap[channelData[1]];
            if ( !type ) {
                return;
            }

            topic = 'Content-Security-Policy';

            var result;
            try {
                result = channel.getResponseHeader(topic);
            } catch (ex) {
                result = null;
            }

            result = vAPI.net.onHeadersReceived.callback({
                hostname: URI.asciiHost,
                parentFrameId: type === 'main_frame' ? -1 : 0,
                responseHeaders: result ? [{name: topic, value: result}] : [],
                tabId: channelData[0],
                type: type,
                url: URI.asciiSpec
            });

            if ( result ) {
                channel.setResponseHeader(
                    topic,
                    result.responseHeaders.pop().value,
                    true
                );
            }

            return;
        }

        // http-on-modify-request

        // The channel was previously serviced.
        if ( channelData !== null ) {
            this.handleRequest(channel, URI, channelData[0], channelData[1]);
            return;
        }

        // The channel was never serviced.
        var tabId;
        var pendingRequest = this.lookupPendingRequest(URI.asciiSpec);
        var rawType = 1;
        var loadInfo = channel.loadInfo;

        // https://github.com/gorhill/uMatrix/issues/390#issuecomment-155717004
        if ( loadInfo ) {
            rawType = loadInfo.externalContentPolicyType !== undefined ?
                loadInfo.externalContentPolicyType :
                loadInfo.contentPolicyType;
            if ( !rawType ) {
                rawType = 1;
            }
        }

        if ( pendingRequest !== null ) {
            tabId = pendingRequest.tabId;
            // https://github.com/gorhill/uBlock/issues/654
            // Use the request type from the HTTP observer point of view.
            if ( rawType !== 1 ) {
                pendingRequest.rawType = rawType;
            } else {
                rawType = pendingRequest.rawType;
            }
        } else {
            tabId = this.tabIdFromChannel(channel);
        }

        if ( this.handleRequest(channel, URI, tabId, rawType) ) {
            return;
        }

        if ( channel instanceof Ci.nsIWritablePropertyBag === false ) {
            return;
        }

        // Carry data for behind-the-scene redirects
        channel.setProperty(this.REQDATAKEY, [tabId, rawType]);
    },

    // contentPolicy.shouldLoad doesn't detect redirects, this needs to be used
    asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) {
        // If error thrown, the redirect will fail
        try {
            var URI = newChannel.URI;
            if ( !URI.schemeIs('http') && !URI.schemeIs('https') ) {
                return;
            }

            if ( newChannel instanceof Ci.nsIWritablePropertyBag === false ) {
                return;
            }

            var channelData = this.channelDataFromChannel(oldChannel);
            if ( channelData === null ) {
                return;
            }

            // Carry the data on in case of multiple redirects
            newChannel.setProperty(this.REQDATAKEY, channelData);
        } catch (ex) {
            // console.error(ex);
        } finally {
            callback.onRedirectVerifyCallback(this.ACCEPT);
        }
    }
};

/******************************************************************************/

vAPI.net = {};

/******************************************************************************/

vAPI.net.registerListeners = function() {
    this.onBeforeRequest.types = this.onBeforeRequest.types ?
        new Set(this.onBeforeRequest.types) :
        null;
    this.onBeforeSendHeaders.types = this.onBeforeSendHeaders.types ?
        new Set(this.onBeforeSendHeaders.types) :
        null;

    var shouldLoadListenerMessageName = location.host + ':shouldLoad';
    var shouldLoadListener = function(e) {
        var details = e.data;
        var pendingReq = httpObserver.createPendingRequest(details.url);
        pendingReq.rawType = details.rawType;
        pendingReq.tabId = tabWatcher.tabIdFromTarget(e.target);
    };

    // https://github.com/gorhill/uMatrix/issues/200
    // We need this only for Firefox 34 and less: the tab id is derived from
    // the origin of the message.
    if ( !vAPI.modernFirefox ) {
        vAPI.messaging.globalMessageManager.addMessageListener(
            shouldLoadListenerMessageName,
            shouldLoadListener
        );
    }

    httpObserver.register();

    cleanupTasks.push(function() {
        if ( !vAPI.modernFirefox ) {
            vAPI.messaging.globalMessageManager.removeMessageListener(
                shouldLoadListenerMessageName,
                shouldLoadListener
            );
        }

        httpObserver.unregister();
    });
};

/******************************************************************************/
/******************************************************************************/

vAPI.toolbarButton = {
    id: location.host + '-button',
    type: 'view',
    viewId: location.host + '-panel',
    label: vAPI.app.name,
    tooltiptext: vAPI.app.name,
    tabs: {/*tabId: {badge: 0, img: boolean}*/},
    init: null,
    codePath: ''
};

/******************************************************************************/

// Non-Fennec: common code paths.

(function() {
    if ( vAPI.fennec ) {
        return;
    }

    var tbb = vAPI.toolbarButton;
    var popupCommittedWidth = 0;
    var popupCommittedHeight = 0;

    tbb.onViewShowing = function({target}) {
        popupCommittedWidth = popupCommittedHeight = 0;
        target.firstChild.setAttribute('src', vAPI.getURL('popup.html'));
    };

    tbb.onViewHiding = function({target}) {
        target.parentNode.style.maxWidth = '';
        target.firstChild.setAttribute('src', 'about:blank');
    };

    tbb.updateState = function(win, tabId) {
        var button = win.document.getElementById(this.id);

        if ( !button ) {
            return;
        }

        var icon = this.tabs[tabId];
        button.setAttribute('badge', icon && icon.badge || '');
        button.classList.toggle('off', !icon || !icon.img);

        var iconId = icon && icon.img ? icon.img : 'off';
        icon = 'url(' + vAPI.getURL('img/browsericons/icon19-' + iconId + '.png') + ')';
        button.style.listStyleImage = icon;
    };

    tbb.populatePanel = function(doc, panel) {
        panel.setAttribute('id', this.viewId);

        var iframe = doc.createElement('iframe');
        iframe.setAttribute('type', 'content');

        panel.appendChild(iframe);

        var toPx = function(pixels) {
            return pixels.toString() + 'px';
        };

        var scrollBarWidth = 0;
        var resizeTimer = null;

        var resizePopupDelayed = function(attempts) {
            if ( resizeTimer !== null ) {
                return false;
            }

            // Sanity check
            attempts = (attempts || 0) + 1;
            if ( attempts > 1/*000*/ ) {
                //console.error('uMatrix> resizePopupDelayed: giving up after too many attempts');
                return false;
            }

            resizeTimer = vAPI.setTimeout(resizePopup, 10, attempts);
            return true;
        };

        var resizePopup = function(attempts) {
            resizeTimer = null;

            panel.parentNode.style.maxWidth = 'none';
            var body = iframe.contentDocument.body;

            // https://github.com/gorhill/uMatrix/issues/301
            // Don't resize if committed size did not change.
            if (
                popupCommittedWidth === body.clientWidth &&
                popupCommittedHeight === body.clientHeight
            ) {
                return;
            }

            // We set a limit for height
            var height = Math.min(body.clientHeight, 600);

            // https://github.com/chrisaljoudi/uBlock/issues/730
            // Voodoo programming: this recipe works
            panel.style.setProperty('height', toPx(height));
            iframe.style.setProperty('height', toPx(height));

            // Adjust width for presence/absence of vertical scroll bar which may
            // have appeared as a result of last operation.
            var contentWindow = iframe.contentWindow;
            var width = body.clientWidth;
            if ( contentWindow.scrollMaxY !== 0 ) {
                width += scrollBarWidth;
            }
            panel.style.setProperty('width', toPx(width));

            // scrollMaxX should always be zero once we know the scrollbar width
            if ( contentWindow.scrollMaxX !== 0 ) {
                scrollBarWidth = contentWindow.scrollMaxX;
                width += scrollBarWidth;
                panel.style.setProperty('width', toPx(width));
            }

            if ( iframe.clientHeight !== height || panel.clientWidth !== width ) {
                if ( resizePopupDelayed(attempts) ) {
                    return;
                }
                // resizePopupDelayed won't be called again, so commit
                // dimentsions.
            }

            popupCommittedWidth = body.clientWidth;
            popupCommittedHeight = body.clientHeight;
        };

        var onResizeRequested = function() {
            var body = iframe.contentDocument.body;
            if ( body.getAttribute('data-resize-popup') !== 'true' ) {
                return;
            }
            body.removeAttribute('data-resize-popup');
            resizePopupDelayed();
        };

        var onPopupReady = function() {
            var win = this.contentWindow;

            if ( !win || win.location.host !== location.host ) {
                return;
            }

            if ( typeof tbb.onBeforePopupReady === 'function' ) {
                tbb.onBeforePopupReady.call(this);
            }

            resizePopupDelayed();

            var body = win.document.body;
            body.removeAttribute('data-resize-popup');
            var mutationObserver = new win.MutationObserver(onResizeRequested);
            mutationObserver.observe(body, {
                attributes: true,
                attributeFilter: [ 'data-resize-popup' ]
            });
        };

        iframe.addEventListener('load', onPopupReady, true);
    };
})();

/******************************************************************************/

// Firefox 28 and less

(function() {
    var tbb = vAPI.toolbarButton;
    if ( tbb.init !== null ) {
        return;
    }
    var CustomizableUI = null;
    var forceLegacyToolbarButton = vAPI.localStorage.getBool('forceLegacyToolbarButton');
    if ( !forceLegacyToolbarButton ) {
        try {
            CustomizableUI = Cu.import('resource:///modules/CustomizableUI.jsm', null).CustomizableUI;
        } catch (ex) {
        }
    }
    if ( CustomizableUI !== null ) {
        return;
    }

    tbb.codePath = 'legacy';
    tbb.id = 'umatrix-legacy-button';   // NOTE: must match legacy-toolbar-button.css
    tbb.viewId = tbb.id + '-panel';

    var styleSheetUri = null;

    var createToolbarButton = function(window) {
        var document = window.document;

        var toolbarButton = document.createElement('toolbarbutton');
        toolbarButton.setAttribute('id', tbb.id);
        // type = panel would be more accurate, but doesn't look as good
        toolbarButton.setAttribute('type', 'menu');
        toolbarButton.setAttribute('removable', 'true');
        toolbarButton.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional');
        toolbarButton.setAttribute('label', tbb.label);
        toolbarButton.setAttribute('tooltiptext', tbb.label);

        var toolbarButtonPanel = document.createElement('panel');
        // NOTE: Setting level to parent breaks the popup for PaleMoon under
        // linux (mouse pointer misaligned with content). For some reason.
        // toolbarButtonPanel.setAttribute('level', 'parent');
        tbb.populatePanel(document, toolbarButtonPanel);
        toolbarButtonPanel.addEventListener('popupshowing', tbb.onViewShowing);
        toolbarButtonPanel.addEventListener('popuphiding', tbb.onViewHiding);
        toolbarButton.appendChild(toolbarButtonPanel);

        return toolbarButton;
    };

    var addLegacyToolbarButton = function(window) {
        // uMatrix's stylesheet lazily added.
        if ( styleSheetUri === null ) {
            var sss = Cc["@mozilla.org/content/style-sheet-service;1"]
                        .getService(Ci.nsIStyleSheetService);
            styleSheetUri = Services.io.newURI(vAPI.getURL("css/legacy-toolbar-button.css"), null, null);

            // Register global so it works in all windows, including palette
            if ( !sss.sheetRegistered(styleSheetUri, sss.AUTHOR_SHEET) ) {
                sss.loadAndRegisterSheet(styleSheetUri, sss.AUTHOR_SHEET);
            }
        }

        var document = window.document;

        // https://github.com/gorhill/uMatrix/issues/357
        // Already installed?
        if ( document.getElementById(tbb.id) !== null ) {
            return;
        }

        var toolbox = document.getElementById('navigator-toolbox') ||
                      document.getElementById('mail-toolbox');
        if ( toolbox === null ) {
            return;
        }

        var toolbarButton = createToolbarButton(window);

        // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/toolbarpalette
        var palette = toolbox.palette;
        if ( palette && palette.querySelector('#' + tbb.id) === null ) {
            palette.appendChild(toolbarButton);
        }

        // Find the place to put the button.
        // Pale Moon: `toolbox.externalToolbars` can be undefined. Seen while
        //   testing popup test number 3:
        //   http://raymondhill.net/ublock/popup.html
        var toolbars = toolbox.externalToolbars ? toolbox.externalToolbars.slice() : [];
        for ( var child of toolbox.children ) {
            if ( child.localName === 'toolbar' ) {
                toolbars.push(child);
            }
        }

        for ( var toolbar of toolbars ) {
            var currentsetString = toolbar.getAttribute('currentset');
            if ( !currentsetString ) {
                continue;
            }
            var currentset = currentsetString.split(/\s*,\s*/);
            var index = currentset.indexOf(tbb.id);
            if ( index === -1 ) {
                continue;
            }
            // This can occur with Pale Moon:
            //   "TypeError: toolbar.insertItem is not a function"
            if ( typeof toolbar.insertItem !== 'function' ) {
                continue;
            }
            // Found our button on this toolbar - but where on it?
            var before = null;
            for ( var i = index + 1; i < currentset.length; i++ ) {
                before = toolbar.querySelector('[id="' + currentset[i] + '"]');
                if ( before !== null ) {
                    break;
                }
            }
            // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Method/insertItem
            toolbar.insertItem(tbb.id, before);
            break;
        }

        // https://github.com/gorhill/uBlock/issues/763
        // We are done if our toolbar button is already installed in one of the
        // toolbar.
        if ( palette !== null && toolbarButton.parentElement !== palette ) {
            return;
        }

        // No button yet so give it a default location. If forcing the button,
        // just put in in the palette rather than on any specific toolbar (who
        // knows what toolbars will be available or visible!)
        var navbar = document.getElementById('nav-bar');
        if ( navbar !== null && !vAPI.localStorage.getBool('legacyToolbarButtonAdded') ) {
            // https://github.com/gorhill/uBlock/issues/264
            // Find a child customizable palette, if any.
            navbar = navbar.querySelector('.customization-target') || navbar;
            navbar.appendChild(toolbarButton);
            navbar.setAttribute('currentset', navbar.currentSet);
            document.persist(navbar.id, 'currentset');
            vAPI.localStorage.setBool('legacyToolbarButtonAdded', 'true');
        }
    };

    var canAddLegacyToolbarButton = function(window) {
        var document = window.document;
        if (
            !document ||
            document.readyState !== 'complete' ||
            document.getElementById('nav-bar') === null
        ) {
            return false;
        }
        var toolbox = document.getElementById('navigator-toolbox') ||
                      document.getElementById('mail-toolbox');
        return toolbox !== null && !!toolbox.palette;
    };

    var onPopupCloseRequested = function({target}) {
        var document = target.ownerDocument;
        if ( !document ) {
            return;
        }
        var toolbarButtonPanel = document.getElementById(tbb.viewId);
        if ( toolbarButtonPanel === null ) {
            return;
        }
        // `hidePopup` reported as not existing while testing legacy button
        //  on FF 41.0.2.
        // https://bugzilla.mozilla.org/show_bug.cgi?id=1151796
        if ( typeof toolbarButtonPanel.hidePopup === 'function' ) {
            toolbarButtonPanel.hidePopup();
        }
    };

    var shutdown = function() {
        for ( var win of winWatcher.getWindows() ) {
            var toolbarButton = win.document.getElementById(tbb.id);
            if ( toolbarButton ) {
                toolbarButton.parentNode.removeChild(toolbarButton);
            }
        }
        if ( sss === null ) {
            return;
        }
        if ( sss.sheetRegistered(styleSheetUri, sss.AUTHOR_SHEET) ) {
            sss.unregisterSheet(styleSheetUri, sss.AUTHOR_SHEET);
        }
        sss = null;
        styleSheetUri = null;

        vAPI.messaging.globalMessageManager.removeMessageListener(
            location.host + ':closePopup',
            onPopupCloseRequested
        );
    };

    tbb.attachToNewWindow = function(win) {
        deferUntil(
            canAddLegacyToolbarButton.bind(null, win),
            addLegacyToolbarButton.bind(null, win)
        );
    };

    tbb.init = function() {
        vAPI.messaging.globalMessageManager.addMessageListener(
            location.host + ':closePopup',
            onPopupCloseRequested
        );

        cleanupTasks.push(shutdown);
    };
})();

/******************************************************************************/

// Firefox Australis < 36.

(function() {
    var tbb = vAPI.toolbarButton;
    if ( tbb.init !== null ) {
        return;
    }
    if ( Services.vc.compare(Services.appinfo.platformVersion, '36.0') >= 0 ) {
        return null;
    }
    if ( vAPI.localStorage.getBool('forceLegacyToolbarButton') ) {
        return null;
    }
    var CustomizableUI = null;
    try {
        CustomizableUI = Cu.import('resource:///modules/CustomizableUI.jsm', null).CustomizableUI;
    } catch (ex) {
    }
    if ( CustomizableUI === null ) {
        return;
    }
    tbb.codePath = 'australis';
    tbb.CustomizableUI = CustomizableUI;
    tbb.defaultArea = CustomizableUI.AREA_NAVBAR;

    var styleURI = null;

    var onPopupCloseRequested = function({target}) {
        if ( typeof tbb.closePopup === 'function' ) {
            tbb.closePopup(target);
        }
    };

    var shutdown = function() {
        CustomizableUI.destroyWidget(tbb.id);

        for ( var win of winWatcher.getWindows() ) {
            var panel = win.document.getElementById(tbb.viewId);
            panel.parentNode.removeChild(panel);
            win.QueryInterface(Ci.nsIInterfaceRequestor)
               .getInterface(Ci.nsIDOMWindowUtils)
               .removeSheet(styleURI, 1);
        }

        vAPI.messaging.globalMessageManager.removeMessageListener(
            location.host + ':closePopup',
            onPopupCloseRequested
        );
    };

    tbb.onBeforeCreated = function(doc) {
        var panel = doc.createElement('panelview');

        this.populatePanel(doc, panel);

        doc.getElementById('PanelUI-multiView').appendChild(panel);

        doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
            .getInterface(Ci.nsIDOMWindowUtils)
            .loadSheet(styleURI, 1);
    };

    tbb.onBeforePopupReady = function() {
        // https://github.com/gorhill/uBlock/issues/83
        // Add `portrait` class if width is constrained.
        try {
            this.contentDocument.body.classList.toggle(
                'portrait',
                CustomizableUI.getWidget(tbb.id).areaType === CustomizableUI.TYPE_MENU_PANEL
            );
        } catch (ex) {
            /* noop */
        }
    };

    tbb.init = function() {
        vAPI.messaging.globalMessageManager.addMessageListener(
            location.host + ':closePopup',
            onPopupCloseRequested
        );

        var style = [
            '#' + this.id + '.off {',
                'list-style-image: url(',
                    vAPI.getURL('img/browsericons/icon19-off.png'),
                ');',
            '}',
            '#' + this.id + ' {',
                'list-style-image: url(',
                    vAPI.getURL('img/browsericons/icon19.png'),
                ');',
            '}',
            '#' + this.viewId + ', #' + this.viewId + ' > iframe {',
                'width: 160px;',
                'height: 290px;',
                'overflow: hidden !important;',
            '}',
            '#' + this.id + '[badge]:not([badge=""])::after {',
                'position: absolute;',
                'margin-left: -16px;',
                'margin-top: 3px;',
                'padding: 1px 2px;',
                'font-size: 9px;',
                'font-weight: bold;',
                'color: #fff;',
                'background: #000;',
                'content: attr(badge);',
            '}'
        ];

        styleURI = Services.io.newURI(
            'data:text/css,' + encodeURIComponent(style.join('')),
            null,
            null
        );

        this.closePopup = function(tabBrowser) {
            CustomizableUI.hidePanelForNode(
                tabBrowser.ownerDocument.getElementById(this.viewId)
            );
        };

        CustomizableUI.createWidget(this);

        cleanupTasks.push(shutdown);
    };
})();

/******************************************************************************/

// Firefox Australis >= 36.

(function() {
    var tbb = vAPI.toolbarButton;
    if ( tbb.init !== null ) {
        return;
    }
    if ( Services.vc.compare(Services.appinfo.platformVersion, '36.0') < 0 ) {
        return null;
    }
    if ( vAPI.localStorage.getBool('forceLegacyToolbarButton') ) {
        return null;
    }
    var CustomizableUI = null;
    try {
        CustomizableUI = Cu.import('resource:///modules/CustomizableUI.jsm', null).CustomizableUI;
    } catch (ex) {
    }
    if ( CustomizableUI === null ) {
        return null;
    }
    tbb.codePath = 'australis';
    tbb.CustomizableUI = CustomizableUI;
    tbb.defaultArea = CustomizableUI.AREA_NAVBAR;

    var CUIEvents = {};

    var badgeCSSRules = [
        'background: #000',
        'color: #fff'
    ].join(';');

    var updateBadgeStyle = function() {
        for ( var win of winWatcher.getWindows() ) {
            var button = win.document.getElementById(tbb.id);
            if ( button === null ) {
                continue;
            }
            var badge = button.ownerDocument.getAnonymousElementByAttribute(
                button,
                'class',
                'toolbarbutton-badge'
            );
            if ( !badge ) {
                continue;
            }

            badge.style.cssText = badgeCSSRules;
        }
    };

    var updateBadge = function() {
        var wId = tbb.id;
        var buttonInPanel = CustomizableUI.getWidget(wId).areaType === CustomizableUI.TYPE_MENU_PANEL;

        for ( var win of winWatcher.getWindows() ) {
            var button = win.document.getElementById(wId);
            if ( button === null ) {
                continue;
            }
            if ( buttonInPanel ) {
                button.classList.remove('badged-button');
                continue;
            }
            button.classList.add('badged-button');
        }

        if ( buttonInPanel ) {
            return;
        }

        // Anonymous elements need some time to be reachable
        vAPI.setTimeout(updateBadgeStyle, 250);
    }.bind(CUIEvents);

    // https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/CustomizableUI.jsm#Listeners
    CUIEvents.onCustomizeEnd = updateBadge;
    CUIEvents.onWidgetAdded = updateBadge;
    CUIEvents.onWidgetUnderflow = updateBadge;

    var onPopupCloseRequested = function({target}) {
        if ( typeof tbb.closePopup === 'function' ) {
            tbb.closePopup(target);
        }
    };

    var shutdown = function() {
        for ( var win of winWatcher.getWindows() ) {
            var panel = win.document.getElementById(tbb.viewId);
            if ( panel !== null && panel.parentNode !== null ) {
                panel.parentNode.removeChild(panel);
            }
            win.QueryInterface(Ci.nsIInterfaceRequestor)
                .getInterface(Ci.nsIDOMWindowUtils)
                .removeSheet(styleURI, 1);
        }

        CustomizableUI.removeListener(CUIEvents);
        CustomizableUI.destroyWidget(tbb.id);

        vAPI.messaging.globalMessageManager.removeMessageListener(
            location.host + ':closePopup',
            onPopupCloseRequested
        );
    };

    var styleURI = null;

    tbb.onBeforeCreated = function(doc) {
        var panel = doc.createElement('panelview');

        this.populatePanel(doc, panel);

        doc.getElementById('PanelUI-multiView').appendChild(panel);

        doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
            .getInterface(Ci.nsIDOMWindowUtils)
            .loadSheet(styleURI, 1);
    };

    tbb.onCreated = function(button) {
        button.setAttribute('badge', '');
        vAPI.setTimeout(updateBadge, 250);
    };

    tbb.onBeforePopupReady = function() {
        // https://github.com/gorhill/uBlock/issues/83
        // Add `portrait` class if width is constrained.
        try {
            this.contentDocument.body.classList.toggle(
                'portrait',
                CustomizableUI.getWidget(tbb.id).areaType === CustomizableUI.TYPE_MENU_PANEL
            );
        } catch (ex) {
            /* noop */
        }
    };

    tbb.closePopup = function(tabBrowser) {
        CustomizableUI.hidePanelForNode(
            tabBrowser.ownerDocument.getElementById(tbb.viewId)
        );
    };

    tbb.init = function() {
        vAPI.messaging.globalMessageManager.addMessageListener(
            location.host + ':closePopup',
            onPopupCloseRequested
        );

        CustomizableUI.addListener(CUIEvents);

        var style = [
            '#' + this.id + '.off {',
                'list-style-image: url(',
                    vAPI.getURL('img/browsericons/icon19-off.png'),
                ');',
            '}',
            '#' + this.id + ' {',
                'list-style-image: url(',
                    vAPI.getURL('img/browsericons/icon19-19.png'),
                ');',
            '}',
            '#' + this.viewId + ', #' + this.viewId + ' > iframe {',
                'width: 160px;',
                'height: 290px;',
                'overflow: hidden !important;',
            '}'
        ];

        styleURI = Services.io.newURI(
            'data:text/css,' + encodeURIComponent(style.join('')),
            null,
            null
        );

        CustomizableUI.createWidget(this);

        cleanupTasks.push(shutdown);
    };
})();

/******************************************************************************/

// No toolbar button.

(function() {
    // Just to ensure the number of cleanup tasks is as expected: toolbar
    // button code is one single cleanup task regardless of platform.
    if ( vAPI.toolbarButton.init === null ) {
        cleanupTasks.push(function(){});
    }
})();

/******************************************************************************/

if ( vAPI.toolbarButton.init !== null ) {
    vAPI.toolbarButton.init();
}

/******************************************************************************/
/******************************************************************************/

vAPI.contextMenu = {
    contextMap: {
        frame: 'inFrame',
        link: 'onLink',
        image: 'onImage',
        audio: 'onAudio',
        video: 'onVideo',
        editable: 'onEditableArea'
    }
};

/******************************************************************************/

vAPI.contextMenu.displayMenuItem = function({target}) {
    var doc = target.ownerDocument;
    var gContextMenu = doc.defaultView.gContextMenu;
    if ( !gContextMenu.browser ) {
        return;
    }

    var menuitem = doc.getElementById(vAPI.contextMenu.menuItemId);
    var currentURI = gContextMenu.browser.currentURI;

    // https://github.com/chrisaljoudi/uBlock/issues/105
    // TODO: Should the element picker works on any kind of pages?
    if ( !currentURI.schemeIs('http') && !currentURI.schemeIs('https') ) {
        menuitem.setAttribute('hidden', true);
        return;
    }

    var ctx = vAPI.contextMenu.contexts;

    if ( !ctx ) {
        menuitem.setAttribute('hidden', false);
        return;
    }

    var ctxMap = vAPI.contextMenu.contextMap;

    for ( var context of ctx ) {
        if (
            context === 'page' &&
            !gContextMenu.onLink &&
            !gContextMenu.onImage &&
            !gContextMenu.onEditableArea &&
            !gContextMenu.inFrame &&
            !gContextMenu.onVideo &&
            !gContextMenu.onAudio
        ) {
            menuitem.setAttribute('hidden', false);
            return;
        }

        if (
            ctxMap.hasOwnProperty(context) &&
            gContextMenu[ctxMap[context]]
        ) {
            menuitem.setAttribute('hidden', false);
            return;
        }
    }

    menuitem.setAttribute('hidden', true);
};

/******************************************************************************/

vAPI.contextMenu.register = (function() {
    var register = function(doc) {
        if ( !this.menuItemId ) {
            return;
        }

        // Already installed?
        if ( doc.getElementById(this.menuItemId) !== null ) {
            return;
        }

        var contextMenu = doc.getElementById('contentAreaContextMenu');
        var menuitem = doc.createElement('menuitem');
        menuitem.setAttribute('id', this.menuItemId);
        menuitem.setAttribute('label', this.menuLabel);
        menuitem.setAttribute('image', vAPI.getURL('img/browsericons/icon19-19.png'));
        menuitem.setAttribute('class', 'menuitem-iconic');
        menuitem.addEventListener('command', this.onCommand);
        contextMenu.addEventListener('popupshowing', this.displayMenuItem);
        contextMenu.insertBefore(menuitem, doc.getElementById('inspect-separator'));
    };

    // https://github.com/gorhill/uBlock/issues/906
    // Be sure document.readyState is 'complete': it could happen at launch
    // time that we are called by vAPI.contextMenu.create() directly before
    // the environment is properly initialized.
    var registerSafely = function(doc, tryCount) {
        if ( doc.readyState === 'complete' ) {
            register.call(this, doc);
            return;
        }
        if ( typeof tryCount !== 'number' ) {
            tryCount = 0;
        }
        tryCount += 1;
        if ( tryCount < 8 ) {
            vAPI.setTimeout(registerSafely.bind(this, doc, tryCount), 200);
        }
    };

    return registerSafely;
})();

/******************************************************************************/

vAPI.contextMenu.unregister = function(doc) {
    if ( !this.menuItemId ) {
        return;
    }

    var menuitem = doc.getElementById(this.menuItemId);
    if ( menuitem === null ) {
        return;
    }
    var contextMenu = menuitem.parentNode;
    menuitem.removeEventListener('command', this.onCommand);
    contextMenu.removeEventListener('popupshowing', this.displayMenuItem);
    contextMenu.removeChild(menuitem);
};

/******************************************************************************/

vAPI.contextMenu.create = function(details, callback) {
    this.menuItemId = details.id;
    this.menuLabel = details.title;
    this.contexts = details.contexts;

    if ( Array.isArray(this.contexts) && this.contexts.length ) {
        this.contexts = this.contexts.indexOf('all') === -1 ? this.contexts : null;
    } else {
        // default in Chrome
        this.contexts = ['page'];
    }

    this.onCommand = function() {
        var gContextMenu = getOwnerWindow(this).gContextMenu;
        var details = {
            menuItemId: this.id
        };

        if ( gContextMenu.inFrame ) {
            details.tagName = 'iframe';
            // Probably won't work with e10s
            details.frameUrl = gContextMenu.focusedWindow.location.href;
        } else if ( gContextMenu.onImage ) {
            details.tagName = 'img';
            details.srcUrl = gContextMenu.mediaURL;
        } else if ( gContextMenu.onAudio ) {
            details.tagName = 'audio';
            details.srcUrl = gContextMenu.mediaURL;
        } else if ( gContextMenu.onVideo ) {
            details.tagName = 'video';
            details.srcUrl = gContextMenu.mediaURL;
        } else if ( gContextMenu.onLink ) {
            details.tagName = 'a';
            details.linkUrl = gContextMenu.linkURL;
        }

        callback(details, {
            id: tabWatcher.tabIdFromTarget(gContextMenu.browser),
            url: gContextMenu.browser.currentURI.asciiSpec
        });
    };

    for ( var win of winWatcher.getWindows() ) {
        this.register(win.document);
    }
};

/******************************************************************************/

vAPI.contextMenu.remove = function() {
    for ( var win of winWatcher.getWindows() ) {
        this.unregister(win.document);
    }

    this.menuItemId = null;
    this.menuLabel = null;
    this.contexts = null;
    this.onCommand = null;
};

/******************************************************************************/
/******************************************************************************/

var optionsObserver = (function() {
    var addonId = 'uMatrix@raymondhill.net';

    var commandHandler = function() {
        switch ( this.id ) {
        case 'showDashboardButton':
            vAPI.tabs.open({ url: 'dashboard.html', index: -1 });
            break;
        case 'showLoggerButton':
            vAPI.tabs.open({ url: 'logger-ui.html', index: -1 });
            break;
        default:
            break;
        }
    };

    var setupOptionsButton = function(doc, id) {
        var button = doc.getElementById(id);
        if ( button === null ) {
            return;
        }
        button.addEventListener('command', commandHandler);
        button.label = vAPI.i18n(id);
    };

    var setupOptionsButtons = function(doc) {
        setupOptionsButton(doc, 'showDashboardButton');
        setupOptionsButton(doc, 'showLoggerButton');
    };

    var observer = {
        observe: function(doc, topic, id) {
            if ( id !== addonId ) {
                return;
            }

            setupOptionsButtons(doc);
        }
    };

    // https://github.com/gorhill/uBlock/issues/948
    // Older versions of Firefox can throw here when looking up `currentURI`.

    var canInit = function() {
        try {
            var tabBrowser = tabWatcher.currentBrowser();
            return tabBrowser &&
                   tabBrowser.currentURI &&
                   tabBrowser.currentURI.spec === 'about:addons' &&
                   tabBrowser.contentDocument &&
                   tabBrowser.contentDocument.readyState === 'complete';
        } catch (ex) {
        }
    };

    // Manually add the buttons if the `about:addons` page is already opened.

    var init = function() {
        if ( canInit() ) {
            setupOptionsButtons(tabWatcher.currentBrowser().contentDocument);
        }
    };

    var unregister = function() {
        Services.obs.removeObserver(observer, 'addon-options-displayed');
    };

    var register = function() {
        Services.obs.addObserver(observer, 'addon-options-displayed', false);
        cleanupTasks.push(unregister);
        deferUntil(canInit, init, { next: 463 });
    };

    return {
        register: register,
        unregister: unregister
    };
})();

optionsObserver.register();

/******************************************************************************/
/******************************************************************************/

vAPI.lastError = function() {
    return null;
};

/******************************************************************************/
/******************************************************************************/

// This is called only once, when everything has been loaded in memory after
// the extension was launched. It can be used to inject content scripts
// in already opened web pages, to remove whatever nuisance could make it to
// the web pages before uBlock was ready.

vAPI.onLoadAllCompleted = function() {
    for ( var browser of tabWatcher.browsers() ) {
        browser.messageManager.sendAsyncMessage(
            location.host + '-load-completed'
        );
    }
};

/******************************************************************************/
/******************************************************************************/

// Likelihood is that we do not have to punycode: given punycode overhead,
// it's faster to check and skip than do it unconditionally all the time.

var punycodeHostname = punycode.toASCII;
var isNotASCII = /[^\x21-\x7F]/;

vAPI.punycodeHostname = function(hostname) {
    return isNotASCII.test(hostname) ? punycodeHostname(hostname) : hostname;
};

vAPI.punycodeURL = function(url) {
    if ( isNotASCII.test(url) ) {
        return Services.io.newURI(url, null, null).asciiSpec;
    }
    return url;
};

/******************************************************************************/
/******************************************************************************/

vAPI.cloud = (function() {
    var extensionBranchPath = 'extensions.' + location.host;
    var cloudBranchPath = extensionBranchPath + '.cloudStorage';

    // https://github.com/gorhill/uBlock/issues/80#issuecomment-132081658
    //   We must use get/setComplexValue in order to properly handle strings
    //   with unicode characters.
    var iss = Ci.nsISupportsString;
    var argstr = Components.classes['@mozilla.org/supports-string;1']
                           .createInstance(iss);

    var options = {
        defaultDeviceName: '',
        deviceName: ''
    };

    // User-supplied device name.
    try {
        options.deviceName = Services.prefs
                                     .getBranch(extensionBranchPath + '.')
                                     .getComplexValue('deviceName', iss)
                                     .data;
    } catch(ex) {
    }

    var getDefaultDeviceName = function() {
        var name = '';
        try {
            name = Services.prefs
                           .getBranch('services.sync.client.')
                           .getComplexValue('name', iss)
                           .data;
        } catch(ex) {
        }

        return name || window.navigator.platform || window.navigator.oscpu;
    };

    var start = function(dataKeys) {
        var extensionBranch = Services.prefs.getBranch(extensionBranchPath + '.');
        var syncBranch = Services.prefs.getBranch('services.sync.prefs.sync.');

        // Mark config entries as syncable
        argstr.data = '';
        var dataKey;
        for ( var i = 0; i < dataKeys.length; i++ ) {
            dataKey = dataKeys[i];
            if ( extensionBranch.prefHasUserValue('cloudStorage.' + dataKey) === false ) {
                extensionBranch.setComplexValue('cloudStorage.' + dataKey, iss, argstr);
            }
            syncBranch.setBoolPref(cloudBranchPath + '.' + dataKey, true);
        }
    };

    var push = function(datakey, data, callback) {
        var branch = Services.prefs.getBranch(cloudBranchPath + '.');
        var bin = {
            'source': options.deviceName || getDefaultDeviceName(),
            'tstamp': Date.now(),
            'data': data,
            'size': 0
        };
        bin.size = JSON.stringify(bin).length;
        argstr.data = JSON.stringify(bin);
        branch.setComplexValue(datakey, iss, argstr);
        if ( typeof callback === 'function' ) {
            callback();
        }
    };

    var pull = function(datakey, callback) {
        var result = null;
        var branch = Services.prefs.getBranch(cloudBranchPath + '.');
        try {
            var json = branch.getComplexValue(datakey, iss).data;
            if ( typeof json === 'string' ) {
                result = JSON.parse(json);
            }
        } catch(ex) {
        }
        callback(result);
    };

    var getOptions = function(callback) {
        if ( typeof callback !== 'function' ) {
            return;
        }
        options.defaultDeviceName = getDefaultDeviceName();
        callback(options);
    };

    var setOptions = function(details, callback) {
        if ( typeof details !== 'object' || details === null ) {
            return;
        }

        var branch = Services.prefs.getBranch(extensionBranchPath + '.');

        if ( typeof details.deviceName === 'string' ) {
            argstr.data = details.deviceName;
            branch.setComplexValue('deviceName', iss, argstr);
            options.deviceName = details.deviceName;
        }

        getOptions(callback);
    };

    return {
        start: start,
        push: push,
        pull: pull,
        getOptions: getOptions,
        setOptions: setOptions
    };
})();

/******************************************************************************/
/******************************************************************************/

vAPI.browserData = {};

/******************************************************************************/

// https://developer.mozilla.org/en-US/docs/HTTP_Cache

vAPI.browserData.clearCache = function(callback) {
    // PURGE_DISK_DATA_ONLY:1
    // PURGE_DISK_ALL:2
    // PURGE_EVERYTHING:3
    // However I verified that no argument does clear the cache data.
    // There is no cache2 for older versions of Firefox.
    if ( Services.cache2 ) {
        Services.cache2.clear();
    } else if ( Services.cache ) {
        Services.cache.evictEntries(Services.cache.STORE_ON_DISK);
    }
    if ( typeof callback === 'function' ) {
        callback();
    }
};

/******************************************************************************/

vAPI.browserData.clearOrigin = function(/* domain */) {
    // TODO
};

/******************************************************************************/
/******************************************************************************/

// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsICookieManager2
// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsICookie2
// https://developer.mozilla.org/en-US/docs/Observer_Notifications#Cookies

vAPI.cookies = {};

/******************************************************************************/

vAPI.cookies.CookieEntry = function(ffCookie) {
    this.domain = ffCookie.host;
    this.name = ffCookie.name;
    this.path = ffCookie.path;
    this.secure = ffCookie.isSecure === true;
    this.session = ffCookie.expires === 0;
    this.value = ffCookie.value;
};

/******************************************************************************/

vAPI.cookies.start = function() {
    Services.obs.addObserver(this, 'cookie-changed', false);
    Services.obs.addObserver(this, 'private-cookie-changed', false);
    cleanupTasks.push(this.stop.bind(this));
};

/******************************************************************************/

vAPI.cookies.stop = function() {
    Services.obs.removeObserver(this, 'cookie-changed');
    Services.obs.removeObserver(this, 'private-cookie-changed');
};

/******************************************************************************/

vAPI.cookies.observe = function(subject, topic, reason) {
    //if ( topic !== 'cookie-changed' && topic !== 'private-cookie-changed' ) {
    //    return;
    //}
    //
    if ( reason === 'cleared' && typeof this.onAllRemoved === 'function' ) {
        this.onAllRemoved();
        return;
    }
    if ( subject === null ) {
        return;
    }
    if ( subject instanceof Ci.nsICookie2 === false ) {
        try {
            subject = subject.QueryInterface(Ci.nsICookie2);
        } catch (ex) {
            return;
        }
    }
    if ( reason === 'deleted' ) {
        if ( typeof this.onRemoved === 'function' ) {
            this.onRemoved(new this.CookieEntry(subject));
        }
        return;
    }
    if ( typeof this.onChanged === 'function' ) {
        this.onChanged(new this.CookieEntry(subject));
    }
};

/******************************************************************************/

// Meant and expected to be asynchronous.

vAPI.cookies.getAll = function(callback) {
    if ( typeof callback !== 'function' ) {
        return;
    }
    var onAsync = function() {
        var out = [];
        var enumerator = Services.cookies.enumerator;
        var ffcookie;
        while ( enumerator.hasMoreElements() ) {
            ffcookie = enumerator.getNext();
            if ( ffcookie instanceof Ci.nsICookie ) {
                out.push(new this.CookieEntry(ffcookie));
            }
        }
        callback(out);
    };
    vAPI.setTimeout(onAsync.bind(this), 0);
};

/******************************************************************************/

vAPI.cookies.remove = function(details, callback) {
    var uri = Services.io.newURI(details.url, null, null);
    var cookies = Services.cookies;
    cookies.remove(uri.asciiHost, details.name, uri.path, false, {});
    cookies.remove( '.' + uri.asciiHost, details.name, uri.path, false, {});
    if ( typeof callback === 'function' ) {
        callback({
            domain: uri.asciiHost,
            name: details.name,
            path: uri.path
        });
    }
};

/******************************************************************************/
/******************************************************************************/

})();

/******************************************************************************/