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) { +} + +/******************************************************************************/ + +})(); + +/******************************************************************************/