diff --git a/platform/webext/img/browsericons/icon19-off.png b/platform/webext/img/browsericons/icon19-off.png
new file mode 100644
index 000000000..79fb3b823
Binary files /dev/null and b/platform/webext/img/browsericons/icon19-off.png differ
diff --git a/platform/webext/img/browsericons/icon19.png b/platform/webext/img/browsericons/icon19.png
new file mode 100644
index 000000000..e6d57bdda
Binary files /dev/null and b/platform/webext/img/browsericons/icon19.png differ
diff --git a/platform/webext/img/browsericons/icon38-off.png b/platform/webext/img/browsericons/icon38-off.png
new file mode 100644
index 000000000..a1b5c00bf
Binary files /dev/null and b/platform/webext/img/browsericons/icon38-off.png differ
diff --git a/platform/webext/img/browsericons/icon38.png b/platform/webext/img/browsericons/icon38.png
new file mode 100644
index 000000000..09a57d2ff
Binary files /dev/null and b/platform/webext/img/browsericons/icon38.png differ
diff --git a/platform/webext/is-webrtc-supported.html b/platform/webext/is-webrtc-supported.html
new file mode 100644
index 000000000..15c155583
--- /dev/null
+++ b/platform/webext/is-webrtc-supported.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/platform/webext/is-webrtc-supported.js b/platform/webext/is-webrtc-supported.js
new file mode 100644
index 000000000..5a3eea1e7
--- /dev/null
+++ b/platform/webext/is-webrtc-supported.js
@@ -0,0 +1,52 @@
+/*******************************************************************************
+
+ uBlock Origin - a browser extension to block requests.
+ Copyright (C) 2015 Raymond Hill
+
+ 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
+*/
+
+// https://github.com/gorhill/uBlock/issues/533#issuecomment-164292868
+// If WebRTC is supported, there won't be an exception if we
+// try to instanciate a peer connection object.
+
+// https://github.com/gorhill/uBlock/issues/533#issuecomment-168097594
+// Because Chromium leaks WebRTC connections after they have been closed
+// and forgotten, we need to test for WebRTC support inside an iframe, this
+// way the closed and forgottetn WebRTC connections are properly garbage
+// collected.
+
+(function() {
+ 'use strict';
+
+ var pc = null;
+ try {
+ var PC = self.RTCPeerConnection || self.webkitRTCPeerConnection;
+ if ( PC ) {
+ pc = new PC(null);
+ }
+ } catch (ex) {
+ console.error(ex);
+ }
+ if ( pc !== null ) {
+ pc.close();
+ }
+
+ window.top.postMessage(
+ pc !== null ? 'webRTCSupported' : 'webRTCNotSupported',
+ window.location.origin
+ );
+})();
diff --git a/platform/webext/managed_storage.json b/platform/webext/managed_storage.json
new file mode 100644
index 000000000..d3f2c864f
--- /dev/null
+++ b/platform/webext/managed_storage.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "http://json-schema.org/draft-03/schema#",
+ "type": "object",
+ "properties": {
+ "adminSettings": {
+ "title": "A valid JSON string compliant with uBO's backup format.",
+ "description": "All entries present will overwrite local settings.",
+ "type": "string"
+ }
+ }
+}
diff --git a/platform/webext/manifest.json b/platform/webext/manifest.json
new file mode 100644
index 000000000..311506494
--- /dev/null
+++ b/platform/webext/manifest.json
@@ -0,0 +1,64 @@
+{
+ "manifest_version": 2,
+
+ "name": "uBlock Origin",
+ "version": "1.9.15.3",
+
+ "default_locale": "en",
+ "description": "__MSG_extShortDesc__",
+ "icons": {
+ "16": "img/icon_16.png",
+ "128": "img/icon_128.png"
+ },
+
+ "browser_action": {
+ "default_icon": {
+ "19": "img/browsericons/icon19.png",
+ "38": "img/browsericons/icon38.png"
+ },
+ "default_title": "uBlock Origin",
+ "default_popup": "popup.html"
+ },
+
+ "author": "All uBlock Origin contributors",
+ "background": {
+ "page": "background.html"
+ },
+ "content_scripts": [
+ {
+ "matches": ["http://*/*", "https://*/*"],
+ "js": ["js/vapi-client.js", "js/contentscript.js"],
+ "run_at": "document_start",
+ "all_frames": true
+ },
+ {
+ "matches": ["http://*/*", "https://*/*"],
+ "js": ["js/scriptlets/subscriber.js"],
+ "run_at": "document_idle",
+ "all_frames": false
+ }
+ ],
+ "incognito": "split",
+ "minimum_chrome_version": "26.0",
+ "optional_permissions": [ "file:///*" ],
+ "options_page": "dashboard.html",
+ "options_ui": {
+ "page": "options_ui.html"
+ },
+ "permissions": [
+ "contextMenus",
+ "privacy",
+ "storage",
+ "tabs",
+ "unlimitedStorage",
+ "webNavigation",
+ "webRequest",
+ "webRequestBlocking",
+ "http://*/*",
+ "https://*/*"
+ ],
+ "short_name": "uBlock₀",
+ "storage": {
+ "managed_schema": "managed_storage.json"
+ }
+}
diff --git a/platform/webext/options_ui.html b/platform/webext/options_ui.html
new file mode 100644
index 000000000..d94c001ef
--- /dev/null
+++ b/platform/webext/options_ui.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+Dashboard
+
+
diff --git a/platform/webext/options_ui.js b/platform/webext/options_ui.js
new file mode 100644
index 000000000..1e7f552b1
--- /dev/null
+++ b/platform/webext/options_ui.js
@@ -0,0 +1,47 @@
+/*******************************************************************************
+
+ µBlock - a browser extension to block requests.
+ Copyright (C) 2015 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
+*/
+
+/******************************************************************************/
+
+(function() {
+
+/******************************************************************************/
+
+'use strict';
+
+vAPI.messaging.send(
+ 'default',
+ {
+ what: 'gotoURL',
+ details: {
+ url: 'dashboard.html',
+ select: true,
+ index: -1
+ }
+ }
+);
+window.close();
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
diff --git a/platform/webext/polyfill.js b/platform/webext/polyfill.js
new file mode 100644
index 000000000..ca8d82cd2
--- /dev/null
+++ b/platform/webext/polyfill.js
@@ -0,0 +1,214 @@
+/*******************************************************************************
+
+ uBlock Origin - a browser extension to block requests.
+ Copyright (C) 2014-2016 The 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/uBlock
+*/
+
+// For background page or non-background pages
+
+'use strict';
+
+/******************************************************************************/
+/******************************************************************************/
+
+// https://github.com/gorhill/uBlock/issues/1067
+// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
+// Firefox 17/Chromium 41 supports `startsWith`.
+
+if ( String.prototype.startsWith instanceof Function === false ) {
+ String.prototype.startsWith = function(needle, pos) {
+ if ( typeof pos !== 'number' ) {
+ pos = 0;
+ }
+ return this.lastIndexOf(needle, pos) === pos;
+ };
+}
+
+/******************************************************************************/
+
+// https://github.com/gorhill/uBlock/issues/1067
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
+// Firefox 17/Chromium 41 supports `endsWith`.
+
+if ( String.prototype.endsWith instanceof Function === false ) {
+ String.prototype.endsWith = function(needle, pos) {
+ if ( typeof pos !== 'number' ) {
+ pos = this.length;
+ }
+ pos -= needle.length;
+ return this.indexOf(needle, pos) === pos;
+ };
+}
+
+/******************************************************************************/
+
+// https://github.com/gorhill/uBlock/issues/1070
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set#Browser_compatibility
+// This polyfill is designed to fulfill *only* what uBlock Origin needs -- this
+// is not an accurate API of the real Set() type.
+
+if ( self.Set instanceof Function === false ) {
+ self.Set = function(iter) {
+ this.clear();
+ if ( Array.isArray(iter) ) {
+ for ( var i = 0, n = iter.length; i < n; i++ ) {
+ this.add(iter[i]);
+ }
+ return;
+ }
+ };
+
+ self.Set.polyfill = true;
+
+ self.Set.prototype.clear = function() {
+ this._set = Object.create(null);
+ this.size = 0;
+ // Iterator stuff
+ this._values = undefined;
+ this._i = undefined;
+ this.value = undefined;
+ this.done = true;
+ };
+
+ self.Set.prototype.add = function(k) {
+ if ( this._set[k] === undefined ) {
+ this._set[k] = true;
+ this.size += 1;
+ }
+ return this;
+ };
+
+ self.Set.prototype.delete = function(k) {
+ if ( this._set[k] !== undefined ) {
+ delete this._set[k];
+ this.size -= 1;
+ return true;
+ }
+ return false;
+ };
+
+ self.Set.prototype.has = function(k) {
+ return this._set[k] !== undefined;
+ };
+
+ self.Set.prototype.next = function() {
+ if ( this._i < this.size ) {
+ this.value = this._values[this._i++];
+ } else {
+ this._values = undefined;
+ this.value = undefined;
+ this.done = true;
+ }
+ return this;
+ };
+
+ self.Set.prototype.values = function() {
+ this._values = Object.keys(this._set);
+ this._i = 0;
+ this.value = undefined;
+ this.done = false;
+ return this;
+ };
+}
+
+/******************************************************************************/
+
+// https://github.com/gorhill/uBlock/issues/1070
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set#Browser_compatibility
+// This polyfill is designed to fulfill *only* what uBlock Origin needs -- this
+// is not an accurate API of the real Map() type.
+
+if ( self.Map instanceof Function === false ) {
+ self.Map = function(iter) {
+ this.clear();
+ if ( Array.isArray(iter) ) {
+ for ( var i = 0, n = iter.length, entry; i < n; i++ ) {
+ entry = iter[i];
+ this.set(entry[0], entry[1]);
+ }
+ return;
+ }
+ };
+
+ self.Map.polyfill = true;
+
+ self.Map.prototype.clear = function() {
+ this._map = Object.create(null);
+ this.size = 0;
+ // Iterator stuff
+ this._keys = undefined;
+ this._i = undefined;
+ this.value = undefined;
+ this.done = true;
+ };
+
+ self.Map.prototype.delete = function(k) {
+ if ( this._map[k] !== undefined ) {
+ delete this._map[k];
+ this.size -= 1;
+ return true;
+ }
+ return false;
+ };
+
+ self.Map.prototype.entries = function() {
+ this._keys = Object.keys(this._map);
+ this._i = 0;
+ this.value = [ undefined, undefined ];
+ this.done = false;
+ return this;
+ };
+
+ self.Map.prototype.get = function(k) {
+ return this._map[k];
+ };
+
+ self.Map.prototype.has = function(k) {
+ return this._map[k] !== undefined;
+ };
+
+ self.Map.prototype.next = function() {
+ if ( this._i < this.size ) {
+ var key = this._keys[this._i++];
+ this.value[0] = key;
+ this.value[1] = this._map[key];
+ } else {
+ this._keys = undefined;
+ this.value = undefined;
+ this.done = true;
+ }
+ return this;
+ };
+
+ self.Map.prototype.set = function(k, v) {
+ if ( v !== undefined ) {
+ if ( this._map[k] === undefined ) {
+ this.size += 1;
+ }
+ this._map[k] = v;
+ } else {
+ if ( this._map[k] !== undefined ) {
+ this.size -= 1;
+ }
+ delete this._map[k];
+ }
+ return this;
+ };
+}
+
+/******************************************************************************/
diff --git a/platform/webext/vapi-background.js b/platform/webext/vapi-background.js
new file mode 100644
index 000000000..5e95622d6
--- /dev/null
+++ b/platform/webext/vapi-background.js
@@ -0,0 +1,1424 @@
+/*******************************************************************************
+
+ uBlock Origin - a browser extension to block requests.
+ Copyright (C) 2014-2016 The 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/uBlock
+*/
+
+// For background page
+
+/******************************************************************************/
+
+(function() {
+
+'use strict';
+
+/******************************************************************************/
+
+var vAPI = self.vAPI = self.vAPI || {};
+
+var chrome = self.chrome;
+var manifest = chrome.runtime.getManifest();
+
+vAPI.chrome = true;
+vAPI.cantWebsocket = true;
+
+var noopFunc = function(){};
+
+/******************************************************************************/
+
+vAPI.app = {
+ name: manifest.name,
+ version: manifest.version
+};
+
+/******************************************************************************/
+
+vAPI.app.restart = function() {
+ chrome.runtime.reload();
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+// chrome.storage.local.get(null, function(bin){ console.debug('%o', bin); });
+
+vAPI.storage = chrome.storage.local;
+
+/******************************************************************************/
+/******************************************************************************/
+
+// https://github.com/gorhill/uMatrix/issues/234
+// https://developer.chrome.com/extensions/privacy#property-network
+
+// 2015-08-12: Wrapped Chrome API in try-catch statements. I had a fluke
+// event in which it appeared Chrome 46 decided to restart uBlock (for
+// unknown reasons) and again for unknown reasons the browser acted as if
+// uBlock did not declare the `privacy` permission in its manifest, putting
+// uBlock in a bad, non-functional state -- because call to `chrome.privacy`
+// API threw an exception.
+
+// https://github.com/gorhill/uBlock/issues/2048
+// Do not mess up with existing settings if not assigning them stricter
+// values.
+
+vAPI.browserSettings = {
+ webRTCSupported: undefined,
+
+ // https://github.com/gorhill/uBlock/issues/875
+ // Must not leave `lastError` unchecked.
+ noopCallback: function() {
+ void chrome.runtime.lastError;
+ },
+
+ // Calling with `true` means IP address leak is not prevented.
+ // https://github.com/gorhill/uBlock/issues/533
+ // We must first check wether this Chromium-based browser was compiled
+ // with WebRTC support. To do this, we use an iframe, this way the
+ // empty RTCPeerConnection object we create to test for support will
+ // be properly garbage collected. This prevents issues such as
+ // a computer unable to enter into sleep mode, as reported in the
+ // Chrome store:
+ // https://github.com/gorhill/uBlock/issues/533#issuecomment-167931681
+ setWebrtcIPAddress: function(setting) {
+ // We don't know yet whether this browser supports WebRTC: find out.
+ if ( this.webRTCSupported === undefined ) {
+ this.webRTCSupported = { setting: setting };
+ var iframe = document.createElement('iframe');
+ var me = this;
+ var messageHandler = function(ev) {
+ if ( ev.origin !== self.location.origin ) {
+ return;
+ }
+ window.removeEventListener('message', messageHandler);
+ var setting = me.webRTCSupported.setting;
+ me.webRTCSupported = ev.data === 'webRTCSupported';
+ me.setWebrtcIPAddress(setting);
+ iframe.parentNode.removeChild(iframe);
+ iframe = null;
+ };
+ window.addEventListener('message', messageHandler);
+ iframe.src = 'is-webrtc-supported.html';
+ document.body.appendChild(iframe);
+ return;
+ }
+
+ // We are waiting for a response from our iframe. This makes the code
+ // safe to re-entrancy.
+ if ( typeof this.webRTCSupported === 'object' ) {
+ this.webRTCSupported.setting = setting;
+ return;
+ }
+
+ // https://github.com/gorhill/uBlock/issues/533
+ // WebRTC not supported: `webRTCMultipleRoutesEnabled` can NOT be
+ // safely accessed. Accessing the property will cause full browser
+ // crash.
+ if ( this.webRTCSupported !== true ) {
+ return;
+ }
+
+ var cp = chrome.privacy,
+ cpn = cp.network;
+
+ // Older version of Chromium do not support this setting, and is
+ // marked as "deprecated" since Chromium 48.
+ if ( typeof cpn.webRTCMultipleRoutesEnabled === 'object' ) {
+ try {
+ if ( setting ) {
+ cpn.webRTCMultipleRoutesEnabled.clear({
+ scope: 'regular'
+ }, this.noopCallback);
+ } else {
+ cpn.webRTCMultipleRoutesEnabled.set({
+ value: false,
+ scope: 'regular'
+ }, this.noopCallback);
+ }
+ } catch(ex) {
+ console.error(ex);
+ }
+ }
+
+ // This setting became available in Chromium 48.
+ if ( typeof cpn.webRTCIPHandlingPolicy === 'object' ) {
+ try {
+ if ( setting ) {
+ cpn.webRTCIPHandlingPolicy.clear({
+ scope: 'regular'
+ }, this.noopCallback);
+ } else {
+ // Respect current stricter setting if any.
+ cpn.webRTCIPHandlingPolicy.get({}, function(details) {
+ var value = details.value === 'disable_non_proxied_udp' ?
+ 'disable_non_proxied_udp' :
+ 'default_public_interface_only';
+ cpn.webRTCIPHandlingPolicy.set({
+ value: value,
+ scope: 'regular'
+ }, this.noopCallback);
+ }.bind(this));
+ }
+ } catch(ex) {
+ console.error(ex);
+ }
+ }
+ },
+
+ set: function(details) {
+ for ( var setting in details ) {
+ if ( details.hasOwnProperty(setting) === false ) {
+ continue;
+ }
+ switch ( setting ) {
+ case 'prefetching':
+ try {
+ if ( !!details[setting] ) {
+ chrome.privacy.network.networkPredictionEnabled.clear({
+ scope: 'regular'
+ }, this.noopCallback);
+ } else {
+ chrome.privacy.network.networkPredictionEnabled.set({
+ value: false,
+ scope: 'regular'
+ }, this.noopCallback);
+ }
+ } catch(ex) {
+ console.error(ex);
+ }
+ break;
+
+ case 'hyperlinkAuditing':
+ try {
+ if ( !!details[setting] ) {
+ chrome.privacy.websites.hyperlinkAuditingEnabled.clear({
+ scope: 'regular'
+ }, this.noopCallback);
+ } else {
+ chrome.privacy.websites.hyperlinkAuditingEnabled.set({
+ value: false,
+ scope: 'regular'
+ }, this.noopCallback);
+ }
+ } catch(ex) {
+ console.error(ex);
+ }
+ break;
+
+ case 'webrtcIPAddress':
+ this.setWebrtcIPAddress(!!details[setting]);
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+vAPI.tabs = {};
+
+/******************************************************************************/
+
+vAPI.isBehindTheSceneTabId = function(tabId) {
+ return tabId.toString() === '-1';
+};
+
+vAPI.noTabId = '-1';
+
+/******************************************************************************/
+
+var toChromiumTabId = function(tabId) {
+ if ( typeof tabId === 'string' ) {
+ tabId = parseInt(tabId, 10);
+ }
+ if ( typeof tabId !== 'number' || isNaN(tabId) || tabId === -1 ) {
+ return 0;
+ }
+ return tabId;
+};
+
+/******************************************************************************/
+
+vAPI.tabs.registerListeners = function() {
+ var onNavigationClient = this.onNavigation || noopFunc;
+ var onUpdatedClient = this.onUpdated || noopFunc;
+
+ // https://developer.chrome.com/extensions/webNavigation
+ // [onCreatedNavigationTarget ->]
+ // onBeforeNavigate ->
+ // onCommitted ->
+ // onDOMContentLoaded ->
+ // onCompleted
+
+ // The chrome.webRequest.onBeforeRequest() won't be called for everything
+ // else than `http`/`https`. Thus, in such case, we will bind the tab as
+ // early as possible in order to increase the likelihood of a context
+ // properly setup if network requests are fired from within the tab.
+ // Example: Chromium + case #6 at
+ // http://raymondhill.net/ublock/popup.html
+ var reGoodForWebRequestAPI = /^https?:\/\//;
+
+ // https://forums.lanik.us/viewtopic.php?f=62&t=32826
+ // Chromium-based browsers: sanitize target URL. I've seen data: URI with
+ // newline characters in standard fields, possibly as a way of evading
+ // filters. As per spec, there should be no whitespaces in a data: URI's
+ // standard fields.
+ var sanitizeURL = function(url) {
+ if ( url.startsWith('data:') === false ) { return url; }
+ var pos = url.indexOf(',');
+ if ( pos === -1 ) { return url; }
+ var s = url.slice(0, pos);
+ if ( s.search(/\s/) === -1 ) { return url; }
+ return s.replace(/\s+/, '') + url.slice(pos);
+ };
+
+ var onCreatedNavigationTarget = function(details) {
+ //console.debug('onCreatedNavigationTarget: popup candidate tab id %d = "%s"', details.tabId, details.url);
+ if ( reGoodForWebRequestAPI.test(details.url) === false ) {
+ details.frameId = 0;
+ details.url = sanitizeURL(details.url);
+ onNavigationClient(details);
+ }
+ if ( typeof vAPI.tabs.onPopupCreated === 'function' ) {
+ vAPI.tabs.onPopupCreated(details.tabId.toString(), details.sourceTabId.toString());
+ }
+ };
+
+ var onBeforeNavigate = function(details) {
+ if ( details.frameId !== 0 ) {
+ return;
+ }
+ };
+
+ var onCommitted = function(details) {
+ if ( details.frameId !== 0 ) {
+ return;
+ }
+ details.url = sanitizeURL(details.url);
+ onNavigationClient(details);
+ };
+
+ var onActivated = function(details) {
+ vAPI.contextMenu.onMustUpdate(details.tabId);
+ };
+
+ var onUpdated = function(tabId, changeInfo, tab) {
+ if ( changeInfo.url ) {
+ changeInfo.url = sanitizeURL(changeInfo.url);
+ }
+ onUpdatedClient(tabId, changeInfo, tab);
+ };
+
+ chrome.webNavigation.onBeforeNavigate.addListener(onBeforeNavigate);
+ chrome.webNavigation.onCommitted.addListener(onCommitted);
+ chrome.webNavigation.onCreatedNavigationTarget.addListener(onCreatedNavigationTarget);
+ chrome.tabs.onActivated.addListener(onActivated);
+ chrome.tabs.onUpdated.addListener(onUpdated);
+
+ if ( typeof this.onClosed === 'function' ) {
+ chrome.tabs.onRemoved.addListener(this.onClosed);
+ }
+
+};
+
+/******************************************************************************/
+
+vAPI.tabs.get = function(tabId, callback) {
+ var onTabReady = function(tab) {
+ // https://code.google.com/p/chromium/issues/detail?id=410868#c8
+ if ( chrome.runtime.lastError ) {
+ /* noop */
+ }
+ // Caller must be prepared to deal with nil tab value
+ callback(tab);
+ };
+
+ if ( tabId !== null ) {
+ tabId = toChromiumTabId(tabId);
+ if ( tabId === 0 ) {
+ onTabReady(null);
+ } else {
+ chrome.tabs.get(tabId, onTabReady);
+ }
+ return;
+ }
+
+ var onTabReceived = function(tabs) {
+ // https://code.google.com/p/chromium/issues/detail?id=410868#c8
+ void chrome.runtime.lastError;
+ callback(tabs[0]);
+ };
+ chrome.tabs.query({ active: true, currentWindow: true }, onTabReceived);
+};
+
+/******************************************************************************/
+
+// 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
+// popup: true // open in a new window
+
+vAPI.tabs.open = function(details) {
+ var targetURL = details.url;
+ if ( typeof targetURL !== 'string' || targetURL === '' ) {
+ return null;
+ }
+
+ // extension pages
+ if ( /^[\w-]{2,}:/.test(targetURL) !== true ) {
+ targetURL = vAPI.getURL(targetURL);
+ }
+
+ // dealing with Chrome's asynchronous API
+ var wrapper = function() {
+ if ( details.active === undefined ) {
+ details.active = true;
+ }
+
+ var subWrapper = function() {
+ var _details = {
+ url: targetURL,
+ active: !!details.active
+ };
+
+ // Opening a tab from incognito window won't focus the window
+ // in which the tab was opened
+ var focusWindow = function(tab) {
+ if ( tab.active ) {
+ chrome.windows.update(tab.windowId, { focused: true });
+ }
+ };
+
+ if ( !details.tabId ) {
+ if ( details.index !== undefined ) {
+ _details.index = details.index;
+ }
+
+ chrome.tabs.create(_details, focusWindow);
+ return;
+ }
+
+ // update doesn't accept index, must use move
+ chrome.tabs.update(toChromiumTabId(details.tabId), _details, function(tab) {
+ // if the tab doesn't exist
+ if ( vAPI.lastError() ) {
+ chrome.tabs.create(_details, focusWindow);
+ } else if ( details.index !== undefined ) {
+ chrome.tabs.move(tab.id, {index: details.index});
+ }
+ });
+ };
+
+ // Open in a standalone window
+ if ( details.popup === true ) {
+ chrome.windows.create({
+ url: details.url,
+ focused: details.active,
+ type: 'popup'
+ });
+ return;
+ }
+
+ if ( details.index !== -1 ) {
+ subWrapper();
+ return;
+ }
+
+ vAPI.tabs.get(null, function(tab) {
+ if ( tab ) {
+ details.index = tab.index + 1;
+ } else {
+ delete details.index;
+ }
+
+ subWrapper();
+ });
+ };
+
+ if ( !details.select ) {
+ wrapper();
+ return;
+ }
+
+ // https://developer.chrome.com/extensions/tabs#method-query
+ // "Note that fragment identifiers are not matched."
+ // It's a lie, fragment identifiers ARE matched. So we need to remove the
+ // fragment.
+ var pos = targetURL.indexOf('#');
+ var targetURLWithoutHash = pos === -1 ? targetURL : targetURL.slice(0, pos);
+
+ chrome.tabs.query({ url: targetURLWithoutHash }, function(tabs) {
+ var tab = tabs[0];
+ if ( !tab ) {
+ wrapper();
+ return;
+ }
+
+ var _details = {
+ active: true,
+ url: undefined
+ };
+ if ( targetURL !== tab.url ) {
+ _details.url = targetURL;
+ }
+ chrome.tabs.update(tab.id, _details, function(tab) {
+ chrome.windows.update(tab.windowId, { focused: true });
+ });
+ });
+};
+
+/******************************************************************************/
+
+// Replace the URL of a tab. Noop if the tab does not exist.
+
+vAPI.tabs.replace = function(tabId, url) {
+ tabId = toChromiumTabId(tabId);
+ if ( tabId === 0 ) {
+ return;
+ }
+
+ var targetURL = url;
+
+ // extension pages
+ if ( /^[\w-]{2,}:/.test(targetURL) !== true ) {
+ targetURL = vAPI.getURL(targetURL);
+ }
+
+ chrome.tabs.update(tabId, { url: targetURL }, function() {
+ // https://code.google.com/p/chromium/issues/detail?id=410868#c8
+ if ( chrome.runtime.lastError ) {
+ /* noop */
+ }
+ });
+};
+
+/******************************************************************************/
+
+vAPI.tabs.remove = function(tabId) {
+ tabId = toChromiumTabId(tabId);
+ if ( tabId === 0 ) {
+ return;
+ }
+
+ var onTabRemoved = function() {
+ // https://code.google.com/p/chromium/issues/detail?id=410868#c8
+ if ( chrome.runtime.lastError ) {
+ /* noop */
+ }
+ };
+
+ chrome.tabs.remove(tabId, onTabRemoved);
+};
+
+/******************************************************************************/
+
+vAPI.tabs.reload = function(tabId /*, flags*/) {
+ tabId = toChromiumTabId(tabId);
+ if ( tabId === 0 ) {
+ return;
+ }
+
+ var onReloaded = function() {
+ // https://code.google.com/p/chromium/issues/detail?id=410868#c8
+ if ( chrome.runtime.lastError ) {
+ /* noop */
+ }
+ };
+
+ chrome.tabs.reload(tabId, onReloaded);
+};
+
+/******************************************************************************/
+
+// Select a specific tab.
+
+vAPI.tabs.select = function(tabId) {
+ tabId = toChromiumTabId(tabId);
+ if ( tabId === 0 ) {
+ return;
+ }
+
+ chrome.tabs.update(tabId, { active: true }, function(tab) {
+ if ( chrome.runtime.lastError ) {
+ /* noop */
+ }
+ if ( !tab ) {
+ return;
+ }
+ chrome.windows.update(tab.windowId, { focused: true });
+ });
+};
+
+/******************************************************************************/
+
+vAPI.tabs.injectScript = function(tabId, details, callback) {
+ var onScriptExecuted = function() {
+ // https://code.google.com/p/chromium/issues/detail?id=410868#c8
+ if ( chrome.runtime.lastError ) {
+ /* noop */
+ }
+ if ( typeof callback === 'function' ) {
+ callback();
+ }
+ };
+ if ( tabId ) {
+ chrome.tabs.executeScript(toChromiumTabId(tabId), details, onScriptExecuted);
+ } else {
+ chrome.tabs.executeScript(details, onScriptExecuted);
+ }
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+// Must read: https://code.google.com/p/chromium/issues/detail?id=410868#c8
+
+// https://github.com/chrisaljoudi/uBlock/issues/19
+// https://github.com/chrisaljoudi/uBlock/issues/207
+// Since we may be called asynchronously, the tab id may not exist
+// anymore, so this ensures it does still exist.
+
+vAPI.setIcon = function(tabId, iconStatus, badge) {
+ tabId = toChromiumTabId(tabId);
+ if ( tabId === 0 ) {
+ return;
+ }
+
+ var onIconReady = function() {
+ if ( vAPI.lastError() ) {
+ return;
+ }
+ chrome.browserAction.setBadgeText({ tabId: tabId, text: badge });
+ if ( badge !== '' ) {
+ chrome.browserAction.setBadgeBackgroundColor({
+ tabId: tabId,
+ color: '#666'
+ });
+ }
+ };
+
+ var iconPaths = iconStatus === 'on' ?
+ { '19': 'img/browsericons/icon19.png', '38': 'img/browsericons/icon38.png' } :
+ { '19': 'img/browsericons/icon19-off.png', '38': 'img/browsericons/icon38-off.png' };
+
+ chrome.browserAction.setIcon({ tabId: tabId, path: iconPaths }, onIconReady);
+ vAPI.contextMenu.onMustUpdate(tabId);
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+vAPI.messaging = {
+ ports: {},
+ listeners: {},
+ defaultHandler: null,
+ NOOPFUNC: noopFunc,
+ UNHANDLED: 'vAPI.messaging.notHandled'
+};
+
+/******************************************************************************/
+
+vAPI.messaging.listen = function(listenerName, callback) {
+ this.listeners[listenerName] = callback;
+};
+
+/******************************************************************************/
+
+vAPI.messaging.onPortMessage = (function() {
+ var messaging = vAPI.messaging;
+ var toAuxPending = {};
+
+ // Use a wrapper to avoid closure and to allow reuse.
+ var CallbackWrapper = function(port, request, timeout) {
+ this.callback = this.proxy.bind(this); // bind once
+ this.init(port, request, timeout);
+ };
+
+ CallbackWrapper.prototype.init = function(port, request, timeout) {
+ this.port = port;
+ this.request = request;
+ this.timerId = timeout !== undefined ?
+ vAPI.setTimeout(this.callback, timeout) :
+ null;
+ return this;
+ };
+
+ CallbackWrapper.prototype.proxy = function(response) {
+ if ( this.timerId !== null ) {
+ clearTimeout(this.timerId);
+ delete toAuxPending[this.timerId];
+ this.timerId = null;
+ }
+ // https://github.com/chrisaljoudi/uBlock/issues/383
+ if ( messaging.ports.hasOwnProperty(this.port.name) ) {
+ this.port.postMessage({
+ auxProcessId: this.request.auxProcessId,
+ channelName: this.request.channelName,
+ msg: response !== undefined ? response : null
+ });
+ }
+ // Mark for reuse
+ this.port = this.request = null;
+ callbackWrapperJunkyard.push(this);
+ };
+
+ var callbackWrapperJunkyard = [];
+
+ var callbackWrapperFactory = function(port, request, timeout) {
+ var wrapper = callbackWrapperJunkyard.pop();
+ if ( wrapper ) {
+ return wrapper.init(port, request, timeout);
+ }
+ return new CallbackWrapper(port, request, timeout);
+ };
+
+ var toAux = function(details, portFrom) {
+ var port, portTo;
+ var chromiumTabId = toChromiumTabId(details.toTabId);
+
+ // TODO: This could be an issue with a lot of tabs: easy to address
+ // with a port name to tab id map.
+ for ( var portName in messaging.ports ) {
+ if ( messaging.ports.hasOwnProperty(portName) === false ) {
+ continue;
+ }
+ // When sending to an auxiliary process, the target is always the
+ // port associated with the root frame.
+ port = messaging.ports[portName];
+ if ( port.sender.frameId === 0 && port.sender.tab.id === chromiumTabId ) {
+ portTo = port;
+ break;
+ }
+ }
+
+ var wrapper;
+ if ( details.auxProcessId !== undefined ) {
+ wrapper = callbackWrapperFactory(portFrom, details, 1023);
+ }
+
+ // Destination not found:
+ if ( portTo === undefined ) {
+ if ( wrapper !== undefined ) {
+ wrapper.callback();
+ }
+ return;
+ }
+
+ // As per HTML5, timer id is always an integer, thus suitable to be
+ // used as a key, and which value is safe to use across process
+ // boundaries.
+ if ( wrapper !== undefined ) {
+ toAuxPending[wrapper.timerId] = wrapper;
+ }
+
+ portTo.postMessage({
+ mainProcessId: wrapper && wrapper.timerId,
+ channelName: details.toChannel,
+ msg: details.msg
+ });
+ };
+
+ var toAuxResponse = function(details) {
+ var mainProcessId = details.mainProcessId;
+ if ( mainProcessId === undefined ) {
+ return;
+ }
+ if ( toAuxPending.hasOwnProperty(mainProcessId) === false ) {
+ return;
+ }
+ var wrapper = toAuxPending[mainProcessId];
+ delete toAuxPending[mainProcessId];
+ wrapper.callback(details.msg);
+ };
+
+ return function(request, port) {
+ // Auxiliary process to auxiliary process
+ if ( request.toTabId !== undefined ) {
+ toAux(request, port);
+ return;
+ }
+
+ // Auxiliary process to auxiliary process: response
+ if ( request.mainProcessId !== undefined ) {
+ toAuxResponse(request);
+ return;
+ }
+
+ // Auxiliary process to main process: prepare response
+ var callback = messaging.NOOPFUNC;
+ if ( request.auxProcessId !== undefined ) {
+ callback = callbackWrapperFactory(port, request).callback;
+ }
+
+ // Auxiliary process to main process: specific handler
+ var r = messaging.UNHANDLED;
+ var listener = messaging.listeners[request.channelName];
+ if ( typeof listener === 'function' ) {
+ r = listener(request.msg, port.sender, callback);
+ }
+ if ( r !== messaging.UNHANDLED ) {
+ return;
+ }
+
+ // Auxiliary process to main process: default handler
+ r = messaging.defaultHandler(request.msg, port.sender, callback);
+ if ( r !== messaging.UNHANDLED ) {
+ return;
+ }
+
+ // Auxiliary process to main process: no handler
+ console.error('uBlock> messaging > unknown request: %o', request);
+
+ // Need to callback anyways in case caller expected an answer, or
+ // else there is a memory leak on caller's side
+ callback();
+ };
+})();
+
+/******************************************************************************/
+
+vAPI.messaging.onPortDisconnect = function(port) {
+ port.onDisconnect.removeListener(vAPI.messaging.onPortDisconnect);
+ port.onMessage.removeListener(vAPI.messaging.onPortMessage);
+ delete vAPI.messaging.ports[port.name];
+};
+
+/******************************************************************************/
+
+vAPI.messaging.onPortConnect = function(port) {
+ port.onDisconnect.addListener(vAPI.messaging.onPortDisconnect);
+ port.onMessage.addListener(vAPI.messaging.onPortMessage);
+ vAPI.messaging.ports[port.name] = port;
+};
+
+/******************************************************************************/
+
+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;
+
+ chrome.runtime.onConnect.addListener(this.onPortConnect);
+};
+
+/******************************************************************************/
+
+vAPI.messaging.broadcast = function(message) {
+ var messageWrapper = {
+ broadcast: true,
+ msg: message
+ };
+
+ for ( var portName in this.ports ) {
+ if ( this.ports.hasOwnProperty(portName) === false ) {
+ continue;
+ }
+ this.ports[portName].postMessage(messageWrapper);
+ }
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+vAPI.net = {};
+
+/******************************************************************************/
+
+vAPI.net.registerListeners = function() {
+ var µb = µBlock;
+ var µburi = µb.URI;
+
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=410382
+ // Between Chromium 38-48, plug-ins' network requests were reported as
+ // type "other" instead of "object".
+ var is_v38_48 = /\bChrom[a-z]+\/(?:3[89]|4[0-8])\.[\d.]+\b/.test(navigator.userAgent),
+ is_v49_55 = /\bChrom[a-z]+\/(?:49|5[012345])\b/.test(navigator.userAgent);
+
+ // Chromium-based browsers understand only these network request types.
+ var validTypes = {
+ 'main_frame': true,
+ 'sub_frame': true,
+ 'stylesheet': true,
+ 'script': true,
+ 'image': true,
+ 'object': true,
+ 'xmlhttprequest': true,
+ 'other': true
+ };
+
+ var denormalizeTypes = function(aa) {
+ if ( aa.length === 0 ) {
+ return Object.keys(validTypes);
+ }
+ var out = [];
+ var i = aa.length,
+ type,
+ needOther = true;
+ while ( i-- ) {
+ type = aa[i];
+ if ( validTypes.hasOwnProperty(type) ) {
+ out.push(type);
+ }
+ if ( type === 'other' ) {
+ needOther = false;
+ }
+ }
+ if ( needOther ) {
+ out.push('other');
+ }
+ return out;
+ };
+
+ var headerValue = function(headers, name) {
+ var i = headers.length;
+ while ( i-- ) {
+ if ( headers[i].name.toLowerCase() === name ) {
+ return headers[i].value.trim();
+ }
+ }
+ return '';
+ };
+
+ var normalizeRequestDetails = function(details) {
+ details.tabId = details.tabId.toString();
+
+ // https://github.com/gorhill/uBlock/issues/1493
+ // Chromium 49+ support a new request type: `ping`, which is fired as
+ // a result of using `navigator.sendBeacon`.
+ if ( details.type === 'ping' ) {
+ details.type = 'beacon';
+ return;
+ }
+
+ // The rest of the function code is to normalize type
+ if ( details.type !== 'other' ) {
+ return;
+ }
+
+ var path = µburi.pathFromURI(details.url);
+ var pos = path.indexOf('.', path.length - 6);
+
+ // https://github.com/chrisaljoudi/uBlock/issues/862
+ // If no transposition possible, transpose to `object` as per
+ // Chromium bug 410382 (see below)
+ if ( pos !== -1 ) {
+ var needle = path.slice(pos) + '.';
+ if ( '.eot.ttf.otf.svg.woff.woff2.'.indexOf(needle) !== -1 ) {
+ details.type = 'font';
+ return;
+ }
+
+ if ( '.mp3.mp4.webm.'.indexOf(needle) !== -1 ) {
+ details.type = 'media';
+ return;
+ }
+
+ // Still need this because often behind-the-scene requests are wrongly
+ // categorized as 'other'
+ if ( '.ico.png.gif.jpg.jpeg.webp.'.indexOf(needle) !== -1 ) {
+ details.type = 'image';
+ return;
+ }
+ }
+
+ // Try to extract type from response headers if present.
+ if ( details.responseHeaders ) {
+ var contentType = headerValue(details.responseHeaders, 'content-type');
+ if ( contentType.startsWith('font/') ) {
+ details.type = 'font';
+ return;
+ }
+ if ( contentType.startsWith('image/') ) {
+ details.type = 'image';
+ return;
+ }
+ if ( contentType.startsWith('audio/') || contentType.startsWith('video/') ) {
+ details.type = 'media';
+ return;
+ }
+ }
+
+ // https://code.google.com/p/chromium/issues/detail?id=410382
+ if ( is_v38_48 ) {
+ details.type = 'object';
+ }
+ };
+
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=129353
+ // https://github.com/gorhill/uBlock/issues/1497
+ // Expose websocket-based network requests to uBO's filtering engine,
+ // logger, etc.
+ // Counterpart of following block of code is found in "vapi-client.js" --
+ // search for "https://github.com/gorhill/uBlock/issues/1497".
+ var onBeforeWebsocketRequest = function(details) {
+ details.type = 'websocket';
+ var matches = /url=([^&]+)/.exec(details.url);
+ details.url = decodeURIComponent(matches[1]);
+ var r = onBeforeRequestClient(details);
+ // Blocked?
+ if ( r && r.cancel ) {
+ return r;
+ }
+ // Returning a 1x1 transparent pixel means "not blocked".
+ return { redirectUrl: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' };
+ };
+
+ var onBeforeRequestClient = this.onBeforeRequest.callback;
+ var onBeforeRequest = function(details) {
+ // https://github.com/gorhill/uBlock/issues/1497
+ if (
+ details.type === 'image' &&
+ details.url.endsWith('ubofix=f41665f3028c7fd10eecf573336216d3')
+ ) {
+ return onBeforeWebsocketRequest(details);
+ }
+
+ normalizeRequestDetails(details);
+ return onBeforeRequestClient(details);
+ };
+
+ // This is needed for Chromium 49-55.
+ var onBeforeSendHeaders = function(details) {
+ if ( details.type !== 'ping' || details.method !== 'POST' ) { return; }
+ var type = headerValue(details.requestHeaders, 'content-type');
+ if ( type === '' ) { return; }
+ if ( type.endsWith('/csp-report') ) {
+ details.type = 'csp_report';
+ return onBeforeRequestClient(details);
+ }
+ };
+
+ var onHeadersReceivedClient = this.onHeadersReceived.callback,
+ onHeadersReceivedClientTypes = this.onHeadersReceived.types.slice(0),
+ onHeadersReceivedTypes = denormalizeTypes(onHeadersReceivedClientTypes);
+ var onHeadersReceived = function(details) {
+ normalizeRequestDetails(details);
+ // Hack to work around Chromium API limitations, where requests of
+ // type `font` are returned as `other`. For example, our normalization
+ // fail at transposing `other` into `font` for URLs which are outside
+ // what is expected. At least when headers are received we can check
+ // for content type `font/*`. Blocking at onHeadersReceived time is
+ // less worse than not blocking at all. Also, due to Chromium bug,
+ // `other` always becomes `object` when it can't be normalized into
+ // something else. Test case for "unfriendly" font URLs:
+ // https://www.google.com/fonts
+ if ( details.type === 'font' ) {
+ var r = onBeforeRequestClient(details);
+ if ( typeof r === 'object' && r.cancel === true ) {
+ return { cancel: true };
+ }
+ }
+ if (
+ onHeadersReceivedClientTypes.length !== 0 &&
+ onHeadersReceivedClientTypes.indexOf(details.type) === -1
+ ) {
+ return;
+ }
+ return onHeadersReceivedClient(details);
+ };
+
+ var installListeners = (function() {
+ var crapi = chrome.webRequest;
+
+ //listener = function(details) {
+ // quickProfiler.start('onBeforeRequest');
+ // var r = onBeforeRequest(details);
+ // quickProfiler.stop();
+ // return r;
+ //};
+ if ( crapi.onBeforeRequest.hasListener(onBeforeRequest) === false ) {
+ crapi.onBeforeRequest.addListener(
+ onBeforeRequest,
+ {
+ 'urls': this.onBeforeRequest.urls || [''],
+ 'types': this.onBeforeRequest.types || undefined
+ },
+ this.onBeforeRequest.extra
+ );
+ }
+
+ // Chromium 48 and lower does not support `ping` type.
+ // Chromium 56 and higher does support `csp_report` stype.
+ if ( is_v49_55 && crapi.onBeforeSendHeaders.hasListener(onBeforeSendHeaders) === false ) {
+ crapi.onBeforeSendHeaders.addListener(
+ onBeforeSendHeaders,
+ {
+ 'urls': [ '' ],
+ 'types': [ 'ping' ]
+ },
+ [ 'blocking', 'requestHeaders' ]
+ );
+ }
+
+ if ( crapi.onHeadersReceived.hasListener(onHeadersReceived) === false ) {
+ crapi.onHeadersReceived.addListener(
+ onHeadersReceived,
+ {
+ 'urls': this.onHeadersReceived.urls || [''],
+ 'types': onHeadersReceivedTypes
+ },
+ this.onHeadersReceived.extra
+ );
+ }
+
+ // https://github.com/gorhill/uBlock/issues/675
+ // Experimental: keep polling to be sure our listeners are still installed.
+ //setTimeout(installListeners, 20000);
+ }).bind(this);
+
+ installListeners();
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+vAPI.contextMenu = {
+ _callback: null,
+ _entries: [],
+ _createEntry: function(entry) {
+ chrome.contextMenus.create(JSON.parse(JSON.stringify(entry)), function() {
+ void chrome.runtime.lastError;
+ });
+ },
+ onMustUpdate: function() {},
+ setEntries: function(entries, callback) {
+ entries = entries || [];
+ var n = Math.max(this._entries.length, entries.length),
+ oldEntryId, newEntry;
+ for ( var i = 0; i < n; i++ ) {
+ oldEntryId = this._entries[i];
+ newEntry = entries[i];
+ if ( oldEntryId && newEntry ) {
+ if ( newEntry.id !== oldEntryId ) {
+ chrome.contextMenus.remove(oldEntryId);
+ this._createEntry(newEntry);
+ this._entries[i] = newEntry.id;
+ }
+ } else if ( oldEntryId && !newEntry ) {
+ chrome.contextMenus.remove(oldEntryId);
+ } else if ( !oldEntryId && newEntry ) {
+ this._createEntry(newEntry);
+ this._entries[i] = newEntry.id;
+ }
+ }
+ n = this._entries.length = entries.length;
+ callback = callback || null;
+ if ( callback === this._callback ) {
+ return;
+ }
+ if ( n !== 0 && callback !== null ) {
+ chrome.contextMenus.onClicked.addListener(callback);
+ this._callback = callback;
+ } else if ( n === 0 && this._callback !== null ) {
+ chrome.contextMenus.onClicked.removeListener(this._callback);
+ this._callback = null;
+ }
+ }
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+vAPI.lastError = function() {
+ return chrome.runtime.lastError;
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+// 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() {
+ // http://code.google.com/p/chromium/issues/detail?id=410868#c11
+ // Need to be sure to access `vAPI.lastError()` to prevent
+ // spurious warnings in the console.
+ var scriptDone = function() {
+ vAPI.lastError();
+ };
+ var scriptStart = function(tabId) {
+ vAPI.tabs.injectScript(tabId, {
+ file: 'js/vapi-client.js',
+ allFrames: true,
+ runAt: 'document_idle'
+ }, function(){ });
+ vAPI.tabs.injectScript(tabId, {
+ file: 'js/contentscript.js',
+ allFrames: true,
+ runAt: 'document_idle'
+ }, scriptDone);
+ };
+ var bindToTabs = function(tabs) {
+ var µb = µBlock;
+ var i = tabs.length, tab;
+ while ( i-- ) {
+ tab = tabs[i];
+ µb.tabContextManager.commit(tab.id, tab.url);
+ µb.bindTabToPageStats(tab.id);
+ // https://github.com/chrisaljoudi/uBlock/issues/129
+ scriptStart(tab.id);
+ }
+ };
+
+ chrome.tabs.query({ url: '' }, bindToTabs);
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+vAPI.punycodeHostname = function(hostname) {
+ return hostname;
+};
+
+vAPI.punycodeURL = function(url) {
+ return url;
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+// https://github.com/gorhill/uBlock/issues/531
+// Storage area dedicated to admin settings. Read-only.
+
+// https://github.com/gorhill/uBlock/commit/43a5ed735b95a575a9339b6e71a1fcb27a99663b#commitcomment-13965030
+// Not all Chromium-based browsers support managed storage. Merely testing or
+// exception handling in this case does NOT work: I don't know why. The
+// extension on Opera ends up in a non-sensical state, whereas vAPI become
+// undefined out of nowhere. So only solution left is to test explicitly for
+// Opera.
+// https://github.com/gorhill/uBlock/issues/900
+// Also, UC Browser: http://www.upsieutoc.com/image/WXuH
+
+vAPI.adminStorage = {
+ getItem: function(key, callback) {
+ var onRead = function(store) {
+ var data;
+ if (
+ !chrome.runtime.lastError &&
+ typeof store === 'object' &&
+ store !== null
+ ) {
+ data = store[key];
+ }
+ callback(data);
+ };
+ try {
+ chrome.storage.managed.get(key, onRead);
+ } catch (ex) {
+ callback();
+ }
+ }
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+vAPI.cloud = (function() {
+ var chunkCountPerFetch = 16; // Must be a power of 2
+
+ // Mind chrome.storage.sync.MAX_ITEMS (512 at time of writing)
+ var maxChunkCountPerItem = Math.floor(512 * 0.75) & ~(chunkCountPerFetch - 1);
+
+ // Mind chrome.storage.sync.QUOTA_BYTES_PER_ITEM (8192 at time of writing)
+ var maxChunkSize = Math.floor(chrome.storage.sync.QUOTA_BYTES_PER_ITEM * 0.75);
+
+ // Mind chrome.storage.sync.QUOTA_BYTES (128 kB at time of writing)
+ var maxStorageSize = chrome.storage.sync.QUOTA_BYTES;
+
+ var options = {
+ defaultDeviceName: window.navigator.platform,
+ deviceName: window.localStorage.getItem('deviceName') || ''
+ };
+
+ // This is used to find out a rough count of how many chunks exists:
+ // We "poll" at specific index in order to get a rough idea of how
+ // large is the stored string.
+ // This allows reading a single item with only 2 sync operations -- a
+ // good thing given chrome.storage.sync.MAX_WRITE_OPERATIONS_PER_MINUTE
+ // and chrome.storage.sync.MAX_WRITE_OPERATIONS_PER_HOUR.
+
+ var getCoarseChunkCount = function(dataKey, callback) {
+ var bin = {};
+ for ( var i = 0; i < maxChunkCountPerItem; i += 16 ) {
+ bin[dataKey + i.toString()] = '';
+ }
+
+ chrome.storage.sync.get(bin, function(bin) {
+ if ( chrome.runtime.lastError ) {
+ callback(0, chrome.runtime.lastError.message);
+ return;
+ }
+
+ var chunkCount = 0;
+ for ( var i = 0; i < maxChunkCountPerItem; i += 16 ) {
+ if ( bin[dataKey + i.toString()] === '' ) {
+ break;
+ }
+ chunkCount = i + 16;
+ }
+
+ callback(chunkCount);
+ });
+ };
+
+ var deleteChunks = function(dataKey, start) {
+ var keys = [];
+
+ // No point in deleting more than:
+ // - The max number of chunks per item
+ // - The max number of chunks per storage limit
+ var n = Math.min(
+ maxChunkCountPerItem,
+ Math.ceil(maxStorageSize / maxChunkSize)
+ );
+ for ( var i = start; i < n; i++ ) {
+ keys.push(dataKey + i.toString());
+ }
+ chrome.storage.sync.remove(keys);
+ };
+
+ var start = function(/* dataKeys */) {
+ };
+
+ var push = function(dataKey, data, callback) {
+ var bin = {
+ 'source': options.deviceName || options.defaultDeviceName,
+ 'tstamp': Date.now(),
+ 'data': data,
+ 'size': 0
+ };
+ bin.size = JSON.stringify(bin).length;
+ var item = JSON.stringify(bin);
+
+ // Chunkify taking into account QUOTA_BYTES_PER_ITEM:
+ // https://developer.chrome.com/extensions/storage#property-sync
+ // "The maximum size (in bytes) of each individual item in sync
+ // "storage, as measured by the JSON stringification of its value
+ // "plus its key length."
+ bin = {};
+ var chunkCount = Math.ceil(item.length / maxChunkSize);
+ for ( var i = 0; i < chunkCount; i++ ) {
+ bin[dataKey + i.toString()] = item.substr(i * maxChunkSize, maxChunkSize);
+ }
+ bin[dataKey + i.toString()] = ''; // Sentinel
+
+ chrome.storage.sync.set(bin, function() {
+ var errorStr;
+ if ( chrome.runtime.lastError ) {
+ errorStr = chrome.runtime.lastError.message;
+ }
+ callback(errorStr);
+
+ // Remove potentially unused trailing chunks
+ deleteChunks(dataKey, chunkCount);
+ });
+ };
+
+ var pull = function(dataKey, callback) {
+ var assembleChunks = function(bin) {
+ if ( chrome.runtime.lastError ) {
+ callback(null, chrome.runtime.lastError.message);
+ return;
+ }
+
+ // Assemble chunks into a single string.
+ var json = [], jsonSlice;
+ var i = 0;
+ for (;;) {
+ jsonSlice = bin[dataKey + i.toString()];
+ if ( jsonSlice === '' ) {
+ break;
+ }
+ json.push(jsonSlice);
+ i += 1;
+ }
+
+ var entry = null;
+ try {
+ entry = JSON.parse(json.join(''));
+ } catch(ex) {
+ }
+ callback(entry);
+ };
+
+ var fetchChunks = function(coarseCount, errorStr) {
+ if ( coarseCount === 0 || typeof errorStr === 'string' ) {
+ callback(null, errorStr);
+ return;
+ }
+
+ var bin = {};
+ for ( var i = 0; i < coarseCount; i++ ) {
+ bin[dataKey + i.toString()] = '';
+ }
+
+ chrome.storage.sync.get(bin, assembleChunks);
+ };
+
+ getCoarseChunkCount(dataKey, fetchChunks);
+ };
+
+ var getOptions = function(callback) {
+ if ( typeof callback !== 'function' ) {
+ return;
+ }
+ callback(options);
+ };
+
+ var setOptions = function(details, callback) {
+ if ( typeof details !== 'object' || details === null ) {
+ return;
+ }
+
+ if ( typeof details.deviceName === 'string' ) {
+ window.localStorage.setItem('deviceName', details.deviceName);
+ options.deviceName = details.deviceName;
+ }
+
+ getOptions(callback);
+ };
+
+ return {
+ start: start,
+ push: push,
+ pull: pull,
+ getOptions: getOptions,
+ setOptions: setOptions
+ };
+})();
+
+/******************************************************************************/
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/
diff --git a/platform/webext/vapi-client.js b/platform/webext/vapi-client.js
new file mode 100644
index 000000000..0dd976fff
--- /dev/null
+++ b/platform/webext/vapi-client.js
@@ -0,0 +1,525 @@
+/*******************************************************************************
+
+ uBlock Origin - a browser extension to block requests.
+ Copyright (C) 2014-2016 The 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/uBlock
+*/
+
+/* global HTMLDocument, XMLDocument */
+
+'use strict';
+
+// For non background pages
+
+/******************************************************************************/
+
+(function(self) {
+
+/******************************************************************************/
+/******************************************************************************/
+
+// https://github.com/chrisaljoudi/uBlock/issues/464
+if ( document instanceof HTMLDocument === false ) {
+ // https://github.com/chrisaljoudi/uBlock/issues/1528
+ // A XMLDocument can be a valid HTML document.
+ if (
+ document instanceof XMLDocument === false ||
+ document.createElement('div') instanceof HTMLDivElement === false
+ ) {
+ return;
+ }
+}
+
+// https://github.com/gorhill/uBlock/issues/1124
+// Looks like `contentType` is on track to be standardized:
+// https://dom.spec.whatwg.org/#concept-document-content-type
+// https://forums.lanik.us/viewtopic.php?f=64&t=31522
+// Skip text/plain documents.
+var contentType = document.contentType || '';
+if ( /^image\/|^text\/plain/.test(contentType) ) {
+ return;
+}
+
+/******************************************************************************/
+
+var vAPI = self.vAPI = self.vAPI || {};
+var chrome = self.chrome;
+
+// https://github.com/chrisaljoudi/uBlock/issues/456
+// Already injected?
+if ( vAPI.sessionId ) {
+ return;
+}
+
+/******************************************************************************/
+
+// Support minimally working Set() for legacy Chromium.
+
+if ( self.Set instanceof Function ) {
+ self.createSet = function() {
+ return new Set();
+ };
+} else {
+ self.createSet = (function() {
+ //console.log('Polyfilling for ES6-like Set().');
+ var PrimitiveSet = function() {
+ this.clear();
+ };
+ PrimitiveSet.prototype = {
+ add: function(k) {
+ if ( this._set[k] === undefined ) {
+ this._set[k] = true;
+ this.size += 1;
+ }
+ return this;
+ },
+ clear: function() {
+ this._set = Object.create(null);
+ this.size = 0;
+ this._values = undefined;
+ this._i = undefined;
+ this.value = undefined;
+ this.done = true;
+ },
+ delete: function(k) {
+ if ( this._set[k] === undefined ) { return false; }
+ delete this._set[k];
+ this.size -= 1;
+ return true;
+ },
+ has: function(k) {
+ return this._set[k] !== undefined;
+ },
+ next: function() {
+ if ( this._i < this.size ) {
+ this.value = this._values[this._i++];
+ } else {
+ this._values = undefined;
+ this.value = undefined;
+ this.done = true;
+ }
+ return this;
+ },
+ polyfill: true,
+ values: function() {
+ this._values = Object.keys(this._set);
+ this._i = 0;
+ this.value = undefined;
+ this.done = false;
+ return this;
+ }
+ };
+ var ReferenceSet = function() {
+ this.clear();
+ };
+ ReferenceSet.prototype = {
+ add: function(k) {
+ if ( this._set.indexOf(k) === -1 ) {
+ this._set.push(k);
+ }
+ },
+ clear: function() {
+ this._set = [];
+ this._i = 0;
+ this.value = undefined;
+ this.done = true;
+ },
+ delete: function(k) {
+ var pos = this._set.indexOf(k);
+ if ( pos === -1 ) { return false; }
+ this._set.splice(pos, 1);
+ return true;
+ },
+ has: function(k) {
+ return this._set.indexOf(k) !== -1;
+ },
+ next: function() {
+ if ( this._i === this._set.length ) {
+ this.value = undefined;
+ this.done = true;
+ } else {
+ this.value = this._set[this._i];
+ this._i += 1;
+ }
+ return this;
+ },
+ polyfill: true,
+ values: function() {
+ this._i = 0;
+ this.done = false;
+ return this;
+ }
+ };
+ Object.defineProperty(ReferenceSet.prototype, 'size', {
+ get: function() { return this._set.length; }
+ });
+ return function(type) {
+ return type === 'object' ? new ReferenceSet() : new PrimitiveSet();
+ };
+ })();
+}
+
+/******************************************************************************/
+
+var referenceCounter = 0;
+
+vAPI.lock = function() {
+ referenceCounter += 1;
+};
+
+vAPI.unlock = function() {
+ referenceCounter -= 1;
+ if ( referenceCounter === 0 ) {
+ // Eventually there will be code here to flush the javascript code
+ // from this file out of memory when it ends up unused.
+
+ }
+};
+
+/******************************************************************************/
+
+vAPI.executionCost = {
+ start: function(){},
+ stop: function(){}
+};
+/*
+vAPI.executionCost = {
+ tcost: 0,
+ tstart: 0,
+ nstart: 0,
+ level: 1,
+ start: function() {
+ if ( this.nstart === 0 ) {
+ this.tstart = window.performance.now();
+ }
+ this.nstart += 1;
+ },
+ stop: function(mark) {
+ this.nstart -= 1;
+ if ( this.nstart !== 0 ) {
+ return;
+ }
+ var tcost = window.performance.now() - this.tstart;
+ this.tcost += tcost;
+ if ( mark === undefined ) {
+ return;
+ }
+ var top = window === window.top;
+ if ( !top && this.level < 2 ) {
+ return;
+ }
+ var context = window === window.top ? ' top' : 'frame';
+ var percent = this.tcost / window.performance.now() * 100;
+ console.log(
+ 'uBO cost (%s): %sms/%s%% (%s: %sms)',
+ context,
+ this.tcost.toFixed(1),
+ percent.toFixed(1),
+ mark,
+ tcost.toFixed(2)
+ );
+ }
+};
+*/
+vAPI.executionCost.start();
+
+/******************************************************************************/
+
+vAPI.randomToken = function() {
+ return String.fromCharCode(Date.now() % 26 + 97) +
+ Math.floor(Math.random() * 982451653 + 982451653).toString(36);
+};
+
+vAPI.sessionId = vAPI.randomToken();
+vAPI.chrome = true;
+vAPI.setTimeout = vAPI.setTimeout || self.setTimeout.bind(self);
+
+/******************************************************************************/
+
+vAPI.shutdown = {
+ jobs: [],
+ add: function(job) {
+ this.jobs.push(job);
+ },
+ exec: function() {
+ var job;
+ while ( (job = this.jobs.pop()) ) {
+ job();
+ }
+ },
+ remove: function(job) {
+ var pos;
+ while ( (pos = this.jobs.indexOf(job)) !== -1 ) {
+ this.jobs.splice(pos, 1);
+ }
+ }
+};
+
+/******************************************************************************/
+/******************************************************************************/
+
+vAPI.messaging = {
+ port: null,
+ portTimer: null,
+ portTimerDelay: 10000,
+ channels: Object.create(null),
+ channelCount: 0,
+ pending: Object.create(null),
+ pendingCount: 0,
+ auxProcessId: 1,
+ shuttingDown: false,
+
+ shutdown: function() {
+ this.shuttingDown = true;
+ this.destroyPort();
+ },
+
+ disconnectListener: function() {
+ this.port = null;
+ vAPI.shutdown.exec();
+ },
+ disconnectListenerCallback: null,
+
+ messageListener: function(details) {
+ if ( !details ) {
+ return;
+ }
+
+ // Sent to all channels
+ if ( details.broadcast === true && !details.channelName ) {
+ for ( var channelName in this.channels ) {
+ this.sendToChannelListeners(channelName, details.msg);
+ }
+ return;
+ }
+
+ // Response to specific message previously sent
+ if ( details.auxProcessId ) {
+ var listener = this.pending[details.auxProcessId];
+ delete this.pending[details.auxProcessId];
+ delete details.auxProcessId; // TODO: why?
+ if ( listener ) {
+ this.pendingCount -= 1;
+ listener(details.msg);
+ return;
+ }
+ }
+
+ // Sent to a specific channel
+ var response = this.sendToChannelListeners(details.channelName, details.msg);
+
+ // Respond back if required
+ if ( details.mainProcessId === undefined ) {
+ return;
+ }
+ var port = this.connect();
+ if ( port !== null ) {
+ port.postMessage({
+ mainProcessId: details.mainProcessId,
+ msg: response
+ });
+ }
+ },
+ messageListenerCallback: null,
+
+ portPoller: function() {
+ this.portTimer = null;
+ if ( this.port !== null ) {
+ if ( this.channelCount !== 0 || this.pendingCount !== 0 ) {
+ this.portTimer = vAPI.setTimeout(this.portPollerCallback, this.portTimerDelay);
+ this.portTimerDelay = Math.min(this.portTimerDelay * 2, 60 * 60 * 1000);
+ return;
+ }
+ }
+ this.destroyPort();
+ },
+ portPollerCallback: null,
+
+ destroyPort: function() {
+ if ( this.portTimer !== null ) {
+ clearTimeout(this.portTimer);
+ this.portTimer = null;
+ }
+ var port = this.port;
+ if ( port !== null ) {
+ port.disconnect();
+ port.onMessage.removeListener(this.messageListenerCallback);
+ port.onDisconnect.removeListener(this.disconnectListenerCallback);
+ this.port = null;
+ }
+ if ( this.channelCount !== 0 ) {
+ this.channels = Object.create(null);
+ this.channelCount = 0;
+ }
+ // service pending callbacks
+ if ( this.pendingCount !== 0 ) {
+ var pending = this.pending, callback;
+ this.pending = Object.create(null);
+ this.pendingCount = 0;
+ for ( var auxId in pending ) {
+ callback = pending[auxId];
+ if ( typeof callback === 'function' ) {
+ callback(null);
+ }
+ }
+ }
+ },
+
+ createPort: function() {
+ if ( this.shuttingDown ) {
+ return null;
+ }
+ if ( this.messageListenerCallback === null ) {
+ this.messageListenerCallback = this.messageListener.bind(this);
+ this.disconnectListenerCallback = this.disconnectListener.bind(this);
+ this.portPollerCallback = this.portPoller.bind(this);
+ }
+ try {
+ this.port = chrome.runtime.connect({name: vAPI.sessionId}) || null;
+ } catch (ex) {
+ this.port = null;
+ }
+ if ( this.port !== null ) {
+ this.port.onMessage.addListener(this.messageListenerCallback);
+ this.port.onDisconnect.addListener(this.disconnectListenerCallback);
+ }
+ this.portTimerDelay = 10000;
+ if ( this.portTimer === null ) {
+ this.portTimer = vAPI.setTimeout(this.portPollerCallback, this.portTimerDelay);
+ }
+ return this.port;
+ },
+
+ connect: function() {
+ return this.port !== null ? this.port : this.createPort();
+ },
+
+ send: function(channelName, message, callback) {
+ this.sendTo(channelName, message, undefined, undefined, callback);
+ },
+
+ sendTo: function(channelName, message, toTabId, toChannel, callback) {
+ // Too large a gap between the last request and the last response means
+ // the main process is no longer reachable: memory leaks and bad
+ // performance become a risk -- especially for long-lived, dynamic
+ // pages. Guard against this.
+ if ( this.pendingCount > 25 ) {
+ vAPI.shutdown.exec();
+ }
+ var port = this.connect();
+ if ( port === null ) {
+ if ( typeof callback === 'function' ) {
+ callback();
+ }
+ return;
+ }
+ var auxProcessId;
+ if ( callback ) {
+ auxProcessId = this.auxProcessId++;
+ this.pending[auxProcessId] = callback;
+ this.pendingCount += 1;
+ }
+ port.postMessage({
+ channelName: channelName,
+ auxProcessId: auxProcessId,
+ toTabId: toTabId,
+ toChannel: toChannel,
+ msg: message
+ });
+ },
+
+ addChannelListener: function(channelName, callback) {
+ if ( typeof callback !== 'function' ) {
+ return;
+ }
+ var listeners = this.channels[channelName];
+ if ( listeners !== undefined && listeners.indexOf(callback) !== -1 ) {
+ console.error('Duplicate listener on channel "%s"', channelName);
+ return;
+ }
+ if ( listeners === undefined ) {
+ this.channels[channelName] = [callback];
+ this.channelCount += 1;
+ } else {
+ listeners.push(callback);
+ }
+ this.connect();
+ },
+
+ removeChannelListener: function(channelName, callback) {
+ if ( typeof callback !== 'function' ) {
+ return;
+ }
+ var listeners = this.channels[channelName];
+ if ( listeners === undefined ) {
+ return;
+ }
+ var pos = this.listeners.indexOf(callback);
+ if ( pos === -1 ) {
+ console.error('Listener not found on channel "%s"', channelName);
+ return;
+ }
+ listeners.splice(pos, 1);
+ if ( listeners.length === 0 ) {
+ delete this.channels[channelName];
+ this.channelCount -= 1;
+ }
+ },
+
+ removeAllChannelListeners: function(channelName) {
+ var listeners = this.channels[channelName];
+ if ( listeners === undefined ) {
+ return;
+ }
+ delete this.channels[channelName];
+ this.channelCount -= 1;
+ },
+
+ sendToChannelListeners: function(channelName, msg) {
+ var listeners = this.channels[channelName];
+ if ( listeners === undefined ) {
+ return;
+ }
+ var response;
+ for ( var i = 0, n = listeners.length; i < n; i++ ) {
+ response = listeners[i](msg);
+ if ( response !== undefined ) {
+ break;
+ }
+ }
+ return response;
+ }
+};
+
+/******************************************************************************/
+
+vAPI.shutdown.add(function() {
+ vAPI.messaging.shutdown();
+ delete window.vAPI;
+});
+
+// https://www.youtube.com/watch?v=rT5zCHn0tsg
+// https://www.youtube.com/watch?v=E-jS4e3zacI
+
+vAPI.executionCost.stop('vapi-client.js');
+
+/******************************************************************************/
+/******************************************************************************/
+
+})(this);
+
+/******************************************************************************/
diff --git a/platform/webext/vapi-common.js b/platform/webext/vapi-common.js
new file mode 100644
index 000000000..6020d6665
--- /dev/null
+++ b/platform/webext/vapi-common.js
@@ -0,0 +1,101 @@
+/*******************************************************************************
+
+ uBlock Origin - a browser extension to block requests.
+ Copyright (C) 2014-2016 The 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/uBlock
+*/
+
+// For background page or non-background pages
+
+'use strict';
+
+/******************************************************************************/
+/******************************************************************************/
+
+(function() {
+
+var vAPI = self.vAPI = self.vAPI || {};
+var chrome = self.chrome;
+
+/******************************************************************************/
+
+vAPI.setTimeout = vAPI.setTimeout || self.setTimeout.bind(self);
+
+/******************************************************************************/
+
+// 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');
+ a.href = details.url;
+ a.setAttribute('download', details.filename || '');
+ a.dispatchEvent(new MouseEvent('click'));
+};
+
+/******************************************************************************/
+
+vAPI.insertHTML = function(node, html) {
+ node.innerHTML = html;
+};
+
+/******************************************************************************/
+
+vAPI.getURL = chrome.runtime.getURL;
+
+/******************************************************************************/
+
+vAPI.i18n = chrome.i18n.getMessage;
+
+setScriptDirection(vAPI.i18n('@@ui_locale'));
+
+/******************************************************************************/
+
+vAPI.closePopup = function() {
+ window.open('','_self').close();
+};
+
+/******************************************************************************/
+
+// A localStorage-like object which should be accessible from the
+// background page or auxiliary pages.
+// This storage is optional, but it is nice to have, for a more polished user
+// experience.
+
+// This can throw in some contexts (like in devtool).
+try {
+ vAPI.localStorage = window.localStorage;
+} catch (ex) {
+}
+
+/******************************************************************************/
+
+})();
+
+/******************************************************************************/