diff --git a/platform/firefox/bootstrap.js b/platform/firefox/bootstrap.js
new file mode 100644
index 000000000..f9fae1756
--- /dev/null
+++ b/platform/firefox/bootstrap.js
@@ -0,0 +1,46 @@
+/* global Services, APP_STARTUP, APP_SHUTDOWN */
+/* exported startup, shutdown, install, uninstall */
+
+'use strict';
+
+var bgProcess;
+Components.utils['import']('resource://gre/modules/Services.jsm');
+
+function startup(data, reason) {
+ bgProcess = function(ev) {
+ if (ev) {
+ this.removeEventListener('load', bgProcess);
+ }
+
+ bgProcess = Services.appShell.hiddenDOMWindow.document;
+ bgProcess = bgProcess.documentElement.appendChild(
+ bgProcess.createElementNS('http://www.w3.org/1999/xhtml', 'iframe')
+ );
+ bgProcess.setAttribute('src', 'chrome://ublock/content/background.html');
+ };
+
+ if (reason === APP_STARTUP) {
+ Services.ww.registerNotification({
+ observe: function(subject) {
+ Services.ww.unregisterNotification(this);
+ subject.addEventListener('load', bgProcess);
+ }
+ });
+ }
+ else {
+ bgProcess();
+ }
+}
+
+function shutdown(data, reason) {
+ if (reason !== APP_SHUTDOWN) {
+ bgProcess.parentNode.removeChild(bgProcess);
+ }
+}
+
+function install() {
+ // https://bugzil.la/719376
+ Services.strings.flushBundles();
+}
+
+function uninstall() {}
\ No newline at end of file
diff --git a/platform/firefox/chrome.manifest b/platform/firefox/chrome.manifest
new file mode 100644
index 000000000..1206c29cd
--- /dev/null
+++ b/platform/firefox/chrome.manifest
@@ -0,0 +1,31 @@
+content ublock ./
+locale ublock en ./locale/en/
+locale ublock ar ./locale/ar/
+locale ublock cs ./locale/cs/
+locale ublock da ./locale/da/
+locale ublock de ./locale/de/
+locale ublock el ./locale/el/
+locale ublock es ./locale/es/
+locale ublock et ./locale/et/
+locale ublock fi ./locale/fi/
+locale ublock fr ./locale/fr/
+locale ublock he ./locale/he/
+locale ublock hi ./locale/hi/
+locale ublock hr ./locale/hr/
+locale ublock hu ./locale/hu/
+locale ublock id ./locale/id/
+locale ublock it ./locale/it/
+locale ublock ja ./locale/ja/
+locale ublock mr ./locale/mr/
+locale ublock nb ./locale/nb/
+locale ublock nl ./locale/nl/
+locale ublock pl ./locale/pl/
+locale ublock pt-BR ./locale/pt-BR/
+locale ublock pt-PT ./locale/pt-PT/
+locale ublock ro ./locale/ro/
+locale ublock ru ./locale/ru/
+locale ublock sv ./locale/sv/
+locale ublock tr ./locale/tr/
+locale ublock uk ./locale/uk/
+locale ublock vi ./locale/vi/
+locale ublock zh-CN ./locale/zh-CN/
\ No newline at end of file
diff --git a/platform/firefox/frameScript.js b/platform/firefox/frameScript.js
new file mode 100644
index 000000000..30dd94acb
--- /dev/null
+++ b/platform/firefox/frameScript.js
@@ -0,0 +1,32 @@
+/* globals Services, sendAsyncMessage, addMessageListener, removeMessageListener, content */
+
+(function() {
+
+'use strict';
+
+var
+ app_name = 'ublock',
+ app_baseURI = 'chrome://' + app_name + '/content/js/',
+ listeners = {},
+ _addMessageListener = function(id, fn) {
+ _removeMessageListener(id);
+ listeners[id] = function(msg) {
+ fn(msg.data);
+ };
+ addMessageListener(id, listeners[id]);
+ },
+ _removeMessageListener = function(id) {
+ if (listeners[id]) {
+ removeMessageListener(id, listeners[id]);
+ }
+
+ delete listeners[id];
+ };
+
+addMessageListener('µBlock:broadcast', function(msg) {
+ for (var id in listeners) {
+ listeners[id](msg.data);
+ }
+});
+
+})();
\ No newline at end of file
diff --git a/platform/firefox/install.rdf b/platform/firefox/install.rdf
new file mode 100644
index 000000000..bc2f7b3b5
--- /dev/null
+++ b/platform/firefox/install.rdf
@@ -0,0 +1,34 @@
+
+
+
+ {2b10c1c8-a11f-4bad-fe9c-1c11e82cac42}
+ 0.7.0.11
+ µBlock
+ Finally, an efficient blocker. Easy on CPU and memory.
+ https://github.com/gorhill/uBlock
+ Raymond Hill
+ 2
+ true
+ true
+ 3
+ chrome://ublock/content/dashboard.html
+
+
+
+
+ {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
+ 24.0
+ 37.0
+
+
+
+
+
+
+ {92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}
+ 2.21
+ 2.34
+
+
+
+
diff --git a/platform/firefox/vapi-background.js b/platform/firefox/vapi-background.js
new file mode 100644
index 000000000..a80bc51e1
--- /dev/null
+++ b/platform/firefox/vapi-background.js
@@ -0,0 +1,181 @@
+/*******************************************************************************
+
+ µBlock - a Chromium browser extension to block requests.
+ Copyright (C) 2014 The µBlock 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/uBlock
+*/
+
+/* global Services */
+
+// For background page
+
+/******************************************************************************/
+
+(function() {
+
+'use strict';
+
+/******************************************************************************/
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu['import']('resource://gre/modules/Services.jsm');
+
+/******************************************************************************/
+
+self.vAPI = self.vAPI || {};
+
+vAPI.firefox = true;
+
+/******************************************************************************/
+
+vAPI.messaging = {
+ gmm: Cc['@mozilla.org/globalmessagemanager;1'].getService(Ci.nsIMessageListenerManager),
+ frameScript: 'chrome://ublock/content/frameScript.js',
+ listeners: {},
+ defaultHandler: null,
+ NOOPFUNC: function(){},
+ UNHANDLED: 'vAPI.messaging.notHandled'
+};
+
+/******************************************************************************/
+
+vAPI.messaging.gmm.loadFrameScript(vAPI.messaging.frameScript, true);
+
+/******************************************************************************/
+
+vAPI.messaging.listen = function(listenerName, callback) {
+ this.listeners[listenerName] = callback;
+};
+
+/******************************************************************************/
+
+vAPI.messaging.onMessage = function(request) {
+ var messageManager = request.target.messageManager;
+ var listenerId = request.data.portName.split('|');
+ var portName = listenerId[1];
+ listenerId = listenerId[0];
+
+ var callback = vAPI.messaging.NOOPFUNC;
+ if ( request.data.requestId !== undefined ) {
+ callback = function(response) {
+ messageManager.sendAsyncMessage(
+ listenerId,
+ JSON.stringify({
+ requestId: request.data.requestId,
+ portName: portName,
+ msg: response !== undefined ? response : null
+ })
+ );
+ };
+ }
+
+ // TODO:
+ var sender = {
+ tab: {
+ id: 0
+ }
+ };
+
+ // Specific handler
+ var r = vAPI.messaging.UNHANDLED;
+ var listener = vAPI.messaging.listeners[portName];
+ if ( typeof listener === 'function' ) {
+ r = listener(request.data.msg, sender, callback);
+ }
+ if ( r !== vAPI.messaging.UNHANDLED ) {
+ return;
+ }
+
+ // Default handler
+ r = vAPI.messaging.defaultHandler(request.data.msg, sender, callback);
+ if ( r !== vAPI.messaging.UNHANDLED ) {
+ return;
+ }
+
+ console.error('µBlock> messaging > unknown request: %o', request.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.gmm.addMessageListener(vAPI.app.name + ':background', this.onMessage);
+};
+
+/******************************************************************************/
+
+vAPI.messaging.broadcast = function(msg) {
+ this.gmm.broadcastAsyncMessage(vAPI.app.name + ':broadcast', msg);
+};
+
+/******************************************************************************/
+
+vAPI.lastError = function() {
+ return null;
+};
+
+/******************************************************************************/
+
+// clean up when the extension is disabled
+
+window.addEventListener('unload', function() {
+ vAPI.messaging.gmm.removeMessageListener(
+ app.name + ':background',
+ vAPI.messaging.postMessage
+ );
+ vAPI.messaging.gmm.removeDelayedFrameScript(vAPI.messaging.frameScript);
+
+ // close extension tabs
+ var enumerator = Services.wm.getEnumerator('navigator:browser');
+ var host = 'ublock';
+ var gBrowser, tabs, i, extURI;
+
+ while (enumerator.hasMoreElements()) {
+ gBrowser = enumerator.getNext().gBrowser;
+ tabs = gBrowser.tabs;
+ i = tabs.length;
+
+ while (i--) {
+ extURI = tabs[i].linkedBrowser.currentURI;
+
+ if (extURI.scheme === 'chrome' && extURI.host === host) {
+ gBrowser.removeTab(tabs[i]);
+ }
+ }
+ }
+});
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
diff --git a/platform/firefox/vapi-client.js b/platform/firefox/vapi-client.js
new file mode 100644
index 000000000..871996dea
--- /dev/null
+++ b/platform/firefox/vapi-client.js
@@ -0,0 +1,148 @@
+/*******************************************************************************
+
+ µBlock - a Chromium browser extension to block requests.
+ Copyright (C) 2014 The µBlock 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/uBlock
+*/
+
+// For non background pages
+
+/******************************************************************************/
+
+(function() {
+
+'use strict';
+
+/******************************************************************************/
+
+self.vAPI = self.vAPI || {};
+vAPI.firefox = true;
+
+/******************************************************************************/
+
+var messagingConnector = function(response) {
+ if ( !response ) {
+ return;
+ }
+
+ var channels = vAPI.messaging.channels;
+ var channel, listener;
+
+ if ( response.broadcast === true ) {
+ for ( channel in channels ) {
+ if ( channels.hasOwnProperty(channel) === false ) {
+ continue;
+ }
+ listener = channels[channel].listener;
+ if ( typeof listener === 'function' ) {
+ listener(response.msg);
+ }
+ }
+ return;
+ }
+
+ if ( response.requestId ) {
+ listener = vAPI.messaging.listeners[response.requestId];
+ delete vAPI.messaging.listeners[response.requestId];
+ delete response.requestId;
+ }
+
+ if ( !listener ) {
+ channel = channels[response.portName];
+ listener = channel && channel.listener;
+ }
+
+ if ( typeof listener === 'function' ) {
+ listener(response.msg);
+ }
+};
+
+/******************************************************************************/
+
+var uniqueId = function() {
+ return parseInt(Math.random() * 1e10, 10).toString(36);
+};
+
+/******************************************************************************/
+
+vAPI.messaging = {
+ channels: {},
+ listeners: {},
+ requestId: 1,
+ connectorId: uniqueId(),
+
+ setup: function() {
+ this.connector = function(msg) {
+ messagingConnector(JSON.parse(msg));
+ };
+ addMessageListener(this.connectorId, this.connector);
+ },
+
+ close: function() {
+ if (this.connector) {
+ removeMessageListener(this.connectorId, this.connector);
+ this.connector = null;
+ this.channels = {};
+ this.listeners = {};
+ }
+ },
+
+ channel: function(channelName, callback) {
+ if ( !channelName ) {
+ return;
+ }
+
+ this.channels[channelName] = {
+ portName: channelName,
+ listener: typeof callback === 'function' ? callback : null,
+ send: function(message, callback) {
+ if ( !vAPI.messaging.connector ) {
+ vAPI.messaging.setup();
+ }
+
+ message = {
+ portName: vAPI.messaging.connectorId + '|' + this.portName,
+ msg: message
+ };
+
+ if ( callback ) {
+ message.requestId = vAPI.messaging.requestId++;
+ vAPI.messaging.listeners[message.requestId] = callback;
+ }
+
+ sendAsyncMessage('µBlock:background', message);
+ },
+ close: function() {
+ delete vAPI.messaging.channels[this.portName];
+ }
+ };
+
+ return this.channels[channelName];
+ }
+};
+
+/******************************************************************************/
+
+vAPI.canExecuteContentScript = function() {
+ return true;
+};
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
diff --git a/platform/firefox/vapi-common.js b/platform/firefox/vapi-common.js
new file mode 100644
index 000000000..45825e77d
--- /dev/null
+++ b/platform/firefox/vapi-common.js
@@ -0,0 +1,96 @@
+/*******************************************************************************
+
+ µBlock - a Chromium browser extension to block requests.
+ Copyright (C) 2014 The µBlock 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/uBlock
+*/
+
+// For background page or non-background pages
+
+/******************************************************************************/
+
+(function() {
+
+'use strict';
+
+self.vAPI = self.vAPI || {};
+
+/******************************************************************************/
+
+// http://www.w3.org/International/questions/qa-scripts#directions
+
+var setScriptDirection = function(language) {
+ document.body.setAttribute(
+ 'dir',
+ ['ar', 'he', 'fa', 'ps', 'ur'].indexOf(language) !== -1 ? 'rtl' : 'ltr'
+ );
+};
+
+/******************************************************************************/
+
+vAPI.download = function(details) {
+ if ( !details.url ) {
+ return;
+ }
+
+ var a = document.createElement('a');
+
+ if ( 'download' in a ) {
+ a.href = details.url;
+ a.setAttribute('download', details.filename || '');
+ a.dispatchEvent(new MouseEvent('click'));
+ return;
+ }
+ var messager = vAPI.messaging.channel('_download');
+ messager.send({
+ what: 'gotoURL',
+ details: {
+ url: details.url,
+ index: -1
+ }
+ });
+ messager.close();
+};
+
+/******************************************************************************/
+
+vAPI.getURL = function(path) {
+ return 'chrome://ublock/content/' + path.replace(/^\/+/, '');
+};
+
+vAPI.i18n = (function() {
+ var stringBundle = Components.classes['@mozilla.org/intl/stringbundle;1']
+ .getService(Components.interfaces.nsIStringBundleService)
+ .createBundle('chrome://ublock/locale/messages.properties');
+
+ return function(s) {
+ try {
+ return stringBundle.GetStringFromName(s);
+ } catch (ex) {
+ return s;
+ }
+ };
+})();
+
+setScriptDirection(navigator.language);
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
+
diff --git a/platform/safari/Settings.plist b/platform/safari/Settings.plist
index 6c80bbb43..40cc1ff16 100644
--- a/platform/safari/Settings.plist
+++ b/platform/safari/Settings.plist
@@ -2,21 +2,21 @@
-
- DefaultValue
-
- FalseValue
-
- Key
- open_prefs
- Secure
-
- Title
- Click to see the Preferences
- TrueValue
-
- Type
- CheckBox
-
+
+ DefaultValue
+
+ FalseValue
+
+ Key
+ open_prefs
+ Secure
+
+ Title
+ Click to see the Preferences
+ TrueValue
+
+ Type
+ CheckBox
+
diff --git a/platform/safari/Update.plist b/platform/safari/Update.plist
index 969fe646e..1dea60702 100644
--- a/platform/safari/Update.plist
+++ b/platform/safari/Update.plist
@@ -14,7 +14,7 @@
CFBundleVersion
{buildNumber}
URL
- https://.../uBlock.safariextz
+ https://github.com/gorhill/uBlock/releases/download/0.7.0.11/uBlock-0.7.0.11.safariextz
diff --git a/tools/make-firefox.sh b/tools/make-firefox.sh
new file mode 100644
index 000000000..aeb6c31b7
--- /dev/null
+++ b/tools/make-firefox.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+#
+# This script assumes a linux environment
+
+echo "*** uBlock_xpi: Copying files"
+# use underscore instead of a dot!
+DES=dist/build/uBlock_xpi
+rm -r $DES
+mkdir -p $DES
+cp -R assets $DES/
+rm $DES/assets/*.sh
+cp -R src/css $DES/
+cp -R src/img $DES/
+cp -R src/js $DES/
+cp -R src/lib $DES/
+cp -R src/_locales $DES/
+cp src/*.html $DES/
+cp src/img/icon_128.png $DES/icon.png
+cp platform/vapi-appinfo.js $DES/js/
+cp platform/firefox/vapi-*.js $DES/js/
+cp platform/firefox/bootstrap.js $DES/
+cp platform/firefox/frameScript.js $DES/
+cp platform/firefox/chrome.manifest $DES/
+cp platform/firefox/install.rdf $DES/
+
+python tools/xpi-convert-locale.py $DES/
+
+echo "*** uBlock_xpi: Package done."
diff --git a/tools/xpi-convert-locale.py b/tools/xpi-convert-locale.py
new file mode 100644
index 000000000..1467f3612
--- /dev/null
+++ b/tools/xpi-convert-locale.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+
+import os
+import json
+import sys
+from shutil import rmtree as rmt
+from collections import OrderedDict
+
+if not sys.argv[1]:
+ raise SystemExit('Build dir missing.')
+
+osp = os.path
+pj = osp.join
+
+
+def rmtree(path):
+ if osp.exists(path):
+ rmt(path)
+
+
+def mkdirs(path):
+ try:
+ os.makedirs(path)
+ finally:
+ return osp.exists(path)
+
+
+build_dir = osp.abspath(sys.argv[1])
+source_locale_dir = pj(build_dir, '_locales')
+target_locale_dir = pj(build_dir, 'locale')
+
+for alpha2 in os.listdir(source_locale_dir):
+ locale_path = pj(source_locale_dir, alpha2, 'messages.json')
+ with open(locale_path, encoding='utf-8') as f:
+ string_data = json.load(f, object_pairs_hook=OrderedDict)
+
+ alpha2 = alpha2.replace('_', '-')
+
+ mkdirs(pj(target_locale_dir, alpha2))
+
+ locale_path = pj(target_locale_dir, alpha2, 'messages.properties')
+ with open(locale_path, 'wt', encoding='utf-8', newline='\n') as f:
+ for string_name in string_data:
+ f.write(string_name)
+ f.write('=')
+ f.write(string_data[string_name]['message'].replace('\n', r'\n'))
+ f.write('\n')
+
+rmtree(source_locale_dir)