diff options
Diffstat (limited to 'js')
-rw-r--r-- | js/browsercache.js | 4 | ||||
-rw-r--r-- | js/contentscript-start.js | 4 | ||||
-rw-r--r-- | js/traffic.js | 10 | ||||
-rw-r--r-- | js/vapi-background.js | 4434 | ||||
-rw-r--r-- | js/vapi-browser.js | 241 | ||||
-rw-r--r-- | js/vapi-client.js | 340 | ||||
-rw-r--r-- | js/vapi-cloud.js | 156 | ||||
-rw-r--r-- | js/vapi-common.js | 282 | ||||
-rw-r--r-- | js/vapi-contextmenu.js | 212 | ||||
-rw-r--r-- | js/vapi-cookies.js | 118 | ||||
-rw-r--r-- | js/vapi-core.js | 131 | ||||
-rw-r--r-- | js/vapi-messaging.js | 137 | ||||
-rw-r--r-- | js/vapi-net.js | 69 | ||||
-rw-r--r-- | js/vapi-storage.js | 330 | ||||
-rw-r--r-- | js/vapi-tabs.js | 724 | ||||
-rw-r--r-- | js/vapi-window.js | 183 |
16 files changed, 3673 insertions, 3702 deletions
diff --git a/js/browsercache.js b/js/browsercache.js index 20a53f2..e1986db 100644 --- a/js/browsercache.js +++ b/js/browsercache.js @@ -46,7 +46,7 @@ var clearCache = function() { return; } - vAPI.browserData.clearCache(); + vAPI.browser.data.clearCache(); ηm.clearBrowserCacheCycle = ηm.userSettings.clearBrowserCacheAfter; ηm.browserCacheClearedCounter++; @@ -54,7 +54,7 @@ var clearCache = function() { // TODO: i18n ηm.logger.writeOne('', 'info', vAPI.i18n('loggerEntryBrowserCacheCleared')); - //console.debug('clearBrowserCacheCallback()> vAPI.browserData.clearCache() called'); + //console.debug('clearBrowserCacheCallback()> vAPI.browser.data.clearCache() called'); }; vAPI.setTimeout(clearCache, 15 * 60 * 1000); diff --git a/js/contentscript-start.js b/js/contentscript-start.js index 710504e..dc93c1d 100644 --- a/js/contentscript-start.js +++ b/js/contentscript-start.js @@ -34,7 +34,7 @@ vAPI.selfWorkerSrcReported = vAPI.selfWorkerSrcReported || false; - var reGoodWorkerSrc = /(?:child|worker)-src[^;,]+?'none'/; + var reGoodWorkerSrc = /(?:frame|worker)-src[^;,]+?'none'/; var handler = function(ev) { if ( @@ -48,7 +48,7 @@ // 'effectiveDirective' property. if ( ev.effectiveDirective.startsWith('worker-src') === false && - ev.effectiveDirective.startsWith('child-src') === false + ev.effectiveDirective.startsWith('frame-src') === false ) { return false; } diff --git a/js/traffic.js b/js/traffic.js index b9efa4b..80af782 100644 --- a/js/traffic.js +++ b/js/traffic.js @@ -366,12 +366,12 @@ var onHeadersReceived = function(details) { /******************************************************************************/ var cspNoWorkerInit = function() { - if ( vAPI.webextFlavor === undefined ) { - return "child-src 'none'; frame-src data: blob: *; report-uri about:blank"; + if (ηMatrix.cspNoWorker === undefined) { + ηMatrix.cspNoWorker = "worker-src 'none'; " + +"frame-src data: blob: *; " + +"report-uri about:blank"; } - ηMatrix.cspNoWorker = /^Mozilla-Firefox-5[67]/.test(vAPI.webextFlavor) ? - "child-src 'none'; frame-src data: blob: *; report-uri about:blank" : - "worker-src 'none'; report-uri about:blank" ; + return ηMatrix.cspNoWorker; }; diff --git a/js/vapi-background.js b/js/vapi-background.js index 957a3b8..6100c18 100644 --- a/js/vapi-background.js +++ b/js/vapi-background.js @@ -30,3502 +30,1236 @@ /******************************************************************************/ -(function() { - -/******************************************************************************/ - -// Useful links -// -// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface -// https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Services.jsm - -/******************************************************************************/ - -const {classes: Cc, interfaces: Ci, utils: Cu} = Components; -const {Services} = Cu.import('resource://gre/modules/Services.jsm', null); - -/******************************************************************************/ - -var vAPI = self.vAPI = self.vAPI || {}; -vAPI.firefox = true; - vAPI.modernFirefox = - Services.appinfo.ID === '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}' - && Services.vc.compare(Services.appinfo.version, '44') > 0; - -/******************************************************************************/ - -var deferUntil = function(testFn, mainFn, details) { - if ( typeof details !== 'object' ) { - details = {}; - } - - var now = 0; - var next = details.next || 200; - var until = details.until || 2000; - - var check = function() { - if ( testFn() === true || now >= until ) { - mainFn(); - return; - } - now += next; - vAPI.setTimeout(check, next); - }; - - if ( 'sync' in details && details.sync === true ) { - check(); - } else { - vAPI.setTimeout(check, 1); - } -}; - -/******************************************************************************/ - -vAPI.app = { - name: 'eMatrix', - version: location.hash.slice(1) -}; - -/******************************************************************************/ - -vAPI.app.start = function() { -}; - -/******************************************************************************/ - -vAPI.app.stop = function() { -}; - -/******************************************************************************/ - -vAPI.app.restart = function() { - // Listening in bootstrap.js - Cc['@mozilla.org/childprocessmessagemanager;1'] - .getService(Ci.nsIMessageSender) - .sendAsyncMessage(location.host + '-restart'); -}; - -/******************************************************************************/ - -// List of things that needs to be destroyed when disabling the extension -// Only functions should be added to it - -var cleanupTasks = []; - -// This must be updated manually, every time a new task is added/removed - -// Fixed by github.com/AlexVallat: -// https://github.com/AlexVallat/uBlock/commit/7b781248f00cbe3d61b1cc367c440db80fa06049 -// 7 instances of cleanupTasks.push, but one is unique to fennec, and one to desktop. -var expectedNumberOfCleanups = 7; - -window.addEventListener('unload', function() { - if ( typeof vAPI.app.onShutdown === 'function' ) { - vAPI.app.onShutdown(); - } - - // IMPORTANT: cleanup tasks must be executed using LIFO order. - var i = cleanupTasks.length; - while ( i-- ) { - cleanupTasks[i](); - } - - if ( cleanupTasks.length < expectedNumberOfCleanups ) { - console.error( - 'eMatrix> Cleanup tasks performed: %s (out of %s)', - cleanupTasks.length, - expectedNumberOfCleanups - ); - } - - // frameModule needs to be cleared too - var frameModuleURL = vAPI.getURL('frameModule.js'); - var frameModule = {}; - Cu.import(frameModuleURL, frameModule); - frameModule.contentObserver.unregister(); - Cu.unload(frameModuleURL); -}); - -/******************************************************************************/ - -// For now, only booleans. - -vAPI.browserSettings = { - originalValues: {}, - - rememberOriginalValue: function(path, setting) { - var key = path + '.' + setting; - if ( this.originalValues.hasOwnProperty(key) ) { - return; - } - var hasUserValue; - var branch = Services.prefs.getBranch(path + '.'); - try { - hasUserValue = branch.prefHasUserValue(setting); - } catch (ex) { - } - if ( hasUserValue !== undefined ) { - this.originalValues[key] = hasUserValue ? this.getValue(path, setting) : undefined; - } - }, - - clear: function(path, setting) { - var key = path + '.' + setting; - - // Value was not overriden -- nothing to restore - if ( this.originalValues.hasOwnProperty(key) === false ) { - return; - } - - var value = this.originalValues[key]; - // https://github.com/gorhill/uBlock/issues/292#issuecomment-109621979 - // Forget the value immediately, it may change outside of - // uBlock control. - delete this.originalValues[key]; - - // Original value was a default one - if ( value === undefined ) { - try { - Services.prefs.getBranch(path + '.').clearUserPref(setting); - } catch (ex) { - } - return; - } - - // Reset to original value - this.setValue(path, setting, value); - }, - - getValue: function(path, setting) { - var branch = Services.prefs.getBranch(path + '.'); - var getMethod; - - // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPrefBranch#getPrefType%28%29 - switch ( branch.getPrefType(setting) ) { - case 64: // PREF_INT - getMethod = 'getIntPref'; - break; - case 128: // PREF_BOOL - getMethod = 'getBoolPref'; - break; - default: // not supported - return; - } - - try { - return branch[getMethod](setting); - } catch (ex) { - } - }, - - setValue: function(path, setting, value) { - var setMethod; - switch ( typeof value ) { - case 'number': - setMethod = 'setIntPref'; - break; - case 'boolean': - setMethod = 'setBoolPref'; - break; - default: // not supported - return; - } - - try { - Services.prefs.getBranch(path + '.')[setMethod](setting, value); - } catch (ex) { - } - }, - - setSetting: function(setting, value) { - var prefName, prefVal; - switch ( setting ) { - case 'prefetching': - this.rememberOriginalValue('network', 'prefetch-next'); - // http://betanews.com/2015/08/15/firefox-stealthily-loads-webpages-when-you-hover-over-links-heres-how-to-stop-it/ - // https://bugzilla.mozilla.org/show_bug.cgi?id=814169 - // Sigh. - this.rememberOriginalValue('network.http', 'speculative-parallel-limit'); - // https://github.com/gorhill/uBlock/issues/292 - // "true" means "do not disable", i.e. leave entry alone - if ( value ) { - this.clear('network', 'prefetch-next'); - this.clear('network.http', 'speculative-parallel-limit'); - } else { - this.setValue('network', 'prefetch-next', false); - this.setValue('network.http', 'speculative-parallel-limit', 0); - } - break; - - case 'hyperlinkAuditing': - this.rememberOriginalValue('browser', 'send_pings'); - this.rememberOriginalValue('beacon', 'enabled'); - // https://github.com/gorhill/uBlock/issues/292 - // "true" means "do not disable", i.e. leave entry alone - if ( value ) { - this.clear('browser', 'send_pings'); - this.clear('beacon', 'enabled'); - } else { - this.setValue('browser', 'send_pings', false); - this.setValue('beacon', 'enabled', false); - } - break; - - // https://github.com/gorhill/uBlock/issues/894 - // Do not disable completely WebRTC if it can be avoided. FF42+ - // has a `media.peerconnection.ice.default_address_only` pref which - // purpose is to prevent local IP address leakage. - case 'webrtcIPAddress': - if ( this.getValue('media.peerconnection', 'ice.default_address_only') !== undefined ) { - prefName = 'ice.default_address_only'; - prefVal = true; - } else { - prefName = 'enabled'; - prefVal = false; - } - - this.rememberOriginalValue('media.peerconnection', prefName); - if ( value ) { - this.clear('media.peerconnection', prefName); - } else { - this.setValue('media.peerconnection', prefName, prefVal); - } - break; - - default: - break; - } - }, - - set: function(details) { - for ( var setting in details ) { - if ( details.hasOwnProperty(setting) === false ) { - continue; - } - this.setSetting(setting, !!details[setting]); - } - }, - - restoreAll: function() { - var pos; - for ( var key in this.originalValues ) { - if ( this.originalValues.hasOwnProperty(key) === false ) { - continue; - } - pos = key.lastIndexOf('.'); - this.clear(key.slice(0, pos), key.slice(pos + 1)); - } - } -}; - -cleanupTasks.push(vAPI.browserSettings.restoreAll.bind(vAPI.browserSettings)); - -/******************************************************************************/ - -// API matches that of chrome.storage.local: -// https://developer.chrome.com/extensions/storage - -vAPI.storage = (function() { - var db = null; - var vacuumTimer = null; - - var close = function() { - if ( vacuumTimer !== null ) { - clearTimeout(vacuumTimer); - vacuumTimer = null; - } - if ( db === null ) { +(function () { + Cu.import('chrome://ematrix/content/HttpRequestHeaders.jsm'); + + // Icon-related stuff + vAPI.setIcon = function (tabId, iconId, badge) { + // If badge is undefined, then setIcon was called from the + // TabSelect event + let win; + if (badge === undefined) { + win = iconId; + } else { + win = vAPI.window.getCurrentWindow(); + } + + let tabBrowser = vAPI.browser.getTabBrowser(win); + if (tabBrowser === null) { return; - } - db.asyncClose(); - db = null; - }; - - var open = function() { - if ( db !== null ) { - return db; - } - - // Create path - let path = Services.dirsvc.get('ProfD', Ci.nsIFile); - path.append('ematrix-data'); - if ( !path.exists() ) { - path.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt('0774', 8)); - } - if ( !path.isDirectory() ) { - throw Error('Should be a directory...'); - } + } - let path2 = Services.dirsvc.get('ProfD', Ci.nsIFile); - path2.append('extension-data'); - path2.append(location.host + '.sqlite'); - if (path2.exists()) { - path2.moveTo(path, location.host+'.sqlite'); + let curTabId = vAPI.tabs.manager.tabIdFromTarget(tabBrowser.selectedTab); + let tb = vAPI.toolbarButton; + + // from 'TabSelect' event + if (tabId === undefined) { + tabId = curTabId; + } else if (badge !== undefined) { + tb.tabs[tabId] = { + badge: badge, img: iconId + }; } - path.append(location.host + '.sqlite'); - - // Open database - try { - db = Services.storage.openDatabase(path); - if ( db.connectionReady === false ) { - db.asyncClose(); - db = null; - } - } catch (ex) { - } - - if ( db === null ) { - return null; - } - - // Database was opened, register cleanup task - cleanupTasks.push(close); - - // Setup database - db.createAsyncStatement('CREATE TABLE IF NOT EXISTS "settings" ("name" TEXT PRIMARY KEY NOT NULL, "value" TEXT);') - .executeAsync(); - - if ( vacuum !== null ) { - vacuumTimer = vAPI.setTimeout(vacuum, 60000); - } - - return db; - }; - - // https://developer.mozilla.org/en-US/docs/Storage/Performance#Vacuuming_and_zero-fill - // Vacuum only once, and only while idle - var vacuum = function() { - vacuumTimer = null; - if ( db === null ) { - return; - } - var idleSvc = Cc['@mozilla.org/widget/idleservice;1'] - .getService(Ci.nsIIdleService); - if ( idleSvc.idleTime < 60000 ) { - vacuumTimer = vAPI.setTimeout(vacuum, 60000); - return; - } - db.createAsyncStatement('VACUUM').executeAsync(); - vacuum = null; - }; - - // Execute a query - var runStatement = function(stmt, callback) { - var result = {}; - - stmt.executeAsync({ - handleResult: function(rows) { - if ( !rows || typeof callback !== 'function' ) { - return; - } - - var row; - - while ( (row = rows.getNextRow()) ) { - // we assume that there will be two columns, since we're - // using it only for preferences - result[row.getResultByIndex(0)] = row.getResultByIndex(1); - } - }, - handleCompletion: function(reason) { - if ( typeof callback === 'function' && reason === 0 ) { - callback(result); - } - }, - handleError: function(error) { - console.error('SQLite error ', error.result, error.message); - // Caller expects an answer regardless of failure. - if ( typeof callback === 'function' ) { - callback(null); - } - } - }); - }; - - var bindNames = function(stmt, names) { - if ( Array.isArray(names) === false || names.length === 0 ) { - return; - } - var params = stmt.newBindingParamsArray(); - var i = names.length, bp; - while ( i-- ) { - bp = params.newBindingParams(); - bp.bindByName('name', names[i]); - params.addParams(bp); - } - stmt.bindParameters(params); + if (tabId === curTabId) { + tb.updateState(win, tabId); + } }; - var clear = function(callback) { - if ( open() === null ) { - if ( typeof callback === 'function' ) { - callback(); + vAPI.httpObserver = { + classDescription: 'net-channel-event-sinks for ' + location.host, + classID: Components.ID('{5d2e2797-6d68-42e2-8aeb-81ce6ba16b95}'), + contractID: '@' + location.host + '/net-channel-event-sinks;1', + REQDATAKEY: location.host + 'reqdata', + ABORT: Components.results.NS_BINDING_ABORTED, + ACCEPT: Components.results.NS_SUCCEEDED, + // Request types: + // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIContentPolicy#Constants + // eMatrix: we can just use nsIContentPolicy's built-in + // constants, can we? + frameTypeMap: { + 6: 'main_frame', + 7: 'sub_frame' + }, + typeMap: { + 1: 'other', + 2: 'script', + 3: 'image', + 4: 'stylesheet', + 5: 'object', + 6: 'main_frame', + 7: 'sub_frame', + 9: 'xbl', + 10: 'ping', + 11: 'xmlhttprequest', + 12: 'object', + 13: 'xml_dtd', + 14: 'font', + 15: 'media', + 16: 'websocket', + 17: 'csp_report', + 18: 'xslt', + 19: 'beacon', + 20: 'xmlhttprequest', + 21: 'imageset', + 22: 'web_manifest' + }, + mimeTypeMap: { + 'audio': 15, + 'video': 15 + }, + get componentRegistrar () { + return Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + }, + get categoryManager () { + return Cc['@mozilla.org/categorymanager;1'] + .getService(Ci.nsICategoryManager); + }, + QueryInterface: (function () { + var {XPCOMUtils} = + Cu.import('resource://gre/modules/XPCOMUtils.jsm', null); + + return XPCOMUtils.generateQI([ + Ci.nsIFactory, + Ci.nsIObserver, + Ci.nsIChannelEventSink, + Ci.nsISupportsWeakReference + ]); + })(), + createInstance: function (outer, iid) { + if (outer) { + throw Components.results.NS_ERROR_NO_AGGREGATION; + } + + return this.QueryInterface(iid); + }, + register: function () { + this.pendingRingBufferInit(); + + Services.obs.addObserver(this, 'http-on-modify-request', true); + Services.obs.addObserver(this, 'http-on-examine-response', true); + Services.obs.addObserver(this, 'http-on-examine-cached-response', true); + + // Guard against stale instances not having been unregistered + if (this.componentRegistrar.isCIDRegistered(this.classID)) { + try { + this.componentRegistrar + .unregisterFactory(this.classID, + Components.manager + .getClassObject(this.classID, + Ci.nsIFactory)); + } catch (ex) { + console.error('eMatrix> httpObserver > ' + +'unable to unregister stale instance: ', ex); + } } - return; - } - runStatement(db.createAsyncStatement('DELETE FROM "settings";'), callback); - }; - var getBytesInUse = function(keys, callback) { - if ( typeof callback !== 'function' ) { - return; - } - - if ( open() === null ) { - callback(0); - return; - } - - var stmt; - if ( Array.isArray(keys) ) { - stmt = db.createAsyncStatement('SELECT "size" AS "size", SUM(LENGTH("value")) FROM "settings" WHERE "name" = :name'); - bindNames(keys); - } else { - stmt = db.createAsyncStatement('SELECT "size" AS "size", SUM(LENGTH("value")) FROM "settings"'); - } - - runStatement(stmt, function(result) { - callback(result.size); - }); - }; - - var read = function(details, callback) { - if ( typeof callback !== 'function' ) { - return; - } - - var prepareResult = function(result) { - var key; - for ( key in result ) { - if ( result.hasOwnProperty(key) === false ) { - continue; - } - result[key] = JSON.parse(result[key]); - } - if ( typeof details === 'object' && details !== null ) { - for ( key in details ) { - if ( result.hasOwnProperty(key) === false ) { - result[key] = details[key]; - } - } + this.componentRegistrar.registerFactory(this.classID, + this.classDescription, + this.contractID, + this); + this.categoryManager.addCategoryEntry('net-channel-event-sinks', + this.contractID, + this.contractID, + false, + true); + }, + unregister: function () { + Services.obs.removeObserver(this, 'http-on-modify-request'); + Services.obs.removeObserver(this, 'http-on-examine-response'); + Services.obs.removeObserver(this, 'http-on-examine-cached-response'); + + this.componentRegistrar.unregisterFactory(this.classID, this); + this.categoryManager.deleteCategoryEntry('net-channel-event-sinks', + this.contractID, + false); + }, + PendingRequest: function () { + this.rawType = 0; + this.tabId = 0; + this._key = ''; // key is url, from URI.spec + }, + // If all work fine, this map should not grow indefinitely. It + // can have stale items in it, but these will be taken care of + // when entries in the ring buffer are overwritten. + pendingURLToIndex: new Map(), + pendingWritePointer: 0, + pendingRingBuffer: new Array(256), + pendingRingBufferInit: function () { + // Use and reuse pre-allocated PendingRequest objects = + // less memory churning. + for (let i=this.pendingRingBuffer.length-1; i>=0; --i) { + this.pendingRingBuffer[i] = new this.PendingRequest(); + } + }, + createPendingRequest: function (url) { + // Pending request ring buffer: + // +-------+-------+-------+-------+-------+-------+------- + // |0 |1 |2 |3 |4 |5 |... + // +-------+-------+-------+-------+-------+-------+------- + // + // URL to ring buffer index map: + // { k = URL, s = ring buffer indices } + // + // s is a string which character codes map to ring buffer + // indices -- for when the same URL is received multiple times + // by shouldLoadListener() before the existing one is serviced + // by the network request observer. I believe the use of a + // string in lieu of an array reduces memory churning. + let bucket; + let i = this.pendingWritePointer; + this.pendingWritePointer = i + 1 & 255; + + let preq = this.pendingRingBuffer[i]; + let si = String.fromCharCode(i); + + // Cleanup unserviced pending request + if (preq._key !== '') { + bucket = this.pendingURLToIndex.get(preq._key); + if (bucket.length === 1) { + this.pendingURLToIndex.delete(preq._key); + } else { + let pos = bucket.indexOf(si); + this.pendingURLToIndex.set(preq._key, + bucket.slice(0, pos) + + bucket.slice(pos + 1)); + } } - callback(result); - }; - - if ( open() === null ) { - prepareResult({}); - return; - } - - var names = []; - if ( details !== null ) { - if ( Array.isArray(details) ) { - names = details; - } else if ( typeof details === 'object' ) { - names = Object.keys(details); + + bucket = this.pendingURLToIndex.get(url); + this.pendingURLToIndex.set(url, bucket === undefined + ? si + : bucket + si); + preq._key = url; + return preq; + }, + lookupPendingRequest: function (url) { + let bucket = this.pendingURLToIndex.get(url); + if (bucket === undefined) { + return null; + } + + let i = bucket.charCodeAt(0); + if (bucket.length === 1) { + this.pendingURLToIndex.delete(url); } else { - names = [details.toString()]; - } - } - - var stmt; - if ( names.length === 0 ) { - stmt = db.createAsyncStatement('SELECT * FROM "settings"'); - } else { - stmt = db.createAsyncStatement('SELECT * FROM "settings" WHERE "name" = :name'); - bindNames(stmt, names); - } - - runStatement(stmt, prepareResult); - }; - - var remove = function(keys, callback) { - if ( open() === null ) { - if ( typeof callback === 'function' ) { - callback(); + this.pendingURLToIndex.set(url, bucket.slice(1)); + } + + let preq = this.pendingRingBuffer[i]; + preq._key = ''; // mark as "serviced" + return preq; + }, + handleRequest: function (channel, URI, tabId, rawType) { + let type = this.typeMap[rawType] || 'other'; + + let onBeforeRequest = vAPI.net.onBeforeRequest; + if (onBeforeRequest.types === null + || onBeforeRequest.types.has(type)) { + let result = onBeforeRequest.callback({ + parentFrameId: type === 'main_frame' ? -1 : 0, + tabId: tabId, + type: type, + url: URI.asciiSpec + }); + + if (typeof result === 'object') { + channel.cancel(this.ABORT); + return true; + } } - return; - } - var stmt = db.createAsyncStatement('DELETE FROM "settings" WHERE "name" = :name'); - bindNames(stmt, typeof keys === 'string' ? [keys] : keys); - runStatement(stmt, callback); - }; - var write = function(details, callback) { - if ( open() === null ) { - if ( typeof callback === 'function' ) { - callback(); + let onBeforeSendHeaders = vAPI.net.onBeforeSendHeaders; + if (onBeforeSendHeaders.types === null + || onBeforeSendHeaders.types.has(type)) { + let requestHeaders = HTTPRequestHeaders.factory(channel); + let newHeaders = onBeforeSendHeaders.callback({ + parentFrameId: type === 'main_frame' ? -1 : 0, + requestHeaders: requestHeaders.headers, + tabId: tabId, + type: type, + url: URI.asciiSpec, + method: channel.requestMethod + }); + + if (newHeaders) { + requestHeaders.update(); + } + requestHeaders.dispose(); } - return; - } - var stmt = db.createAsyncStatement('INSERT OR REPLACE INTO "settings" ("name", "value") VALUES(:name, :value)'); - var params = stmt.newBindingParamsArray(), bp; - for ( var key in details ) { - if ( details.hasOwnProperty(key) === false ) { - continue; + return false; + }, + channelDataFromChannel: function (channel) { + if (channel instanceof Ci.nsIWritablePropertyBag) { + try { + return channel.getProperty(this.REQDATAKEY) || null; + } catch (ex) { + // Ignore + } } - bp = params.newBindingParams(); - bp.bindByName('name', key); - bp.bindByName('value', JSON.stringify(details[key])); - params.addParams(bp); - } - if ( params.length === 0 ) { - return; - } - - stmt.bindParameters(params); - runStatement(stmt, callback); - }; - - // Export API - var api = { - QUOTA_BYTES: 100 * 1024 * 1024, - clear: clear, - get: read, - getBytesInUse: getBytesInUse, - remove: remove, - set: write - }; - return api; -})(); - -vAPI.cacheStorage = vAPI.storage; - -/******************************************************************************/ - -// This must be executed/setup early. - -var winWatcher = (function() { - var windowToIdMap = new Map(); - var windowIdGenerator = 1; - var api = { - onOpenWindow: null, - onCloseWindow: null - }; - - // https://github.com/gorhill/uMatrix/issues/586 - // This is necessary hack because on SeaMonkey 2.40, for unknown reasons - // private windows do not have the attribute `windowtype` set to - // `navigator:browser`. As a fallback, the code here will also test whether - // the id attribute is `main-window`. - api.toBrowserWindow = function(win) { - var docElement = win && win.document && win.document.documentElement; - if ( !docElement ) { + return null; - } - if ( vAPI.thunderbird ) { - return docElement.getAttribute('windowtype') === 'mail:3pane' ? win : null; - } - return docElement.getAttribute('windowtype') === 'navigator:browser' || - docElement.getAttribute('id') === 'main-window' ? - win : null; - }; - - api.getWindows = function() { - return windowToIdMap.keys(); - }; - - api.idFromWindow = function(win) { - return windowToIdMap.get(win) || 0; - }; - - api.getCurrentWindow = function() { - return this.toBrowserWindow(Services.wm.getMostRecentWindow(null)); - }; - - var addWindow = function(win) { - if ( !win || windowToIdMap.has(win) ) { - return; - } - windowToIdMap.set(win, windowIdGenerator++); - if ( typeof api.onOpenWindow === 'function' ) { - api.onOpenWindow(win); - } - }; - - var removeWindow = function(win) { - if ( !win || windowToIdMap.delete(win) !== true ) { - return; - } - if ( typeof api.onCloseWindow === 'function' ) { - api.onCloseWindow(win); - } - }; - - // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIWindowMediator - // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIWindowWatcher - // https://github.com/gorhill/uMatrix/issues/357 - // Use nsIWindowMediator for being notified of opened/closed windows. - var listeners = { - onOpenWindow: function(aWindow) { - var win; + }, + // https://github.com/gorhill/uMatrix/issues/165 + // https://developer.mozilla.org/en-US/Firefox/Releases/3.5/Updating_extensions#Getting_a_load_context_from_a_request + // Not sure `ematrix:shouldLoad` is still needed, eMatrix does + // not care about embedded frames topography. + // Also: + // https://developer.mozilla.org/en-US/Firefox/Multiprocess_Firefox/Limitations_of_chrome_scripts + tabIdFromChannel: function (channel) { + let lc; try { - win = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow); - } catch (ex) { + lc = channel.notificationCallbacks.getInterface(Ci.nsILoadContext); + } catch(ex) { + // Ignore + } + + if (!lc) { + try { + lc = channel.loadGroup.notificationCallbacks + .getInterface(Ci.nsILoadContext); + } catch(ex) { + // Ignore + } + + if (!lc) { + return vAPI.noTabId; + } } - addWindow(win); - }, - - onCloseWindow: function(aWindow) { - var win; + + if (lc.topFrameElement) { + return vAPI.tabs.manager.tabIdFromTarget(lc.topFrameElement); + } + + let win; try { - win = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow); + win = lc.associatedWindow; } catch (ex) { - } - removeWindow(win); - }, - - observe: function(aSubject, topic) { - // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIWindowWatcher#registerNotification%28%29 - // "aSubject - the window being opened or closed, sent as an - // "nsISupports which can be ... QueryInterfaced to an - // "nsIDOMWindow." - var win; + // Ignore + } + + if (!win) { + return vAPI.noTabId; + } + + if (win.top) { + win = win.top; + } + + let tabBrowser; try { - win = aSubject.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow); + tabBrowser = + vAPI.browser.getTabBrowser + (win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell).rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow)); } catch (ex) { - } - if ( !win ) { return; } - if ( topic === 'domwindowopened' ) { - addWindow(win); - return; - } - if ( topic === 'domwindowclosed' ) { - removeWindow(win); - return; - } - } - }; - - (function() { - var winumerator, win; - - // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIWindowMediator#getEnumerator%28%29 - winumerator = Services.wm.getEnumerator(null); - while ( winumerator.hasMoreElements() ) { - win = winumerator.getNext(); - if ( !win.closed ) { - windowToIdMap.set(win, windowIdGenerator++); - } - } - - // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIWindowWatcher#getWindowEnumerator%28%29 - winumerator = Services.ww.getWindowEnumerator(); - while ( winumerator.hasMoreElements() ) { - win = winumerator.getNext() - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow); - if ( !win.closed ) { - windowToIdMap.set(win, windowIdGenerator++); - } - } - - Services.wm.addListener(listeners); - Services.ww.registerNotification(listeners); - })(); - - cleanupTasks.push(function() { - Services.wm.removeListener(listeners); - Services.ww.unregisterNotification(listeners); - windowToIdMap.clear(); - }); - - return api; -})(); - -/******************************************************************************/ - -var getTabBrowser = function(win) { - return win && win.gBrowser || null; -}; - -/******************************************************************************/ - -var getOwnerWindow = function(target) { - if ( target.ownerDocument ) { - return target.ownerDocument.defaultView; - } - return null; -}; - -/******************************************************************************/ - -vAPI.isBehindTheSceneTabId = function(tabId) { - return tabId.toString() === '-1'; -}; - -vAPI.noTabId = '-1'; - -/******************************************************************************/ - -vAPI.tabs = {}; - -/******************************************************************************/ - -vAPI.tabs.registerListeners = function() { - tabWatcher.start(); -}; - -/******************************************************************************/ - -// Firefox: -// https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Tabbed_browser -// -// browser --> ownerDocument --> defaultView --> gBrowser --> browsers --+ -// ^ | -// | | -// +------------------------------------------------------------------- -// -// browser (browser) -// contentTitle -// currentURI -// ownerDocument (XULDocument) -// defaultView (ChromeWindow) -// gBrowser (tabbrowser OR browser) -// browsers (browser) -// selectedBrowser -// selectedTab -// tabs (tab.tabbrowser-tab) -// -// Fennec: (what I figured so far) -// -// tab --> browser windows --> window --> BrowserApp --> tabs --+ -// ^ window | -// | | -// +---------------------------------------------------------------+ -// -// tab -// browser -// [manual search to go back to tab from list of windows] - -vAPI.tabs.get = function(tabId, callback) { - var browser; - - if ( tabId === null ) { - browser = tabWatcher.currentBrowser(); - tabId = tabWatcher.tabIdFromTarget(browser); - } else { - browser = tabWatcher.browserFromTabId(tabId); - } - - // For internal use - if ( typeof callback !== 'function' ) { - return browser; - } - - if ( !browser || !browser.currentURI ) { - callback(); - return; - } - - var win = getOwnerWindow(browser); - var tabBrowser = getTabBrowser(win); - - // https://github.com/gorhill/uMatrix/issues/540 - // The `index` property is nowhere used by eMatrix at this point, so we - // will refrain from returning this information for the time being. - - callback({ - id: tabId, - index: undefined, - windowId: winWatcher.idFromWindow(win), - active: tabBrowser !== null && browser === tabBrowser.selectedBrowser, - url: browser.currentURI.asciiSpec, - title: browser.contentTitle - }); -}; - -/******************************************************************************/ - -vAPI.tabs.getAllSync = function(window) { - var win, tab; - var tabs = []; - - for ( win of winWatcher.getWindows() ) { - if ( window && window !== win ) { - continue; - } - - var tabBrowser = getTabBrowser(win); - if ( tabBrowser === null ) { - continue; - } - - // This can happens if a tab-less window is currently opened. - // Example of a tab-less window: one opened from clicking - // "View Page Source". - if ( !tabBrowser.tabs ) { - continue; - } - - for ( tab of tabBrowser.tabs ) { - tabs.push(tab); - } - } - - return tabs; -}; - -/******************************************************************************/ - -vAPI.tabs.getAll = function(callback) { - var tabs = [], tab; - - for ( var browser of tabWatcher.browsers() ) { - tab = tabWatcher.tabFromBrowser(browser); - if ( tab === null ) { - continue; - } - if ( tab.hasAttribute('pending') ) { - continue; - } - tabs.push({ - id: tabWatcher.tabIdFromTarget(browser), - url: browser.currentURI.asciiSpec - }); - } - - callback(tabs); -}; - -/******************************************************************************/ - -// properties of the details object: -// url: 'URL', // the address that will be opened -// tabId: 1, // the tab is used if set, instead of creating a new one -// index: -1, // undefined: end of the list, -1: following tab, or after index -// active: false, // opens the tab in background - true and undefined: foreground -// select: true // if a tab is already opened with that url, then select it instead of opening a new one - -vAPI.tabs.open = function(details) { - if ( !details.url ) { - return null; - } - // extension pages - if ( /^[\w-]{2,}:/.test(details.url) === false ) { - details.url = vAPI.getURL(details.url); - } - - var tab; + // Ignore + } + + if (!tabBrowser) { + return vAPI.noTabId; + } + + if (tabBrowser.getBrowserForContentWindow) { + return vAPI.tabs.manager + .tabIdFromTarget(tabBrowser.getBrowserForContentWindow(win)); + } + + // Falling back onto _getTabForContentWindow to ensure older + // versions of Firefox work well. + return tabBrowser._getTabForContentWindow + ? vAPI.tabs.manager + .tabIdFromTarget(tabBrowser._getTabForContentWindow(win)) + : vAPI.noTabId; + }, + rawtypeFromContentType: function (channel) { + let mime = channel.contentType; + if (!mime) { + return 0; + } + + let pos = mime.indexOf('/'); + if (pos === -1) { + pos = mime.length; + } + + return this.mimeTypeMap[mime.slice(0, pos)] || 0; + }, + observe: function (channel, topic) { + if (channel instanceof Ci.nsIHttpChannel === false) { + return; + } + + let URI = channel.URI; + let channelData = this.channelDataFromChannel(channel); + + if (topic.lastIndexOf('http-on-examine-', 0) === 0) { + if (channelData === null) { + return; + } - if ( details.select ) { - var URI = Services.io.newURI(details.url, null, null); + let type = this.frameTypeMap[channelData[1]]; + if (!type) { + return; + } - for ( tab of this.getAllSync() ) { - var browser = tabWatcher.browserFromTarget(tab); - // https://github.com/gorhill/uBlock/issues/2558 - if ( browser === null ) { continue; } + // topic = ['Content-Security-Policy', + // 'Content-Security-Policy-Report-Only']; + // + // Can send empty responseHeaders as these headers are + // only added to and then merged. + // + // TODO: Find better place for this, needs to be set + // before onHeadersReceived.callback. Web workers not + // blocked in Pale Moon as child-src currently + // unavailable, see: + // + // https://github.com/MoonchildProductions/Pale-Moon/issues/949 + // + // eMatrix: as of Pale Moon 28 it seems child-src is + // available and depracated(?) + if (ηMatrix.cspNoWorker === undefined) { + // ηMatrix.cspNoWorker = "child-src 'none'; " + // +"frame-src data: blob: *; " + // +"report-uri about:blank"; + ηMatrix.cspNoWorker = "worker-src 'none'; " + +"frame-src data: blob: *; " + +"report-uri about:blank"; + } - // Or simply .equals if we care about the fragment - if ( URI.equalsExceptRef(browser.currentURI) === false ) { - continue; + let result = vAPI.net.onHeadersReceived.callback({ + parentFrameId: type === 'main_frame' ? -1 : 0, + responseHeaders: [], + tabId: channelData[0], + type: type, + url: URI.asciiSpec + }); + + if (result) { + for (let header of result.responseHeaders) { + channel.setResponseHeader(header.name, + header.value, + true); + } + } + + return; + } + + // http-on-modify-request + // The channel was previously serviced. + if (channelData !== null) { + this.handleRequest(channel, URI, + channelData[0], channelData[1]); + return; + } + + // The channel was never serviced. + let tabId; + let pendingRequest = this.lookupPendingRequest(URI.asciiSpec); + let rawType = 1; + let loadInfo = channel.loadInfo; + + // https://github.com/gorhill/uMatrix/issues/390#issuecomment-155717004 + if (loadInfo) { + rawType = (loadInfo.externalContentPolicyType !== undefined) + ? loadInfo.externalContentPolicyType + : loadInfo.contentPolicyType; + if (!rawType) { + rawType = 1; + } } - this.select(tab); - - // Update URL if fragment is different - if ( URI.equals(browser.currentURI) === false ) { - browser.loadURI(URI.asciiSpec); + if (pendingRequest !== null) { + tabId = pendingRequest.tabId; + // https://github.com/gorhill/uBlock/issues/654 + // Use the request type from the HTTP observer point of view. + if (rawType !== 1) { + pendingRequest.rawType = rawType; + } else { + rawType = pendingRequest.rawType; + } + } else { + tabId = this.tabIdFromChannel(channel); } - return; - } - } - - if ( details.active === undefined ) { - details.active = true; - } - - if ( details.tabId ) { - tab = tabWatcher.browserFromTabId(details.tabId); - if ( tab ) { - tabWatcher.browserFromTarget(tab).loadURI(details.url); - return; - } - } - - // Open in a standalone window - if ( details.popup === true ) { - Services.ww.openWindow( - self, - details.url, - null, - 'location=1,menubar=1,personalbar=1,resizable=1,toolbar=1', - null - ); - return; - } - - var win = winWatcher.getCurrentWindow(); - var tabBrowser = getTabBrowser(win); - if ( tabBrowser === null ) { - return; - } - - if ( details.index === -1 ) { - details.index = tabBrowser.browsers.indexOf(tabBrowser.selectedBrowser) + 1; - } - - tab = tabBrowser.loadOneTab(details.url, { inBackground: !details.active }); - if ( details.index !== undefined ) { - tabBrowser.moveTabTo(tab, details.index); - } -}; - -/******************************************************************************/ - -// Replace the URL of a tab. Noop if the tab does not exist. - -vAPI.tabs.replace = function(tabId, url) { - var targetURL = url; - - // extension pages - if ( /^[\w-]{2,}:/.test(targetURL) !== true ) { - targetURL = vAPI.getURL(targetURL); - } - - var browser = tabWatcher.browserFromTabId(tabId); - if ( browser ) { - browser.loadURI(targetURL); - } -}; - -/******************************************************************************/ - -vAPI.tabs._remove = function(tab, tabBrowser) { - if ( tabBrowser ) { - tabBrowser.removeTab(tab); - } -}; - -/******************************************************************************/ - -vAPI.tabs.remove = function(tabId) { - var browser = tabWatcher.browserFromTabId(tabId); - if ( !browser ) { - return; - } - var tab = tabWatcher.tabFromBrowser(browser); - if ( !tab ) { - return; - } - this._remove(tab, getTabBrowser(getOwnerWindow(browser))); -}; - -/******************************************************************************/ - -vAPI.tabs.reload = function(tabId) { - var browser = tabWatcher.browserFromTabId(tabId); - if ( !browser ) { - return; - } - - browser.webNavigation.reload(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE); -}; - -/******************************************************************************/ - -vAPI.tabs.select = function(tab) { - if ( typeof tab !== 'object' ) { - tab = tabWatcher.tabFromBrowser(tabWatcher.browserFromTabId(tab)); - } - if ( !tab ) { - return; - } - - // https://github.com/gorhill/uBlock/issues/470 - var win = getOwnerWindow(tab); - win.focus(); - - var tabBrowser = getTabBrowser(win); - if ( tabBrowser ) { - tabBrowser.selectedTab = tab; - } -}; - -/******************************************************************************/ - -vAPI.tabs.injectScript = function(tabId, details, callback) { - var browser = tabWatcher.browserFromTabId(tabId); - if ( !browser ) { - return; - } - - if ( typeof details.file !== 'string' ) { - return; - } - - details.file = vAPI.getURL(details.file); - browser.messageManager.sendAsyncMessage( - location.host + ':broadcast', - JSON.stringify({ - broadcast: true, - channelName: 'vAPI', - msg: { - cmd: 'injectScript', - details: details + if (this.handleRequest(channel, URI, tabId, rawType)) { + return; } - }) - ); - - if ( typeof callback === 'function' ) { - vAPI.setTimeout(callback, 13); - } -}; - -/******************************************************************************/ - -var tabWatcher = (function() { - // TODO: find out whether we need a janitor to take care of stale entries. - - // https://github.com/gorhill/uMatrix/issues/540 - // Use only weak references to hold onto browser references. - var browserToTabIdMap = new WeakMap(); - var tabIdToBrowserMap = new Map(); - var tabIdGenerator = 1; - - var indexFromBrowser = function(browser) { - if ( !browser ) { - return -1; - } - var win = getOwnerWindow(browser); - if ( !win ) { - return -1; - } - var tabBrowser = getTabBrowser(win); - if ( tabBrowser === null ) { - return -1; - } - // This can happen, for example, the `view-source:` window, there is - // no tabbrowser object, the browser object sits directly in the - // window. - if ( tabBrowser === browser ) { - return 0; - } - return tabBrowser.browsers.indexOf(browser); - }; - - var indexFromTarget = function(target) { - return indexFromBrowser(browserFromTarget(target)); - }; - - var tabFromBrowser = function(browser) { - var i = indexFromBrowser(browser); - if ( i === -1 ) { - return null; - } - var win = getOwnerWindow(browser); - if ( !win ) { - return null; - } - var tabBrowser = getTabBrowser(win); - if ( tabBrowser === null ) { - return null; - } - if ( !tabBrowser.tabs || i >= tabBrowser.tabs.length ) { - return null; - } - return tabBrowser.tabs[i]; - }; - var browserFromTarget = function(target) { - if ( !target ) { - return null; - } - if ( target.linkedPanel ) { // target is a tab - target = target.linkedBrowser; - } - if ( target.localName !== 'browser' ) { - return null; - } - return target; - }; - - var tabIdFromTarget = function(target) { - var browser = browserFromTarget(target); - if ( browser === null ) { - return vAPI.noTabId; - } - var tabId = browserToTabIdMap.get(browser); - if ( tabId === undefined ) { - tabId = '' + tabIdGenerator++; - browserToTabIdMap.set(browser, tabId); - tabIdToBrowserMap.set(tabId, Cu.getWeakReference(browser)); - } - return tabId; - }; - - var browserFromTabId = function(tabId) { - var weakref = tabIdToBrowserMap.get(tabId); - var browser = weakref && weakref.get(); - return browser || null; - }; + if (channel instanceof Ci.nsIWritablePropertyBag === false) { + return; + } - var currentBrowser = function() { - var win = winWatcher.getCurrentWindow(); - // https://github.com/gorhill/uBlock/issues/399 - // getTabBrowser() can return null at browser launch time. - var tabBrowser = getTabBrowser(win); - if ( tabBrowser === null ) { - return null; - } - return browserFromTarget(tabBrowser.selectedTab); - }; + // Carry data for behind-the-scene redirects + channel.setProperty(this.REQDATAKEY, [tabId, rawType]); + }, + asyncOnChannelRedirect: function (oldChannel, newChannel, + flags, callback) { + // contentPolicy.shouldLoad doesn't detect redirects, this + // needs to be used + // If error thrown, the redirect will fail + try { + let URI = newChannel.URI; + if (!URI.schemeIs('http') && !URI.schemeIs('https')) { + return; + } - var removeBrowserEntry = function(tabId, browser) { - if ( tabId && tabId !== vAPI.noTabId ) { - vAPI.tabs.onClosed(tabId); - delete vAPI.toolbarButton.tabs[tabId]; - tabIdToBrowserMap.delete(tabId); - } - if ( browser ) { - browserToTabIdMap.delete(browser); - } - }; + if (newChannel instanceof Ci.nsIWritablePropertyBag === false) { + return; + } - var removeTarget = function(target) { - onClose({ target: target }); - }; + let channelData = this.channelDataFromChannel(oldChannel); + if (channelData === null) { + return; + } - var getAllBrowsers = function() { - var browsers = [], browser; - for ( var [tabId, weakref] of tabIdToBrowserMap ) { - browser = weakref.get(); - // TODO: - // Maybe call removeBrowserEntry() if the browser no longer exists? - if ( browser ) { - browsers.push(browser); + // Carry the data on in case of multiple redirects + newChannel.setProperty(this.REQDATAKEY, channelData); + } catch (ex) { + // console.error(ex); + // Ignore + } finally { + callback.onRedirectVerifyCallback(this.ACCEPT); } - } - return browsers; - }; - - // https://developer.mozilla.org/en-US/docs/Web/Events/TabOpen - //var onOpen = function({target}) { - // var tabId = tabIdFromTarget(target); - // var browser = browserFromTabId(tabId); - // vAPI.tabs.onNavigation({ - // frameId: 0, - // tabId: tabId, - // url: browser.currentURI.asciiSpec, - // }); - //}; - - // https://developer.mozilla.org/en-US/docs/Web/Events/TabShow - var onShow = function({target}) { - tabIdFromTarget(target); - }; - - // https://developer.mozilla.org/en-US/docs/Web/Events/TabClose - var onClose = function({target}) { - // target is tab in Firefox, browser in Fennec - var browser = browserFromTarget(target); - var tabId = browserToTabIdMap.get(browser); - removeBrowserEntry(tabId, browser); + } }; - // https://developer.mozilla.org/en-US/docs/Web/Events/TabSelect - // This is an entry point: when creating a new tab, it is not always - // reported through onLocationChanged... Sigh. It is "reported" here - // however. - var onSelect = function({target}) { - var browser = browserFromTarget(target); - var tabId = browserToTabIdMap.get(browser); - if ( tabId === undefined ) { - tabId = tabIdFromTarget(target); - vAPI.tabs.onNavigation({ - frameId: 0, - tabId: tabId, - url: browser.currentURI.asciiSpec - }); - } - vAPI.setIcon(tabId, getOwnerWindow(target)); + vAPI.toolbarButton = { + id: location.host + '-button', + type: 'view', + viewId: location.host + '-panel', + label: vAPI.app.name, + tooltiptext: vAPI.app.name, + tabs: {/*tabId: {badge: 0, img: boolean}*/}, + init: null, + codePath: '' }; - var locationChangedMessageName = location.host + ':locationChanged'; - - var onLocationChanged = function(e) { - var vapi = vAPI; - var details = e.data; - - // Ignore notifications related to our popup - if ( details.url.lastIndexOf(vapi.getURL('popup.html'), 0) === 0 ) { - return; - } - - var browser = e.target; - var tabId = tabIdFromTarget(browser); - if ( tabId === vapi.noTabId ) { + // Non-Fennec: common code paths. + (function () { + if (vAPI.fennec) { return; - } - - // LOCATION_CHANGE_SAME_DOCUMENT = "did not load a new document" - if ( details.flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT ) { - vapi.tabs.onUpdated(tabId, {url: details.url}, { - frameId: 0, - tabId: tabId, - url: browser.currentURI.asciiSpec - }); - return; - } - - // https://github.com/chrisaljoudi/uBlock/issues/105 - // Allow any kind of pages - vapi.tabs.onNavigation({ - frameId: 0, - tabId: tabId, - url: details.url - }); - }; - - var attachToTabBrowser = function(window) { - if ( typeof vAPI.toolbarButton.attachToNewWindow === 'function' ) { - vAPI.toolbarButton.attachToNewWindow(window); - } - - var tabBrowser = getTabBrowser(window); - if ( tabBrowser === null ) { - return; - } - - var tabContainer; - if ( tabBrowser.deck ) { // Fennec - tabContainer = tabBrowser.deck; - } else if ( tabBrowser.tabContainer ) { // Firefox - tabContainer = tabBrowser.tabContainer; - vAPI.contextMenu.register(document); - } - - // https://github.com/gorhill/uBlock/issues/697 - // Ignore `TabShow` events: unfortunately the `pending` attribute is - // not set when a tab is opened as a result of session restore -- it is - // set *after* the event is fired in such case. - if ( tabContainer ) { - tabContainer.addEventListener('TabShow', onShow); - tabContainer.addEventListener('TabClose', onClose); - // when new window is opened TabSelect doesn't run on the selected tab? - tabContainer.addEventListener('TabSelect', onSelect); - } - }; + } - // https://github.com/gorhill/uBlock/issues/906 - // Ensure the environment is ready before trying to attaching. - var canAttachToTabBrowser = function(window) { - var document = window && window.document; - if ( !document || document.readyState !== 'complete' ) { - return false; - } - - // On some platforms, the tab browser isn't immediately available, - // try waiting a bit if this happens. - // https://github.com/gorhill/uBlock/issues/763 - // Not getting a tab browser should not prevent from attaching ourself - // to the window. - var tabBrowser = getTabBrowser(window); - if ( tabBrowser === null ) { - return false; - } + let tbb = vAPI.toolbarButton; + let popupCommittedWidth = 0; + let popupCommittedHeight = 0; - return winWatcher.toBrowserWindow(window) !== null; - }; + tbb.onViewShowing = function ({target}) { + popupCommittedWidth = popupCommittedHeight = 0; + target.firstChild.setAttribute('src', vAPI.getURL('popup.html')); + }; - var onWindowLoad = function(win) { - deferUntil( - canAttachToTabBrowser.bind(null, win), - attachToTabBrowser.bind(null, win) - ); - }; + tbb.onViewHiding = function ({target}) { + target.parentNode.style.maxWidth = ''; + target.firstChild.setAttribute('src', 'about:blank'); + }; - var onWindowUnload = function(win) { - vAPI.contextMenu.unregister(win.document); + tbb.updateState = function (win, tabId) { + let button = win.document.getElementById(this.id); - var tabBrowser = getTabBrowser(win); - if ( tabBrowser === null ) { - return; - } - - var tabContainer = tabBrowser.tabContainer; - if ( tabContainer ) { - tabContainer.removeEventListener('TabShow', onShow); - tabContainer.removeEventListener('TabClose', onClose); - tabContainer.removeEventListener('TabSelect', onSelect); - } - - // https://github.com/gorhill/uBlock/issues/574 - // To keep in mind: not all windows are tab containers, - // sometimes the window IS the tab. - var tabs; - if ( tabBrowser.tabs ) { - tabs = tabBrowser.tabs; - } else if ( tabBrowser.localName === 'browser' ) { - tabs = [tabBrowser]; - } else { - tabs = []; - } - - var browser, URI, tabId; - var tabindex = tabs.length, tab; - while ( tabindex-- ) { - tab = tabs[tabindex]; - browser = browserFromTarget(tab); - if ( browser === null ) { - continue; - } - URI = browser.currentURI; - // Close extension tabs - if ( URI.schemeIs('chrome') && URI.host === location.host ) { - vAPI.tabs._remove(tab, getTabBrowser(win)); + if (!button) { + return; } - tabId = browserToTabIdMap.get(browser); - if ( tabId !== undefined ) { - removeBrowserEntry(tabId, browser); - tabIdToBrowserMap.delete(tabId); - } - browserToTabIdMap.delete(browser); - } - }; - - // Initialize map with existing active tabs - var start = function() { - var tabBrowser, tabs, tab; - for ( var win of winWatcher.getWindows() ) { - onWindowLoad(win); - tabBrowser = getTabBrowser(win); - if ( tabBrowser === null ) { - continue; - } - for ( tab of tabBrowser.tabs ) { - if ( !tab.hasAttribute('pending') ) { - tabIdFromTarget(tab); - } - } - } - - winWatcher.onOpenWindow = onWindowLoad; - winWatcher.onCloseWindow = onWindowUnload; - - vAPI.messaging.globalMessageManager.addMessageListener( - locationChangedMessageName, - onLocationChanged - ); - }; - - var stop = function() { - winWatcher.onOpenWindow = null; - winWatcher.onCloseWindow = null; - - vAPI.messaging.globalMessageManager.removeMessageListener( - locationChangedMessageName, - onLocationChanged - ); - - for ( var win of winWatcher.getWindows() ) { - onWindowUnload(win); - } - - browserToTabIdMap = new WeakMap(); - tabIdToBrowserMap.clear(); - }; - - cleanupTasks.push(stop); - - return { - browsers: getAllBrowsers, - browserFromTabId: browserFromTabId, - browserFromTarget: browserFromTarget, - currentBrowser: currentBrowser, - indexFromTarget: indexFromTarget, - removeTarget: removeTarget, - start: start, - tabFromBrowser: tabFromBrowser, - tabIdFromTarget: tabIdFromTarget - }; -})(); - -/******************************************************************************/ - -vAPI.setIcon = function(tabId, iconId, badge) { - // If badge is undefined, then setIcon was called from the TabSelect event - var win; - if ( badge === undefined ) { - win = iconId; - } else { - win = winWatcher.getCurrentWindow(); - } - var tabBrowser = getTabBrowser(win); - if ( tabBrowser === null ) { - return; - } - var curTabId = tabWatcher.tabIdFromTarget(tabBrowser.selectedTab); - var tb = vAPI.toolbarButton; - - // from 'TabSelect' event - if ( tabId === undefined ) { - tabId = curTabId; - } else if ( badge !== undefined ) { - tb.tabs[tabId] = { badge: badge, img: iconId }; - } - - if ( tabId === curTabId ) { - tb.updateState(win, tabId); - } -}; - -/******************************************************************************/ - -vAPI.messaging = { - get globalMessageManager() { - return Cc['@mozilla.org/globalmessagemanager;1'] - .getService(Ci.nsIMessageListenerManager); - }, - frameScript: vAPI.getURL('frameScript.js'), - listeners: {}, - defaultHandler: null, - NOOPFUNC: function(){}, - UNHANDLED: 'vAPI.messaging.notHandled' -}; - -/******************************************************************************/ - -vAPI.messaging.listen = function(listenerName, callback) { - this.listeners[listenerName] = callback; -}; - -/******************************************************************************/ - -vAPI.messaging.onMessage = function({target, data}) { - var messageManager = target.messageManager; - - if ( !messageManager ) { - // Message came from a popup, and its message manager is not usable. - // So instead we broadcast to the parent window. - messageManager = getOwnerWindow( - target.webNavigation.QueryInterface(Ci.nsIDocShell).chromeEventHandler - ).messageManager; - } - - var channelNameRaw = data.channelName; - var pos = channelNameRaw.indexOf('|'); - var channelName = channelNameRaw.slice(pos + 1); - - var callback = vAPI.messaging.NOOPFUNC; - if ( data.requestId !== undefined ) { - callback = CallbackWrapper.factory( - messageManager, - channelName, - channelNameRaw.slice(0, pos), - data.requestId - ).callback; - } - - var sender = { - tab: { - id: tabWatcher.tabIdFromTarget(target) - } - }; - - // Specific handler - var r = vAPI.messaging.UNHANDLED; - var listener = vAPI.messaging.listeners[channelName]; - if ( typeof listener === 'function' ) { - r = listener(data.msg, sender, callback); - } - if ( r !== vAPI.messaging.UNHANDLED ) { - return; - } - - // Default handler - r = vAPI.messaging.defaultHandler(data.msg, sender, callback); - if ( r !== vAPI.messaging.UNHANDLED ) { - return; - } - - console.error('eMatrix> messaging > unknown request: %o', data); - // Unhandled: - // Need to callback anyways in case caller expected an answer, or - // else there is a memory leak on caller's side - callback(); -}; + let icon = this.tabs[tabId]; + button.setAttribute('badge', icon && icon.badge || ''); + button.classList.toggle('off', !icon || !icon.img); -/******************************************************************************/ - -vAPI.messaging.setup = function(defaultHandler) { - // Already setup? - if ( this.defaultHandler !== null ) { - return; - } + let iconId = icon && icon.img ? icon.img : 'off'; + icon = 'url(' + vAPI.getURL('img/browsericons/icon19-' + iconId + '.png') + ')'; + button.style.listStyleImage = icon; + }; - if ( typeof defaultHandler !== 'function' ) { - defaultHandler = function(){ return vAPI.messaging.UNHANDLED; }; - } - this.defaultHandler = defaultHandler; + tbb.populatePanel = function (doc, panel) { + panel.setAttribute('id', this.viewId); - this.globalMessageManager.addMessageListener( - location.host + ':background', - this.onMessage - ); + let iframe = doc.createElement('iframe'); + iframe.setAttribute('type', 'content'); - this.globalMessageManager.loadFrameScript(this.frameScript, true); + panel.appendChild(iframe); - cleanupTasks.push(function() { - var gmm = vAPI.messaging.globalMessageManager; + let toPx = function (pixels) { + return pixels.toString() + 'px'; + }; - gmm.removeDelayedFrameScript(vAPI.messaging.frameScript); - gmm.removeMessageListener( - location.host + ':background', - vAPI.messaging.onMessage - ); - }); -}; + let scrollBarWidth = 0; + let resizeTimer = null; -/******************************************************************************/ - -vAPI.messaging.broadcast = function(message) { - this.globalMessageManager.broadcastAsyncMessage( - location.host + ':broadcast', - JSON.stringify({broadcast: true, msg: message}) - ); -}; + let resizePopupDelayed = function (attempts) { + if (resizeTimer !== null) { + return false; + } -/******************************************************************************/ + // Sanity check + attempts = (attempts || 0) + 1; + if ( attempts > 1/*000*/ ) { + //console.error('eMatrix> resizePopupDelayed: giving up after too many attempts'); + return false; + } -// This allows to avoid creating a closure for every single message which -// expects an answer. Having a closure created each time a message is processed -// has been always bothering me. Another benefit of the implementation here -// is to reuse the callback proxy object, so less memory churning. -// -// https://developers.google.com/speed/articles/optimizing-javascript -// "Creating a closure is significantly slower then creating an inner -// function without a closure, and much slower than reusing a static -// function" -// -// http://hacksoflife.blogspot.ca/2015/01/the-four-horsemen-of-performance.html -// "the dreaded 'uniformly slow code' case where every function takes 1% -// of CPU and you have to make one hundred separate performance optimizations -// to improve performance at all" -// -// http://jsperf.com/closure-no-closure/2 - -var CallbackWrapper = function(messageManager, channelName, listenerId, requestId) { - this.callback = this.proxy.bind(this); // bind once - this.init(messageManager, channelName, listenerId, requestId); -}; - -CallbackWrapper.junkyard = []; - -CallbackWrapper.factory = function(messageManager, channelName, listenerId, requestId) { - var wrapper = CallbackWrapper.junkyard.pop(); - if ( wrapper ) { - wrapper.init(messageManager, channelName, listenerId, requestId); - return wrapper; - } - return new CallbackWrapper(messageManager, channelName, listenerId, requestId); -}; - -CallbackWrapper.prototype.init = function(messageManager, channelName, listenerId, requestId) { - this.messageManager = messageManager; - this.channelName = channelName; - this.listenerId = listenerId; - this.requestId = requestId; -}; - -CallbackWrapper.prototype.proxy = function(response) { - var message = JSON.stringify({ - requestId: this.requestId, - channelName: this.channelName, - msg: response !== undefined ? response : null - }); - - if ( this.messageManager.sendAsyncMessage ) { - this.messageManager.sendAsyncMessage(this.listenerId, message); - } else { - this.messageManager.broadcastAsyncMessage(this.listenerId, message); - } + resizeTimer = vAPI.setTimeout(resizePopup, 10, attempts); + return true; + }; - // Mark for reuse - this.messageManager = - this.channelName = - this.requestId = - this.listenerId = null; - CallbackWrapper.junkyard.push(this); -}; + let resizePopup = function (attempts) { + resizeTimer = null; -/******************************************************************************/ + panel.parentNode.style.maxWidth = 'none'; + let body = iframe.contentDocument.body; -var httpRequestHeadersFactory = function(channel) { - var entry = httpRequestHeadersFactory.junkyard.pop(); - if ( entry ) { - return entry.init(channel); - } - return new HTTPRequestHeaders(channel); -}; - -httpRequestHeadersFactory.junkyard = []; - -var HTTPRequestHeaders = function(channel) { - this.init(channel); -}; - -HTTPRequestHeaders.prototype.init = function(channel) { - this.channel = channel; - this.headers = new Array(); - this.originalHeaderNames = new Array(); - channel.visitRequestHeaders({visitHeader: function(name, value) { - this.headers.push({name: name, value: value}); - this.originalHeaderNames.push(name); - }.bind(this)}); - return this; -}; - -HTTPRequestHeaders.prototype.dispose = function() { - this.channel = null; - this.headers = null; - this.originalHeaderNames = null; - httpRequestHeadersFactory.junkyard.push(this); -}; - -HTTPRequestHeaders.prototype.update = function() { - var newHeaderNames = new Set(); - for ( var header of this.headers ) { - this.setHeader(header.name, header.value, true); - newHeaderNames.add(header.name); - } - //Clear any headers that were removed - for ( var name of this.originalHeaderNames ) { - if ( !newHeaderNames.has(name) ) { - this.channel.setRequestHeader(name, '', false); - } - } -} + // https://github.com/gorhill/uMatrix/issues/301 + // Don't resize if committed size did not change. + if (popupCommittedWidth === body.clientWidth + && popupCommittedHeight === body.clientHeight) { + return; + } -HTTPRequestHeaders.prototype.getHeader = function(name) { - try { - return this.channel.getRequestHeader(name); - } catch (e) { - } - return ''; -}; + // We set a limit for height + let height = Math.min(body.clientHeight, 600); -HTTPRequestHeaders.prototype.setHeader = function(name, newValue, create) { - var oldValue = this.getHeader(name); - if ( newValue === oldValue ) { - return false; - } - if ( oldValue === '' && create !== true ) { - return false; - } - this.channel.setRequestHeader(name, newValue, false); - return true; -}; + // https://github.com/chrisaljoudi/uBlock/issues/730 + // Voodoo programming: this recipe works + panel.style.setProperty('height', toPx(height)); + iframe.style.setProperty('height', toPx(height)); -/******************************************************************************/ + // Adjust width for presence/absence of vertical scroll bar which may + // have appeared as a result of last operation. + let contentWindow = iframe.contentWindow; + let width = body.clientWidth; + if (contentWindow.scrollMaxY !== 0) { + width += scrollBarWidth; + } + panel.style.setProperty('width', toPx(width)); -var httpObserver = { - classDescription: 'net-channel-event-sinks for ' + location.host, - classID: Components.ID('{5d2e2797-6d68-42e2-8aeb-81ce6ba16b95}'), - contractID: '@' + location.host + '/net-channel-event-sinks;1', - REQDATAKEY: location.host + 'reqdata', - ABORT: Components.results.NS_BINDING_ABORTED, - ACCEPT: Components.results.NS_SUCCEEDED, - // Request types: - // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIContentPolicy#Constants - frameTypeMap: { - 6: 'main_frame', - 7: 'sub_frame' - }, - typeMap: { - 1: 'other', - 2: 'script', - 3: 'image', - 4: 'stylesheet', - 5: 'object', - 6: 'main_frame', - 7: 'sub_frame', - 9: 'xbl', - 10: 'ping', - 11: 'xmlhttprequest', - 12: 'object', - 13: 'xml_dtd', - 14: 'font', - 15: 'media', - 16: 'websocket', - 17: 'csp_report', - 18: 'xslt', - 19: 'beacon', - 20: 'xmlhttprequest', - 21: 'imageset', - 22: 'web_manifest' - }, - mimeTypeMap: { - 'audio': 15, - 'video': 15 - }, - - get componentRegistrar() { - return Components.manager.QueryInterface(Ci.nsIComponentRegistrar); - }, - - get categoryManager() { - return Cc['@mozilla.org/categorymanager;1'] - .getService(Ci.nsICategoryManager); - }, + // scrollMaxX should always be zero once we know the scrollbar width + if (contentWindow.scrollMaxX !== 0) { + scrollBarWidth = contentWindow.scrollMaxX; + width += scrollBarWidth; + panel.style.setProperty('width', toPx(width)); + } - QueryInterface: (function() { - var {XPCOMUtils} = Cu.import('resource://gre/modules/XPCOMUtils.jsm', null); + if (iframe.clientHeight !== height + || panel.clientWidth !== width) { + if (resizePopupDelayed(attempts)) { + return; + } + // resizePopupDelayed won't be called again, so commit + // dimentsions. + } - return XPCOMUtils.generateQI([ - Ci.nsIFactory, - Ci.nsIObserver, - Ci.nsIChannelEventSink, - Ci.nsISupportsWeakReference - ]); - })(), + popupCommittedWidth = body.clientWidth; + popupCommittedHeight = body.clientHeight; + }; - createInstance: function(outer, iid) { - if ( outer ) { - throw Components.results.NS_ERROR_NO_AGGREGATION; - } + let onResizeRequested = function () { + let body = iframe.contentDocument.body; + if (body.getAttribute('data-resize-popup') !== 'true') { + return; + } + body.removeAttribute('data-resize-popup'); + resizePopupDelayed(); + }; - return this.QueryInterface(iid); - }, + let onPopupReady = function () { + let win = this.contentWindow; - register: function() { - this.pendingRingBufferInit(); + if (!win || win.location.host !== location.host) { + return; + } - // https://developer.mozilla.org/en/docs/Observer_Notifications#HTTP_requests - Services.obs.addObserver(this, 'http-on-modify-request', true); - Services.obs.addObserver(this, 'http-on-examine-response', true); - Services.obs.addObserver(this, 'http-on-examine-cached-response', true); + if (typeof tbb.onBeforePopupReady === 'function') { + tbb.onBeforePopupReady.call(this); + } - // Guard against stale instances not having been unregistered - if ( this.componentRegistrar.isCIDRegistered(this.classID) ) { - try { - this.componentRegistrar.unregisterFactory(this.classID, Components.manager.getClassObject(this.classID, Ci.nsIFactory)); - } catch (ex) { - console.error('eMatrix> httpObserver > unable to unregister stale instance: ', ex); - } - } - - this.componentRegistrar.registerFactory( - this.classID, - this.classDescription, - this.contractID, - this - ); - this.categoryManager.addCategoryEntry( - 'net-channel-event-sinks', - this.contractID, - this.contractID, - false, - true - ); - }, - - unregister: function() { - Services.obs.removeObserver(this, 'http-on-modify-request'); - Services.obs.removeObserver(this, 'http-on-examine-response'); - Services.obs.removeObserver(this, 'http-on-examine-cached-response'); - - this.componentRegistrar.unregisterFactory(this.classID, this); - this.categoryManager.deleteCategoryEntry( - 'net-channel-event-sinks', - this.contractID, - false - ); - }, - - PendingRequest: function() { - this.rawType = 0; - this.tabId = 0; - this._key = ''; // key is url, from URI.spec - }, - - // If all work fine, this map should not grow indefinitely. It can have - // stale items in it, but these will be taken care of when entries in - // the ring buffer are overwritten. - pendingURLToIndex: new Map(), - pendingWritePointer: 0, - pendingRingBuffer: new Array(256), - pendingRingBufferInit: function() { - // Use and reuse pre-allocated PendingRequest objects = less memory - // churning. - var i = this.pendingRingBuffer.length; - while ( i-- ) { - this.pendingRingBuffer[i] = new this.PendingRequest(); - } - }, - - // Pending request ring buffer: - // +-------+-------+-------+-------+-------+-------+------- - // |0 |1 |2 |3 |4 |5 |... - // +-------+-------+-------+-------+-------+-------+------- - // - // URL to ring buffer index map: - // { k = URL, s = ring buffer indices } - // - // s is a string which character codes map to ring buffer indices -- for - // when the same URL is received multiple times by shouldLoadListener() - // before the existing one is serviced by the network request observer. - // I believe the use of a string in lieu of an array reduces memory - // churning. - - createPendingRequest: function(url) { - var bucket; - var i = this.pendingWritePointer; - this.pendingWritePointer = i + 1 & 255; - var preq = this.pendingRingBuffer[i]; - var si = String.fromCharCode(i); - // Cleanup unserviced pending request - if ( preq._key !== '' ) { - bucket = this.pendingURLToIndex.get(preq._key); - if ( bucket.length === 1 ) { - this.pendingURLToIndex.delete(preq._key); - } else { - var pos = bucket.indexOf(si); - this.pendingURLToIndex.set(preq._key, bucket.slice(0, pos) + bucket.slice(pos + 1)); - } - } - bucket = this.pendingURLToIndex.get(url); - this.pendingURLToIndex.set(url, bucket === undefined ? si : bucket + si); - preq._key = url; - return preq; - }, - - lookupPendingRequest: function(url) { - var bucket = this.pendingURLToIndex.get(url); - if ( bucket === undefined ) { - return null; - } - var i = bucket.charCodeAt(0); - if ( bucket.length === 1 ) { - this.pendingURLToIndex.delete(url); - } else { - this.pendingURLToIndex.set(url, bucket.slice(1)); - } - var preq = this.pendingRingBuffer[i]; - preq._key = ''; // mark as "serviced" - return preq; - }, - - handleRequest: function(channel, URI, tabId, rawType) { - var type = this.typeMap[rawType] || 'other'; - - var onBeforeRequest = vAPI.net.onBeforeRequest; - if ( onBeforeRequest.types === null || onBeforeRequest.types.has(type) ) { - var result = onBeforeRequest.callback({ - parentFrameId: type === 'main_frame' ? -1 : 0, - tabId: tabId, - type: type, - url: URI.asciiSpec - }); - if ( typeof result === 'object' ) { - channel.cancel(this.ABORT); - return true; - } - } - - var onBeforeSendHeaders = vAPI.net.onBeforeSendHeaders; - if ( onBeforeSendHeaders.types === null || onBeforeSendHeaders.types.has(type) ) { - var requestHeaders = httpRequestHeadersFactory(channel); - var newHeaders = onBeforeSendHeaders.callback({ - parentFrameId: type === 'main_frame' ? -1 : 0, - requestHeaders: requestHeaders.headers, - tabId: tabId, - type: type, - url: URI.asciiSpec, - method: channel.requestMethod - }); - if ( newHeaders ) { - requestHeaders.update(); - } - requestHeaders.dispose(); - } + resizePopupDelayed(); + + let body = win.document.body; + body.removeAttribute('data-resize-popup'); + + let mutationObserver = + new win.MutationObserver(onResizeRequested); + + mutationObserver.observe(body, { + attributes: true, + attributeFilter: [ 'data-resize-popup' ] + }); + }; + + iframe.addEventListener('load', onPopupReady, true); + }; + })(); - return false; - }, + /******************************************************************************/ - channelDataFromChannel: function(channel) { - if ( channel instanceof Ci.nsIWritablePropertyBag ) { - try { - return channel.getProperty(this.REQDATAKEY) || null; - } catch (ex) { - } - } - return null; - }, - - // https://github.com/gorhill/uMatrix/issues/165 - // https://developer.mozilla.org/en-US/Firefox/Releases/3.5/Updating_extensions#Getting_a_load_context_from_a_request - // Not sure `ematrix:shouldLoad` is still needed, eMatrix does not - // care about embedded frames topography. - // Also: - // https://developer.mozilla.org/en-US/Firefox/Multiprocess_Firefox/Limitations_of_chrome_scripts - tabIdFromChannel: function(channel) { - var lc; - try { - lc = channel.notificationCallbacks.getInterface(Ci.nsILoadContext); - } catch(ex) { - } - if ( !lc ) { - try { - lc = channel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext); - } catch(ex) { - } - if ( !lc ) { - return vAPI.noTabId; - } - } - if ( lc.topFrameElement ) { - return tabWatcher.tabIdFromTarget(lc.topFrameElement); - } - var win; - try { - win = lc.associatedWindow; - } catch (ex) { } - if ( !win ) { - return vAPI.noTabId; - } - if ( win.top ) { - win = win.top; - } - var tabBrowser; - try { - tabBrowser = getTabBrowser( - win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShell).rootTreeItem - .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow) - ); - } catch (ex) { } - if ( !tabBrowser ) { - return vAPI.noTabId; - } - if ( tabBrowser.getBrowserForContentWindow ) { - return tabWatcher.tabIdFromTarget(tabBrowser.getBrowserForContentWindow(win)); - } - // Falling back onto _getTabForContentWindow to ensure older versions - // of Firefox work well. - return tabBrowser._getTabForContentWindow ? - tabWatcher.tabIdFromTarget(tabBrowser._getTabForContentWindow(win)) : - vAPI.noTabId; - }, - - rawtypeFromContentType: function(channel) { - var mime = channel.contentType; - if ( !mime ) { - return 0; - } - var pos = mime.indexOf('/'); - if ( pos === -1 ) { - pos = mime.length; - } - return this.mimeTypeMap[mime.slice(0, pos)] || 0; - }, - - observe: function(channel, topic) { - if ( channel instanceof Ci.nsIHttpChannel === false ) { + (function () { + // Add toolbar button for not-Basilisk + if (Services.appinfo.ID === "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}") { + return; + } + + let tbb = vAPI.toolbarButton; + if (tbb.init !== null) { return; - } - - var URI = channel.URI; - var channelData = this.channelDataFromChannel(channel); - - if ( topic.lastIndexOf('http-on-examine-', 0) === 0 ) { - if ( channelData === null ) { - return; - } + } - var type = this.frameTypeMap[channelData[1]]; - if ( !type ) { - return; + tbb.codePath = 'legacy'; + tbb.viewId = tbb.id + '-panel'; + + let styleSheetUri = null; + + let createToolbarButton = function (window) { + let document = window.document; + + let toolbarButton = document.createElement('toolbarbutton'); + toolbarButton.setAttribute('id', tbb.id); + // type = panel would be more accurate, but doesn't look as good + toolbarButton.setAttribute('type', 'menu'); + toolbarButton.setAttribute('removable', 'true'); + toolbarButton.setAttribute('class', 'toolbarbutton-1 ' + +'chromeclass-toolbar-additional'); + toolbarButton.setAttribute('label', tbb.label); + toolbarButton.setAttribute('tooltiptext', tbb.tooltiptext); + + let toolbarButtonPanel = document.createElement('panel'); + // NOTE: Setting level to parent breaks the popup for PaleMoon under + // linux (mouse pointer misaligned with content). For some reason. + // eMatrix: TODO check if it's still true + // toolbarButtonPanel.setAttribute('level', 'parent'); + tbb.populatePanel(document, toolbarButtonPanel); + toolbarButtonPanel.addEventListener('popupshowing', + tbb.onViewShowing); + toolbarButtonPanel.addEventListener('popuphiding', + tbb.onViewHiding); + toolbarButton.appendChild(toolbarButtonPanel); + + toolbarButtonPanel.setAttribute('tooltiptext', ''); + + return toolbarButton; + }; + + let addLegacyToolbarButton = function (window) { + // eMatrix's stylesheet lazily added. + if (styleSheetUri === null) { + var sss = Cc["@mozilla.org/content/style-sheet-service;1"] + .getService(Ci.nsIStyleSheetService); + styleSheetUri = Services.io + .newURI(vAPI.getURL("css/legacy-toolbar-button.css"), + null, null); + + // Register global so it works in all windows, including palette + if (!sss.sheetRegistered(styleSheetUri, sss.AUTHOR_SHEET)) { + sss.loadAndRegisterSheet(styleSheetUri, sss.AUTHOR_SHEET); + } } - //topic = ['Content-Security-Policy', 'Content-Security-Policy-Report-Only']; - // Can send empty responseHeaders as these headers are only added to and then merged. + let document = window.document; - // TODO: Find better place for this, needs to be set before onHeadersReceived.callback. - // Web workers not blocked in Pale Moon as child-src currently unavailable, see: - // https://github.com/MoonchildProductions/Pale-Moon/issues/949 - if ( ηMatrix.cspNoWorker === undefined ) { - ηMatrix.cspNoWorker = "child-src 'none'; frame-src data: blob: *; report-uri about:blank"; + // https://github.com/gorhill/uMatrix/issues/357 + // Already installed? + if (document.getElementById(tbb.id) !== null) { + return; } - var result = vAPI.net.onHeadersReceived.callback({ - parentFrameId: type === 'main_frame' ? -1 : 0, - responseHeaders: [], - tabId: channelData[0], - type: type, - url: URI.asciiSpec - }); - - if ( result ) { - for ( let header of result.responseHeaders ) { - channel.setResponseHeader( - header.name, - header.value, - true - ); - } + let toolbox = document.getElementById('navigator-toolbox') + || document.getElementById('mail-toolbox'); + + if (toolbox === null) { + return; } - return; - } - - // http-on-modify-request + let toolbarButton = createToolbarButton(window); - // The channel was previously serviced. - if ( channelData !== null ) { - this.handleRequest(channel, URI, channelData[0], channelData[1]); - return; - } - - // The channel was never serviced. - var tabId; - var pendingRequest = this.lookupPendingRequest(URI.asciiSpec); - var rawType = 1; - var loadInfo = channel.loadInfo; - - // https://github.com/gorhill/uMatrix/issues/390#issuecomment-155717004 - if ( loadInfo ) { - rawType = loadInfo.externalContentPolicyType !== undefined ? - loadInfo.externalContentPolicyType : - loadInfo.contentPolicyType; - if ( !rawType ) { - rawType = 1; + let palette = toolbox.palette; + if (palette && palette.querySelector('#' + tbb.id) === null) { + palette.appendChild(toolbarButton); } - } - - if ( pendingRequest !== null ) { - tabId = pendingRequest.tabId; - // https://github.com/gorhill/uBlock/issues/654 - // Use the request type from the HTTP observer point of view. - if ( rawType !== 1 ) { - pendingRequest.rawType = rawType; - } else { - rawType = pendingRequest.rawType; - } - } else { - tabId = this.tabIdFromChannel(channel); - } - if ( this.handleRequest(channel, URI, tabId, rawType) ) { - return; - } - - if ( channel instanceof Ci.nsIWritablePropertyBag === false ) { - return; - } - - // Carry data for behind-the-scene redirects - channel.setProperty(this.REQDATAKEY, [tabId, rawType]); - }, - - // contentPolicy.shouldLoad doesn't detect redirects, this needs to be used - asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) { - // If error thrown, the redirect will fail - try { - var URI = newChannel.URI; - if ( !URI.schemeIs('http') && !URI.schemeIs('https') ) { - return; + // Find the place to put the button. + // Pale Moon: `toolbox.externalToolbars` can be + // undefined. Seen while testing popup test number 3: + // http://raymondhill.net/ublock/popup.html + let toolbars = toolbox.externalToolbars + ? toolbox.externalToolbars.slice() + : []; + + for (let child of toolbox.children) { + if (child.localName === 'toolbar') { + toolbars.push(child); + } } - if ( newChannel instanceof Ci.nsIWritablePropertyBag === false ) { - return; - } + for (let toolbar of toolbars) { + let currentsetString = toolbar.getAttribute('currentset'); + if (!currentsetString) { + continue; + } + + let currentset = currentsetString.split(/\s*,\s*/); + let index = currentset.indexOf(tbb.id); + if (index === -1) { + continue; + } + + // This can occur with Pale Moon: + // "TypeError: toolbar.insertItem is not a function" + if (typeof toolbar.insertItem !== 'function') { + continue; + } + + // Found our button on this toolbar - but where on it? + let before = null; + for (let i = index+1; i<currentset.length; ++i) { + // The [id=...] notation doesn't work on + // space elements as they get a random ID each session + // (or something like that) + // https://gitlab.com/vannilla/ematrix/issues/5 + // https://gitlab.com/vannilla/ematrix/issues/6 + + // Based on JustOff's snippet from the Pale Moon + // forum. It was reorganized because I find it + // more readable like this, but he did most of the + // work. + let space = /^(spring|spacer|separator)$/.exec(currentset[i]); + if (space !== null) { + let elems = toolbar.querySelectorAll('toolbar'+space[1]); + + let count = currentset.slice(i-currentset.length) + .filter(function (x) {return x == space[1];}) + .length; + + before = + toolbar.querySelector('[id="' + + elems[elems.length-count].id + + '"]'); + } else { + before = toolbar.querySelector('[id="'+currentset[i]+'"]'); + } + if ( before !== null ) { + break; + } + } - var channelData = this.channelDataFromChannel(oldChannel); - if ( channelData === null ) { - return; + toolbar.insertItem(tbb.id, before); + break; + } + + // https://github.com/gorhill/uBlock/issues/763 + // We are done if our toolbar button is already installed + // in one of the toolbar. + if (palette !== null && toolbarButton.parentElement !== palette) { + return; + } + + // No button yet so give it a default location. If forcing + // the button, just put in in the palette rather than on + // any specific toolbar (who knows what toolbars will be + // available or visible!) + let navbar = document.getElementById('nav-bar'); + if (navbar !== null + && !vAPI.localStorage.getBool('legacyToolbarButtonAdded')) { + // https://github.com/gorhill/uBlock/issues/264 + // Find a child customizable palette, if any. + navbar = navbar.querySelector('.customization-target') || navbar; + navbar.appendChild(toolbarButton); + navbar.setAttribute('currentset', navbar.currentSet); + document.persist(navbar.id, 'currentset'); + vAPI.localStorage.setBool('legacyToolbarButtonAdded', 'true'); + } + }; + + let canAddLegacyToolbarButton = function (window) { + let document = window.document; + if (!document + || document.readyState !== 'complete' + || document.getElementById('nav-bar') === null) { + return false; + } + + let toolbox = document.getElementById('navigator-toolbox') + || document.getElementById('mail-toolbox'); + return toolbox !== null && !!toolbox.palette; + }; + + let onPopupCloseRequested = function ({target}) { + let document = target.ownerDocument; + if (!document) { + return; + } + + let toolbarButtonPanel = document.getElementById(tbb.viewId); + if (toolbarButtonPanel === null) { + return; + } + + // `hidePopup` reported as not existing while testing + // legacy button on FF 41.0.2. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1151796 + if (typeof toolbarButtonPanel.hidePopup === 'function') { + toolbarButtonPanel.hidePopup(); + } + }; + + let shutdown = function () { + for (let win of vAPI.window.getWindows()) { + let toolbarButton = win.document.getElementById(tbb.id); + if (toolbarButton) { + toolbarButton.parentNode.removeChild(toolbarButton); + } } - // Carry the data on in case of multiple redirects - newChannel.setProperty(this.REQDATAKEY, channelData); - } catch (ex) { - // console.error(ex); - } finally { - callback.onRedirectVerifyCallback(this.ACCEPT); - } - } -}; - -/******************************************************************************/ - -vAPI.net = {}; - -/******************************************************************************/ - -vAPI.net.registerListeners = function() { - this.onBeforeRequest.types = this.onBeforeRequest.types ? - new Set(this.onBeforeRequest.types) : - null; - this.onBeforeSendHeaders.types = this.onBeforeSendHeaders.types ? - new Set(this.onBeforeSendHeaders.types) : - null; - - var shouldLoadListenerMessageName = location.host + ':shouldLoad'; - var shouldLoadListener = function(e) { - var details = e.data; - var pendingReq = httpObserver.createPendingRequest(details.url); - pendingReq.rawType = details.rawType; - pendingReq.tabId = tabWatcher.tabIdFromTarget(e.target); - }; - - // https://github.com/gorhill/uMatrix/issues/200 - // We need this only for Firefox 34 and less: the tab id is derived from - // the origin of the message. - if ( !vAPI.modernFirefox ) { - vAPI.messaging.globalMessageManager.addMessageListener( - shouldLoadListenerMessageName, - shouldLoadListener - ); - } - - httpObserver.register(); - - cleanupTasks.push(function() { - if ( !vAPI.modernFirefox ) { - vAPI.messaging.globalMessageManager.removeMessageListener( - shouldLoadListenerMessageName, - shouldLoadListener - ); - } - - httpObserver.unregister(); - }); -}; - -/******************************************************************************/ -/******************************************************************************/ - -vAPI.toolbarButton = { - id: location.host + '-button', - type: 'view', - viewId: location.host + '-panel', - label: vAPI.app.name, - tooltiptext: vAPI.app.name, - tabs: {/*tabId: {badge: 0, img: boolean}*/}, - init: null, - codePath: '' -}; - -/******************************************************************************/ - -// Non-Fennec: common code paths. - -(function() { - if ( vAPI.fennec ) { - return; - } - - var tbb = vAPI.toolbarButton; - var popupCommittedWidth = 0; - var popupCommittedHeight = 0; - - tbb.onViewShowing = function({target}) { - popupCommittedWidth = popupCommittedHeight = 0; - target.firstChild.setAttribute('src', vAPI.getURL('popup.html')); - }; - - tbb.onViewHiding = function({target}) { - target.parentNode.style.maxWidth = ''; - target.firstChild.setAttribute('src', 'about:blank'); - }; - - tbb.updateState = function(win, tabId) { - var button = win.document.getElementById(this.id); + if (styleSheetUri !== null) { + var sss = Cc["@mozilla.org/content/style-sheet-service;1"] + .getService(Ci.nsIStyleSheetService); + if (sss.sheetRegistered(styleSheetUri, sss.AUTHOR_SHEET)) { + sss.unregisterSheet(styleSheetUri, sss.AUTHOR_SHEET); + } + styleSheetUri = null; + } + + vAPI.messaging.globalMessageManager + .removeMessageListener(location.host + ':closePopup', + onPopupCloseRequested); + }; + + tbb.attachToNewWindow = function (win) { + vAPI.deferUntil(canAddLegacyToolbarButton.bind(null, win), + addLegacyToolbarButton.bind(null, win)); + }; + + tbb.init = function () { + vAPI.messaging.globalMessageManager + .addMessageListener(location.host + ':closePopup', + onPopupCloseRequested); + + vAPI.addCleanUpTask(shutdown); + }; + })(); - if ( !button ) { + (function() { + // Add toolbar button for Basilisk + if (Services.appinfo.ID !== "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}") { + return; + } + + let tbb = vAPI.toolbarButton; + if (tbb.init !== null) { return; - } - - var icon = this.tabs[tabId]; - button.setAttribute('badge', icon && icon.badge || ''); - button.classList.toggle('off', !icon || !icon.img); - - var iconId = icon && icon.img ? icon.img : 'off'; - icon = 'url(' + vAPI.getURL('img/browsericons/icon19-' + iconId + '.png') + ')'; - button.style.listStyleImage = icon; - }; - - tbb.populatePanel = function(doc, panel) { - panel.setAttribute('id', this.viewId); - - var iframe = doc.createElement('iframe'); - iframe.setAttribute('type', 'content'); - - panel.appendChild(iframe); - - var toPx = function(pixels) { - return pixels.toString() + 'px'; - }; - - var scrollBarWidth = 0; - var resizeTimer = null; - - var resizePopupDelayed = function(attempts) { - if ( resizeTimer !== null ) { - return false; - } - - // Sanity check - attempts = (attempts || 0) + 1; - if ( attempts > 1/*000*/ ) { - //console.error('eMatrix> resizePopupDelayed: giving up after too many attempts'); - return false; - } - - resizeTimer = vAPI.setTimeout(resizePopup, 10, attempts); - return true; - }; - - var resizePopup = function(attempts) { - resizeTimer = null; - - panel.parentNode.style.maxWidth = 'none'; - var body = iframe.contentDocument.body; - - // https://github.com/gorhill/uMatrix/issues/301 - // Don't resize if committed size did not change. - if ( - popupCommittedWidth === body.clientWidth && - popupCommittedHeight === body.clientHeight - ) { - return; - } - - // We set a limit for height - var height = Math.min(body.clientHeight, 600); - - // https://github.com/chrisaljoudi/uBlock/issues/730 - // Voodoo programming: this recipe works - panel.style.setProperty('height', toPx(height)); - iframe.style.setProperty('height', toPx(height)); + } + // if ( Services.vc.compare(Services.appinfo.version, '36.0') < 0 ) { + // return null; + // } + let CustomizableUI = null; + try { + CustomizableUI = + Cu.import('resource:///modules/CustomizableUI.jsm', null) + .CustomizableUI; + } catch (ex) { + // Ignore + } + if (CustomizableUI === null) { + return null; + } + tbb.codePath = 'australis'; + tbb.CustomizableUI = CustomizableUI; + tbb.defaultArea = CustomizableUI.AREA_NAVBAR; - // Adjust width for presence/absence of vertical scroll bar which may - // have appeared as a result of last operation. - var contentWindow = iframe.contentWindow; - var width = body.clientWidth; - if ( contentWindow.scrollMaxY !== 0 ) { - width += scrollBarWidth; - } - panel.style.setProperty('width', toPx(width)); + let CUIEvents = {}; - // scrollMaxX should always be zero once we know the scrollbar width - if ( contentWindow.scrollMaxX !== 0 ) { - scrollBarWidth = contentWindow.scrollMaxX; - width += scrollBarWidth; - panel.style.setProperty('width', toPx(width)); - } + let badgeCSSRules = 'background: #000;color: #fff'; - if ( iframe.clientHeight !== height || panel.clientWidth !== width ) { - if ( resizePopupDelayed(attempts) ) { - return; - } - // resizePopupDelayed won't be called again, so commit - // dimentsions. - } - - popupCommittedWidth = body.clientWidth; - popupCommittedHeight = body.clientHeight; - }; + let updateBadgeStyle = function () { + for (let win of vAPI.window.getWindows()) { + let button = win.document.getElementById(tbb.id); + if (button === null) { + continue; + } + let badge = button.ownerDocument + .getAnonymousElementByAttribute(button, + 'class', + 'toolbarbutton-badge'); + if (!badge) { + continue; + } - var onResizeRequested = function() { - var body = iframe.contentDocument.body; - if ( body.getAttribute('data-resize-popup') !== 'true' ) { - return; + badge.style.cssText = badgeCSSRules; } - body.removeAttribute('data-resize-popup'); - resizePopupDelayed(); - }; + }; - var onPopupReady = function() { - var win = this.contentWindow; + let updateBadge = function () { + let wId = tbb.id; + let buttonInPanel = + CustomizableUI.getWidget(wId).areaType + === CustomizableUI.TYPE_MENU_PANEL; - if ( !win || win.location.host !== location.host ) { - return; - } - - if ( typeof tbb.onBeforePopupReady === 'function' ) { - tbb.onBeforePopupReady.call(this); + for (let win of vAPI.window.getWindows()) { + let button = win.document.getElementById(wId); + if (button === null) { + continue; + } + + if (buttonInPanel) { + button.classList.remove('badged-button'); + continue; + } + + button.classList.add('badged-button'); } - resizePopupDelayed(); - - var body = win.document.body; - body.removeAttribute('data-resize-popup'); - var mutationObserver = new win.MutationObserver(onResizeRequested); - mutationObserver.observe(body, { - attributes: true, - attributeFilter: [ 'data-resize-popup' ] - }); - }; - - iframe.addEventListener('load', onPopupReady, true); - }; -})(); - -/******************************************************************************/ - -(function() { - // Add toolbar button for not-Basilisk - if (Services.appinfo.ID === "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}") { - return; - } - - var tbb = vAPI.toolbarButton; - if ( tbb.init !== null ) { - return; - } - - tbb.codePath = 'legacy'; - tbb.viewId = tbb.id + '-panel'; - - var styleSheetUri = null; - - var createToolbarButton = function(window) { - var document = window.document; - - var toolbarButton = document.createElement('toolbarbutton'); - toolbarButton.setAttribute('id', tbb.id); - // type = panel would be more accurate, but doesn't look as good - toolbarButton.setAttribute('type', 'menu'); - toolbarButton.setAttribute('removable', 'true'); - toolbarButton.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional'); - toolbarButton.setAttribute('label', tbb.label); - toolbarButton.setAttribute('tooltiptext', tbb.tooltiptext); - - var toolbarButtonPanel = document.createElement('panel'); - // NOTE: Setting level to parent breaks the popup for PaleMoon under - // linux (mouse pointer misaligned with content). For some reason. - // toolbarButtonPanel.setAttribute('level', 'parent'); - tbb.populatePanel(document, toolbarButtonPanel); - toolbarButtonPanel.addEventListener('popupshowing', tbb.onViewShowing); - toolbarButtonPanel.addEventListener('popuphiding', tbb.onViewHiding); - toolbarButton.appendChild(toolbarButtonPanel); - - toolbarButtonPanel.setAttribute('tooltiptext', ''); - - return toolbarButton; - }; - - var addLegacyToolbarButton = function(window) { - // eMatrix's stylesheet lazily added. - if ( styleSheetUri === null ) { - var sss = Cc["@mozilla.org/content/style-sheet-service;1"] - .getService(Ci.nsIStyleSheetService); - styleSheetUri = Services.io.newURI(vAPI.getURL("css/legacy-toolbar-button.css"), null, null); - - // Register global so it works in all windows, including palette - if ( !sss.sheetRegistered(styleSheetUri, sss.AUTHOR_SHEET) ) { - sss.loadAndRegisterSheet(styleSheetUri, sss.AUTHOR_SHEET); + if (buttonInPanel) { + return; } - } - var document = window.document; + // Anonymous elements need some time to be reachable + vAPI.setTimeout(updateBadgeStyle, 250); + }.bind(CUIEvents); - // https://github.com/gorhill/uMatrix/issues/357 - // Already installed? - if ( document.getElementById(tbb.id) !== null ) { - return; - } + CUIEvents.onCustomizeEnd = updateBadge; + CUIEvents.onWidgetAdded = updateBadge; + CUIEvents.onWidgetUnderflow = updateBadge; - var toolbox = document.getElementById('navigator-toolbox') || - document.getElementById('mail-toolbox'); - if ( toolbox === null ) { - return; - } - - var toolbarButton = createToolbarButton(window); - - // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/toolbarpalette - var palette = toolbox.palette; - if ( palette && palette.querySelector('#' + tbb.id) === null ) { - palette.appendChild(toolbarButton); - } - - // Find the place to put the button. - // Pale Moon: `toolbox.externalToolbars` can be undefined. Seen while - // testing popup test number 3: - // http://raymondhill.net/ublock/popup.html - var toolbars = toolbox.externalToolbars ? toolbox.externalToolbars.slice() : []; - for ( var child of toolbox.children ) { - if ( child.localName === 'toolbar' ) { - toolbars.push(child); + let onPopupCloseRequested = function ({target}) { + if (typeof tbb.closePopup === 'function') { + tbb.closePopup(target); } - } + }; - for ( var toolbar of toolbars ) { - var currentsetString = toolbar.getAttribute('currentset'); - if ( !currentsetString ) { - continue; - } - var currentset = currentsetString.split(/\s*,\s*/); - var index = currentset.indexOf(tbb.id); - if ( index === -1 ) { - continue; - } - // This can occur with Pale Moon: - // "TypeError: toolbar.insertItem is not a function" - if ( typeof toolbar.insertItem !== 'function' ) { - continue; - } - // Found our button on this toolbar - but where on it? - var before = null; - for ( var i = index + 1; i < currentset.length; i++ ) { - // The [id=...] notation doesn't work on - // space elements as they get a random ID each session - // (or something like that) - // https://gitlab.com/vannilla/ematrix/issues/5 - // https://gitlab.com/vannilla/ematrix/issues/6 - - // Based on JustOff's snippet from the Pale Moon forum. - // It was reorganized because I find it more readable like this, - // but he did most of the work. - let space = /^(spring|spacer|separator)$/.exec(currentset[i]); - if (space !== null) { - let elems = toolbar.querySelectorAll('toolbar'+space[1]); - - let count = currentset.slice(i-currentset.length) - .filter(function (x) {return x == space[1];}) - .length; - - before = - toolbar.querySelector('[id="' - + elems[elems.length-count].id - + '"]'); - } else { - before = toolbar.querySelector('[id="'+currentset[i]+'"]'); + let shutdown = function () { + for (let win of vAPI.window.getWindows()) { + let panel = win.document.getElementById(tbb.viewId); + if (panel !== null && panel.parentNode !== null) { + panel.parentNode.removeChild(panel); } - if ( before !== null ) { - break; - } - } - // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Method/insertItem - toolbar.insertItem(tbb.id, before); - break; - } - - // https://github.com/gorhill/uBlock/issues/763 - // We are done if our toolbar button is already installed in one of the - // toolbar. - if ( palette !== null && toolbarButton.parentElement !== palette ) { - return; - } - - // No button yet so give it a default location. If forcing the button, - // just put in in the palette rather than on any specific toolbar (who - // knows what toolbars will be available or visible!) - var navbar = document.getElementById('nav-bar'); - if ( navbar !== null && !vAPI.localStorage.getBool('legacyToolbarButtonAdded') ) { - // https://github.com/gorhill/uBlock/issues/264 - // Find a child customizable palette, if any. - navbar = navbar.querySelector('.customization-target') || navbar; - navbar.appendChild(toolbarButton); - navbar.setAttribute('currentset', navbar.currentSet); - document.persist(navbar.id, 'currentset'); - vAPI.localStorage.setBool('legacyToolbarButtonAdded', 'true'); - } - }; - - var canAddLegacyToolbarButton = function(window) { - var document = window.document; - if ( - !document || - document.readyState !== 'complete' || - document.getElementById('nav-bar') === null - ) { - return false; - } - var toolbox = document.getElementById('navigator-toolbox') || - document.getElementById('mail-toolbox'); - return toolbox !== null && !!toolbox.palette; - }; - - var onPopupCloseRequested = function({target}) { - var document = target.ownerDocument; - if ( !document ) { - return; - } - var toolbarButtonPanel = document.getElementById(tbb.viewId); - if ( toolbarButtonPanel === null ) { - return; - } - // `hidePopup` reported as not existing while testing legacy button - // on FF 41.0.2. - // https://bugzilla.mozilla.org/show_bug.cgi?id=1151796 - if ( typeof toolbarButtonPanel.hidePopup === 'function' ) { - toolbarButtonPanel.hidePopup(); - } - }; - - var shutdown = function() { - for ( var win of winWatcher.getWindows() ) { - var toolbarButton = win.document.getElementById(tbb.id); - if ( toolbarButton ) { - toolbarButton.parentNode.removeChild(toolbarButton); + + win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .removeSheet(styleURI, 1); } - } - - if ( styleSheetUri !== null ) { - var sss = Cc["@mozilla.org/content/style-sheet-service;1"] - .getService(Ci.nsIStyleSheetService); - if ( sss.sheetRegistered(styleSheetUri, sss.AUTHOR_SHEET) ) { - sss.unregisterSheet(styleSheetUri, sss.AUTHOR_SHEET); - } - styleSheetUri = null; - } - - vAPI.messaging.globalMessageManager.removeMessageListener( - location.host + ':closePopup', - onPopupCloseRequested - ); - }; - - tbb.attachToNewWindow = function(win) { - deferUntil( - canAddLegacyToolbarButton.bind(null, win), - addLegacyToolbarButton.bind(null, win) - ); - }; - - tbb.init = function() { - vAPI.messaging.globalMessageManager.addMessageListener( - location.host + ':closePopup', - onPopupCloseRequested - ); - - cleanupTasks.push(shutdown); - }; -})(); - -/******************************************************************************/ - -/* -(function() { - // Add toolbar button for Basilisk - if (Services.appinfo.ID !== "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}") { - return; - } - - var tbb = vAPI.toolbarButton; - if ( tbb.init !== null ) { - return; - } - - if ( Services.vc.compare(Services.appinfo.version, '36.0') >= 0 ) { - return null; - } - var CustomizableUI = null; - try { - CustomizableUI = Cu.import('resource:///modules/CustomizableUI.jsm', null).CustomizableUI; - } catch (ex) { - } - if ( CustomizableUI === null ) { - return; - } - tbb.codePath = 'australis'; - tbb.CustomizableUI = CustomizableUI; - tbb.defaultArea = CustomizableUI.AREA_NAVBAR; - - var styleURI = null; - - var onPopupCloseRequested = function({target}) { - if ( typeof tbb.closePopup === 'function' ) { - tbb.closePopup(target); - } - }; - - var shutdown = function() { - CustomizableUI.destroyWidget(tbb.id); - - for ( var win of winWatcher.getWindows() ) { - var panel = win.document.getElementById(tbb.viewId); - panel.parentNode.removeChild(panel); - win.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils) - .removeSheet(styleURI, 1); - } - - vAPI.messaging.globalMessageManager.removeMessageListener( - location.host + ':closePopup', - onPopupCloseRequested - ); - }; - - tbb.onBeforeCreated = function(doc) { - var panel = doc.createElement('panelview'); - - this.populatePanel(doc, panel); - - doc.getElementById('PanelUI-multiView').appendChild(panel); - - doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils) - .loadSheet(styleURI, 1); - }; - tbb.onBeforePopupReady = function() { - // https://github.com/gorhill/uBlock/issues/83 - // Add `portrait` class if width is constrained. - try { - this.contentDocument.body.classList.toggle( - 'portrait', - CustomizableUI.getWidget(tbb.id).areaType === CustomizableUI.TYPE_MENU_PANEL - ); - } catch (ex) { - // noop - } - }; - - tbb.init = function() { - vAPI.messaging.globalMessageManager.addMessageListener( - location.host + ':closePopup', - onPopupCloseRequested - ); - - var style = [ - '#' + this.id + '.off {', - 'list-style-image: url(', - vAPI.getURL('img/browsericons/icon19-off.png'), - ');', - '}', - '#' + this.id + ' {', - 'list-style-image: url(', - vAPI.getURL('img/browsericons/icon19.png'), - ');', - '}', - '#' + this.viewId + ', #' + this.viewId + ' > iframe {', - 'width: 160px;', - 'height: 290px;', - 'overflow: hidden !important;', - '}', - '#' + this.id + '[badge]:not([badge=""])::after {', - 'position: absolute;', - 'margin-left: -16px;', - 'margin-top: 3px;', - 'padding: 1px 2px;', - 'font-size: 9px;', - 'font-weight: bold;', - 'color: #fff;', - 'background: #000;', - 'content: attr(badge);', - '}' - ]; - - styleURI = Services.io.newURI( - 'data:text/css,' + encodeURIComponent(style.join('')), - null, - null - ); - - this.closePopup = function(tabBrowser) { - CustomizableUI.hidePanelForNode( - tabBrowser.ownerDocument.getElementById(this.viewId) - ); - }; - - CustomizableUI.createWidget(this); - - cleanupTasks.push(shutdown); - }; -})(); -*/ + CustomizableUI.removeListener(CUIEvents); + CustomizableUI.destroyWidget(tbb.id); -/******************************************************************************/ + vAPI.messaging.globalMessageManager + .removeMessageListener(location.host + ':closePopup', + onPopupCloseRequested); + }; -(function() { - // It appears that this branch actually works on the latest - // Basilisk. Maybe we can simply use this one directly instead of - // making checks like it's done now. + let styleURI = null; - // It was decided to use this branch unconditionally. It's still - // experimental though. + tbb.onBeforeCreated = function (doc) { + let panel = doc.createElement('panelview'); - // Add toolbar button for Basilisk - if (Services.appinfo.ID !== "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}") { - return; - } - - var tbb = vAPI.toolbarButton; - if ( tbb.init !== null ) { - return; - } - // if ( Services.vc.compare(Services.appinfo.version, '36.0') < 0 ) { - // return null; - // } - var CustomizableUI = null; - try { - CustomizableUI = Cu.import('resource:///modules/CustomizableUI.jsm', null).CustomizableUI; - } catch (ex) { - } - if ( CustomizableUI === null ) { - return null; - } - tbb.codePath = 'australis'; - tbb.CustomizableUI = CustomizableUI; - tbb.defaultArea = CustomizableUI.AREA_NAVBAR; - - var CUIEvents = {}; - - var badgeCSSRules = [ - 'background: #000', - 'color: #fff' - ].join(';'); - - var updateBadgeStyle = function() { - for ( var win of winWatcher.getWindows() ) { - var button = win.document.getElementById(tbb.id); - if ( button === null ) { - continue; - } - var badge = button.ownerDocument.getAnonymousElementByAttribute( - button, - 'class', - 'toolbarbutton-badge' - ); - if ( !badge ) { - continue; - } + this.populatePanel(doc, panel); - badge.style.cssText = badgeCSSRules; - } - }; - - var updateBadge = function() { - var wId = tbb.id; - var buttonInPanel = CustomizableUI.getWidget(wId).areaType === CustomizableUI.TYPE_MENU_PANEL; - - for ( var win of winWatcher.getWindows() ) { - var button = win.document.getElementById(wId); - if ( button === null ) { - continue; - } - if ( buttonInPanel ) { - button.classList.remove('badged-button'); - continue; - } - button.classList.add('badged-button'); - } + doc.getElementById('PanelUI-multiView').appendChild(panel); - if ( buttonInPanel ) { - return; - } + doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .loadSheet(styleURI, 1); + }; - // Anonymous elements need some time to be reachable - vAPI.setTimeout(updateBadgeStyle, 250); - }.bind(CUIEvents); + tbb.onCreated = function (button) { + button.setAttribute('badge', ''); + vAPI.setTimeout(updateBadge, 250); + }; - // https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/CustomizableUI.jsm#Listeners - CUIEvents.onCustomizeEnd = updateBadge; - CUIEvents.onWidgetAdded = updateBadge; - CUIEvents.onWidgetUnderflow = updateBadge; - - var onPopupCloseRequested = function({target}) { - if ( typeof tbb.closePopup === 'function' ) { - tbb.closePopup(target); - } - }; - - var shutdown = function() { - for ( var win of winWatcher.getWindows() ) { - var panel = win.document.getElementById(tbb.viewId); - if ( panel !== null && panel.parentNode !== null ) { - panel.parentNode.removeChild(panel); + tbb.onBeforePopupReady = function () { + // https://github.com/gorhill/uBlock/issues/83 + // Add `portrait` class if width is constrained. + try { + this.contentDocument.body + .classList.toggle('portrait', + CustomizableUI.getWidget(tbb.id).areaType + === CustomizableUI.TYPE_MENU_PANEL); + } catch (ex) { + // Ignore } - win.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils) - .removeSheet(styleURI, 1); - } - - CustomizableUI.removeListener(CUIEvents); - CustomizableUI.destroyWidget(tbb.id); - - vAPI.messaging.globalMessageManager.removeMessageListener( - location.host + ':closePopup', - onPopupCloseRequested - ); - }; - - var styleURI = null; - - tbb.onBeforeCreated = function(doc) { - var panel = doc.createElement('panelview'); - - this.populatePanel(doc, panel); - - doc.getElementById('PanelUI-multiView').appendChild(panel); - - doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils) - .loadSheet(styleURI, 1); - }; - - tbb.onCreated = function(button) { - button.setAttribute('badge', ''); - vAPI.setTimeout(updateBadge, 250); - }; - - tbb.onBeforePopupReady = function() { - // https://github.com/gorhill/uBlock/issues/83 - // Add `portrait` class if width is constrained. - try { - this.contentDocument.body.classList.toggle( - 'portrait', - CustomizableUI.getWidget(tbb.id).areaType === CustomizableUI.TYPE_MENU_PANEL - ); - } catch (ex) { - /* noop */ - } - }; + }; - tbb.closePopup = function(tabBrowser) { - CustomizableUI.hidePanelForNode( - tabBrowser.ownerDocument.getElementById(tbb.viewId) - ); - }; + tbb.closePopup = function (tabBrowser) { + CustomizableUI.hidePanelForNode(tabBrowser + .ownerDocument + .getElementById(tbb.viewId)); + }; - tbb.init = function() { - vAPI.messaging.globalMessageManager.addMessageListener( - location.host + ':closePopup', - onPopupCloseRequested - ); + tbb.init = function () { + vAPI.messaging.globalMessageManager + .addMessageListener(location.host + ':closePopup', + onPopupCloseRequested); - CustomizableUI.addListener(CUIEvents); + CustomizableUI.addListener(CUIEvents); - var style = [ - '#' + this.id + '.off {', + var style = [ + '#' + this.id + '.off {', 'list-style-image: url(', - vAPI.getURL('img/browsericons/icon19-off.png'), + vAPI.getURL('img/browsericons/icon19-off.png'), ');', - '}', - '#' + this.id + ' {', + '}', + '#' + this.id + ' {', 'list-style-image: url(', - vAPI.getURL('img/browsericons/icon19-19.png'), + vAPI.getURL('img/browsericons/icon19-19.png'), ');', - '}', - '#' + this.viewId + ', #' + this.viewId + ' > iframe {', + '}', + '#' + this.viewId + ', #' + this.viewId + ' > iframe {', 'height: 290px;', 'max-width: none !important;', 'min-width: 0 !important;', 'overflow: hidden !important;', 'padding: 0 !important;', 'width: 160px;', - '}' - ]; - - styleURI = Services.io.newURI( - 'data:text/css,' + encodeURIComponent(style.join('')), - null, - null - ); - - CustomizableUI.createWidget(this); - - cleanupTasks.push(shutdown); - }; -})(); - -/******************************************************************************/ - -// No toolbar button. - -(function() { - // Just to ensure the number of cleanup tasks is as expected: toolbar - // button code is one single cleanup task regardless of platform. - if ( vAPI.toolbarButton.init === null ) { - cleanupTasks.push(function(){}); - } -})(); - -/******************************************************************************/ - -if ( vAPI.toolbarButton.init !== null ) { - vAPI.toolbarButton.init(); -} - -/******************************************************************************/ -/******************************************************************************/ - -vAPI.contextMenu = { - contextMap: { - frame: 'inFrame', - link: 'onLink', - image: 'onImage', - audio: 'onAudio', - video: 'onVideo', - editable: 'onEditableArea' - } -}; - -/******************************************************************************/ - -vAPI.contextMenu.displayMenuItem = function({target}) { - var doc = target.ownerDocument; - var gContextMenu = doc.defaultView.gContextMenu; - if ( !gContextMenu.browser ) { - return; - } - - var menuitem = doc.getElementById(vAPI.contextMenu.menuItemId); - var currentURI = gContextMenu.browser.currentURI; - - // https://github.com/chrisaljoudi/uBlock/issues/105 - // TODO: Should the element picker works on any kind of pages? - if ( !currentURI.schemeIs('http') && !currentURI.schemeIs('https') ) { - menuitem.setAttribute('hidden', true); - return; - } - - var ctx = vAPI.contextMenu.contexts; - - if ( !ctx ) { - menuitem.setAttribute('hidden', false); - return; - } - - var ctxMap = vAPI.contextMenu.contextMap; - - for ( var context of ctx ) { - if ( - context === 'page' && - !gContextMenu.onLink && - !gContextMenu.onImage && - !gContextMenu.onEditableArea && - !gContextMenu.inFrame && - !gContextMenu.onVideo && - !gContextMenu.onAudio - ) { - menuitem.setAttribute('hidden', false); - return; - } - - if ( - ctxMap.hasOwnProperty(context) && - gContextMenu[ctxMap[context]] - ) { - menuitem.setAttribute('hidden', false); - return; - } - } - - menuitem.setAttribute('hidden', true); -}; - -/******************************************************************************/ - -vAPI.contextMenu.register = (function() { - var register = function(doc) { - if ( !this.menuItemId ) { - return; - } - - // Already installed? - if ( doc.getElementById(this.menuItemId) !== null ) { - return; - } - - var contextMenu = doc.getElementById('contentAreaContextMenu'); - var menuitem = doc.createElement('menuitem'); - menuitem.setAttribute('id', this.menuItemId); - menuitem.setAttribute('label', this.menuLabel); - menuitem.setAttribute('image', vAPI.getURL('img/browsericons/icon19-19.png')); - menuitem.setAttribute('class', 'menuitem-iconic'); - menuitem.addEventListener('command', this.onCommand); - contextMenu.addEventListener('popupshowing', this.displayMenuItem); - contextMenu.insertBefore(menuitem, doc.getElementById('inspect-separator')); - }; - - // https://github.com/gorhill/uBlock/issues/906 - // Be sure document.readyState is 'complete': it could happen at launch - // time that we are called by vAPI.contextMenu.create() directly before - // the environment is properly initialized. - var registerSafely = function(doc, tryCount) { - if ( doc.readyState === 'complete' ) { - register.call(this, doc); - return; - } - if ( typeof tryCount !== 'number' ) { - tryCount = 0; - } - tryCount += 1; - if ( tryCount < 8 ) { - vAPI.setTimeout(registerSafely.bind(this, doc, tryCount), 200); - } - }; - - return registerSafely; -})(); - -/******************************************************************************/ - -vAPI.contextMenu.unregister = function(doc) { - if ( !this.menuItemId ) { - return; - } - - var menuitem = doc.getElementById(this.menuItemId); - if ( menuitem === null ) { - return; - } - var contextMenu = menuitem.parentNode; - menuitem.removeEventListener('command', this.onCommand); - contextMenu.removeEventListener('popupshowing', this.displayMenuItem); - contextMenu.removeChild(menuitem); -}; - -/******************************************************************************/ - -vAPI.contextMenu.create = function(details, callback) { - this.menuItemId = details.id; - this.menuLabel = details.title; - this.contexts = details.contexts; - - if ( Array.isArray(this.contexts) && this.contexts.length ) { - this.contexts = this.contexts.indexOf('all') === -1 ? this.contexts : null; - } else { - // default in Chrome - this.contexts = ['page']; - } - - this.onCommand = function() { - var gContextMenu = getOwnerWindow(this).gContextMenu; - var details = { - menuItemId: this.id - }; - - if ( gContextMenu.inFrame ) { - details.tagName = 'iframe'; - // Probably won't work with e10s - details.frameUrl = gContextMenu.focusedWindow.location.href; - } else if ( gContextMenu.onImage ) { - details.tagName = 'img'; - details.srcUrl = gContextMenu.mediaURL; - } else if ( gContextMenu.onAudio ) { - details.tagName = 'audio'; - details.srcUrl = gContextMenu.mediaURL; - } else if ( gContextMenu.onVideo ) { - details.tagName = 'video'; - details.srcUrl = gContextMenu.mediaURL; - } else if ( gContextMenu.onLink ) { - details.tagName = 'a'; - details.linkUrl = gContextMenu.linkURL; - } - - callback(details, { - id: tabWatcher.tabIdFromTarget(gContextMenu.browser), - url: gContextMenu.browser.currentURI.asciiSpec - }); - }; - - for ( var win of winWatcher.getWindows() ) { - this.register(win.document); - } -}; - -/******************************************************************************/ - -vAPI.contextMenu.remove = function() { - for ( var win of winWatcher.getWindows() ) { - this.unregister(win.document); - } - - this.menuItemId = null; - this.menuLabel = null; - this.contexts = null; - this.onCommand = null; -}; - -/******************************************************************************/ -/******************************************************************************/ - -var optionsObserver = (function() { - var addonId = 'eMatrix@vannilla.org'; - - var commandHandler = function() { - switch ( this.id ) { - case 'showDashboardButton': - vAPI.tabs.open({ url: 'dashboard.html', index: -1 }); - break; - case 'showLoggerButton': - vAPI.tabs.open({ url: 'logger-ui.html', index: -1 }); - break; - default: - break; - } - }; - - var setupOptionsButton = function(doc, id) { - var button = doc.getElementById(id); - if ( button === null ) { - return; - } - button.addEventListener('command', commandHandler); - button.label = vAPI.i18n(id); - }; - - var setupOptionsButtons = function(doc) { - setupOptionsButton(doc, 'showDashboardButton'); - setupOptionsButton(doc, 'showLoggerButton'); - }; - - var observer = { - observe: function(doc, topic, id) { - if ( id !== addonId ) { - return; - } - - setupOptionsButtons(doc); - } - }; - - // https://github.com/gorhill/uBlock/issues/948 - // Older versions of Firefox can throw here when looking up `currentURI`. - - var canInit = function() { - try { - var tabBrowser = tabWatcher.currentBrowser(); - return tabBrowser && - tabBrowser.currentURI && - tabBrowser.currentURI.spec === 'about:addons' && - tabBrowser.contentDocument && - tabBrowser.contentDocument.readyState === 'complete'; - } catch (ex) { - } - }; - - // Manually add the buttons if the `about:addons` page is already opened. - - var init = function() { - if ( canInit() ) { - setupOptionsButtons(tabWatcher.currentBrowser().contentDocument); - } - }; - - var unregister = function() { - Services.obs.removeObserver(observer, 'addon-options-displayed'); - }; - - var register = function() { - Services.obs.addObserver(observer, 'addon-options-displayed', false); - cleanupTasks.push(unregister); - deferUntil(canInit, init, { next: 463 }); - }; - - return { - register: register, - unregister: unregister - }; -})(); - -optionsObserver.register(); - -/******************************************************************************/ -/******************************************************************************/ - -vAPI.lastError = function() { - return null; -}; - -/******************************************************************************/ -/******************************************************************************/ - -// This is called only once, when everything has been loaded in memory after -// the extension was launched. It can be used to inject content scripts -// in already opened web pages, to remove whatever nuisance could make it to -// the web pages before uBlock was ready. - -vAPI.onLoadAllCompleted = function() { - for ( var browser of tabWatcher.browsers() ) { - browser.messageManager.sendAsyncMessage( - location.host + '-load-completed' - ); - } -}; - -/******************************************************************************/ -/******************************************************************************/ - -// Likelihood is that we do not have to punycode: given punycode overhead, -// it's faster to check and skip than do it unconditionally all the time. - -var punycodeHostname = punycode.toASCII; -var isNotASCII = /[^\x21-\x7F]/; - -vAPI.punycodeHostname = function(hostname) { - return isNotASCII.test(hostname) ? punycodeHostname(hostname) : hostname; -}; - -vAPI.punycodeURL = function(url) { - if ( isNotASCII.test(url) ) { - return Services.io.newURI(url, null, null).asciiSpec; - } - return url; -}; - -/******************************************************************************/ -/******************************************************************************/ + '}' + ]; -vAPI.cloud = (function() { - var extensionBranchPath = 'extensions.' + location.host; - var cloudBranchPath = extensionBranchPath + '.cloudStorage'; + styleURI = + Services.io.newURI('data:text/css,' + +encodeURIComponent(style.join('')), + null, + null); - // https://github.com/gorhill/uBlock/issues/80#issuecomment-132081658 - // We must use get/setComplexValue in order to properly handle strings - // with unicode characters. - var iss = Ci.nsISupportsString; - var argstr = Components.classes['@mozilla.org/supports-string;1'] - .createInstance(iss); + CustomizableUI.createWidget(this); - var options = { - defaultDeviceName: '', - deviceName: '' - }; + vAPI.addCleanUpTask(shutdown); + }; + })(); - // User-supplied device name. - try { - options.deviceName = Services.prefs - .getBranch(extensionBranchPath + '.') - .getComplexValue('deviceName', iss) - .data; - } catch(ex) { - } + // No toolbar button. + (function () { + // Just to ensure the number of cleanup tasks is as expected: toolbar + // button code is one single cleanup task regardless of platform. + // eMatrix: might not be needed anymore + if (vAPI.toolbarButton.init === null) { + vAPI.addCleanUpTask(function(){}); + } + })(); - var getDefaultDeviceName = function() { - var name = ''; - try { - name = Services.prefs - .getBranch('services.sync.client.') - .getComplexValue('name', iss) - .data; - } catch(ex) { - } - - return name || window.navigator.platform || window.navigator.oscpu; - }; + if (vAPI.toolbarButton.init !== null) { + vAPI.toolbarButton.init(); + } + + let optionsObserver = (function () { + let addonId = 'eMatrix@vannilla.org'; + + let commandHandler = function () { + switch (this.id) { + case 'showDashboardButton': + vAPI.tabs.open({ + url: 'dashboard.html', + index: -1, + }); + break; + case 'showLoggerButton': + vAPI.tabs.open({ + url: 'logger-ui.html', + index: -1, + }); + break; + default: + break; + } + }; + + let setupOptionsButton = function (doc, id) { + let button = doc.getElementById(id); + if (button === null) { + return; + } + button.addEventListener('command', commandHandler); + button.label = vAPI.i18n(id); + }; + + let setupOptionsButtons = function (doc) { + setupOptionsButton(doc, 'showDashboardButton'); + setupOptionsButton(doc, 'showLoggerButton'); + }; + + let observer = { + observe: function (doc, topic, id) { + if (id !== addonId) { + return; + } - var start = function(dataKeys) { - var extensionBranch = Services.prefs.getBranch(extensionBranchPath + '.'); - var syncBranch = Services.prefs.getBranch('services.sync.prefs.sync.'); - - // Mark config entries as syncable - argstr.data = ''; - var dataKey; - for ( var i = 0; i < dataKeys.length; i++ ) { - dataKey = dataKeys[i]; - if ( extensionBranch.prefHasUserValue('cloudStorage.' + dataKey) === false ) { - extensionBranch.setComplexValue('cloudStorage.' + dataKey, iss, argstr); + setupOptionsButtons(doc); } - syncBranch.setBoolPref(cloudBranchPath + '.' + dataKey, true); - } - }; - - var push = function(datakey, data, callback) { - var branch = Services.prefs.getBranch(cloudBranchPath + '.'); - var bin = { - 'source': options.deviceName || getDefaultDeviceName(), - 'tstamp': Date.now(), - 'data': data, - 'size': 0 - }; - bin.size = JSON.stringify(bin).length; - argstr.data = JSON.stringify(bin); - branch.setComplexValue(datakey, iss, argstr); - if ( typeof callback === 'function' ) { - callback(); - } - }; + }; - var pull = function(datakey, callback) { - var result = null; - var branch = Services.prefs.getBranch(cloudBranchPath + '.'); - try { - var json = branch.getComplexValue(datakey, iss).data; - if ( typeof json === 'string' ) { - result = JSON.parse(json); - } - } catch(ex) { - } - callback(result); - }; + var canInit = function() { + // https://github.com/gorhill/uBlock/issues/948 + // Older versions of Firefox can throw here when looking + // up `currentURI`. + try { + let tabBrowser = vAPI.tabs.manager.currentBrowser(); + return tabBrowser + && tabBrowser.currentURI + && tabBrowser.currentURI.spec === 'about:addons' + && tabBrowser.contentDocument + && tabBrowser.contentDocument.readyState === 'complete'; + } catch (ex) { + // Ignore + } + }; + + // Manually add the buttons if the `about:addons` page is + // already opened. + let init = function () { + if (canInit()) { + setupOptionsButtons(vAPI.tabs.manager + .currentBrowser().contentDocument); + } + }; + + let unregister = function () { + Services.obs.removeObserver(observer, 'addon-options-displayed'); + }; + + let register = function () { + Services.obs.addObserver(observer, + 'addon-options-displayed', + false); + vAPI.addCleanUpTask(unregister); + vAPI.deferUntil(canInit, init, { next: 463 }); + }; + + return { + register: register, + unregister: unregister + }; + })(); - var getOptions = function(callback) { - if ( typeof callback !== 'function' ) { - return; - } - options.defaultDeviceName = getDefaultDeviceName(); - callback(options); + optionsObserver.register(); + + vAPI.onLoadAllCompleted = function() { + // 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. + for (let browser of vAPI.tabs.manager.browsers()) { + browser.messageManager + .sendAsyncMessage(location.host + '-load-completed'); + } }; - var setOptions = function(details, callback) { - if ( typeof details !== 'object' || details === null ) { - return; - } - - var branch = Services.prefs.getBranch(extensionBranchPath + '.'); - - if ( typeof details.deviceName === 'string' ) { - argstr.data = details.deviceName; - branch.setComplexValue('deviceName', iss, argstr); - options.deviceName = details.deviceName; - } + // Likelihood is that we do not have to punycode: given punycode overhead, + // it's faster to check and skip than do it unconditionally all the time. + var punycodeHostname = punycode.toASCII; + var isNotASCII = /[^\x21-\x7F]/; - getOptions(callback); + vAPI.punycodeHostname = function (hostname) { + return isNotASCII.test(hostname) + ? punycodeHostname(hostname) + : hostname; }; - return { - start: start, - push: push, - pull: pull, - getOptions: getOptions, - setOptions: setOptions - }; -})(); - -/******************************************************************************/ -/******************************************************************************/ - -vAPI.browserData = {}; - -/******************************************************************************/ - -// https://developer.mozilla.org/en-US/docs/HTTP_Cache - -vAPI.browserData.clearCache = function(callback) { - // PURGE_DISK_DATA_ONLY:1 - // PURGE_DISK_ALL:2 - // PURGE_EVERYTHING:3 - // However I verified that no argument does clear the cache data. - // There is no cache2 for older versions of Firefox. - if ( Services.cache2 ) { - Services.cache2.clear(); - } else if ( Services.cache ) { - Services.cache.evictEntries(Services.cache.STORE_ON_DISK); - } - if ( typeof callback === 'function' ) { - callback(); - } -}; - -/******************************************************************************/ - -vAPI.browserData.clearOrigin = function(/* domain */) { - // TODO -}; - -/******************************************************************************/ -/******************************************************************************/ - -// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsICookieManager2 -// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsICookie2 -// https://developer.mozilla.org/en-US/docs/Observer_Notifications#Cookies - -vAPI.cookies = {}; - -/******************************************************************************/ - -vAPI.cookies.CookieEntry = function(ffCookie) { - this.domain = ffCookie.host; - this.name = ffCookie.name; - this.path = ffCookie.path; - this.secure = ffCookie.isSecure === true; - this.session = ffCookie.expires === 0; - this.value = ffCookie.value; -}; - -/******************************************************************************/ - -vAPI.cookies.start = function() { - Services.obs.addObserver(this, 'cookie-changed', false); - Services.obs.addObserver(this, 'private-cookie-changed', false); - cleanupTasks.push(this.stop.bind(this)); -}; - -/******************************************************************************/ - -vAPI.cookies.stop = function() { - Services.obs.removeObserver(this, 'cookie-changed'); - Services.obs.removeObserver(this, 'private-cookie-changed'); -}; - -/******************************************************************************/ - -vAPI.cookies.observe = function(subject, topic, reason) { - //if ( topic !== 'cookie-changed' && topic !== 'private-cookie-changed' ) { - // return; - //} - // - if ( reason === 'cleared' && typeof this.onAllRemoved === 'function' ) { - this.onAllRemoved(); - return; - } - if ( subject === null ) { - return; - } - if ( subject instanceof Ci.nsICookie2 === false ) { - try { - subject = subject.QueryInterface(Ci.nsICookie2); - } catch (ex) { - return; - } - } - if ( reason === 'deleted' ) { - if ( typeof this.onRemoved === 'function' ) { - this.onRemoved(new this.CookieEntry(subject)); - } - return; - } - if ( typeof this.onChanged === 'function' ) { - this.onChanged(new this.CookieEntry(subject)); - } -}; - -/******************************************************************************/ - -// Meant and expected to be asynchronous. - -vAPI.cookies.getAll = function(callback) { - if ( typeof callback !== 'function' ) { - return; - } - var onAsync = function() { - var out = []; - var enumerator = Services.cookies.enumerator; - var ffcookie; - while ( enumerator.hasMoreElements() ) { - ffcookie = enumerator.getNext(); - if ( ffcookie instanceof Ci.nsICookie ) { - out.push(new this.CookieEntry(ffcookie)); - } - } - callback(out); + vAPI.punycodeURL = function (url) { + if (isNotASCII.test(url)) { + return Services.io.newURI(url, null, null).asciiSpec; + } + + return url; }; - vAPI.setTimeout(onAsync.bind(this), 0); -}; - -/******************************************************************************/ - -vAPI.cookies.remove = function(details, callback) { - var uri = Services.io.newURI(details.url, null, null); - var cookies = Services.cookies; - cookies.remove(uri.asciiHost, details.name, uri.path, false, {}); - cookies.remove( '.' + uri.asciiHost, details.name, uri.path, false, {}); - if ( typeof callback === 'function' ) { - callback({ - domain: uri.asciiHost, - name: details.name, - path: uri.path - }); - } -}; - -/******************************************************************************/ -/******************************************************************************/ - })(); - -/******************************************************************************/ diff --git a/js/vapi-browser.js b/js/vapi-browser.js new file mode 100644 index 0000000..b3c9a5f --- /dev/null +++ b/js/vapi-browser.js @@ -0,0 +1,241 @@ +/******************************************************************************* + + ηMatrix - a browser extension to black/white list requests. + Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors + Copyright (C) 2019 Alessio Vanni + + 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://gitlab.com/vannilla/ematrix + uMatrix Home: https://github.com/gorhill/uMatrix +*/ + +'use strict'; + +/******************************************************************************/ + +(function () { + vAPI.browser = {}; + + vAPI.browser.getTabBrowser = function (win) { + return win && win.gBrowser || null; + }; + + vAPI.browser.getOwnerWindow = function (target) { + if (target.ownerDocument) { + return target.ownerDocument.defaultView; + } + + return null; + }; + + vAPI.browser.settings = { + // For now, only booleans. + originalValues: {}, + + rememberOriginalValue: function (path, setting) { + let key = path + '.' + setting; + if (this.originalValues.hasOwnProperty(key)) { + return; + } + + let hasUserValue; + let branch = Services.prefs.getBranch(path + '.'); + + try { + hasUserValue = branch.prefHasUserValue(setting); + } catch (ex) { + // Ignore + } + + if (hasUserValue !== undefined) { + this.originalValues[key] = hasUserValue + ? this.getValue(path, setting) + : undefined; + } + }, + clear: function (path, setting) { + let key = path + '.' + setting; + + // Value was not overriden -- nothing to restore + if (this.originalValues.hasOwnProperty(key) === false) { + return; + } + + let value = this.originalValues[key]; + + // https://github.com/gorhill/uBlock/issues/292#issuecomment-109621979 + // Forget the value immediately, it may change outside of + // uBlock control. + delete this.originalValues[key]; + + // Original value was a default one + if (value === undefined) { + try { + Services.prefs.getBranch(path + '.').clearUserPref(setting); + } catch (ex) { + // Ignore + } + return; + } + + // Reset to original value + this.setValue(path, setting, value); + }, + getValue: function (path, setting) { + let branch = Services.prefs.getBranch(path + '.'); + + try { + switch (branch.getPrefType(setting)) { + case branch.PREF_INT: + return branch.getIntPref(setting); + case branch.PREF_BOOL: + return branch.getBoolPref(setting); + default: + // not supported + return; + } + } catch (e) { + // Ignore + } + }, + setValue: function (path, setting, value) { + let branch = Services.prefs.getBranch(path + '.'); + + try { + switch (typeof value) { + case 'number': + return branch.setIntPref(setting, value); + case 'boolean': + return branch.setBoolPref(setting, value); + default: + // not supported + return; + } + } catch (e) { + // Ignore + } + }, + setSetting: function (setting, value) { + switch (setting) { + case 'prefetching': + this.rememberOriginalValue('network', 'prefetch-next'); + // https://bugzilla.mozilla.org/show_bug.cgi?id=814169 + // Sigh. + // eMatrix: doesn't seem the case for Pale + // Moon/Basilisk, but let's keep this anyway + this.rememberOriginalValue('network.http', 'speculative-parallel-limit'); + + // https://github.com/gorhill/uBlock/issues/292 + // "true" means "do not disable", i.e. leave entry alone + if (value) { + this.clear('network', 'prefetch-next'); + this.clear('network.http', 'speculative-parallel-limit'); + } else { + this.setValue('network', 'prefetch-next', false); + this.setValue('network.http', + 'speculative-parallel-limit', 0); + } + break; + case 'hyperlinkAuditing': + this.rememberOriginalValue('browser', 'send_pings'); + this.rememberOriginalValue('beacon', 'enabled'); + + // https://github.com/gorhill/uBlock/issues/292 + // "true" means "do not disable", i.e. leave entry alone + if (value) { + this.clear('browser', 'send_pings'); + this.clear('beacon', 'enabled'); + } else { + this.setValue('browser', 'send_pings', false); + this.setValue('beacon', 'enabled', false); + } + break; + case 'webrtcIPAddress': + let prefName; + let prefVal; + + // https://github.com/gorhill/uBlock/issues/894 + // Do not disable completely WebRTC if it can be avoided. FF42+ + // has a `media.peerconnection.ice.default_address_only` pref which + // purpose is to prevent local IP address leakage. + if (this.getValue('media.peerconnection', + 'ice.default_address_only') !== undefined) { + prefName = 'ice.default_address_only'; + prefVal = true; + } else { + prefName = 'enabled'; + prefVal = false; + } + + this.rememberOriginalValue('media.peerconnection', prefName); + if (value) { + this.clear('media.peerconnection', prefName); + } else { + this.setValue('media.peerconnection', prefName, prefVal); + } + break; + default: + break; + } + }, + set: function (details) { + for (let setting in details) { + if (details.hasOwnProperty(setting) === false) { + continue; + } + this.setSetting(setting, !!details[setting]); + } + }, + restoreAll: function () { + let pos; + for (let key in this.originalValues) { + if (this.originalValues.hasOwnProperty(key) === false) { + continue; + } + + pos = key.lastIndexOf('.'); + this.clear(key.slice(0, pos), key.slice(pos + 1)); + } + }, + }; + + vAPI.addCleanUpTask(vAPI.browser.settings + .restoreAll.bind(vAPI.browser.settings)); + + vAPI.browser.data = {}; + + vAPI.browser.data.clearCache = function (callback) { + // PURGE_DISK_DATA_ONLY:1 + // PURGE_DISK_ALL:2 + // PURGE_EVERYTHING:3 + // However I verified that no argument does clear the cache data. + // There is no cache2 for older versions of Firefox. + if (Services.cache2) { + Services.cache2.clear(); + } else if (Services.cache) { + Services.cache.evictEntries(Services.cache.STORE_ON_DISK); + } + + if (typeof callback === 'function') { + callback(); + } + }; + + vAPI.browser.data.clearOrigin = function(/* domain */) { + // TODO + // eMatrix: is this actually needed? I don't really know what + // it's supposed to do anyway. + }; +})(); diff --git a/js/vapi-client.js b/js/vapi-client.js index 20e16f9..e45cc4d 100644 --- a/js/vapi-client.js +++ b/js/vapi-client.js @@ -21,208 +21,172 @@ uMatrix Home: https://github.com/gorhill/uMatrix */ -/* jshint esnext: true */ -/* global addMessageListener, removeMessageListener, sendAsyncMessage */ - // For non background pages 'use strict'; -/******************************************************************************/ - -(function(self) { - -/******************************************************************************/ - -// https://bugs.chromium.org/p/project-zero/issues/detail?id=1225&desc=6#c10 -if ( self.vAPI === undefined || self.vAPI.eMatrix !== true ) { - self.vAPI = { eMatrix: true }; -} - -var vAPI = self.vAPI; -vAPI.firefox = true; -vAPI.sessionId = String.fromCharCode(Date.now() % 25 + 97) + - Math.random().toString(36).slice(2); - -/******************************************************************************/ +(function (self) { + vAPI.sessionId = String.fromCharCode(Date.now() % 25 + 97) + + Math.random().toString(36).slice(2); -vAPI.setTimeout = vAPI.setTimeout || function(callback, delay) { - return setTimeout(function() { callback(); }, delay); -}; + vAPI.shutdown = (function () { + let jobs = []; -/******************************************************************************/ + let add = function (job) { + jobs.push(job); + }; -vAPI.shutdown = (function() { - var jobs = []; - - var add = function(job) { - jobs.push(job); - }; + let exec = function () { + //console.debug('Shutting down...'); + let job; + while ((job = jobs.pop())) { + job(); + } + }; + + return { + add: add, + exec: exec + }; + })(); + + vAPI.messaging = { + listeners: new Set(), + pending: new Map(), + requestId: 1, + connected: false, + messageListenerCallback: null, + toggleListenerCallback: null, + + start: function () { + this.addListener(this.builtinListener); + if (this.toggleListenerCallback === null) { + this.toggleListenerCallback = this.toggleListener.bind(this); + } + + window.addEventListener('pagehide', + this.toggleListenerCallback, true); + window.addEventListener('pageshow', + this.toggleListenerCallback, true); + }, + shutdown: function () { + if (this.toggleListenerCallback !== null) { + window.removeEventListener('pagehide', + this.toggleListenerCallback, true); + window.removeEventListener('pageshow', + this.toggleListenerCallback, true); + } + this.removeAllListeners(); + + //service pending callbacks + var pending = this.pending; + this.pending.clear(); + for (let callback of pending.values()) { + if (typeof callback === 'function') { + callback(null); + } + } + }, + connect: function () { + if (!this.connected) { + if (this.messageListenerCallback === null) { + this.messageListenerCallback = + this.messageListener.bind(this); + } + addMessageListener(this.messageListenerCallback); + this.connected = true; + } + }, + disconnect: function () { + if (this.connected) { + removeMessageListener(); + this.connected = false; + } + }, + messageListener: function (msg) { + let details = JSON.parse(msg); + if (!details) { + return; + } - var exec = function() { - //console.debug('Shutting down...'); - var job; - while ( (job = jobs.pop()) ) { - job(); - } - }; + if (details.broadcast) { + this.sendToListeners(details.msg); + return; + } - return { - add: add, - exec: exec - }; -})(); - -/******************************************************************************/ - -vAPI.messaging = { - listeners: new Set(), - pending: new Map(), - requestId: 1, - connected: false, - - start: function() { - this.addListener(this.builtinListener); - if ( this.toggleListenerCallback === null ) { - this.toggleListenerCallback = this.toggleListener.bind(this); - } - window.addEventListener('pagehide', this.toggleListenerCallback, true); - window.addEventListener('pageshow', this.toggleListenerCallback, true); - }, - - shutdown: function() { - if ( this.toggleListenerCallback !== null ) { - window.removeEventListener('pagehide', this.toggleListenerCallback, true); - window.removeEventListener('pageshow', this.toggleListenerCallback, true); - } - this.removeAllListeners(); - //service pending callbacks - var pending = this.pending; - this.pending.clear(); - for ( var callback of pending.values() ) { - if ( typeof callback === 'function' ) { - callback(null); + if (details.requestId) { + let listener = this.pending.get(details.requestId); + if (listener !== undefined) { + this.pending.delete(details.requestId); + listener(details.msg); + return; + } + } + }, + builtinListener: function (msg) { + if (typeof msg.cmd === 'string' && msg.cmd === 'injectScript') { + let details = msg.details; + if (!details.allFrames && window !== window.top) { + return; + } + self.injectScript(details.file); + } + }, + send: function (channelName, message, callback) { + this.connect() + + message = { + channelName: self._sandboxId_ + '|' + channelName, + msg: message + }; + + if (callback) { + message.requestId = this.requestId++; + this.pending.set(message.requestId, callback); } - } - }, - connect: function() { - if ( !this.connected ) { - if ( this.messageListenerCallback === null ) { - this.messageListenerCallback = this.messageListener.bind(this); + sendAsyncMessage('ematrix:background', message); + }, + toggleListener: function ({type, persisted}) { + if (type === 'pagehide' && !persisted) { + vAPI.shutdown.exec(); + this.shutdown(); + return; } - addMessageListener(this.messageListenerCallback); - this.connected = true; - } - }, - - disconnect: function() { - if ( this.connected ) { - removeMessageListener(); - this.connected = false; - } - }, - - messageListener: function(msg) { - var details = JSON.parse(msg); - if ( !details ) { - return; - } - - if ( details.broadcast ) { - this.sendToListeners(details.msg); - return; - } - - if ( details.requestId ) { - var listener = this.pending.get(details.requestId); - if ( listener !== undefined ) { - this.pending.delete(details.requestId); - listener(details.msg); - return; + + if (type === 'pagehide') { + this.disconnect(); + } else { + this.connect(); } - } - }, - messageListenerCallback: null, - - builtinListener: function(msg) { - if ( typeof msg.cmd === 'string' && msg.cmd === 'injectScript' ) { - var details = msg.details; - if ( !details.allFrames && window !== window.top ) { - return; + }, + sendToListeners: function (msg) { + for (let listener of this.listeners) { + listener(msg); } - self.injectScript(details.file); - } - }, - - send: function(channelName, message, callback) { - this.connect() - - message = { - channelName: self._sandboxId_ + '|' + channelName, - msg: message - }; - - if ( callback ) { - message.requestId = this.requestId++; - this.pending.set(message.requestId, callback); - } - - sendAsyncMessage('ematrix:background', message); - }, - - toggleListener: function({type, persisted}) { - if ( type === 'pagehide' && !persisted ) { - vAPI.shutdown.exec(); - this.shutdown(); - return; - } - - if ( type === 'pagehide' ) { + }, + addListener: function (listener) { + this.listeners.add(listener); + this.connect() + }, + removeListener: function (listener) { + this.listeners.delete(listener); + }, + removeAllListeners: function () { this.disconnect(); - } else /* if ( type === 'pageshow' ) */ { - this.connect(); - } - }, - toggleListenerCallback: null, - - sendToListeners: function(msg) { - for ( var listener of this.listeners ) { - listener(msg); - } - }, - - addListener: function(listener) { - this.listeners.add(listener); - this.connect() - }, - - removeListener: function(listener) { - this.listeners.delete(listener); - }, - - removeAllListeners: function() { - this.disconnect(); - this.listeners.clear();; - } -}; - -vAPI.messaging.start() - -/******************************************************************************/ - -// No need to have vAPI client linger around after shutdown if -// we are not a top window (because element picker can still -// be injected in top window). -// Needs more investigating -/*if ( window !== window.top ) { - vAPI.shutdown.add(function() { - vAPI = null; - }); -}*/ - -/******************************************************************************/ + this.listeners.clear(); + } + }; + vAPI.messaging.start() + + // No need to have vAPI client linger around after shutdown if + // we are not a top window (because element picker can still + // be injected in top window). + // Needs more investigating + // if ( window !== window.top ) { + // vAPI.shutdown.add(function() { + // vAPI = null; + // }); + // } })(this); - -/******************************************************************************/ diff --git a/js/vapi-cloud.js b/js/vapi-cloud.js new file mode 100644 index 0000000..90b077e --- /dev/null +++ b/js/vapi-cloud.js @@ -0,0 +1,156 @@ +/******************************************************************************* + + ηMatrix - a browser extension to black/white list requests. + Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors + Copyright (C) 2019 Alessio Vanni + + 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://gitlab.com/vannilla/ematrix + uMatrix Home: https://github.com/gorhill/uMatrix +*/ + +'use strict'; + +/******************************************************************************/ + +(function () { + vAPI.cloud = (function () { + let extensionBranchPath = 'extensions.' + location.host; + let cloudBranchPath = extensionBranchPath + '.cloudStorage'; + + // https://github.com/gorhill/uBlock/issues/80#issuecomment-132081658 + // We must use get/setComplexValue in order to properly handle strings + // with unicode characters. + let iss = Ci.nsISupportsString; + let argstr = Components.classes['@mozilla.org/supports-string;1'] + .createInstance(iss); + + let options = { + defaultDeviceName: '', + deviceName: '' + }; + + // User-supplied device name. + try { + options.deviceName = Services.prefs + .getBranch(extensionBranchPath + '.') + .getComplexValue('deviceName', iss) + .data; + } catch(ex) { + // Ignore + } + + var getDefaultDeviceName = function() { + var name = ''; + try { + name = Services.prefs + .getBranch('services.sync.client.') + .getComplexValue('name', iss) + .data; + } catch(ex) { + // Ignore + } + + return name || window.navigator.platform || window.navigator.oscpu; + }; + + let start = function (dataKeys) { + let extensionBranch = + Services.prefs.getBranch(extensionBranchPath + '.'); + let syncBranch = + Services.prefs.getBranch('services.sync.prefs.sync.'); + + // Mark config entries as syncable + argstr.data = ''; + let dataKey; + for (let i=0; i<dataKeys.length; ++i) { + dataKey = dataKeys[i]; + if (extensionBranch.prefHasUserValue('cloudStorage.' + dataKey) + === false) { + extensionBranch.setComplexValue('cloudStorage.' + dataKey, + iss, argstr); + } + + syncBranch.setBoolPref(cloudBranchPath + '.' + dataKey, true); + } + }; + + let push = function (datakey, data, callback) { + let branch = Services.prefs.getBranch(cloudBranchPath + '.'); + let bin = { + 'source': options.deviceName || getDefaultDeviceName(), + 'tstamp': Date.now(), + 'data': data, + 'size': 0 + }; + bin.size = JSON.stringify(bin).length; + argstr.data = JSON.stringify(bin); + branch.setComplexValue(datakey, iss, argstr); + + if (typeof callback === 'function') { + callback(); + } + }; + + let pull = function (datakey, callback) { + let result = null; + let branch = Services.prefs.getBranch(cloudBranchPath + '.'); + + try { + let json = branch.getComplexValue(datakey, iss).data; + if (typeof json === 'string') { + result = JSON.parse(json); + } + } catch(ex) { + // Ignore + } + + callback(result); + }; + + let getOptions = function (callback) { + if (typeof callback !== 'function') { + return; + } + + options.defaultDeviceName = getDefaultDeviceName(); + callback(options); + }; + + let setOptions = function (details, callback) { + if (typeof details !== 'object' || details === null) { + return; + } + + let branch = Services.prefs.getBranch(extensionBranchPath + '.'); + + if (typeof details.deviceName === 'string') { + argstr.data = details.deviceName; + branch.setComplexValue('deviceName', iss, argstr); + options.deviceName = details.deviceName; + } + + getOptions(callback); + }; + + return { + start: start, + push: push, + pull: pull, + getOptions: getOptions, + setOptions: setOptions + }; + })(); +})(); diff --git a/js/vapi-common.js b/js/vapi-common.js index 6946a9c..b7c5635 100644 --- a/js/vapi-common.js +++ b/js/vapi-common.js @@ -21,173 +21,145 @@ uMatrix Home: https://github.com/gorhill/uMatrix */ -/* global sendAsyncMessage */ - // For background page or non-background pages 'use strict'; -/******************************************************************************/ - -(function(self) { - -/******************************************************************************/ - -const {Services} = Components.utils.import( - 'resource://gre/modules/Services.jsm', - null -); - -// https://bugs.chromium.org/p/project-zero/issues/detail?id=1225&desc=6#c10 -if ( self.vAPI === undefined || self.vAPI.eMatrix !== true ) { - self.vAPI = { eMatrix: true }; -} - -var vAPI = self.vAPI; - -/******************************************************************************/ - -vAPI.setTimeout = vAPI.setTimeout || function(callback, delay, extra) { - return setTimeout(function(a) { callback(a); }, delay, extra); -}; - -/******************************************************************************/ - -// 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' - ); -}; +let vAPI = {}; -/******************************************************************************/ +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; +Cu.import('resource://gre/modules/Services.jsm'); -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() { - const parser = Components.classes['@mozilla.org/parserutils;1'] - .getService(Components.interfaces.nsIParserUtils); - - // https://github.com/gorhill/uBlock/issues/845 - // Apparently dashboard pages execute with `about:blank` principal. - - return function(node, html) { - while ( node.firstChild ) { - node.removeChild(node.firstChild); - } - - node.appendChild(parser.parseFragment( - html, - parser.SanitizerAllowStyle, - false, - Services.io.newURI('about:blank', null, null), - document.documentElement - )); +(function (self) { + vAPI.setTimeout = vAPI.setTimeout || function (callback, delay, extra) { + return setTimeout(function (a) { + callback(a); + }, delay, extra); }; -})(); -/******************************************************************************/ + // http://www.w3.org/International/questions/qa-scripts#directions + let setScriptDirection = function(language) { + let dir = + ['ar', 'he', 'fa', 'ps', 'ur'].indexOf(language) !== -1 + ? 'rtl' + : 'ltr'; + + document.body.setAttribute('dir', dir); + }; -vAPI.getURL = function(path) { - return 'chrome://' + location.host + '/content/' + path.replace(/^\/+/, ''); -}; + vAPI.download = function (details) { + if (!details.url) { + return; + } -/******************************************************************************/ + let a = document.createElement('a'); + a.href = details.url; + a.setAttribute('download', details.filename || ''); + a.dispatchEvent(new MouseEvent('click')); + }; -vAPI.i18n = (function() { - var stringBundle = Services.strings.createBundle( - 'chrome://' + location.host + '/locale/messages.properties' - ); + vAPI.insertHTML = (function () { + const parser = Cc['@mozilla.org/parserutils;1'] + .getService(Ci.nsIParserUtils); + + // https://github.com/gorhill/uBlock/issues/845 + // Apparently dashboard pages execute with `about:blank` principal. + + return function (node, html) { + while (node.firstChild) { + node.removeChild(node.firstChild); + } + + let parsed = + parser.parseFragment(html, + parser.SanitizerAllowStyle, + false, + Services.io.newURI('about:blank', + null, null), + document.documentElement); + + node.appendChild(parsed); + }; + })(); + + vAPI.getURL = function (path) { + return 'chrome://' + + location.host + + '/content/' + + path.replace(/^\/+/, ''); + }; + + vAPI.i18n = (function () { + let stringBundle = + Services.strings.createBundle('chrome://' + + location.host + + '/locale/messages.properties'); + + return function (s) { + try { + return stringBundle.GetStringFromName(s); + } catch (ex) { + return ''; + } + }; + })(); + + setScriptDirection(navigator.language); + + vAPI.closePopup = function() { + sendAsyncMessage(location.host + ':closePopup'); + }; - return function(s) { - try { - return stringBundle.GetStringFromName(s); - } catch (ex) { - return ''; - } + // 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. + vAPI.localStorage = { + pbName: '', + pb: null, + str: Cc['@mozilla.org/supports-string;1'] + .createInstance(Ci.nsISupportsString), + + init: function (pbName) { + this.pbName = pbName; + this.pb = Services.prefs.getBranch(pbName); + }, + getItem: function (key) { + try { + return this.pb + .getComplexValue(key, + Ci.nsISupportsString).data; + } catch (ex) { + return null; + } + }, + setItem: function (key, value) { + this.str.data = value; + this.pb.setComplexValue(key, + Ci.nsISupportsString, + this.str); + }, + getBool: function (key) { + try { + return this.pb.getBoolPref(key); + } catch (ex) { + return null; + } + }, + setBool: function (key, value) { + this.pb.setBoolPref(key, value); + }, + setDefaultBool: function (key, defaultValue) { + Services.prefs.getDefaultBranch(this.pbName) + .setBoolPref(key, defaultValue); + }, + removeItem: function (key) { + this.pb.clearUserPref(key); + }, + clear: function () { + this.pb.deleteBranch(''); + } }; -})(); - -setScriptDirection(navigator.language); - -/******************************************************************************/ - -vAPI.closePopup = function() { - sendAsyncMessage(location.host + ':closePopup'); -}; - -/******************************************************************************/ - -// 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. - -vAPI.localStorage = { - pbName: '', - pb: null, - str: Components.classes['@mozilla.org/supports-string;1'] - .createInstance(Components.interfaces.nsISupportsString), - init: function(pbName) { - this.pbName = pbName; - this.pb = Services.prefs.getBranch(pbName); - }, - getItem: function(key) { - try { - return this.pb.getComplexValue( - key, - Components.interfaces.nsISupportsString - ).data; - } catch (ex) { - return null; - } - }, - setItem: function(key, value) { - this.str.data = value; - this.pb.setComplexValue( - key, - Components.interfaces.nsISupportsString, - this.str - ); - }, - getBool: function(key) { - try { - return this.pb.getBoolPref(key); - } catch (ex) { - return null; - } - }, - setBool: function(key, value) { - this.pb.setBoolPref(key, value); - }, - setDefaultBool: function(key, defaultValue) { - Services.prefs.getDefaultBranch(this.pbName).setBoolPref(key, defaultValue); - }, - removeItem: function(key) { - this.pb.clearUserPref(key); - }, - clear: function() { - this.pb.deleteBranch(''); - } -}; - -vAPI.localStorage.init('extensions.' + location.host + '.'); - -/******************************************************************************/ + vAPI.localStorage.init('extensions.' + location.host + '.'); })(this); - -/******************************************************************************/ diff --git a/js/vapi-contextmenu.js b/js/vapi-contextmenu.js new file mode 100644 index 0000000..b0f8694 --- /dev/null +++ b/js/vapi-contextmenu.js @@ -0,0 +1,212 @@ +/******************************************************************************* + + ηMatrix - a browser extension to black/white list requests. + Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors + Copyright (C) 2019 Alessio Vanni + + 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://gitlab.com/vannilla/ematrix + uMatrix Home: https://github.com/gorhill/uMatrix +*/ + +'use strict'; + +/******************************************************************************/ + +(function () { + vAPI.contextMenu = { + contextMap: { + frame: 'inFrame', + link: 'onLink', + image: 'onImage', + audio: 'onAudio', + video: 'onVideo', + editable: 'onEditableArea' + } + }; + + vAPI.contextMenu.displayMenuItem = function ({target}) { + let doc = target.ownerDocument; + let gContextMenu = doc.defaultView.gContextMenu; + if (!gContextMenu.browser) { + return; + } + + let menuitem = doc.getElementById(vAPI.contextMenu.menuItemId); + let currentURI = gContextMenu.browser.currentURI; + + // https://github.com/chrisaljoudi/uBlock/issues/105 + // TODO: Should the element picker works on any kind of pages? + if (!currentURI.schemeIs('http') && !currentURI.schemeIs('https')) { + menuitem.setAttribute('hidden', true); + return; + } + + let ctx = vAPI.contextMenu.contexts; + + if (!ctx) { + menuitem.setAttribute('hidden', false); + return; + } + + let ctxMap = vAPI.contextMenu.contextMap; + + for (let context of ctx) { + if (context === 'page' + && !gContextMenu.onLink + && !gContextMenu.onImage + && !gContextMenu.onEditableArea + && !gContextMenu.inFrame + && !gContextMenu.onVideo + && !gContextMenu.onAudio) { + menuitem.setAttribute('hidden', false); + return; + } + + if (ctxMap.hasOwnProperty(context) + && gContextMenu[ctxMap[context]]) { + menuitem.setAttribute('hidden', false); + return; + } + } + + menuitem.setAttribute('hidden', true); + }; + + vAPI.contextMenu.register = (function () { + let register = function (doc) { + if (!this.menuItemId) { + return; + } + + // Already installed? + if (doc.getElementById(this.menuItemId) !== null) { + return; + } + + let contextMenu = doc.getElementById('contentAreaContextMenu'); + + let menuitem = doc.createElement('menuitem'); + menuitem.setAttribute('id', this.menuItemId); + menuitem.setAttribute('label', this.menuLabel); + menuitem.setAttribute('image', vAPI.getURL('img/browsericons/icon19-19.png')); + menuitem.setAttribute('class', 'menuitem-iconic'); + menuitem.addEventListener('command', this.onCommand); + + contextMenu.addEventListener('popupshowing', this.displayMenuItem); + contextMenu.insertBefore(menuitem, doc.getElementById('inspect-separator')); + }; + + let registerSafely = function (doc, tryCount) { + // https://github.com/gorhill/uBlock/issues/906 + // Be sure document.readyState is 'complete': it could happen + // at launch time that we are called by + // vAPI.contextMenu.create() directly before the environment + // is properly initialized. + if (doc.readyState === 'complete') { + register.call(this, doc); + return; + } + + if (typeof tryCount !== 'number') { + tryCount = 0; + } + + tryCount += 1; + if (tryCount < 8) { + vAPI.setTimeout(registerSafely.bind(this, doc, tryCount), 200); + } + }; + + return registerSafely; + })(); + + vAPI.contextMenu.unregister = function (doc) { + if (!this.menuItemId) { + return; + } + + let menuitem = doc.getElementById(this.menuItemId); + if (menuitem === null) { + return; + } + + let contextMenu = menuitem.parentNode; + menuitem.removeEventListener('command', this.onCommand); + contextMenu.removeEventListener('popupshowing', this.displayMenuItem); + contextMenu.removeChild(menuitem); + }; + + vAPI.contextMenu.create = function (details, callback) { + this.menuItemId = details.id; + this.menuLabel = details.title; + this.contexts = details.contexts; + + if (Array.isArray(this.contexts) && this.contexts.length) { + this.contexts = this.contexts.indexOf('all') === -1 + ? this.contexts + : null; + } else { + // default in Chrome + this.contexts = ['page']; + } + + this.onCommand = function () { + let gContextMenu = vAPI.browser.getOwnerWindow(this).gContextMenu; + let details = { + menuItemId: this.id + }; + + if (gContextMenu.inFrame) { + details.tagName = 'iframe'; + // Probably won't work with e10s + // eMatrix: doesn't matter ;) + details.frameUrl = gContextMenu.focusedWindow.location.href; + } else if (gContextMenu.onImage) { + details.tagName = 'img'; + details.srcUrl = gContextMenu.mediaURL; + } else if (gContextMenu.onAudio) { + details.tagName = 'audio'; + details.srcUrl = gContextMenu.mediaURL; + } else if (gContextMenu.onVideo) { + details.tagName = 'video'; + details.srcUrl = gContextMenu.mediaURL; + } else if (gContextMenu.onLink) { + details.tagName = 'a'; + details.linkUrl = gContextMenu.linkURL; + } + + callback(details, { + id: vAPI.tabs.manager.tabIdFromTarget(gContextMenu.browser), + url: gContextMenu.browser.currentURI.asciiSpec + }); + }; + + for (let win of vAPI.window.getWindows()) { + this.register(win.document); + } + }; + + vAPI.contextMenu.remove = function () { + for (let win of vAPI.window.getWindows()) { + this.unregister(win.document); + } + + this.menuItemId = null; + this.menuLabel = null; + this.contexts = null; + this.onCommand = null; + }; +})(); diff --git a/js/vapi-cookies.js b/js/vapi-cookies.js new file mode 100644 index 0000000..ff36d97 --- /dev/null +++ b/js/vapi-cookies.js @@ -0,0 +1,118 @@ +/******************************************************************************* + + ηMatrix - a browser extension to black/white list requests. + Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors + Copyright (C) 2019 Alessio Vanni + + 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://gitlab.com/vannilla/ematrix + uMatrix Home: https://github.com/gorhill/uMatrix +*/ + +'use strict'; + +/******************************************************************************/ + +(function () { + vAPI.cookies = {}; + + vAPI.cookies.CookieEntry = function (ffCookie) { + this.domain = ffCookie.host; + this.name = ffCookie.name; + this.path = ffCookie.path; + this.secure = ffCookie.isSecure === true; + this.session = ffCookie.expires === 0; + this.value = ffCookie.value; + }; + + vAPI.cookies.start = function () { + Services.obs.addObserver(this, 'cookie-changed', false); + Services.obs.addObserver(this, 'private-cookie-changed', false); + vAPI.addCleanUpTask(this.stop.bind(this)); + }; + + vAPI.cookies.stop = function () { + Services.obs.removeObserver(this, 'cookie-changed'); + Services.obs.removeObserver(this, 'private-cookie-changed'); + }; + + vAPI.cookies.observe = function (subject, topic, reason) { + //if ( topic !== 'cookie-changed' && topic !== 'private-cookie-changed' ) { + // return; + //} + // + if (reason === 'cleared' && typeof this.onAllRemoved === 'function') { + this.onAllRemoved(); + return; + } + if (subject === null) { + return; + } + if (subject instanceof Ci.nsICookie2 === false) { + try { + subject = subject.QueryInterface(Ci.nsICookie2); + } catch (ex) { + return; + } + } + if (reason === 'deleted') { + if (typeof this.onRemoved === 'function') { + this.onRemoved(new this.CookieEntry(subject)); + } + return; + } + if (typeof this.onChanged === 'function') { + this.onChanged(new this.CookieEntry(subject)); + } + }; + + vAPI.cookies.getAll = function(callback) { + // Meant and expected to be asynchronous. + if (typeof callback !== 'function') { + return; + } + + let onAsync = function () { + let out = []; + let enumerator = Services.cookies.enumerator; + let ffcookie; + while (enumerator.hasMoreElements()) { + ffcookie = enumerator.getNext(); + if (ffcookie instanceof Ci.nsICookie) { + out.push(new this.CookieEntry(ffcookie)); + } + } + + callback(out); + }; + + vAPI.setTimeout(onAsync.bind(this), 0); + }; + + vAPI.cookies.remove = function (details, callback) { + let uri = Services.io.newURI(details.url, null, null); + let cookies = Services.cookies; + cookies.remove(uri.asciiHost, details.name, uri.path, false, {}); + cookies.remove( '.' + uri.asciiHost, details.name, uri.path, false, {}); + + if (typeof callback === 'function') { + callback({ + domain: uri.asciiHost, + name: details.name, + path: uri.path + }); + } + }; +})(); diff --git a/js/vapi-core.js b/js/vapi-core.js new file mode 100644 index 0000000..0c907f7 --- /dev/null +++ b/js/vapi-core.js @@ -0,0 +1,131 @@ +/******************************************************************************* + + ηMatrix - a browser extension to black/white list requests. + Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors + Copyright (C) 2019 Alessio Vanni + + 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://gitlab.com/vannilla/ematrix + uMatrix Home: https://github.com/gorhill/uMatrix +*/ + +'use strict'; + +/******************************************************************************/ + +(function (self) { + vAPI.modernFirefox = + Services.appinfo.ID === '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}' + && Services.vc.compare(Services.appinfo.version, '44') > 0; + + vAPI.app = { + name: 'eMatrix', + version: location.hash.slice(1), + + start: function () { + return; + }, + stop: function () { + return; + }, + restart: function () { + Cc['@mozilla.org/childprocessmessagemanager;1'] + .getService(Ci.nsIMessageSender) + .sendAsyncMessage(location.host + '-restart'); + }, + }; + + // List of things that needs to be destroyed when disabling the extension + // Only functions should be added to it + // eMatrix: taken care by vAPI.addCleanUpTask --- use that function + let cleanupTasks = []; + + // This must be updated manually, every time a new task is added/removed + // eMatrix: do we? + let expectedNumberOfCleanups = 7; + + vAPI.addCleanUpTask = function (task) { + if (typeof task !== 'function') { + return; + } + + cleanupTasks.push(task); + }; + + vAPI.deferUntil = function (testFn, mainFn, details) { + let dtls = (typeof details !== 'object') ? {} : details; + let now = 0; + let next = dtls.next || 200; + let until = dtls.until || 2000; + + let check = function () { + if (testFn() === true || now >= until) { + mainFn(); + return; + } + now += next; + vAPI.setTimeout(check, next); + }; + + if ('sync' in dtls && dtls.sync === true) { + check(); + } else { + vAPI.setTimeout(check, 1); + } + }; + + window.addEventListener('unload', function () { + // if (typeof vAPI.app.onShutdown === 'function') { + // vAPI.app.onShutdown(); + // } + + // IMPORTANT: cleanup tasks must be executed using LIFO order. + for (let i=cleanupTasks.length-1; i>=0; --i) { + try { + cleanupTasks[i](); + } catch (e) { + // Just in case a clean up task ends up throwing for + // no reason + console.error(e); + } + } + + // eMatrix: temporarily disabled + // if (cleanupTasks.length < expectedNumberOfCleanups) { + // console.error + // ('eMatrix> Cleanup tasks performed: %s (out of %s)', + // cleanupTasks.length, + // expectedNumberOfCleanups); + // } + + // frameModule needs to be cleared too + let frameModuleURL = vAPI.getURL('frameModule.js'); + let frameModule = {}; + + Cu.import(frameModuleURL, frameModule); + frameModule.contentObserver.unregister(); + Cu.unload(frameModuleURL); + }); + + vAPI.noTabId = '-1'; + + vAPI.isBehindTheSceneTabId = function (tabId) { + return tabId.toString() === '-1'; + }; + + vAPI.lastError = function () { + return null; + }; +})(this); diff --git a/js/vapi-messaging.js b/js/vapi-messaging.js new file mode 100644 index 0000000..d0a3333 --- /dev/null +++ b/js/vapi-messaging.js @@ -0,0 +1,137 @@ +/******************************************************************************* + + ηMatrix - a browser extension to black/white list requests. + Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors + Copyright (C) 2019 Alessio Vanni + + 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://gitlab.com/vannilla/ematrix + uMatrix Home: https://github.com/gorhill/uMatrix +*/ + +'use strict'; + +/******************************************************************************/ + +(function () { + Cu.import('chrome://ematrix/content/CallbackWrapper.jsm'); + + vAPI.messaging = { + get globalMessageManager() { + return Cc['@mozilla.org/globalmessagemanager;1'] + .getService(Ci.nsIMessageListenerManager); + }, + frameScript: vAPI.getURL('frameScript.js'), + listeners: {}, + defaultHandler: null, + NOOPFUNC: function(){}, + UNHANDLED: 'vAPI.messaging.notHandled' + }; + + vAPI.messaging.listen = function (listenerName, callback) { + this.listeners[listenerName] = callback; + }; + + vAPI.messaging.onMessage = function ({target, data}) { + let messageManager = target.messageManager; + + if (!messageManager) { + // Message came from a popup, and its message manager is + // not usable. So instead we broadcast to the parent + // window. + messageManager = + vAPI.browser. + getOwnerWindow(target.webNavigation + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler).messageManager; + } + + let channelNameRaw = data.channelName; + let pos = channelNameRaw.indexOf('|'); + let channelName = channelNameRaw.slice(pos + 1); + + let callback = vAPI.messaging.NOOPFUNC; + if (data.requestId !== undefined) { + callback = CallbackWrapper.factory(messageManager, + channelName, + channelNameRaw.slice(0, pos), + data.requestId).callback; + } + + let sender = { + tab: { + id: vAPI.tabs.manager.tabIdFromTarget(target) + } + }; + + // Specific handler + let r = vAPI.messaging.UNHANDLED; + let listener = vAPI.messaging.listeners[channelName]; + + if (typeof listener === 'function') { + r = listener(data.msg, sender, callback); + } + if (r !== vAPI.messaging.UNHANDLED) { + return; + } + + // Default handler + r = vAPI.messaging.defaultHandler(data.msg, sender, callback); + if (r !== vAPI.messaging.UNHANDLED) { + return; + } + + console.error('eMatrix> messaging > unknown request: %o', data); + + // Unhandled: Need to callback anyways in case caller expected + // an answer, or else there is a memory leak on caller's side + callback(); + }; + + vAPI.messaging.setup = function (defaultHandler) { + // Already setup? + if (this.defaultHandler !== null) { + return; + } + + if (typeof defaultHandler !== 'function') { + defaultHandler = function () { + return vAPI.messaging.UNHANDLED; + }; + } + + this.defaultHandler = defaultHandler; + this.globalMessageManager.addMessageListener(location.host + + ':background', + this.onMessage); + this.globalMessageManager.loadFrameScript(this.frameScript, true); + + vAPI.addCleanUpTask(function () { + let gmm = vAPI.messaging.globalMessageManager; + + gmm.removeDelayedFrameScript(vAPI.messaging.frameScript); + gmm.removeMessageListener(location.host + ':background', + vAPI.messaging.onMessage); + }); + }; + + vAPI.messaging.broadcast = function (message) { + this.globalMessageManager + .broadcastAsyncMessage(location.host + ':broadcast', + JSON.stringify({ + broadcast: true, + msg: message})); + }; +})(); diff --git a/js/vapi-net.js b/js/vapi-net.js new file mode 100644 index 0000000..7accbc0 --- /dev/null +++ b/js/vapi-net.js @@ -0,0 +1,69 @@ +/******************************************************************************* + + ηMatrix - a browser extension to black/white list requests. + Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors + Copyright (C) 2019 Alessio Vanni + + 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://gitlab.com/vannilla/ematrix + uMatrix Home: https://github.com/gorhill/uMatrix +*/ + +'use strict'; + +/******************************************************************************/ + +(function () { + vAPI.net = {}; + + vAPI.net.registerListeners = function () { + this.onBeforeRequest.types = this.onBeforeRequest.types + ? new Set(this.onBeforeRequest.types) + : null; + + this.onBeforeSendHeaders.types = this.onBeforeSendHeaders.types + ? new Set(this.onBeforeSendHeaders.types) + : null; + + let shouldLoadListenerMessageName = location.host + ':shouldLoad'; + let shouldLoadListener = function (e) { + let details = e.data; + let pendingReq = vAPI.httpObserver.createPendingRequest(details.url); + pendingReq.rawType = details.rawType; + pendingReq.tabId = vAPI.tabs.manager.tabIdFromTarget(e.target); + }; + + // https://github.com/gorhill/uMatrix/issues/200 + // We need this only for Firefox 34 and less: the tab id is derived from + // the origin of the message. + if (!vAPI.modernFirefox) { + vAPI.messaging.globalMessageManager + .addMessageListener(shouldLoadListenerMessageName, + shouldLoadListener); + } + + vAPI.httpObserver.register(); + + vAPI.addCleanUpTask(function () { + if (!vAPI.modernFirefox) { + vAPI.messaging.globalMessageManager + .removeMessageListener(shouldLoadListenerMessageName, + shouldLoadListener); + } + + vAPI.httpObserver.unregister(); + }); + }; +})(); diff --git a/js/vapi-storage.js b/js/vapi-storage.js new file mode 100644 index 0000000..7aa873c --- /dev/null +++ b/js/vapi-storage.js @@ -0,0 +1,330 @@ +/******************************************************************************* + + ηMatrix - a browser extension to black/white list requests. + Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors + Copyright (C) 2019 Alessio Vanni + + 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://gitlab.com/vannilla/ematrix + uMatrix Home: https://github.com/gorhill/uMatrix +*/ + +'use strict'; + +/******************************************************************************/ + +(function () { + // API matches that of chrome.storage.local: + // https://developer.chrome.com/extensions/storage + vAPI.storage = (function () { + let db = null; + let vacuumTimer = null; + + let close = function () { + if (vacuumTimer !== null) { + clearTimeout(vacuumTimer); + vacuumTimer = null; + } + + if (db === null) { + return; + } + + db.asyncClose(); + db = null; + }; + + let open = function () { + if (db !== null) { + return db; + } + + // Create path + let path = Services.dirsvc.get('ProfD', Ci.nsIFile); + path.append('ematrix-data'); + if (!path.exists()) { + path.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt('0774', 8)); + } + if (!path.isDirectory()) { + throw Error('Should be a directory...'); + } + + let path2 = Services.dirsvc.get('ProfD', Ci.nsIFile); + path2.append('extension-data'); + path2.append(location.host + '.sqlite'); + if (path2.exists()) { + path2.moveTo(path, location.host+'.sqlite'); + } + + path.append(location.host + '.sqlite'); + + // Open database + try { + db = Services.storage.openDatabase(path); + if (db.connectionReady === false) { + db.asyncClose(); + db = null; + } + } catch (ex) { + // Ignore + } + + if (db === null) { + return null; + } + + // Database was opened, register cleanup task + vAPI.addCleanUpTask(close); + + // Setup database + db.createAsyncStatement('CREATE TABLE IF NOT EXISTS ' + +'"settings" ("name" ' + +'TEXT PRIMARY KEY NOT NULL, ' + +'"value" TEXT);') + .executeAsync(); + + if (vacuum !== null) { + vacuumTimer = vAPI.setTimeout(vacuum, 60000); + } + + return db; + }; + + // Vacuum only once, and only while idle + let vacuum = function () { + vacuumTimer = null; + if (db === null) { + return; + } + let idleSvc = + Cc['@mozilla.org/widget/idleservice;1'] + .getService(Ci.nsIIdleService); + + if (idleSvc.idleTime < 60000) { + vacuumTimer = vAPI.setTimeout(vacuum, 60000); + return; + } + + db.createAsyncStatement('VACUUM').executeAsync(); + vacuum = null; + }; + + // Execute a query + let runStatement = function (stmt, callback) { + let result = {}; + + stmt.executeAsync({ + handleResult: function (rows) { + if (!rows || typeof callback !== 'function') { + return; + } + + let row; + while ((row = rows.getNextRow())) { + // we assume that there will be two columns, since we're + // using it only for preferences + // eMatrix: the above comment is obsolete + // (it's not used just for preferences + // anymore), but we still expect two columns. + let res = row.getResultByIndex(0); + result[res] = row.getResultByIndex(1); + } + }, + handleCompletion: function (reason) { + if (typeof callback === 'function' && reason === 0) { + callback(result); + } + }, + handleError: function (error) { + console.error('SQLite error ', error.result, error.message); + + // Caller expects an answer regardless of failure. + if (typeof callback === 'function' ) { + callback(null); + } + }, + }); + }; + + let bindNames = function (stmt, names) { + if (Array.isArray(names) === false || names.length === 0) { + return; + } + + let params = stmt.newBindingParamsArray(); + + for (let i=names.length-1; i>=0; --i) { + let bp = params.newBindingParams(); + bp.bindByName('name', names[i]); + params.addParams(bp); + } + + stmt.bindParameters(params); + }; + + let clear = function (callback) { + if (open() === null) { + if (typeof callback === 'function') { + callback(); + } + return; + } + + runStatement(db.createAsyncStatement('DELETE FROM "settings";'), + callback); + }; + + let getBytesInUse = function (keys, callback) { + if (typeof callback !== 'function') { + return; + } + + if (open() === null) { + callback(0); + return; + } + + let stmt; + if (Array.isArray(keys)) { + stmt = db.createAsyncStatement('SELECT "size" AS "size", ' + +'SUM(LENGTH("value")) ' + +'FROM "settings" WHERE ' + +'"name" = :name'); + bindNames(keys); + } else { + stmt = db.createAsyncStatement('SELECT "size" AS "size", ' + +'SUM(LENGTH("value")) ' + +'FROM "settings"'); + } + + runStatement(stmt, function (result) { + callback(result.size); + }); + }; + + let read = function (details, callback) { + if (typeof callback !== 'function') { + return; + } + + let prepareResult = function (result) { + for (let key in result) { + if (result.hasOwnProperty(key) === false) { + continue; + } + + result[key] = JSON.parse(result[key]); + } + + if (typeof details === 'object' && details !== null) { + for (let key in details) { + if (result.hasOwnProperty(key) === false) { + result[key] = details[key]; + } + } + } + + callback(result); + }; + + if (open() === null) { + prepareResult({}); + return; + } + + let names = []; + if (details !== null) { + if (Array.isArray(details)) { + names = details; + } else if (typeof details === 'object') { + names = Object.keys(details); + } else { + names = [details.toString()]; + } + } + + let stmt; + if (names.length === 0) { + stmt = db.createAsyncStatement('SELECT * FROM "settings"'); + } else { + stmt = db.createAsyncStatement('SELECT * FROM "settings" ' + +'WHERE "name" = :name'); + bindNames(stmt, names); + } + + runStatement(stmt, prepareResult); + }; + + let remove = function (keys, callback) { + if (open() === null) { + if (typeof callback === 'function') { + callback(); + } + return; + } + + var stmt = db.createAsyncStatement('DELETE FROM "settings" ' + +'WHERE "name" = :name'); + bindNames(stmt, typeof keys === 'string' ? [keys] : keys); + runStatement(stmt, callback); + }; + + let write = function (details, callback) { + if (open() === null) { + if (typeof callback === 'function') { + callback(); + } + return; + } + + let stmt = db.createAsyncStatement('INSERT OR REPLACE INTO ' + +'"settings" ("name", "value") ' + +'VALUES(:name, :value)'); + let params = stmt.newBindingParamsArray(); + + for (let key in details) { + if (details.hasOwnProperty(key) === false) { + continue; + } + + let bp = params.newBindingParams(); + bp.bindByName('name', key); + bp.bindByName('value', JSON.stringify(details[key])); + params.addParams(bp); + } + + if (params.length === 0) { + return; + } + + stmt.bindParameters(params); + runStatement(stmt, callback); + }; + + // Export API + var api = { + QUOTA_BYTES: 100 * 1024 * 1024, + clear: clear, + get: read, + getBytesInUse: getBytesInUse, + remove: remove, + set: write + }; + + return api; + })(); + + vAPI.cacheStorage = vAPI.storage; +})(); diff --git a/js/vapi-tabs.js b/js/vapi-tabs.js new file mode 100644 index 0000000..d88d184 --- /dev/null +++ b/js/vapi-tabs.js @@ -0,0 +1,724 @@ +/******************************************************************************* + + ηMatrix - a browser extension to black/white list requests. + Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors + Copyright (C) 2019 Alessio Vanni + + 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://gitlab.com/vannilla/ematrix + uMatrix Home: https://github.com/gorhill/uMatrix +*/ + +'use strict'; + +/******************************************************************************/ + +(function () { + vAPI.tabs = {}; + + vAPI.tabs.registerListeners = function() { + vAPI.tabs.manager.start(); + }; + + vAPI.tabs.get = function (tabId, callback) { + // eMatrix: the following might be obsoleted (though probably + // still relevant at least for Pale Moon.) + // + // Firefox: + // + // browser -> ownerDocument -> defaultView -> gBrowser -> browsers --+ + // ^ | + // | | + // +--------------------------------------------------------------+ + // + // browser (browser) + // contentTitle + // currentURI + // ownerDocument (XULDocument) + // defaultView (ChromeWindow) + // gBrowser (tabbrowser OR browser) + // browsers (browser) + // selectedBrowser + // selectedTab + // tabs (tab.tabbrowser-tab) + // + // Fennec: (what I figured so far) + // + // tab -> browser windows -> window -> BrowserApp -> tabs --+ + // ^ window | + // | | + // +-----------------------------------------------------------+ + // + // tab + // browser + // [manual search to go back to tab from list of windows] + let browser; + + if (tabId === null) { + browser = vAPI.tabs.manager.currentBrowser(); + tabId = vAPI.tabs.manager.tabIdFromTarget(browser); + } else { + browser = vAPI.tabs.manager.browserFromTabId(tabId); + } + + // For internal use + if (typeof callback !== 'function') { + return browser; + } + + if (!browser || !browser.currentURI) { + callback(); + return; + } + + let win = vAPI.browser.getOwnerWindow(browser); + let tabBrowser = vAPI.browser.getTabBrowser(win); + + callback({ + id: tabId, + windowId: vAPI.window.idFromWindow(win), + active: tabBrowser !== null + && browser === tabBrowser.selectedBrowser, + url: browser.currentURI.asciiSpec, + title: browser.contentTitle + }); + }; + + vAPI.tabs.getAllSync = function (window) { + let win; + let tabs = []; + + for (let win of vAPI.window.getWindows()) { + if (window && window !== win) { + continue; + } + + let tabBrowser = vAPI.browser.getTabBrowser(win); + if (tabBrowser === null) { + continue; + } + + // This can happens if a tab-less window is currently opened. + // Example of a tab-less window: one opened from clicking + // "View Page Source". + if (!tabBrowser.tabs) { + continue; + } + + for (let tab of tabBrowser.tabs) { + tabs.push(tab); + } + } + + return tabs; + }; + + vAPI.tabs.getAll = function (callback) { + let tabs = []; + + for (let browser of vAPI.tabs.manager.browsers()) { + let tab = vAPI.tabs.manager.tabFromBrowser(browser); + + if (tab === null) { + continue; + } + + if (tab.hasAttribute('pending')) { + continue; + } + + tabs.push({ + id: vAPI.tabs.manager.tabIdFromTarget(browser), + url: browser.currentURI.asciiSpec + }); + } + + callback(tabs); + }; + + vAPI.tabs.open = function (details) { + // properties of the details object: + // + url - the address that will be opened + // + tabId:- the tab is used if set, instead of creating a new one + // + index: - undefined: end of the list, -1: following tab, or + // after index + // + active: - opens the tab in background - true and undefined: + // foreground + // + select: - if a tab is already opened with that url, then + // select it instead of opening a new one + if (!details.url) { + return null; + } + + // extension pages + if (/^[\w-]{2,}:/.test(details.url) === false) { + details.url = vAPI.getURL(details.url); + } + + if (details.select) { + let URI = Services.io.newURI(details.url, null, null); + + for (let tab of this.getAllSync()) { + let browser = vAPI.tabs.manager.browserFromTarget(tab); + + // https://github.com/gorhill/uBlock/issues/2558 + if (browser === null) { + continue; + } + + // Or simply .equals if we care about the fragment + if (URI.equalsExceptRef(browser.currentURI) === false) { + continue; + } + + this.select(tab); + + // Update URL if fragment is different + if (URI.equals(browser.currentURI) === false) { + browser.loadURI(URI.asciiSpec); + } + + return; + } + } + + if (details.active === undefined) { + details.active = true; + } + + if (details.tabId) { + let tab = vAPI.tabs.manager.browserFromTabId(details.tabId); + + if (tab) { + vAPI.tabs.manager.browserFromTarget(tab).loadURI(details.url); + return; + } + } + + // Open in a standalone window + if (details.popup === true) { + Services.ww.openWindow(self, + details.url, + null, + 'location=1,menubar=1,personalbar=1,' + +'resizable=1,toolbar=1', + null); + return; + } + + let win = vAPI.window.getCurrentWindow(); + let tabBrowser = vAPI.browser.getTabBrowser(win); + + if (tabBrowser === null) { + return; + } + + if (details.index === -1) { + details.index = + tabBrowser.browsers.indexOf(tabBrowser.selectedBrowser) + 1; + } + + let tab = tabBrowser.loadOneTab(details.url, { + inBackground: !details.active + }); + + if (details.index !== undefined) { + tabBrowser.moveTabTo(tab, details.index); + } + }; + + vAPI.tabs.replace = function (tabId, url) { + // Replace the URL of a tab. Noop if the tab does not exist. + let targetURL = url; + + // extension pages + if (/^[\w-]{2,}:/.test(targetURL) !== true) { + targetURL = vAPI.getURL(targetURL); + } + + let browser = vAPI.tabs.manager.browserFromTabId(tabId); + if (browser) { + browser.loadURI(targetURL); + } + }; + + function removeInternal(tab, tabBrowser) { + if (tabBrowser) { + tabBrowser.removeTab(tab); + } + } + + vAPI.tabs.remove = function (tabId) { + let browser = vAPI.tabs.manager.browserFromTabId(tabId); + if (!browser) { + return; + } + + let tab = vAPI.tabs.manager.tabFromBrowser(browser); + if (!tab) { + return; + } + + removeInternal(tab, + vAPI.browser.getTabBrowser + (vAPI.browser.getOwnerWindow(browser))); + }; + + vAPI.tabs.reload = function (tabId) { + let browser = vAPI.tabs.manager.browserFromTabId(tabId); + if (!browser) { + return; + } + + browser.webNavigation.reload(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE); + }; + + vAPI.tabs.select = function (tab) { + if (typeof tab !== 'object') { + tab = vAPI.tabs.manager + .tabFromBrowser(vAPI.tabs.manager.browserFromTabId(tab)); + } + if (!tab) { + return; + } + + // https://github.com/gorhill/uBlock/issues/470 + let win = vAPI.browser.getOwnerWindow(tab); + win.focus(); + + let tabBrowser = vAPI.browser.getTabBrowser(win); + if (tabBrowser) { + tabBrowser.selectedTab = tab; + } + }; + + vAPI.tabs.injectScript = function (tabId, details, callback) { + let browser = vAPI.tabs.manager.browserFromTabId(tabId); + if (!browser) { + return; + } + + if (typeof details.file !== 'string') { + return; + } + + details.file = vAPI.getURL(details.file); + browser.messageManager.sendAsyncMessage(location.host + ':broadcast', + JSON.stringify({ + broadcast: true, + channelName: 'vAPI', + msg: { + cmd: 'injectScript', + details: details + } + })); + + if (typeof callback === 'function') { + vAPI.setTimeout(callback, 13); + } + }; + + vAPI.tabs.manager = (function () { + // TODO: find out whether we need a janitor to take care of stale entries. + + // https://github.com/gorhill/uMatrix/issues/540 + // Use only weak references to hold onto browser references. + let browserToTabIdMap = new WeakMap(); + let tabIdToBrowserMap = new Map(); + let tabIdGenerator = 1; + + let indexFromBrowser = function (browser) { + if (!browser) { + return -1; + } + + let win = vAPI.browser.getOwnerWindow(browser); + if (!win) { + return -1; + } + + let tabBrowser = vAPI.browser.getTabBrowser(win); + if (tabBrowser === null) { + return -1; + } + + // This can happen, for example, the `view-source:` + // window, there is no tabbrowser object, the browser + // object sits directly in the window. + if (tabBrowser === browser) { + return 0; + } + + return tabBrowser.browsers.indexOf(browser); + }; + + let indexFromTarget = function (target) { + return indexFromBrowser(browserFromTarget(target)); + }; + + let tabFromBrowser = function (browser) { + let i = indexFromBrowser(browser); + if (i === -1) { + return null; + } + + let win = vAPI.browser.getOwnerWindow(browser); + if (!win) { + return null; + } + + let tabBrowser = vAPI.browser.getTabBrowser(win); + if (tabBrowser === null) { + return null; + } + + if (!tabBrowser.tabs || i >= tabBrowser.tabs.length) { + return null; + } + + return tabBrowser.tabs[i]; + }; + + let browserFromTarget = function (target) { + if (!target) { + return null; + } + + if (target.linkedPanel) { + // target is a tab + target = target.linkedBrowser; + } + + if (target.localName !== 'browser') { + return null; + } + + return target; + }; + + let tabIdFromTarget = function (target) { + let browser = browserFromTarget(target); + if (browser === null) { + return vAPI.noTabId; + } + + let tabId = browserToTabIdMap.get(browser); + if (tabId === undefined) { + tabId = '' + tabIdGenerator++; + browserToTabIdMap.set(browser, tabId); + tabIdToBrowserMap.set(tabId, Cu.getWeakReference(browser)); + } + + return tabId; + }; + + let browserFromTabId = function (tabId) { + let weakref = tabIdToBrowserMap.get(tabId); + let browser = weakref && weakref.get(); + + return browser || null; + }; + + let currentBrowser = function () { + let win = vAPI.window.getCurrentWindow(); + + // https://github.com/gorhill/uBlock/issues/399 + // getTabBrowser() can return null at browser launch time. + let tabBrowser = vAPI.browser.getTabBrowser(win); + if (tabBrowser === null) { + return null; + } + + return browserFromTarget(tabBrowser.selectedTab); + }; + + let removeBrowserEntry = function (tabId, browser) { + if (tabId && tabId !== vAPI.noTabId) { + vAPI.tabs.onClosed(tabId); + delete vAPI.toolbarButton.tabs[tabId]; + tabIdToBrowserMap.delete(tabId); + } + + if (browser) { + browserToTabIdMap.delete(browser); + } + }; + + let removeTarget = function (target) { + onClose({ + target: target + }); + }; + + let getAllBrowsers = function () { + let browsers = []; + + for (let [tabId, weakref] of tabIdToBrowserMap) { + let browser = weakref.get(); + + // TODO: Maybe call removeBrowserEntry() if the + // browser no longer exists? + if (browser) { + browsers.push(browser); + } + } + + return browsers; + }; + + // var onOpen = function (target) { + // var tabId = tabIdFromTarget(target); + // var browser = browserFromTabId(tabId); + // vAPI.tabs.onNavigation({ + // frameId: 0, + // tabId: tabId, + // url: browser.currentURI.asciiSpec, + // }); + // }; + + var onShow = function ({target}) { + tabIdFromTarget(target); + }; + + var onClose = function ({target}) { + // target is tab in Firefox, browser in Fennec + let browser = browserFromTarget(target); + let tabId = browserToTabIdMap.get(browser); + removeBrowserEntry(tabId, browser); + }; + + var onSelect = function ({target}) { + // This is an entry point: when creating a new tab, it is + // not always reported through onLocationChanged... + // Sigh. It is "reported" here however. + let browser = browserFromTarget(target); + let tabId = browserToTabIdMap.get(browser); + + if (tabId === undefined) { + tabId = tabIdFromTarget(target); + vAPI.tabs.onNavigation({ + frameId: 0, + tabId: tabId, + url: browser.currentURI.asciiSpec + }); + } + + vAPI.setIcon(tabId, vAPI.browser.getOwnerWindow(target)); + }; + + let locationChangedMessageName = location.host + ':locationChanged'; + + let onLocationChanged = function (e) { + let details = e.data; + + // Ignore notifications related to our popup + if (details.url.lastIndexOf(vAPI.getURL('popup.html'), 0) === 0) { + return; + } + + let browser = e.target; + let tabId = tabIdFromTarget(browser); + if (tabId === vAPI.noTabId) { + return; + } + + // LOCATION_CHANGE_SAME_DOCUMENT = "did not load a new document" + if (details.flags + & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) { + vAPI.tabs.onUpdated(tabId, {url: details.url}, { + frameId: 0, + tabId: tabId, + url: browser.currentURI.asciiSpec + }); + return; + } + + // https://github.com/chrisaljoudi/uBlock/issues/105 + // Allow any kind of pages + vAPI.tabs.onNavigation({ + frameId: 0, + tabId: tabId, + url: details.url + }); + }; + + let attachToTabBrowser = function (window) { + if (typeof vAPI.toolbarButton.attachToNewWindow === 'function') { + vAPI.toolbarButton.attachToNewWindow(window); + } + + let tabBrowser = vAPI.browser.getTabBrowser(window); + if (tabBrowser === null) { + return; + } + + let tabContainer; + if (tabBrowser.deck) { + // Fennec + tabContainer = tabBrowser.deck; + } else if (tabBrowser.tabContainer) { + // Firefox + tabContainer = tabBrowser.tabContainer; + } + + // https://github.com/gorhill/uBlock/issues/697 + // Ignore `TabShow` events: unfortunately the `pending` + // attribute is not set when a tab is opened as a result + // of session restore -- it is set *after* the event is + // fired in such case. + if (tabContainer) { + tabContainer.addEventListener('TabShow', onShow); + tabContainer.addEventListener('TabClose', onClose); + // when new window is opened TabSelect doesn't run on + // the selected tab? + tabContainer.addEventListener('TabSelect', onSelect); + } + }; + + var canAttachToTabBrowser = function (window) { + // https://github.com/gorhill/uBlock/issues/906 + // Ensure the environment is ready before trying to attaching. + let document = window && window.document; + if (!document || document.readyState !== 'complete') { + return false; + } + + // On some platforms, the tab browser isn't immediately + // available, try waiting a bit if this + // https://github.com/gorhill/uBlock/issues/763 + // Not getting a tab browser should not prevent from + // attaching ourself to the window. + let tabBrowser = vAPI.browser.getTabBrowser(window); + if (tabBrowser === null) { + return false; + } + + return vAPI.window.toBrowserWindow(window) !== null; + }; + + let onWindowLoad = function (win) { + vAPI.deferUntil(canAttachToTabBrowser.bind(null, win), + attachToTabBrowser.bind(null, win)); + }; + + let onWindowUnload = function (win) { + let tabBrowser = vAPI.browser.getTabBrowser(win); + if (tabBrowser === null) { + return; + } + + let tabContainer = tabBrowser.tabContainer; + if (tabContainer) { + tabContainer.removeEventListener('TabShow', onShow); + tabContainer.removeEventListener('TabClose', onClose); + tabContainer.removeEventListener('TabSelect', onSelect); + } + + // https://github.com/gorhill/uBlock/issues/574 + // To keep in mind: not all windows are tab containers, + // sometimes the window IS the tab. + let tabs; + if (tabBrowser.tabs) { + tabs = tabBrowser.tabs; + } else if (tabBrowser.localName === 'browser') { + tabs = [tabBrowser]; + } else { + tabs = []; + } + + let browser; + let URI; + let tabId; + for (let i=tabs.length-1; i>=0; --i) { + let tab = tabs[i]; + browser = browserFromTarget(tab); + if (browser === null) { + continue; + } + + URI = browser.currentURI; + // Close extension tabs + if (URI.schemeIs('chrome') && URI.host === location.host) { + removeInternal(tab, vAPI.browser.getTabBrowser(win)); + } + + tabId = browserToTabIdMap.get(browser); + if (tabId !== undefined) { + removeBrowserEntry(tabId, browser); + tabIdToBrowserMap.delete(tabId); + } + browserToTabIdMap.delete(browser); + } + }; + + var start = function () { + // Initialize map with existing active tabs + let tabBrowser; + let tabs; + + for (let win of vAPI.window.getWindows()) { + onWindowLoad(win); + + tabBrowser = vAPI.browser.getTabBrowser(win); + if (tabBrowser === null) { + continue; + } + + for (let tab of tabBrowser.tabs) { + if (!tab.hasAttribute('pending')) { + tabIdFromTarget(tab); + } + } + } + + vAPI.window.onOpenWindow = onWindowLoad; + vAPI.window.onCloseWindow = onWindowUnload; + + vAPI.messaging.globalMessageManager + .addMessageListener(locationChangedMessageName, + onLocationChanged); + }; + + let stop = function () { + vAPI.window.onOpenWindow = null; + vAPI.window.onCloseWindow = null; + + vAPI.messaging.globalMessageManager + .removeMessageListener(locationChangedMessageName, + onLocationChanged); + + for (let win of vAPI.window.getWindows()) { + onWindowUnload(win); + } + + browserToTabIdMap = new WeakMap(); + tabIdToBrowserMap.clear(); + }; + + vAPI.addCleanUpTask(stop); + + return { + browsers: getAllBrowsers, + browserFromTabId: browserFromTabId, + browserFromTarget: browserFromTarget, + currentBrowser: currentBrowser, + indexFromTarget: indexFromTarget, + removeTarget: removeTarget, + start: start, + tabFromBrowser: tabFromBrowser, + tabIdFromTarget: tabIdFromTarget + }; + })(); +})(); diff --git a/js/vapi-window.js b/js/vapi-window.js new file mode 100644 index 0000000..e01dc7a --- /dev/null +++ b/js/vapi-window.js @@ -0,0 +1,183 @@ +/******************************************************************************* + + ηMatrix - a browser extension to black/white list requests. + Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors + Copyright (C) 2019 Alessio Vanni + + 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://gitlab.com/vannilla/ematrix + uMatrix Home: https://github.com/gorhill/uMatrix +*/ + +'use strict'; + +/******************************************************************************/ + +(function () { + vAPI.window = (function () { + let windowToIdMap = new Map(); + let windowIdGenerator = 1; + let api = { + onOpenWindow: null, + onCloseWindow: null + }; + + // https://github.com/gorhill/uMatrix/issues/586 This is + // necessary hack because on SeaMonkey 2.40, for unknown + // reasons private windows do not have the attribute + // `windowtype` set to `navigator:browser`. As a fallback, the + // code here will also test whether the id attribute is + // `main-window`. + api.toBrowserWindow = function (win) { + let docElement = win && win.document + && win.document.documentElement; + + if (!docElement) { + return null; + } + if (vAPI.thunderbird) { + return docElement.getAttribute('windowtype') === 'mail:3pane' + ? win + : null; + } + + return docElement.getAttribute('windowtype') === 'navigator:browser' + || docElement.getAttribute('id') === 'main-window' + ? win + : null; + }; + + api.getWindows = function () { + return windowToIdMap.keys(); + }; + + api.idFromWindow = function (win) { + return windowToIdMap.get(win) || 0; + }; + + api.getCurrentWindow = function () { + return this.toBrowserWindow(Services.wm.getMostRecentWindow(null)); + }; + + let addWindow = function (win) { + if (!win || windowToIdMap.has(win)) { + return; + } + + windowToIdMap.set(win, windowIdGenerator++); + + if (typeof api.onOpenWindow === 'function') { + api.onOpenWindow(win); + } + }; + + let removeWindow = function (win) { + if (!win || windowToIdMap.delete(win) !== true) { + return; + } + + if (typeof api.onCloseWindow === 'function') { + api.onCloseWindow(win); + } + }; + + // https://github.com/gorhill/uMatrix/issues/357 + // Use nsIWindowMediator for being notified of opened/closed windows. + let listeners = { + onOpenWindow: function (aWindow) { + let win; + try { + win = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + } catch (e) { + // Ignore + } + + addWindow(win); + }, + onCloseWindow: function (aWindow) { + let win; + try { + win = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + } catch (e) { + // Ignore + } + + removeWindow(win); + }, + observe: function (aSubject, topic) { + let win; + try { + win = aSubject.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + } catch (e) { + // Ignore + } + + if (!win) { + return; + } + + switch (topic) { + case 'domwindowopened': + addWindow(win); + break; + case 'domwindowclosed': + removeWindow(win); + break; + default: + console.error('unknown observer topic'); + break; + } + } + }; + + (function() { + let winumerator; + + winumerator = Services.wm.getEnumerator(null); + while (winumerator.hasMoreElements()) { + let win = winumerator.getNext(); + + if (!win.closed) { + windowToIdMap.set(win, windowIdGenerator++); + } + } + + winumerator = Services.ww.getWindowEnumerator(); + while (winumerator.hasMoreElements()) { + let win = winumerator.getNext() + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + + if (!win.closed) { + windowToIdMap.set(win, windowIdGenerator++); + } + } + + Services.wm.addListener(listeners); + Services.ww.registerNotification(listeners); + })(); + + vAPI.addCleanUpTask(function() { + Services.wm.removeListener(listeners); + Services.ww.unregisterNotification(listeners); + windowToIdMap.clear(); + }); + + return api; + })(); +})(); |