From ed67045360f334f8a353d1ebf3e4253a943e5f76 Mon Sep 17 00:00:00 2001 From: gorhill Date: Sat, 11 Apr 2015 17:15:57 -0400 Subject: [PATCH] first pass --- .../{icon19.png => icon19-off.png} | Bin .../{icon38.png => icon38-off.png} | Bin platform/chromium/manifest.json | 4 +- platform/chromium/vapi-background.js | 176 ++- platform/chromium/vapi-client.js | 2 +- platform/chromium/vapi-common.js | 4 +- src/about.html | 3 +- src/asset-viewer.html | 2 +- src/background.html | 5 +- src/dashboard.html | 2 + src/hosts-files.html | 3 +- src/info.html | 3 +- src/js/about.js | 22 +- src/js/asset-viewer.js | 8 +- src/js/assets.js | 785 ++++++++++--- src/js/async.js | 8 +- src/js/background.js | 7 +- src/js/commands.js | 4 +- src/js/contentscript-end.js | 167 +-- src/js/contentscript-start.js | 142 +-- src/js/cookies.js | 8 +- src/js/dashboard.js | 6 +- src/js/hosts-files.js | 54 +- src/js/httpsb.js | 20 +- src/js/i18n.js | 4 +- src/js/info.js | 20 +- src/js/messaging-client.js | 141 --- src/js/messaging-handlers.js | 816 ------------- src/js/messaging.js | 1014 ++++++++++++++--- src/js/pagestats.js | 14 +- src/js/popup.js | 49 +- src/js/privacy.js | 12 +- src/js/settings.js | 10 +- src/js/start.js | 86 +- src/js/storage.js | 12 +- src/js/tab.js | 489 +++++++- src/js/traffic.js | 127 +-- src/js/udom.js | 74 +- src/js/uritools.js | 102 +- src/js/user-rules.js | 35 +- src/js/utils.js | 64 +- src/js/xal.js | 49 +- src/popup.html | 3 +- src/privacy.html | 3 +- src/settings.html | 3 +- src/user-rules.html | 3 +- 46 files changed, 2496 insertions(+), 2069 deletions(-) rename platform/chromium/img/browsericons/{icon19.png => icon19-off.png} (100%) rename platform/chromium/img/browsericons/{icon38.png => icon38-off.png} (100%) delete mode 100644 src/js/messaging-client.js delete mode 100644 src/js/messaging-handlers.js diff --git a/platform/chromium/img/browsericons/icon19.png b/platform/chromium/img/browsericons/icon19-off.png similarity index 100% rename from platform/chromium/img/browsericons/icon19.png rename to platform/chromium/img/browsericons/icon19-off.png diff --git a/platform/chromium/img/browsericons/icon38.png b/platform/chromium/img/browsericons/icon38-off.png similarity index 100% rename from platform/chromium/img/browsericons/icon38.png rename to platform/chromium/img/browsericons/icon38-off.png diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index 36310c7..7df8b94 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -10,7 +10,7 @@ }, "browser_action": { "default_icon": { - "19": "img/browsericons/icon19.png" + "19": "img/browsericons/icon19-off.png" }, "default_title": "__MSG_extName__", "default_popup": "popup.html" @@ -52,7 +52,7 @@ "content_scripts": [ { "matches": ["http://*/*", "https://*/*"], - "js": ["js/contentscript-start.js"], + "js": ["js/vapi-client.js", "js/contentscript-start.js"], "run_at": "document_start", "all_frames": true }, diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index 4826689..05795d5 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -1,7 +1,7 @@ /******************************************************************************* - µBlock - a browser extension to block requests. - Copyright (C) 2014 The µBlock authors + µMatrix - a browser extension to block requests. + Copyright (C) 2014 The uBlock authors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,7 +19,7 @@ Home: https://github.com/gorhill/uBlock */ -/* global self, µBlock */ +/* global self, µMatrix */ // For background page @@ -49,6 +49,34 @@ vAPI.app = { /******************************************************************************/ +vAPI.app.start = function() { + // rhill 2013-12-07: + // Relinquish control over javascript execution to the user. + // https://github.com/gorhill/httpswitchboard/issues/74 + chrome.contentSettings.javascript.clear({}); +}; + +/******************************************************************************/ + +vAPI.app.stop = function() { + chrome.contentSettings.javascript.clear({}); + + // rhill 2013-12-07: + // Tell Chromium to allow all javascript: µMatrix will control whether + // javascript execute through `Content-Policy-Directive` and webRequest. + // https://github.com/gorhill/httpswitchboard/issues/74 + chrome.contentSettings.javascript.set({ + primaryPattern: 'https://*/*', + setting: 'allow' + }); + chrome.contentSettings.javascript.set({ + primaryPattern: 'http://*/*', + setting: 'allow' + }); +}; + +/******************************************************************************/ + vAPI.app.restart = function() { chrome.runtime.reload(); }; @@ -114,7 +142,8 @@ vAPI.tabs.registerListeners = function() { if ( popup !== undefined ) { return; } - return popupCandidates[details.tabId] = new PopupCandidate(details); + popup = popupCandidates[details.tabId] = new PopupCandidate(details); + return popup; }; var popupCandidateTest = function(details) { @@ -190,11 +219,12 @@ vAPI.tabs.registerListeners = function() { if ( typeof this.onClosed === 'function' ) { chrome.tabs.onRemoved.addListener(this.onClosed); } - }; /******************************************************************************/ +// tabId: null, // active tab + vAPI.tabs.get = function(tabId, callback) { var onTabReady = function(tab) { // https://code.google.com/p/chromium/issues/detail?id=410868#c8 @@ -223,6 +253,12 @@ vAPI.tabs.get = function(tabId, callback) { /******************************************************************************/ +vAPI.tabs.getAll = function(callback) { + chrome.tabs.query({ url: '' }, callback); +}; + +/******************************************************************************/ + // 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 @@ -356,6 +392,9 @@ vAPI.tabs.reload = function(tabId /*, flags*/) { if ( typeof tabId === 'string' ) { tabId = parseInt(tabId, 10); } + if ( isNaN(tabId) ) { + return; + } chrome.tabs.reload(tabId); }; @@ -387,8 +426,11 @@ vAPI.tabs.injectScript = function(tabId, details, callback) { // Since we may be called asynchronously, the tab id may not exist // anymore, so this ensures it does still exist. -vAPI.setIcon = function(tabId, iconStatus, badge) { +vAPI.setIcon = function(tabId, iconId, badge) { tabId = parseInt(tabId, 10); + if ( isNaN(tabId) || tabId <= 0 ) { + return; + } var onIconReady = function() { if ( vAPI.lastError() ) { return; @@ -397,14 +439,16 @@ vAPI.setIcon = function(tabId, iconStatus, badge) { if ( badge !== '' ) { chrome.browserAction.setBadgeBackgroundColor({ tabId: tabId, - color: '#666' + color: '#000' }); } }; - var iconPaths = iconStatus === 'on' ? - { '19': 'img/browsericons/icon19.png', '38': 'img/browsericons/icon38.png' } : - { '19': 'img/browsericons/icon19-off.png', '38': 'img/browsericons/icon38-off.png' }; + var iconSelector = typeof iconId === 'number' ? iconId : 'off'; + var iconPaths = { + '19': 'img/browsericons/icon19-' + iconSelector + '.png'/* , + '38': 'img/browsericons/icon38-' + iconSelector + '.png' */ + }; chrome.browserAction.setIcon({ tabId: tabId, path: iconPaths }, onIconReady); }; @@ -449,7 +493,7 @@ vAPI.messaging.onPortMessage = function(request, port) { return; } - console.error('µBlock> messaging > unknown request: %o', request); + console.error('µMatrix> messaging > unknown request: %o', request); // Unhandled: // Need to callback anyways in case caller expected an answer, or @@ -568,21 +612,21 @@ vAPI.net = {}; /******************************************************************************/ vAPI.net.registerListeners = function() { - var µb = µBlock; - var µburi = µb.URI; + var µm = µMatrix; + var µmuri = µm.URI; var normalizeRequestDetails = function(details) { - µburi.set(details.url); + µmuri.set(details.url); details.tabId = details.tabId.toString(); - details.hostname = µburi.hostnameFromURI(details.url); + details.hostname = µmuri.hostnameFromURI(details.url); // The rest of the function code is to normalize type if ( details.type !== 'other' ) { return; } - var tail = µburi.path.slice(-6); + var tail = µmuri.path.slice(-6); var pos = tail.lastIndexOf('.'); // https://github.com/chrisaljoudi/uBlock/issues/862 @@ -628,6 +672,20 @@ vAPI.net.registerListeners = function() { this.onBeforeRequest.extra ); + var onBeforeSendHeadersClient = this.onBeforeSendHeaders.callback; + var onBeforeSendHeaders = function(details) { + normalizeRequestDetails(details); + return onBeforeSendHeadersClient(details); + }; + chrome.webRequest.onBeforeSendHeaders.addListener( + onBeforeSendHeaders, + { + 'urls': this.onBeforeSendHeaders.urls || [''], + 'types': this.onBeforeSendHeaders.types || [] + }, + this.onBeforeSendHeaders.extra + ); + var onHeadersReceivedClient = this.onHeadersReceived.callback; var onHeadersReceived = function(details) { normalizeRequestDetails(details); @@ -641,6 +699,13 @@ vAPI.net.registerListeners = function() { }, this.onHeadersReceived.extra ); + + chrome.webRequest.onErrorOccurred.addListener( + this.onErrorOccurred.callback, + { + 'urls': this.onErrorOccurred.urls || [''] + } + ); }; /******************************************************************************/ @@ -672,48 +737,6 @@ vAPI.lastError = function() { // the web pages before uBlock was ready. vAPI.onLoadAllCompleted = function() { - // http://code.google.com/p/chromium/issues/detail?id=410868#c11 - // Need to be sure to access `vAPI.lastError()` to prevent - // spurious warnings in the console. - var scriptDone = function() { - vAPI.lastError(); - }; - var scriptEnd = function(tabId) { - if ( vAPI.lastError() ) { - return; - } - vAPI.tabs.injectScript(tabId, { - file: 'js/contentscript-end.js', - allFrames: true, - runAt: 'document_idle' - }, scriptDone); - }; - var scriptStart = function(tabId) { - vAPI.tabs.injectScript(tabId, { - file: 'js/vapi-client.js', - allFrames: true, - runAt: 'document_start' - }, function(){ }); - vAPI.tabs.injectScript(tabId, { - file: 'js/contentscript-start.js', - allFrames: true, - runAt: 'document_start' - }, function(){ scriptEnd(tabId); }); - }; - var bindToTabs = function(tabs) { - var µb = µBlock; - var i = tabs.length, tab; - while ( i-- ) { - tab = tabs[i]; - µb.tabContextManager.commit(tab.id, tab.url); - µb.bindTabToPageStats(tab.id); - // https://github.com/chrisaljoudi/uBlock/issues/129 - scriptStart(tab.id); - } - }; - - chrome.tabs.query({ url: 'http://*/*' }, bindToTabs); - chrome.tabs.query({ url: 'https://*/*' }, bindToTabs); }; /******************************************************************************/ @@ -728,6 +751,43 @@ vAPI.punycodeURL = function(url) { /******************************************************************************/ +vAPI.browserCache = {}; + +/******************************************************************************/ + +vAPI.browserCache.clearByTime = function(since) { + chrome.browsingData.removeCache({ since: 0 }); +}; + +vAPI.browserCache.clearByOrigin = function(/* domain */) { + // unsupported on Chromium +}; + +/******************************************************************************/ + +vAPI.cookies = {}; + +/******************************************************************************/ + +vAPI.cookies.registerListeners = function() { + if ( typeof this.onChanged === 'function' ) { + chrome.cookies.onChanged.addListener(this.onChanged); + } +}; + +/******************************************************************************/ + +vAPI.cookies.getAll = function(callback) { + chrome.cookies.getAll({}, callback); +}; + +/******************************************************************************/ + +vAPI.cookies.remove = function(details, callback) { + chrome.cookies.remove(details, callback || noopFunc); +}; +/******************************************************************************/ + })(); /******************************************************************************/ diff --git a/platform/chromium/vapi-client.js b/platform/chromium/vapi-client.js index eafc1a0..2d03803 100644 --- a/platform/chromium/vapi-client.js +++ b/platform/chromium/vapi-client.js @@ -38,8 +38,8 @@ if ( vAPI.vapiClientInjected ) { //console.debug('vapi-client.js already injected: skipping.'); return; } - vAPI.vapiClientInjected = true; + vAPI.sessionId = String.fromCharCode(Date.now() % 25 + 97) + Math.random().toString(36).slice(2); vAPI.chrome = true; diff --git a/platform/chromium/vapi-common.js b/platform/chromium/vapi-common.js index d67f419..85d4651 100644 --- a/platform/chromium/vapi-common.js +++ b/platform/chromium/vapi-common.js @@ -1,6 +1,6 @@ /******************************************************************************* - µBlock - a browser extension to block requests. + µMatrix - a browser extension to block requests. Copyright (C) 2014 The µBlock authors This program is free software: you can redistribute it and/or modify @@ -16,7 +16,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see {http://www.gnu.org/licenses/}. - Home: https://github.com/gorhill/uBlock + Home: https://github.com/gorhill/uMatrix */ // For background page or non-background pages diff --git a/src/about.html b/src/about.html index 9bb9281..6370067 100644 --- a/src/about.html +++ b/src/about.html @@ -47,10 +47,11 @@ ul { + + - diff --git a/src/asset-viewer.html b/src/asset-viewer.html index c4a0f4c..bca8c9e 100644 --- a/src/asset-viewer.html +++ b/src/asset-viewer.html @@ -12,8 +12,8 @@
+ - diff --git a/src/background.html b/src/background.html index cc02035..adee2b0 100644 --- a/src/background.html +++ b/src/background.html @@ -8,6 +8,8 @@ + + @@ -25,11 +27,10 @@ - + - diff --git a/src/dashboard.html b/src/dashboard.html index 4ab3b3b..e3c3f73 100644 --- a/src/dashboard.html +++ b/src/dashboard.html @@ -90,6 +90,8 @@ iframe { + + diff --git a/src/hosts-files.html b/src/hosts-files.html index 593b103..55f00c5 100644 --- a/src/hosts-files.html +++ b/src/hosts-files.html @@ -29,10 +29,11 @@
+ + - diff --git a/src/info.html b/src/info.html index 10f0a2e..230cccb 100644 --- a/src/info.html +++ b/src/info.html @@ -109,10 +109,11 @@ + + - diff --git a/src/js/about.js b/src/js/about.js index 4a14f67..7becd81 100644 --- a/src/js/about.js +++ b/src/js/about.js @@ -19,24 +19,29 @@ Home: https://github.com/gorhill/uMatrix */ -/* global chrome, messaging, uDom */ +/* global vAPI, uDom */ /******************************************************************************/ uDom.onLoad(function() { +'use strict'; + +/******************************************************************************/ + +var messager = vAPI.messaging.channel('about.js'); + /******************************************************************************/ var backupUserDataToFile = function() { var userDataReady = function(userData) { - chrome.downloads.download({ + vAPI.download({ 'url': 'data:text/plain,' + encodeURIComponent(JSON.stringify(userData)), - 'filename': uDom('[data-i18n="aboutBackupFilename"]').text(), - 'saveAs': true + 'filename': uDom('[data-i18n="aboutBackupFilename"]').text() }); }; - messaging.ask({ what: 'getAllUserData' }, userDataReady); + messager.send({ what: 'getAllUserData' }, userDataReady); }; /******************************************************************************/ @@ -75,7 +80,7 @@ function restoreUserDataFromFile() { .replace('{{time}}', time.toLocaleString()); var proceed = window.confirm(msg); if ( proceed ) { - messaging.tell({ what: 'restoreAllUserData', userData: userData }); + messager.send({ what: 'restoreAllUserData', userData: userData }); } }; @@ -107,7 +112,7 @@ var startRestoreFilePicker = function() { var resetUserData = function() { var proceed = window.confirm(uDom('[data-i18n="aboutResetConfirm"]').text()); if ( proceed ) { - messaging.tell({ what: 'resetAllUserData' }); + messager.send({ what: 'resetAllUserData' }); } }; @@ -123,8 +128,7 @@ var resetUserData = function() { } uDom('#aboutStorageUsed').html(template.replace('{{storageUsed}}', storageUsed)); }; - messaging.start('about.js'); - messaging.ask({ what: 'getSomeStats' }, renderStats); + messager.send({ what: 'getSomeStats' }, renderStats); })(); /******************************************************************************/ diff --git a/src/js/asset-viewer.js b/src/js/asset-viewer.js index de15294..aaeda50 100644 --- a/src/js/asset-viewer.js +++ b/src/js/asset-viewer.js @@ -19,15 +19,17 @@ Home: https://github.com/gorhill/uMatrix */ -/* global chrome, messaging, uDom */ +/* global vAPI, uDom */ /******************************************************************************/ (function() { +'use strict'; + /******************************************************************************/ -messaging.start('asset-viewer.js'); +var messager = vAPI.messaging.channel('asset-viewer.js'); /******************************************************************************/ @@ -43,7 +45,7 @@ if ( !matches || matches.length !== 2 ) { return; } -messaging.ask({ what : 'getAssetContent', url: matches[1] }, onAssetContentReceived); +messager.send({ what : 'getAssetContent', url: matches[1] }, onAssetContentReceived); /******************************************************************************/ diff --git a/src/js/assets.js b/src/js/assets.js index 0c319c7..16b4127 100644 --- a/src/js/assets.js +++ b/src/js/assets.js @@ -1,7 +1,7 @@ /******************************************************************************* - µMatrix - a Chromium browser extension to black/white list requests. - Copyright (C) 2013 Raymond Hill + µMatrix - a browser extension to black/white list requests. + Copyright (C) 2013-2015 Raymond Hill This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,7 +19,22 @@ Home: https://github.com/gorhill/uMatrix */ -/* global chrome, µMatrix */ +/* global vAPI, µMatrix, YaMD5 */ +/* jshint boss: true */ + +/******************************************************************************* + +File system structure: + assets + umatrix + ... + thirdparties + ... + user + filters.txt + ... + +*/ /******************************************************************************/ @@ -27,6 +42,8 @@ µMatrix.assets = (function() { +'use strict'; + /******************************************************************************/ var oneSecond = 1000; @@ -36,19 +53,22 @@ var oneDay = 24 * oneHour; /******************************************************************************/ -var repositoryRoot = µMatrix.projectServerRoot; +var projectRepositoryRoot = µMatrix.projectServerRoot; var nullFunc = function() {}; -var reIsExternalPath = /^https?:\/\/[a-z0-9]/; +var reIsExternalPath = /^[a-z]+:\/\//; var reIsUserPath = /^assets\/user\//; +var reIsCachePath = /^cache:\/\//; var lastRepoMetaTimestamp = 0; +var lastRepoMetaIsRemote = false; var refreshRepoMetaPeriod = 5 * oneHour; - -// TODO: move chrome.i18n.getMessage to vAPI -var errorCantConnectTo = chrome.i18n.getMessage('errorCantConnectTo'); +var errorCantConnectTo = vAPI.i18n('errorCantConnectTo'); var exports = { autoUpdate: true, - autoUpdateDelay: 4 * oneDay + autoUpdateDelay: 4 * oneDay, + + // https://github.com/chrisaljoudi/uBlock/issues/426 + remoteFetchBarrier: 0 }; /******************************************************************************/ @@ -57,7 +77,6 @@ var AssetEntry = function() { this.localChecksum = ''; this.repoChecksum = ''; this.expireTimestamp = 0; - this.homeURL = ''; }; var RepoMetadata = function() { @@ -67,6 +86,21 @@ var RepoMetadata = function() { var repoMetadata = null; +// We need these to persist beyond repoMetaData +var homeURLs = {}; + +/******************************************************************************/ + +var stringIsNotEmpty = function(s) { + return typeof s === 'string' && s !== ''; +}; + +/******************************************************************************/ + +var cacheIsObsolete = function(t) { + return typeof t !== 'number' || (Date.now() - t) >= exports.autoUpdateDelay; +}; + /******************************************************************************/ var cachedAssetsManager = (function() { @@ -82,31 +116,30 @@ var cachedAssetsManager = (function() { // Flush cached non-user assets if these are from a prior version. // https://github.com/gorhill/httpswitchboard/issues/212 var onLastVersionRead = function(store) { - var currentVersion = chrome.runtime.getManifest().version; + var currentVersion = vAPI.app.version; var lastVersion = store.extensionLastVersion || '0.0.0.0'; if ( currentVersion !== lastVersion ) { - chrome.storage.local.set({ 'extensionLastVersion': currentVersion }); - exports.remove(/^assets\/(umatrix|thirdparties)\//); - exports.remove('assets/checksums.txt'); + vAPI.storage.set({ 'extensionLastVersion': currentVersion }); } callback(entries); }; var onLoaded = function(bin) { // https://github.com/gorhill/httpswitchboard/issues/381 - // Maybe the index was requested multiple times and already + // Maybe the index was requested multiple times and already // fetched by one of the occurrences. if ( entries === null ) { - if ( chrome.runtime.lastError ) { + var lastError = vAPI.lastError(); + if ( lastError ) { console.error( - 'assets.js > cachedAssetsManager> getEntries():', - chrome.runtime.lastError.message + 'µMatrix> cachedAssetsManager> getEntries():', + lastError.message ); } entries = bin.cached_asset_entries || {}; } - chrome.storage.local.get('extensionLastVersion', onLastVersionRead); + vAPI.storage.get('extensionLastVersion', onLastVersionRead); }; - chrome.storage.local.get('cached_asset_entries', onLoaded); + vAPI.storage.get('cached_asset_entries', onLoaded); }; exports.entries = getEntries; @@ -119,14 +152,25 @@ var cachedAssetsManager = (function() { }; var cachedContentPath = cachedAssetPathPrefix + path; var onLoaded = function(bin) { - if ( chrome.runtime.lastError ) { - details.error = 'Error: ' + chrome.runtime.lastError.message; - console.error('assets.js > cachedAssetsManager.load():', details.error); + var lastError = vAPI.lastError(); + if ( lastError ) { + details.error = 'Error: ' + lastError.message; + console.error('µMatrix> cachedAssetsManager.load():', details.error); cbError(details); - } else { - details.content = bin[cachedContentPath]; - cbSuccess(details); + return; } + // Not sure how this can happen, but I've seen it happen. It could + // be because the save occurred while I was stepping in the code + // though, which means it would not occur during normal operation. + // Still, just to be safe. + if ( stringIsNotEmpty(bin[cachedContentPath]) === false ) { + exports.remove(path); + details.error = 'Error: not found'; + cbError(details); + return; + } + details.content = bin[cachedContentPath]; + cbSuccess(details); }; var onEntries = function(entries) { if ( entries[path] === undefined ) { @@ -134,7 +178,7 @@ var cachedAssetsManager = (function() { cbError(details); return; } - chrome.storage.local.get(cachedContentPath, onLoaded); + vAPI.storage.get(cachedContentPath, onLoaded); }; getEntries(onEntries); }; @@ -146,22 +190,37 @@ var cachedAssetsManager = (function() { path: path, content: content }; + if ( content === '' ) { + exports.remove(path); + cbSuccess(details); + return; + } var cachedContentPath = cachedAssetPathPrefix + path; var bin = {}; bin[cachedContentPath] = content; + var removedItems = []; var onSaved = function() { - if ( chrome.runtime.lastError ) { - details.error = 'Error: ' + chrome.runtime.lastError.message; - console.error('assets.js > cachedAssetsManager.save():', details.error); + var lastError = vAPI.lastError(); + if ( lastError ) { + details.error = 'Error: ' + lastError.message; + console.error('µMatrix> cachedAssetsManager.save():', details.error); cbError(details); - } else { - cbSuccess(details); + return; } + // Saving over an existing item must be seen as removing an + // existing item and adding a new one. + if ( typeof exports.onRemovedListener === 'function' ) { + exports.onRemovedListener(removedItems); + } + cbSuccess(details); }; var onEntries = function(entries) { + if ( entries.hasOwnProperty(path) ) { + removedItems.push(path); + } entries[path] = Date.now(); bin.cached_asset_entries = entries; - chrome.storage.local.set(bin, onSaved); + vAPI.storage.set(bin, onSaved); }; getEntries(onEntries); }; @@ -169,6 +228,7 @@ var cachedAssetsManager = (function() { exports.remove = function(pattern, before) { var onEntries = function(entries) { var keystoRemove = []; + var removedItems = []; var paths = Object.keys(entries); var i = paths.length; var path; @@ -183,12 +243,16 @@ var cachedAssetsManager = (function() { if ( typeof before === 'number' && entries[path] >= before ) { continue; } + removedItems.push(path); keystoRemove.push(cachedAssetPathPrefix + path); delete entries[path]; } if ( keystoRemove.length ) { - chrome.storage.local.remove(keystoRemove); - chrome.storage.local.set({ 'cached_asset_entries': entries }); + vAPI.storage.remove(keystoRemove); + vAPI.storage.set({ 'cached_asset_entries': entries }); + if ( typeof exports.onRemovedListener === 'function' ) { + exports.onRemovedListener(removedItems); + } } }; getEntries(onEntries); @@ -196,38 +260,74 @@ var cachedAssetsManager = (function() { exports.removeAll = function(callback) { var onEntries = function() { + // Careful! do not remove 'assets/user/' exports.remove(/^https?:\/\/[a-z0-9]+/); exports.remove(/^assets\/(umatrix|thirdparties)\//); + exports.remove(/^cache:\/\//); exports.remove('assets/checksums.txt'); if ( typeof callback === 'function' ) { - callback(); + callback(null); } }; getEntries(onEntries); }; + exports.rmrf = function() { + exports.remove(/./); + }; + + exports.onRemovedListener = null; + return exports; })(); /******************************************************************************/ var getTextFileFromURL = function(url, onLoad, onError) { + // console.log('µMatrix.assets/getTextFileFromURL("%s"):', url); + // https://github.com/gorhill/uMatrix/issues/15 var onResponseReceived = function() { - if ( typeof this.status === 'number' && this.status >= 200 && this.status < 300 ) { - return onLoad.call(this); + this.onload = this.onerror = this.ontimeout = null; + // xhr for local files gives status 0, but actually succeeds + var status = this.status || 200; + if ( status < 200 || status >= 300 ) { + return onError.call(this); } - return onError.call(this); + // consider an empty result to be an error + if ( stringIsNotEmpty(this.responseText) === false ) { + return onError.call(this); + } + // we never download anything else than plain text: discard if response + // appears to be a HTML document: could happen when server serves + // some kind of error page I suppose + var text = this.responseText.trim(); + if ( text.charAt(0) === '<' && text.slice(-1) === '>' ) { + return onError.call(this); + } + return onLoad.call(this); }; - // console.log('assets.js > getTextFileFromURL("%s"):', url); + + var onErrorReceived = function() { + this.onload = this.onerror = this.ontimeout = null; + onError.call(this); + }; + + // Be ready for thrown exceptions: + // I am pretty sure it used to work, but now using a URL such as + // `file:///` on Chromium 40 results in an exception being thrown. var xhr = new XMLHttpRequest(); - xhr.open('get', url, true); - xhr.responseType = 'text'; - xhr.timeout = 15000; - xhr.onload = onResponseReceived; - xhr.onerror = onError; - xhr.ontimeout = onError; - xhr.send(); + try { + xhr.open('get', url, true); + xhr.timeout = 30000; + xhr.onload = onResponseReceived; + xhr.onerror = onErrorReceived; + xhr.ontimeout = onErrorReceived; + xhr.responseType = 'text'; + xhr.send(); + } catch (e) { + onErrorReceived.call(xhr); + } }; /******************************************************************************/ @@ -255,19 +355,27 @@ var updateLocalChecksums = function() { var getRepoMetadata = function(callback) { callback = callback || nullFunc; + // https://github.com/chrisaljoudi/uBlock/issues/515 + // Handle re-entrancy here, i.e. we MUST NOT tamper with the waiting list + // of callers, if any, except to add one at the end of the list. + if ( repoMetadata !== null && repoMetadata.waiting.length !== 0 ) { + repoMetadata.waiting.push(callback); + return; + } + + if ( exports.remoteFetchBarrier === 0 && lastRepoMetaIsRemote === false ) { + lastRepoMetaTimestamp = 0; + } if ( (Date.now() - lastRepoMetaTimestamp) >= refreshRepoMetaPeriod ) { repoMetadata = null; } if ( repoMetadata !== null ) { - if ( repoMetadata.waiting.length !== 0 ) { - repoMetadata.waiting.push(callback); - } else { - callback(repoMetadata); - } + callback(repoMetadata); return; } lastRepoMetaTimestamp = Date.now(); + lastRepoMetaIsRemote = exports.remoteFetchBarrier === 0; var localChecksums; var repoChecksums; @@ -300,9 +408,16 @@ var getRepoMetadata = function(callback) { updateLocalChecksums(); } // Notify all waiting callers - while ( callback = repoMetadata.waiting.pop() ) { - callback(repoMetadata); + // https://github.com/chrisaljoudi/uBlock/issues/515 + // VERY IMPORTANT: because of re-entrancy, we MUST: + // - process the waiting callers in a FIFO manner + // - not cache repoMetadata.waiting.length, we MUST use the live + // value, because it can change while looping + // - not change the waiting list until they are all processed + for ( var i = 0; i < repoMetadata.waiting.length; i++ ) { + repoMetadata.waiting[i](repoMetadata); } + repoMetadata.waiting.length = 0; }; var validateChecksums = function(details) { @@ -358,14 +473,10 @@ var getRepoMetadata = function(callback) { /******************************************************************************/ exports.setHomeURL = function(path, homeURL) { - var onRepoMetadataReady = function(metadata) { - var entry = metadata.entries[path]; - if ( entry === undefined ) { - entry = metadata.entries[path] = new AssetEntry(); - } - entry.homeURL = homeURL; - }; - getRepoMetadata(onRepoMetadataReady); + if ( typeof homeURL !== 'string' || homeURL === '' ) { + return; + } + homeURLs[path] = homeURL; }; /******************************************************************************/ @@ -386,24 +497,22 @@ var readLocalFile = function(path, callback) { }; var onInstallFileLoaded = function() { - this.onload = this.onerror = null; - //console.log('assets.js > readLocalFile("%s") / onInstallFileLoaded()', path); + //console.log('µMatrix> readLocalFile("%s") / onInstallFileLoaded()', path); reportBack(this.responseText); }; var onInstallFileError = function() { - this.onload = this.onerror = null; - console.error('assets.js > readLocalFile("%s") / onInstallFileError()', path); + console.error('µMatrix> readLocalFile("%s") / onInstallFileError()', path); reportBack('', 'Error'); }; var onCachedContentLoaded = function(details) { - //console.log('assets.js > readLocalFile("%s") / onCachedContentLoaded()', path); + //console.log('µMatrix> readLocalFile("%s") / onCachedContentLoaded()', path); reportBack(details.content); }; var onCachedContentError = function(details) { - //console.error('assets.js > readLocalFile("%s") / onCachedContentError()', path); + //console.error('µMatrix> readLocalFile("%s") / onCachedContentError()', path); if ( reIsExternalPath.test(path) ) { reportBack('', 'Error: asset not found'); return; @@ -413,7 +522,7 @@ var readLocalFile = function(path, callback) { reportBack(''); return; } - getTextFileFromURL(chrome.runtime.getURL(details.path), onInstallFileLoaded, onInstallFileError); + getTextFileFromURL(vAPI.getURL(details.path), onInstallFileLoaded, onInstallFileError); }; cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); @@ -426,6 +535,12 @@ var readLocalFile = function(path, callback) { // Get the repository copy of a built-in asset. var readRepoFile = function(path, callback) { + // https://github.com/chrisaljoudi/uBlock/issues/426 + if ( exports.remoteFetchBarrier !== 0 ) { + readLocalFile(path, callback); + return; + } + var reportBack = function(content, err) { var details = { 'path': path, @@ -435,11 +550,10 @@ var readRepoFile = function(path, callback) { callback(details); }; - var repositoryURL = repositoryRoot + path; + var repositoryURL = projectRepositoryRoot + path; var onRepoFileLoaded = function() { - this.onload = this.onerror = null; - //console.log('assets.js > readRepoFile("%s") / onRepoFileLoaded()', path); + //console.log('µMatrix> readRepoFile("%s") / onRepoFileLoaded()', path); // https://github.com/gorhill/httpswitchboard/issues/263 if ( this.status === 200 ) { reportBack(this.responseText); @@ -449,7 +563,6 @@ var readRepoFile = function(path, callback) { }; var onRepoFileError = function() { - this.onload = this.onerror = null; console.error(errorCantConnectTo.replace('{{url}}', repositoryURL)); reportBack('', 'Error'); }; @@ -466,13 +579,14 @@ var readRepoFile = function(path, callback) { // An asset from an external source with a copy shipped with the extension: // Path --> starts with 'assets/(thirdparties|umatrix)/', with a home URL -// External --> +// External --> // Repository --> has checksum (to detect need for update only) // Cache --> has expiration timestamp (in cache) // Local --> install time version var readRepoCopyAsset = function(path, callback) { var assetEntry; + var homeURL = homeURLs[path]; var reportBack = function(content, err) { var details = { @@ -493,52 +607,47 @@ var readRepoCopyAsset = function(path, callback) { }; var onInstallFileLoaded = function() { - this.onload = this.onerror = null; - //console.log('assets.js > readRepoCopyAsset("%s") / onInstallFileLoaded()', path); + //console.log('µMatrix> readRepoCopyAsset("%s") / onInstallFileLoaded()', path); reportBack(this.responseText); }; var onInstallFileError = function() { - this.onload = this.onerror = null; - console.error('assets.js > readRepoCopyAsset("%s") / onInstallFileError():', path, this.statusText); + console.error('µMatrix> readRepoCopyAsset("%s") / onInstallFileError():', path, this.statusText); reportBack('', 'Error'); }; var onCachedContentLoaded = function(details) { - //console.log('assets.js > readRepoCopyAsset("%s") / onCacheFileLoaded()', path); + //console.log('µMatrix> readRepoCopyAsset("%s") / onCacheFileLoaded()', path); reportBack(details.content); }; var onCachedContentError = function(details) { - //console.log('assets.js > readRepoCopyAsset("%s") / onCacheFileError()', path); - getTextFileFromURL(chrome.runtime.getURL(details.path), onInstallFileLoaded, onInstallFileError); + //console.log('µMatrix> readRepoCopyAsset("%s") / onCacheFileError()', path); + getTextFileFromURL(vAPI.getURL(details.path), onInstallFileLoaded, onInstallFileError); }; - var repositoryURL = repositoryRoot + path; + var repositoryURL = projectRepositoryRoot + path; var repositoryURLSkipCache = repositoryURL + '?umatrix=' + Date.now(); var onRepoFileLoaded = function() { - this.onload = this.onerror = null; - if ( typeof this.responseText !== 'string' || this.responseText === '' ) { - console.error('assets.js > readRepoCopyAsset("%s") / onRepoFileLoaded("%s"): error', path, repositoryURL); + if ( stringIsNotEmpty(this.responseText) === false ) { + console.error('µMatrix> readRepoCopyAsset("%s") / onRepoFileLoaded("%s"): error', path, repositoryURL); cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); return; } - //console.log('assets.js > readRepoCopyAsset("%s") / onRepoFileLoaded("%s")', path, repositoryURL); + //console.log('µMatrix> readRepoCopyAsset("%s") / onRepoFileLoaded("%s")', path, repositoryURL); updateChecksum(); cachedAssetsManager.save(path, this.responseText, callback); }; var onRepoFileError = function() { - this.onload = this.onerror = null; console.error(errorCantConnectTo.replace('{{url}}', repositoryURL)); cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); }; var onHomeFileLoaded = function() { - this.onload = this.onerror = null; - if ( typeof this.responseText !== 'string' || this.responseText === '' ) { - console.error('assets.js > readRepoCopyAsset("%s") / onHomeFileLoaded("%s"): no response', path, assetEntry.homeURL); + if ( stringIsNotEmpty(this.responseText) === false ) { + console.error('µMatrix> readRepoCopyAsset("%s") / onHomeFileLoaded("%s"): no response', path, homeURL); // Fetch from repo only if obsolescence was due to repo checksum if ( assetEntry.localChecksum !== assetEntry.repoChecksum ) { getTextFileFromURL(repositoryURLSkipCache, onRepoFileLoaded, onRepoFileError); @@ -547,14 +656,13 @@ var readRepoCopyAsset = function(path, callback) { } return; } - //console.log('assets.js > readRepoCopyAsset("%s") / onHomeFileLoaded("%s")', path, assetEntry.homeURL); + //console.log('µMatrix> readRepoCopyAsset("%s") / onHomeFileLoaded("%s")', path, homeURL); updateChecksum(); cachedAssetsManager.save(path, this.responseText, callback); }; var onHomeFileError = function() { - this.onload = this.onerror = null; - console.error(errorCantConnectTo.replace('{{url}}', assetEntry.homeURL)); + console.error(errorCantConnectTo.replace('{{url}}', homeURL)); // Fetch from repo only if obsolescence was due to repo checksum if ( assetEntry.localChecksum !== assetEntry.repoChecksum ) { getTextFileFromURL(repositoryURLSkipCache, onRepoFileLoaded, onRepoFileError); @@ -567,24 +675,26 @@ var readRepoCopyAsset = function(path, callback) { // Fetch from remote if: // - Auto-update enabled AND (not in cache OR in cache but obsolete) var timestamp = entries[path]; - var homeURL = assetEntry.homeURL; - if ( exports.autoUpdate && typeof homeURL === 'string' && homeURL !== '' ) { - var obsolete = Date.now() - exports.autoUpdateDelay; - if ( typeof timestamp !== 'number' || timestamp <= obsolete ) { - //console.log('assets.js > readRepoCopyAsset("%s") / onCacheMetaReady(): not cached or obsolete', path); + var inCache = typeof timestamp === 'number'; + if ( + exports.remoteFetchBarrier === 0 && + exports.autoUpdate && stringIsNotEmpty(homeURL) + ) { + if ( inCache === false || cacheIsObsolete(timestamp) ) { + //console.log('µMatrix> readRepoCopyAsset("%s") / onCacheMetaReady(): not cached or obsolete', path); getTextFileFromURL(homeURL, onHomeFileLoaded, onHomeFileError); return; } } // In cache - if ( typeof timestamp === 'number' ) { + if ( inCache ) { cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); return; } // Not in cache - getTextFileFromURL(chrome.runtime.getURL(path), onInstallFileLoaded, onInstallFileError); + getTextFileFromURL(vAPI.getURL(path), onInstallFileLoaded, onInstallFileError); }; var onRepoMetaReady = function(meta) { @@ -597,10 +707,13 @@ var readRepoCopyAsset = function(path, callback) { } // Repo copy changed: fetch from home URL - if ( exports.autoUpdate && assetEntry.localChecksum !== assetEntry.repoChecksum ) { - //console.log('assets.js > readRepoCopyAsset("%s") / onRepoMetaReady(): repo has newer version', path); - var homeURL = assetEntry.homeURL; - if ( typeof homeURL === 'string' && homeURL !== '' ) { + if ( + exports.remoteFetchBarrier === 0 && + exports.autoUpdate && + assetEntry.localChecksum !== assetEntry.repoChecksum + ) { + //console.log('µMatrix> readRepoCopyAsset("%s") / onRepoMetaReady(): repo has newer version', path); + if ( stringIsNotEmpty(homeURL) ) { getTextFileFromURL(homeURL, onHomeFileLoaded, onHomeFileError); } else { getTextFileFromURL(repositoryURLSkipCache, onRepoFileLoaded, onRepoFileError); @@ -619,7 +732,7 @@ var readRepoCopyAsset = function(path, callback) { /******************************************************************************/ -// An important asset shipped with the extension -- typically small, or +// An important asset shipped with the extension -- typically small, or // doesn't change often: // Path --> starts with 'assets/(thirdparties|umatrix)/', without a home URL // Repository --> has checksum (to detect need for update and corruption) @@ -642,49 +755,45 @@ var readRepoOnlyAsset = function(path, callback) { }; var onInstallFileLoaded = function() { - this.onload = this.onerror = null; - //console.log('assets.js > readRepoOnlyAsset("%s") / onInstallFileLoaded()', path); + //console.log('µMatrix> readRepoOnlyAsset("%s") / onInstallFileLoaded()', path); reportBack(this.responseText); }; var onInstallFileError = function() { - this.onload = this.onerror = null; - console.error('assets.js > readRepoOnlyAsset("%s") / onInstallFileError()', path); + console.error('µMatrix> readRepoOnlyAsset("%s") / onInstallFileError()', path); reportBack('', 'Error'); }; var onCachedContentLoaded = function(details) { - //console.log('assets.js > readRepoOnlyAsset("%s") / onCachedContentLoaded()', path); + //console.log('µMatrix> readRepoOnlyAsset("%s") / onCachedContentLoaded()', path); reportBack(details.content); }; var onCachedContentError = function() { - //console.log('assets.js > readRepoOnlyAsset("%s") / onCachedContentError()', path); - getTextFileFromURL(chrome.runtime.getURL(path), onInstallFileLoaded, onInstallFileError); + //console.log('µMatrix> readRepoOnlyAsset("%s") / onCachedContentError()', path); + getTextFileFromURL(vAPI.getURL(path), onInstallFileLoaded, onInstallFileError); }; - var repositoryURL = repositoryRoot + path + '?umatrix=' + Date.now(); + var repositoryURL = projectRepositoryRoot + path + '?umatrix=' + Date.now(); var onRepoFileLoaded = function() { - this.onload = this.onerror = null; if ( typeof this.responseText !== 'string' ) { - console.error('assets.js > readRepoOnlyAsset("%s") / onRepoFileLoaded("%s"): no response', path, repositoryURL); + console.error('µMatrix> readRepoOnlyAsset("%s") / onRepoFileLoaded("%s"): no response', path, repositoryURL); cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); return; } if ( YaMD5.hashStr(this.responseText) !== assetEntry.repoChecksum ) { - console.error('assets.js > readRepoOnlyAsset("%s") / onRepoFileLoaded("%s"): bad md5 checksum', path, repositoryURL); + console.error('µMatrix> readRepoOnlyAsset("%s") / onRepoFileLoaded("%s"): bad md5 checksum', path, repositoryURL); cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); return; } - //console.log('assets.js > readRepoOnlyAsset("%s") / onRepoFileLoaded("%s")', path, repositoryURL); + //console.log('µMatrix> readRepoOnlyAsset("%s") / onRepoFileLoaded("%s")', path, repositoryURL); assetEntry.localChecksum = assetEntry.repoChecksum; updateLocalChecksums(); cachedAssetsManager.save(path, this.responseText, callback); }; var onRepoFileError = function() { - this.onload = this.onerror = null; console.error(errorCantConnectTo.replace('{{url}}', repositoryURL)); cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); }; @@ -699,8 +808,12 @@ var readRepoOnlyAsset = function(path, callback) { } // Asset added or changed: load from repo URL and then cache result - if ( exports.autoUpdate && assetEntry.localChecksum !== assetEntry.repoChecksum ) { - //console.log('assets.js > readRepoOnlyAsset("%s") / onRepoMetaReady(): repo has newer version', path); + if ( + exports.remoteFetchBarrier === 0 && + exports.autoUpdate && + assetEntry.localChecksum !== assetEntry.repoChecksum + ) { + //console.log('µMatrix> readRepoOnlyAsset("%s") / onRepoMetaReady(): repo has newer version', path); getTextFileFromURL(repositoryURL, onRepoFileLoaded, onRepoFileError); return; } @@ -744,24 +857,29 @@ var readExternalAsset = function(path, callback) { }; var onCachedContentLoaded = function(details) { - //console.log('assets.js > readExternalAsset("%s") / onCachedContentLoaded()', path); + //console.log('µMatrix> readExternalAsset("%s") / onCachedContentLoaded()', path); reportBack(details.content); }; var onCachedContentError = function() { - console.error('assets.js > readExternalAsset("%s") / onCachedContentError()', path); + console.error('µMatrix> readExternalAsset("%s") / onCachedContentError()', path); reportBack('', 'Error'); }; var onExternalFileLoaded = function() { - this.onload = this.onerror = null; - //console.log('assets.js > readExternalAsset("%s") / onExternalFileLoaded1()', path); + // https://github.com/chrisaljoudi/uBlock/issues/708 + // A successful download should never return an empty file: turn this + // into an error condition. + if ( stringIsNotEmpty(this.responseText) === false ) { + onExternalFileError(); + return; + } + //console.log('µMatrix> readExternalAsset("%s") / onExternalFileLoaded1()', path); cachedAssetsManager.save(path, this.responseText); reportBack(this.responseText); }; var onExternalFileError = function() { - this.onload = this.onerror = null; console.error(errorCantConnectTo.replace('{{url}}', path)); cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); }; @@ -769,11 +887,14 @@ var readExternalAsset = function(path, callback) { var onCacheMetaReady = function(entries) { // Fetch from remote if: // - Not in cache OR - // + // // - Auto-update enabled AND in cache but obsolete var timestamp = entries[path]; - var obsolete = Date.now() - exports.autoUpdateDelay; - if ( typeof timestamp !== 'number' || (exports.autoUpdate && timestamp <= obsolete) ) { + var notInCache = typeof timestamp !== 'number'; + var updateCache = exports.remoteFetchBarrier === 0 && + exports.autoUpdate && + cacheIsObsolete(timestamp); + if ( notInCache || updateCache ) { getTextFileFromURL(path, onExternalFileLoaded, onExternalFileError); return; } @@ -793,12 +914,33 @@ var readExternalAsset = function(path, callback) { var readUserAsset = function(path, callback) { var onCachedContentLoaded = function(details) { - //console.log('assets.js > readUserAsset("%s") / onCachedContentLoaded()', path); + //console.log('µMatrix.assets/readUserAsset("%s")/onCachedContentLoaded()', path); callback({ 'path': path, 'content': details.content }); }; var onCachedContentError = function() { - //console.log('assets.js > readUserAsset("%s") / onCachedContentError()', path); + //console.log('µMatrix.assets/readUserAsset("%s")/onCachedContentError()', path); + callback({ 'path': path, 'content': '' }); + }; + + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); +}; + +/******************************************************************************/ + +// Asset available only from the cache. +// Cache data: +// Path --> starts with 'cache://' +// Cache --> whatever + +var readCacheAsset = function(path, callback) { + var onCachedContentLoaded = function(details) { + //console.log('µMatrix.assets/readCacheAsset("%s")/onCachedContentLoaded()', path); + callback({ 'path': path, 'content': details.content }); + }; + + var onCachedContentError = function() { + //console.log('µMatrix.assets/readCacheAsset("%s")/onCachedContentError()', path); callback({ 'path': path, 'content': '' }); }; @@ -811,7 +953,7 @@ var readUserAsset = function(path, callback) { // // A copy of an asset from an external source shipped with the extension: // Path --> starts with 'assets/(thirdparties|umatrix)/', with a home URL -// External --> +// External --> // Repository --> has checksum (to detect obsolescence) // Cache --> has expiration timestamp (to detect obsolescence) // Local --> install time version @@ -825,7 +967,7 @@ var readUserAsset = function(path, callback) { // // An external filter list: // Path --> starts with 'http' -// External --> +// External --> // Cache --> has expiration timestamp (to detect obsolescence) // // User data: @@ -838,7 +980,7 @@ var readUserAsset = function(path, callback) { // the asset needs to be updated. // // If no update required, an asset if first fetched from the cache. If the -// asset is not cached it is fetched from the closest location: local for +// asset is not cached it is fetched from the closest location: local for // an asset shipped with the extension, external for an asset not shipped // with the extension. @@ -849,6 +991,11 @@ exports.get = function(path, callback) { return; } + if ( reIsCachePath.test(path) ) { + readCacheAsset(path, callback); + return; + } + if ( reIsExternalPath.test(path) ) { readExternalAsset(path, callback); return; @@ -864,7 +1011,7 @@ exports.get = function(path, callback) { } // Asset is repo copy of external content - if ( assetEntry.homeURL !== '' ) { + if ( stringIsNotEmpty(homeURLs[path]) ) { readRepoCopyAsset(path, callback); return; } @@ -890,24 +1037,27 @@ exports.put = function(path, content, callback) { /******************************************************************************/ +exports.rmrf = function() { + cachedAssetsManager.rmrf(); +}; + +/******************************************************************************/ + exports.metadata = function(callback) { var out = {}; - // https://github.com/gorhill/uBlock/issues/186 + // https://github.com/chrisaljoudi/uBlock/issues/186 // We need to check cache obsolescence when both cache and repo meta data // has been gathered. var checkCacheObsolescence = function() { - var obsolete = Date.now() - exports.autoUpdateDelay; var entry; for ( var path in out ) { if ( out.hasOwnProperty(path) === false ) { continue; } entry = out[path]; - entry.cacheObsolete = - typeof entry.homeURL === 'string' && - entry.homeURL !== '' && - (typeof entry.lastModified !== 'number' || entry.lastModified <= obsolete); + entry.cacheObsolete = stringIsNotEmpty(homeURLs[path]) && + cacheIsObsolete(entry.lastModified); } callback(out); }; @@ -926,7 +1076,7 @@ exports.metadata = function(callback) { } entryOut.localChecksum = entryRepo.localChecksum; entryOut.repoChecksum = entryRepo.repoChecksum; - entryOut.homeURL = entryRepo.homeURL; + entryOut.homeURL = homeURLs[path] || ''; entryOut.repoObsolete = entryOut.localChecksum !== entryOut.repoChecksum; } checkCacheObsolescence(); @@ -964,18 +1114,365 @@ exports.purge = function(pattern, before) { cachedAssetsManager.remove(pattern, before); }; -/******************************************************************************/ - exports.purgeAll = function(callback) { cachedAssetsManager.removeAll(callback); }; /******************************************************************************/ -return exports; +exports.onAssetCacheRemoved = { + addEventListener: function(callback) { + cachedAssetsManager.onRemovedListener = callback || null; + } +}; /******************************************************************************/ +return exports; + +})(); + +/******************************************************************************/ +/******************************************************************************/ + +µMatrix.assetUpdater = (function() { + +'use strict'; + +/******************************************************************************/ + +var µm = µMatrix; + +var updateDaemonTimer = null; +var autoUpdateDaemonTimerPeriod = 11 * 60 * 1000; // 11 minutes +var manualUpdateDaemonTimerPeriod = 5 * 1000; // 5 seconds + +var updateCycleFirstPeriod = 7 * 60 * 1000; // 7 minutes +var updateCycleNextPeriod = 11 * 60 * 60 * 1000; // 11 hours +var updateCycleTime = 0; + +var toUpdate = {}; +var toUpdateCount = 0; +var updated = {}; +var updatedCount = 0; +var metadata = null; + +var onStartListener = null; +var onCompletedListener = null; +var onAssetUpdatedListener = null; + +var exports = { + manualUpdate: false, + manualUpdateProgress: { + value: 0, + text: null + } +}; + +/******************************************************************************/ + +var onOneUpdated = function(details) { + // Resource fetched, we can safely restart the daemon. + scheduleUpdateDaemon(); + + var path = details.path; + if ( details.error ) { + manualUpdateNotify(false, updatedCount / (updatedCount + toUpdateCount)); + //console.debug('µMatrix.assetUpdater/onOneUpdated: "%s" failed', path); + return; + } + + //console.debug('µMatrix.assetUpdater/onOneUpdated: "%s"', path); + updated[path] = true; + updatedCount += 1; + + if ( typeof onAssetUpdatedListener === 'function' ) { + onAssetUpdatedListener(details); + } + + manualUpdateNotify(false, updatedCount / (updatedCount + toUpdateCount + 1)); +}; + +/******************************************************************************/ + +var updateOne = function() { + // Because this can be called from outside the daemon's main loop + µm.assets.autoUpdate = µm.userSettings.autoUpdate || exports.manualUpdate; + + var metaEntry; + var updatingCount = 0; + var updatingText = null; + + for ( var path in toUpdate ) { + if ( toUpdate.hasOwnProperty(path) === false ) { + continue; + } + if ( toUpdate[path] !== true ) { + continue; + } + toUpdate[path] = false; + toUpdateCount -= 1; + if ( metadata.hasOwnProperty(path) === false ) { + continue; + } + metaEntry = metadata[path]; + if ( !metaEntry.cacheObsolete && !metaEntry.repoObsolete ) { + continue; + } + + // Will restart the update daemon once the resource is received: the + // fetching of a resource may take some time, possibly beyond the + // next scheduled daemon cycle, so this ensure the daemon won't do + // anything else before the resource is fetched (or times out). + suspendUpdateDaemon(); + + //console.debug('µMatrix.assetUpdater/updateOne: assets.get("%s")', path); + µm.assets.get(path, onOneUpdated); + updatingCount = 1; + updatingText = metaEntry.homeURL || path; + break; + } + + manualUpdateNotify( + false, + (updatedCount + updatingCount/2) / (updatedCount + toUpdateCount + updatingCount + 1), + updatingText + ); +}; + +/******************************************************************************/ + +// Update one asset, fetch metadata if not done yet. + +var safeUpdateOne = function() { + if ( metadata !== null ) { + updateOne(); + return; + } + + // Because this can be called from outside the daemon's main loop + µm.assets.autoUpdate = µm.userSettings.autoUpdate || exports.manualUpdate; + + var onMetadataReady = function(response) { + scheduleUpdateDaemon(); + metadata = response; + updateOne(); + }; + + suspendUpdateDaemon(); + µm.assets.metadata(onMetadataReady); +}; + +/******************************************************************************/ + +var safeStartListener = function(callback) { + // Because this can be called from outside the daemon's main loop + µm.assets.autoUpdate = µm.userSettings.autoUpdate || exports.manualUpdate; + + var onStartListenerDone = function(assets) { + scheduleUpdateDaemon(); + assets = assets || {}; + for ( var path in assets ) { + if ( assets.hasOwnProperty(path) === false ) { + continue; + } + if ( toUpdate.hasOwnProperty(path) ) { + continue; + } + //console.debug('assets.js > µMatrix.assetUpdater/safeStartListener: "%s"', path); + toUpdate[path] = true; + toUpdateCount += 1; + } + if ( typeof callback === 'function' ) { + callback(); + } + }; + + if ( typeof onStartListener === 'function' ) { + suspendUpdateDaemon(); + onStartListener(onStartListenerDone); + } else { + onStartListenerDone(null); + } +}; + +/******************************************************************************/ + +var updateDaemon = function() { + updateDaemonTimer = null; + scheduleUpdateDaemon(); + + µm.assets.autoUpdate = µm.userSettings.autoUpdate || exports.manualUpdate; + + if ( µm.assets.autoUpdate !== true ) { + return; + } + + // Start an update cycle? + if ( updateCycleTime !== 0 ) { + if ( Date.now() >= updateCycleTime ) { + //console.debug('µMatrix.assetUpdater/updateDaemon: update cycle started'); + reset(); + safeStartListener(); + } + return; + } + + // Any asset to update? + if ( toUpdateCount !== 0 ) { + safeUpdateOne(); + return; + } + // Nothing left to update + + // In case of manual update, fire progress notifications + manualUpdateNotify(true, 1, ''); + + // If anything was updated, notify listener + if ( updatedCount !== 0 ) { + if ( typeof onCompletedListener === 'function' ) { + //console.debug('µMatrix.assetUpdater/updateDaemon: update cycle completed'); + onCompletedListener({ + updated: JSON.parse(JSON.stringify(updated)), // give callee its own safe copy + updatedCount: updatedCount + }); + } + } + + // Schedule next update cycle + if ( updateCycleTime === 0 ) { + reset(); + //console.debug('µMatrix.assetUpdater/updateDaemon: update cycle re-scheduled'); + updateCycleTime = Date.now() + updateCycleNextPeriod; + } +}; + +/******************************************************************************/ + +var scheduleUpdateDaemon = function() { + if ( updateDaemonTimer !== null ) { + clearTimeout(updateDaemonTimer); + } + updateDaemonTimer = setTimeout( + updateDaemon, + exports.manualUpdate ? manualUpdateDaemonTimerPeriod : autoUpdateDaemonTimerPeriod + ); +}; + +var suspendUpdateDaemon = function() { + if ( updateDaemonTimer !== null ) { + clearTimeout(updateDaemonTimer); + updateDaemonTimer = null; + } +}; + +scheduleUpdateDaemon(); + +/******************************************************************************/ + +var reset = function() { + toUpdate = {}; + toUpdateCount = 0; + updated = {}; + updatedCount = 0; + updateCycleTime = 0; + metadata = null; +}; + +/******************************************************************************/ + +var manualUpdateNotify = function(done, value, text) { + if ( exports.manualUpdate === false ) { + return; + } + + exports.manualUpdate = !done; + exports.manualUpdateProgress.value = value || 0; + if ( typeof text === 'string' ) { + exports.manualUpdateProgress.text = text; + } + + vAPI.messaging.broadcast({ + what: 'forceUpdateAssetsProgress', + done: !exports.manualUpdate, + progress: exports.manualUpdateProgress, + updatedCount: updatedCount + }); + + // When manually updating, whatever launched the manual update is + // responsible to launch a reload of the filter lists. + if ( exports.manualUpdate !== true ) { + reset(); + } +}; + +/******************************************************************************/ + +// Manual update: just a matter of forcing the update daemon to work on a +// tighter schedule. + +exports.force = function() { + if ( exports.manualUpdate ) { + return; + } + + reset(); + + exports.manualUpdate = true; + + var onStartListenerDone = function() { + if ( toUpdateCount === 0 ) { + updateCycleTime = Date.now() + updateCycleNextPeriod; + manualUpdateNotify(true, 1); + } else { + manualUpdateNotify(false, 0); + safeUpdateOne(); + } + }; + + safeStartListener(onStartListenerDone); +}; + +/******************************************************************************/ + +exports.onStart = { + addEventListener: function(callback) { + onStartListener = callback || null; + if ( typeof onStartListener === 'function' ) { + updateCycleTime = Date.now() + updateCycleFirstPeriod; + } + } +}; + +/******************************************************************************/ + +exports.onAssetUpdated = { + addEventListener: function(callback) { + onAssetUpdatedListener = callback || null; + } +}; + +/******************************************************************************/ + +exports.onCompleted = { + addEventListener: function(callback) { + onCompletedListener = callback || null; + } +}; + +/******************************************************************************/ + +// Typically called when an update has been forced. + +exports.restart = function() { + reset(); + updateCycleTime = Date.now() + updateCycleNextPeriod; +}; + +/******************************************************************************/ + +return exports; + })(); /******************************************************************************/ diff --git a/src/js/async.js b/src/js/async.js index e6c5b62..d529d3e 100644 --- a/src/js/async.js +++ b/src/js/async.js @@ -145,11 +145,7 @@ return asyncJobManager; pageStore.updateBadge(tabId); return; } - µm.XAL.setIcon( - tabId, - { '19': 'img/browsericons/icon19.png' }, - '?' - ); + vAPI.setIcon(tabId, null, '?'); }; var updateBadgeAsync = function(tabId) { @@ -172,7 +168,7 @@ return asyncJobManager; // does not exist. I suspect this could be related to // https://github.com/gorhill/httpswitchboard/issues/58 var urlStatsChangedCallback = function(pageUrl) { - µMatrix.messaging.tell('popup.js', { + vAPI.messaging.broadcast({ what: 'urlStatsChanged', pageURL: pageUrl }); diff --git a/src/js/background.js b/src/js/background.js index 6008b45..465142f 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -49,8 +49,6 @@ var defaultUserAgentStrings = [ /******************************************************************************/ return { - manifest: chrome.runtime.getManifest(), - userSettings: { autoUpdate: false, clearBrowserCache: true, @@ -125,7 +123,6 @@ return { refererHeaderFoiledCounter: 0, hyperlinkAuditingFoiledCounter: 0, browserCacheClearedCounter: 0, - storageQuota: chrome.storage.local.QUOTA_BYTES, storageUsed: 0, userAgentReplaceStr: '', userAgentReplaceStrBirth: 0, @@ -138,8 +135,8 @@ return { // Commonly encountered strings chromeExtensionURLPrefix: 'chrome-extension://', - noopCSSURL: chrome.runtime.getURL('css/noop.css'), - fontCSSURL: chrome.runtime.getURL('css/fonts/Roboto_Condensed/RobotoCondensed-Regular.ttf'), + noopCSSURL: vAPI.getURL('css/noop.css'), + fontCSSURL: vAPI.getURL('css/fonts/Roboto_Condensed/RobotoCondensed-Regular.ttf'), noopFunc: function(){}, diff --git a/src/js/commands.js b/src/js/commands.js index 0fbcbbf..b474d88 100644 --- a/src/js/commands.js +++ b/src/js/commands.js @@ -47,7 +47,7 @@ var onCommand = function(command) { µMatrix.revertAllRules(); break; case 'whitelist-all': - chrome.tabs.query({ active: true }, whitelistAll); + vAPI.tabs.get(null, whitelistAll); break; case 'open-dashboard': µMatrix.utils.gotoExtensionURL('dashboard.html'); @@ -59,7 +59,7 @@ var onCommand = function(command) { /******************************************************************************/ -chrome.commands.onCommand.addListener(onCommand); +// chrome.commands.onCommand.addListener(onCommand); /******************************************************************************/ diff --git a/src/js/contentscript-end.js b/src/js/contentscript-end.js index efe2dd7..4a48567 100644 --- a/src/js/contentscript-end.js +++ b/src/js/contentscript-end.js @@ -19,113 +19,49 @@ Home: https://github.com/gorhill/uMatrix */ -/* jshint multistr: true */ -/* global chrome */ +/* global vAPI */ +/* jshint multistr: true, boss: true */ + +/******************************************************************************/ +/******************************************************************************/ // Injected into content pages -/******************************************************************************/ +(function() { + +'use strict'; + /******************************************************************************/ -// https://github.com/gorhill/httpswitchboard/issues/345 +// https://github.com/chrisaljoudi/uBlock/issues/464 +if ( document instanceof HTMLDocument === false ) { + //console.debug('contentscript-end.js > not a HTLMDocument'); + return false; +} -var messaging = (function(name){ - var port = null; - var requestId = 1; - var requestIdToCallbackMap = {}; - var listenCallback = null; +// This can happen +if ( !vAPI ) { + //console.debug('contentscript-end.js > vAPI not found'); + return; +} - var onPortMessage = function(details) { - if ( typeof details.id !== 'number' ) { - return; - } - // Announcement? - if ( details.id < 0 ) { - if ( listenCallback ) { - listenCallback(details.msg); - } - return; - } - var callback = requestIdToCallbackMap[details.id]; - if ( !callback ) { - return; - } - // Must be removed before calling client to be sure to not execute - // callback again if the client stops the messaging service. - delete requestIdToCallbackMap[details.id]; - callback(details.msg); - }; +// https://github.com/chrisaljoudi/uBlock/issues/587 +// Pointless to execute without the start script having done its job. +if ( !vAPI.contentscriptStartInjected ) { + return; +} - var start = function(name) { - port = chrome.runtime.connect({ name: name }); - port.onMessage.addListener(onPortMessage); +// https://github.com/chrisaljoudi/uBlock/issues/456 +// Already injected? +if ( vAPI.contentscriptEndInjected ) { + //console.debug('contentscript-end.js > content script already injected'); + return; +} +vAPI.contentscriptEndInjected = true; - // https://github.com/gorhill/uBlock/issues/193 - port.onDisconnect.addListener(stop); - }; +/******************************************************************************/ - var stop = function() { - listenCallback = null; - port.disconnect(); - port = null; - flushCallbacks(); - }; - - if ( typeof name === 'string' && name !== '' ) { - start(name); - } - - var ask = function(msg, callback) { - if ( port === null ) { - if ( typeof callback === 'function' ) { - callback(); - } - return; - } - if ( callback === undefined ) { - tell(msg); - return; - } - var id = requestId++; - port.postMessage({ id: id, msg: msg }); - requestIdToCallbackMap[id] = callback; - }; - - var tell = function(msg) { - if ( port !== null ) { - port.postMessage({ id: 0, msg: msg }); - } - }; - - var listen = function(callback) { - listenCallback = callback; - }; - - var flushCallbacks = function() { - var callback; - for ( var id in requestIdToCallbackMap ) { - if ( requestIdToCallbackMap.hasOwnProperty(id) === false ) { - continue; - } - callback = requestIdToCallbackMap[id]; - if ( !callback ) { - continue; - } - // Must be removed before calling client to be sure to not execute - // callback again if the client stops the messaging service. - delete requestIdToCallbackMap[id]; - callback(); - } - }; - - return { - start: start, - stop: stop, - ask: ask, - tell: tell, - listen: listen - }; -})('contentscript-end.js'); +var localMessager = vAPI.messaging.channel('contentscript-end.js'); /******************************************************************************/ /******************************************************************************/ @@ -154,12 +90,10 @@ var checkScriptBlacklistedHandler = function(response) { } }; -messaging.ask({ +localMessager.send({ what: 'checkScriptBlacklisted', url: window.location.href - }, - checkScriptBlacklistedHandler -); +}, checkScriptBlacklistedHandler); /******************************************************************************/ @@ -179,12 +113,10 @@ try { var hasLocalStorage = window.localStorage && window.localStorage.length; var hasSessionStorage = window.sessionStorage && window.sessionStorage.length; if ( hasLocalStorage || hasSessionStorage ) { - messaging.ask({ + localMessager.send({ what: 'contentScriptHasLocalStorage', url: window.location.href - }, - localStorageHandler - ); + }, localStorageHandler); } // TODO: indexedDB @@ -291,28 +223,12 @@ var nodeListsAddedHandler = function(nodeLists) { nodesAddedHandler(nodeLists[i], summary); } if ( summary.mustReport ) { - messaging.tell(summary); + localMessager.send(summary); } }; /******************************************************************************/ -// rhill 2013-11-09: Weird... This code is executed from HTTP Switchboard -// context first time extension is launched. Avoid this. -// TODO: Investigate if this was a fluke or if it can really happen. -// I suspect this could only happen when I was using chrome.tabs.executeScript(), -// because now a delarative content script is used, along with "http{s}" URL -// pattern matching. - -// console.debug('contentscript-end.js > window.location.href = "%s"', window.location.href); - -if ( /^https?:\/\/./.test(window.location.href) === false ) { - console.debug("Huh?"); - return; -} - -/******************************************************************************/ - (function() { var summary = { what: 'contentScriptSummary', @@ -329,7 +245,7 @@ if ( /^https?:\/\/./.test(window.location.href) === false ) { //console.debug('contentscript-end.js > firstObservationHandler(): found %d script tags in "%s"', Object.keys(summary.scriptSources).length, window.location.href); - messaging.tell(summary); + localMessager.send(summary); })(); /******************************************************************************/ @@ -379,3 +295,8 @@ if ( document.body ) { /******************************************************************************/ })(); + +/******************************************************************************/ +/******************************************************************************/ + +})(); diff --git a/src/js/contentscript-start.js b/src/js/contentscript-start.js index 80da5d8..1d73bdf 100644 --- a/src/js/contentscript-start.js +++ b/src/js/contentscript-start.js @@ -19,124 +19,43 @@ Home: https://github.com/gorhill/uMatrix */ +/* global vAPI */ /* jshint multistr: true */ -/* global chrome */ // Injected into content pages /******************************************************************************/ -// OK, I keep changing my mind whether a closure should be used or not. This -// will be the rule: if there are any variables directly accessed on a regular -// basis, use a closure so that they are cached. Otherwise I don't think the -// overhead of a closure is worth it. That's my understanding. - (function() { -/******************************************************************************/ -/******************************************************************************/ - -// https://github.com/gorhill/httpswitchboard/issues/345 - -var messaging = (function(name){ - var port = null; - var requestId = 1; - var requestIdToCallbackMap = {}; - var listenCallback = null; - - var onPortMessage = function(details) { - if ( typeof details.id !== 'number' ) { - return; - } - // Announcement? - if ( details.id < 0 ) { - if ( listenCallback ) { - listenCallback(details.msg); - } - return; - } - var callback = requestIdToCallbackMap[details.id]; - if ( !callback ) { - return; - } - // Must be removed before calling client to be sure to not execute - // callback again if the client stops the messaging service. - delete requestIdToCallbackMap[details.id]; - callback(details.msg); - }; - - var start = function(name) { - port = chrome.runtime.connect({ name: name }); - port.onMessage.addListener(onPortMessage); - - // https://github.com/gorhill/uBlock/issues/193 - port.onDisconnect.addListener(stop); - }; - - var stop = function() { - listenCallback = null; - port.disconnect(); - port = null; - flushCallbacks(); - }; - - if ( typeof name === 'string' && name !== '' ) { - start(name); - } - - var ask = function(msg, callback) { - if ( port === null ) { - if ( typeof callback === 'function' ) { - callback(); - } - return; - } - if ( callback === undefined ) { - tell(msg); - return; - } - var id = requestId++; - port.postMessage({ id: id, msg: msg }); - requestIdToCallbackMap[id] = callback; - }; - - var tell = function(msg) { - if ( port !== null ) { - port.postMessage({ id: 0, msg: msg }); - } - }; - - var listen = function(callback) { - listenCallback = callback; - }; - - var flushCallbacks = function() { - var callback; - for ( var id in requestIdToCallbackMap ) { - if ( requestIdToCallbackMap.hasOwnProperty(id) === false ) { - continue; - } - callback = requestIdToCallbackMap[id]; - if ( !callback ) { - continue; - } - // Must be removed before calling client to be sure to not execute - // callback again if the client stops the messaging service. - delete requestIdToCallbackMap[id]; - callback(); - } - }; - - return { - start: start, - stop: stop, - ask: ask, - tell: tell, - listen: listen - }; -})('contentscript-start.js'); +'use strict'; /******************************************************************************/ + +// https://github.com/chrisaljoudi/uBlock/issues/464 +if ( document instanceof HTMLDocument === false ) { + //console.debug('contentscript-start.js > not a HTLMDocument'); + return false; +} + +// This can happen +if ( !vAPI ) { + //console.debug('contentscript-start.js > vAPI not found'); + return; +} + +// https://github.com/chrisaljoudi/uBlock/issues/456 +// Already injected? +if ( vAPI.contentscriptStartInjected ) { + //console.debug('contentscript-end.js > content script already injected'); + return; +} +vAPI.contentscriptStartInjected = true; + +/******************************************************************************/ + +var localMessager = vAPI.messaging.channel('contentscript-start.js'); + /******************************************************************************/ // If you play with this code, mind: @@ -193,14 +112,13 @@ var injectNavigatorSpoofer = function(spoofedUserAgent) { // The port will never be used again at this point, disconnecting allows // to browser to flush this script from memory. - messaging.stop(); + localMessager.close(); }; -var requestDetails = { +localMessager.send({ what: 'getUserAgentReplaceStr', hostname: window.location.hostname -}; -messaging.ask(requestDetails, injectNavigatorSpoofer); +}, injectNavigatorSpoofer); /******************************************************************************/ /******************************************************************************/ diff --git a/src/js/cookies.js b/src/js/cookies.js index 8513f00..a55faaf 100644 --- a/src/js/cookies.js +++ b/src/js/cookies.js @@ -301,7 +301,7 @@ var chromeCookieRemove = function(url, name) { } }; - chrome.cookies.remove({ url: url, name: name }, callback); + vAPI.cookies.remove({ url: url, name: name }, callback); }; /******************************************************************************/ @@ -485,7 +485,7 @@ var canRemoveCookie = function(cookieKey, srcHostnames) { // Listen to any change in cookieland, we will update page stats accordingly. -var onChromeCookieChanged = function(changeInfo) { +vAPI.cookies.onChanged = function(changeInfo) { if ( changeInfo.removed ) { return; } @@ -524,8 +524,8 @@ var onChromeCookieChanged = function(changeInfo) { /******************************************************************************/ -chrome.cookies.getAll({}, addCookiesToDict); -chrome.cookies.onChanged.addListener(onChromeCookieChanged); +vAPI.cookies.getAll(addCookiesToDict); +vAPI.cookies.registerListeners(); µm.asyncJobs.add('cookieHunterRemove', null, processRemoveQueue, 2 * 60 * 1000, true); µm.asyncJobs.add('cookieHunterClean', null, processClean, 10 * 60 * 1000, true); diff --git a/src/js/dashboard.js b/src/js/dashboard.js index d58462d..3c1bff0 100644 --- a/src/js/dashboard.js +++ b/src/js/dashboard.js @@ -25,10 +25,14 @@ (function() { +'use strict'; + +/******************************************************************************/ + var loadDashboardPanel = function(hash) { var button = uDom(hash); var url = button.attr('data-dashboard-panel-url'); - uDom('iframe').nodeAt(0).src = url; + uDom('iframe').attr('src', url); uDom('.tabButton').forEach(function(button){ button.toggleClass('selected', button.attr('data-dashboard-panel-url') === url); }); diff --git a/src/js/hosts-files.js b/src/js/hosts-files.js index 5aa902a..f5ffeb4 100644 --- a/src/js/hosts-files.js +++ b/src/js/hosts-files.js @@ -19,27 +19,16 @@ Home: https://github.com/gorhill/uMatrix */ -/* global chrome, messaging, uDom */ +/* global vAPI, uDom */ /******************************************************************************/ (function() { -/******************************************************************************/ - -var listDetails = {}; -var externalHostsFiles = ''; -var cacheWasPurged = false; -var needUpdate = false; -var hasCachedContent = false; - -var re3rdPartyExternalAsset = /^https?:\/\/[a-z0-9]+/; -var re3rdPartyRepoAsset = /^assets\/thirdparties\/([^\/]+)/; +'use strict'; /******************************************************************************/ -messaging.start('hosts-files.js'); - var onMessage = function(msg) { switch ( msg.what ) { case 'loadHostsFilesCompleted': @@ -51,7 +40,18 @@ var onMessage = function(msg) { } }; -messaging.listen(onMessage); +var messager = vAPI.messaging.channel('hosts-files.js', onMessage); + +/******************************************************************************/ + +var listDetails = {}; +var externalHostsFiles = ''; +var cacheWasPurged = false; +var needUpdate = false; +var hasCachedContent = false; + +var re3rdPartyExternalAsset = /^https?:\/\/[a-z0-9]+/; +var re3rdPartyRepoAsset = /^assets\/thirdparties\/([^\/]+)/; /******************************************************************************/ @@ -94,9 +94,9 @@ var renderBlacklists = function() { return html.join(''); }; - var purgeButtontext = chrome.i18n.getMessage('hostsFilesExternalListPurge'); - var updateButtontext = chrome.i18n.getMessage('hostsFilesExternalListNew'); - var obsoleteButtontext = chrome.i18n.getMessage('hostsFilesExternalListObsolete'); + var purgeButtontext = vAPI.i18n('hostsFilesExternalListPurge'); + var updateButtontext = vAPI.i18n('hostsFilesExternalListNew'); + var obsoleteButtontext = vAPI.i18n('hostsFilesExternalListObsolete'); var liTemplate = [ '
  • ', '', @@ -107,7 +107,7 @@ var renderBlacklists = function() { '{{homeURL}}', ': ', '', - chrome.i18n.getMessage('hostsFilesPerFileStats'), + vAPI.i18n('hostsFilesPerFileStats'), '' ].join(''); @@ -181,7 +181,7 @@ var renderBlacklists = function() { var html = htmlBuiltin.concat(htmlExternal); uDom('#listsOfBlockedHostsPrompt').text( - chrome.i18n.getMessage('hostsFilesStats') + vAPI.i18n('hostsFilesStats') .replace('{{blockedHostnameCount}}', details.blockedHostnameCount.toLocaleString()) ); uDom('#autoUpdate').prop('checked', listDetails.autoUpdate === true); @@ -191,7 +191,7 @@ var renderBlacklists = function() { updateWidgets(); }; - messaging.ask({ what: 'getLists' }, onListsReceived); + messager.send({ what: 'getLists' }, onListsReceived); }; /******************************************************************************/ @@ -266,7 +266,7 @@ var onListCheckboxChanged = function() { /******************************************************************************/ var onListLinkClicked = function(ev) { - messaging.tell({ + messager.send({ what: 'gotoExtensionURL', url: 'asset-viewer.html?url=' + uDom(this).attr('href') }); @@ -282,7 +282,7 @@ var onPurgeClicked = function() { if ( !href ) { return; } - messaging.tell({ what: 'purgeCache', path: href }); + messager.send({ what: 'purgeCache', path: href }); button.remove(); if ( li.descendants('input').first().prop('checked') ) { cacheWasPurged = true; @@ -312,7 +312,7 @@ var reloadAll = function(update) { off: lis.subset(i, 1).descendants('input').prop('checked') === false }); } - messaging.tell({ + messager.send({ what: 'reloadHostsFiles', switches: switches, update: update @@ -341,13 +341,13 @@ var buttonPurgeAllHandler = function() { var onCompleted = function() { renderBlacklists(); }; - messaging.ask({ what: 'purgeAllCaches' }, onCompleted); + messager.send({ what: 'purgeAllCaches' }, onCompleted); }; /******************************************************************************/ var autoUpdateCheckboxChanged = function() { - messaging.tell({ + messager.send({ what: 'userSettings', name: 'autoUpdate', value: this.checked @@ -361,7 +361,7 @@ var renderExternalLists = function() { uDom('#externalHostsFiles').val(details); externalHostsFiles = details; }; - messaging.ask({ what: 'userSettings', name: 'externalHostsFiles' }, onReceived); + messager.send({ what: 'userSettings', name: 'externalHostsFiles' }, onReceived); }; /******************************************************************************/ @@ -377,7 +377,7 @@ var externalListsChangeHandler = function() { var externalListsApplyHandler = function() { externalHostsFiles = uDom('#externalHostsFiles').val(); - messaging.tell({ + messager.send({ what: 'userSettings', name: 'externalHostsFiles', value: externalHostsFiles diff --git a/src/js/httpsb.js b/src/js/httpsb.js index 949b012..53cbea8 100644 --- a/src/js/httpsb.js +++ b/src/js/httpsb.js @@ -198,27 +198,11 @@ /******************************************************************************/ µMatrix.turnOff = function() { - // rhill 2013-12-07: - // Relinquish control over javascript execution to the user. - // https://github.com/gorhill/httpswitchboard/issues/74 - chrome.contentSettings.javascript.clear({}); + vAPI.app.start(); }; µMatrix.turnOn = function() { - chrome.contentSettings.javascript.clear({}); - - // rhill 2013-12-07: - // Tell Chromium to allow all javascript: µMatrix will control whether - // javascript execute through `Content-Policy-Directive` and webRequest. - // https://github.com/gorhill/httpswitchboard/issues/74 - chrome.contentSettings.javascript.set({ - primaryPattern: 'https://*/*', - setting: 'allow' - }); - chrome.contentSettings.javascript.set({ - primaryPattern: 'http://*/*', - setting: 'allow' - }); + vAPI.app.stop(); }; /******************************************************************************/ diff --git a/src/js/i18n.js b/src/js/i18n.js index da66f6d..397a73b 100644 --- a/src/js/i18n.js +++ b/src/js/i18n.js @@ -37,7 +37,7 @@ window.addEventListener('load', function() { var node; while ( i-- ) { node = nodeList[i]; - node.innerHTML = chrome.i18n.getMessage(node.getAttribute('data-i18n')); + node.innerHTML = vAPI.i18n(node.getAttribute('data-i18n')); } // copy text of

    if any to document title node = document.querySelector('h1'); @@ -49,6 +49,6 @@ window.addEventListener('load', function() { i = nodeList.length; while ( i-- ) { node = nodeList[i]; - node.setAttribute('data-tip', chrome.i18n.getMessage(node.getAttribute('data-i18n-tip'))); + node.setAttribute('data-tip', vAPI.i18n(node.getAttribute('data-i18n-tip'))); } }); diff --git a/src/js/info.js b/src/js/info.js index 9c46b1a..dd64424 100644 --- a/src/js/info.js +++ b/src/js/info.js @@ -19,15 +19,17 @@ Home: https://github.com/gorhill/uMatrix */ -/* global messaging, uDom */ +/* global vAPI, uDom */ /******************************************************************************/ (function() { +'use strict'; + /******************************************************************************/ -messaging.start('info.js'); +var messager = vAPI.messaging.channel('info.js'); var targetUrl = 'all'; var maxRequests = 500; @@ -55,7 +57,7 @@ function updateRequestData(callback) { what: 'getRequestLogs', pageURL: targetUrl !== 'all' ? targetUrl : null }; - messaging.ask(request, onResponseReceived); + messager.send(request, onResponseReceived); } /******************************************************************************/ @@ -65,7 +67,7 @@ function clearRequestData() { what: 'clearRequestLogs', pageURL: targetUrl !== 'all' ? targetUrl : null }; - messaging.tell(request); + messager.send(request); } /******************************************************************************/ @@ -93,7 +95,7 @@ function renderNumbers(set) { var renderLocalized = function(id, map) { var uElem = uDom('#' + id); - var msg = chrome.i18n.getMessage(id); + var msg = vAPI.i18n(id); for ( var k in map ) { if ( map.hasOwnProperty(k) === false ) { continue; @@ -144,7 +146,7 @@ function renderPageUrls() { // Select whatever needs to be selected //uDom('#selectPageUrls > option[value="'+targetUrl+'"]').prop('selected', true); }; - messaging.ask({ what: 'getPageURLs' }, onResponseReceived); + messager.send({ what: 'getPageURLs' }, onResponseReceived); } /******************************************************************************/ @@ -193,7 +195,7 @@ function renderStats() { uDom('a').attr('target', '_blank'); }; - messaging.ask({ + messager.send({ what: 'getStats', pageURL: targetUrl === 'all' ? null : targetUrl }, @@ -285,7 +287,7 @@ var clearRequests = function() { function changeUserSettings(name, value) { cachedUserSettings[name] = value; - messaging.tell({ + messager.send({ what: 'userSettings', name: name, value: value @@ -404,7 +406,7 @@ uDom.onLoad(function(){ installEventHandlers(); }; - messaging.ask({ what: 'getUserSettings' }, onResponseReceived); + messager.send({ what: 'getUserSettings' }, onResponseReceived); renderTransientData(true); updateRequests(); diff --git a/src/js/messaging-client.js b/src/js/messaging-client.js deleted file mode 100644 index a430d1c..0000000 --- a/src/js/messaging-client.js +++ /dev/null @@ -1,141 +0,0 @@ -/******************************************************************************* - - µMatrix - a Chromium browser extension to black/white list requests. - Copyright (C) 2014 Raymond Hill - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see {http://www.gnu.org/licenses/}. - - Home: https://github.com/gorhill/uMatrix -*/ - -// This is the reference client-side implementation of µBlock's messaging -// infrastructure. The "server"-side implementation is in messaging.js. - -// The client-side implementation creates a port in order to connect to -// µBlock's background page. With this port we can "ask", "tell" or "announce": -// -// "ask": send a request and expect an answer using a callback. -// "tell": send a request with no expectation of an answer. -// "announce": send a request to be relayed to all connections -- no answer -// expected. -// -// The tricky part in this implementation is to ensure all the requests are -// uniquely identified, so that the background-page can keep track of these -// until it is ready to send back an answer, which will be tagged with the -// same id. The uniqueness must be true for all ports which connect to the -// background page at any given time. -// -// Currently using Math.random() to generate this id... I don't know about the -// implementation of Math.random(), but as long as I have a good expectation -// of uniqueness, it's ok, we are not dealing with critical stuff here. - -/* global chrome */ - -var messaging = (function(name){ - var port = null; - var requestId = 1; - var requestIdToCallbackMap = {}; - var listenCallback = null; - - var onPortMessage = function(details) { - if ( typeof details.id !== 'number' ) { - return; - } - // Announcement? - if ( details.id < 0 ) { - if ( listenCallback ) { - listenCallback(details.msg); - } - return; - } - var callback = requestIdToCallbackMap[details.id]; - if ( !callback ) { - return; - } - // Must be removed before calling client to be sure to not execute - // callback again if the client stops the messaging service. - delete requestIdToCallbackMap[details.id]; - callback(details.msg); - }; - - var start = function(name) { - port = chrome.runtime.connect({ name: name }); - port.onMessage.addListener(onPortMessage); - - // https://github.com/gorhill/uBlock/issues/193 - port.onDisconnect.addListener(stop); - }; - - var stop = function() { - listenCallback = null; - port.disconnect(); - port = null; - flushCallbacks(); - }; - - if ( typeof name === 'string' && name !== '' ) { - start(name); - } - - var ask = function(msg, callback) { - if ( port === null ) { - if ( typeof callback === 'function' ) { - callback(); - } - return; - } - if ( callback === undefined ) { - tell(msg); - return; - } - var id = requestId++; - port.postMessage({ id: id, msg: msg }); - requestIdToCallbackMap[id] = callback; - }; - - var tell = function(msg) { - if ( port !== null ) { - port.postMessage({ id: 0, msg: msg }); - } - }; - - var listen = function(callback) { - listenCallback = callback; - }; - - var flushCallbacks = function() { - var callback; - for ( var id in requestIdToCallbackMap ) { - if ( requestIdToCallbackMap.hasOwnProperty(id) === false ) { - continue; - } - callback = requestIdToCallbackMap[id]; - if ( !callback ) { - continue; - } - // Must be removed before calling client to be sure to not execute - // callback again if the client stops the messaging service. - delete requestIdToCallbackMap[id]; - callback(); - } - }; - - return { - start: start, - stop: stop, - ask: ask, - tell: tell, - listen: listen - }; -})(); diff --git a/src/js/messaging-handlers.js b/src/js/messaging-handlers.js deleted file mode 100644 index 876871b..0000000 --- a/src/js/messaging-handlers.js +++ /dev/null @@ -1,816 +0,0 @@ -/******************************************************************************* - - µMatrix - a Chromium browser extension to black/white list requests. - Copyright (C) 2014 Raymond Hill - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see {http://www.gnu.org/licenses/}. - - Home: https://github.com/gorhill/uMatrix -*/ - -/* global chrome, µMatrix */ -/* jshint boss: true */ - -/******************************************************************************/ -/******************************************************************************/ - -(function() { - -// popup.js - -var µm = µMatrix; - -/******************************************************************************/ - -var smartReload = function(tabs) { - var i = tabs.length; - while ( i-- ) { - µm.smartReloadTabs(µm.userSettings.smartAutoReload, tabs[i].id); - } -}; - -/******************************************************************************/ - -// Constructor is faster than object literal - -var RowSnapshot = function(srcHostname, desHostname, desDomain) { - this.domain = desDomain; - this.temporary = µm.tMatrix.evaluateRowZXY(srcHostname, desHostname); - this.permanent = µm.pMatrix.evaluateRowZXY(srcHostname, desHostname); - this.counts = RowSnapshot.counts.slice(); - this.totals = RowSnapshot.counts.slice(); -}; - -RowSnapshot.counts = (function() { - var i = Object.keys(µm.Matrix.getColumnHeaders()).length; - var aa = new Array(i); - while ( i-- ) { - aa[i] = 0; - } - return aa; -})(); - -/******************************************************************************/ - -var matrixSnapshot = function(details) { - var µmuser = µm.userSettings; - var r = { - tabId: details.tabId, - url: '', - hostname: '', - domain: '', - blockedCount: 0, - scope: '*', - headers: µm.Matrix.getColumnHeaders(), - tSwitches: {}, - pSwitches: {}, - rows: {}, - rowCount: 0, - diff: [], - userSettings: { - colorBlindFriendly: µmuser.colorBlindFriendly, - displayTextSize: µmuser.displayTextSize, - popupCollapseDomains: µmuser.popupCollapseDomains, - popupCollapseSpecificDomains: µmuser.popupCollapseSpecificDomains, - popupHideBlacklisted: µmuser.popupHideBlacklisted, - popupScopeLevel: µmuser.popupScopeLevel - } - }; - - // Allow examination of behind-the-scene requests - // TODO: Not portable - if ( details.tabURL ) { - if ( details.tabURL.indexOf('chrome-extension://' + chrome.runtime.id + '/') === 0 ) { - details.tabId = µm.behindTheSceneTabId; - } else if ( details.tabURL === µm.behindTheSceneURL ) { - details.tabId = µm.behindTheSceneTabId; - } - } - - var pageStore = µm.pageStatsFromTabId(details.tabId); - if ( !pageStore ) { - return r; - } - - var headers = r.headers; - - r.url = pageStore.pageUrl; - r.hostname = pageStore.pageHostname; - r.domain = pageStore.pageDomain; - r.blockedCount = pageStore.requestStats.blocked.all; - - if ( µmuser.popupScopeLevel === 'site' ) { - r.scope = r.hostname; - } else if ( µmuser.popupScopeLevel === 'domain' ) { - r.scope = r.domain; - } - - var switchNames = µm.Matrix.getSwitchNames(); - for ( var switchName in switchNames ) { - if ( switchNames.hasOwnProperty(switchName) === false ) { - continue; - } - r.tSwitches[switchName] = µm.tMatrix.evaluateSwitchZ(switchName, r.scope); - r.pSwitches[switchName] = µm.pMatrix.evaluateSwitchZ(switchName, r.scope); - } - - // These rows always exist - r.rows['*'] = new RowSnapshot(r.scope, '*', '*'); - r.rows['1st-party'] = new RowSnapshot(r.scope, '1st-party', '1st-party'); - r.rowCount += 1; - - var µmuri = µm.URI; - var reqKey, reqType, reqHostname, reqDomain; - var desHostname; - var row, typeIndex; - var anyIndex = headers['*']; - - var pageRequests = pageStore.requests; - var reqKeys = pageRequests.getRequestKeys(); - var iReqKey = reqKeys.length; - var pos; - - while ( iReqKey-- ) { - reqKey = reqKeys[iReqKey]; - reqType = pageRequests.typeFromRequestKey(reqKey); - reqHostname = pageRequests.hostnameFromRequestKey(reqKey); - // rhill 2013-10-23: hostname can be empty if the request is a data url - // https://github.com/gorhill/httpswitchboard/issues/26 - if ( reqHostname === '' ) { - reqHostname = pageStore.pageHostname; - } - reqDomain = µmuri.domainFromHostname(reqHostname) || reqHostname; - - // We want rows of self and ancestors - desHostname = reqHostname; - for ( ;; ) { - // If row exists, ancestors exist - if ( r.rows.hasOwnProperty(desHostname) !== false ) { - break; - } - r.rows[desHostname] = new RowSnapshot(r.scope, desHostname, reqDomain); - r.rowCount += 1; - if ( desHostname === reqDomain ) { - break; - } - pos = desHostname.indexOf('.'); - if ( pos === -1 ) { - break; - } - desHostname = desHostname.slice(pos + 1); - } - - typeIndex = headers[reqType]; - - row = r.rows[reqHostname]; - row.counts[typeIndex] += 1; - row.counts[anyIndex] += 1; - - row = r.rows[reqDomain]; - row.totals[typeIndex] += 1; - row.totals[anyIndex] += 1; - - row = r.rows['*']; - row.totals[typeIndex] += 1; - row.totals[anyIndex] += 1; - } - - r.diff = µm.tMatrix.diff(µm.pMatrix, r.hostname, Object.keys(r.rows)); - - return r; -}; - -/******************************************************************************/ - -var onMessage = function(request, sender, callback) { - // Async - switch ( request.what ) { - default: - break; - } - - // Sync - var response; - - switch ( request.what ) { - case 'disconnected': - // https://github.com/gorhill/httpswitchboard/issues/94 - if ( µm.userSettings.smartAutoReload ) { - chrome.tabs.query({ active: true }, smartReload); - } - break; - - case 'matrixSnapshot': - response = matrixSnapshot(request); - break; - - case 'toggleMatrixSwitch': - µm.tMatrix.setSwitchZ( - request.switchName, - request.srcHostname, - µm.tMatrix.evaluateSwitchZ(request.switchName, request.srcHostname) === false - ); - break; - - case 'blacklistMatrixCell': - µm.tMatrix.blacklistCell( - request.srcHostname, - request.desHostname, - request.type - ); - break; - - case 'whitelistMatrixCell': - µm.tMatrix.whitelistCell( - request.srcHostname, - request.desHostname, - request.type - ); - break; - - case 'graylistMatrixCell': - µm.tMatrix.graylistCell( - request.srcHostname, - request.desHostname, - request.type - ); - break; - - case 'applyDiffToPermanentMatrix': // aka "persist" - if ( µm.pMatrix.applyDiff(request.diff, µm.tMatrix) ) { - µm.saveMatrix(); - } - break; - - case 'applyDiffToTemporaryMatrix': // aka "revert" - µm.tMatrix.applyDiff(request.diff, µm.pMatrix); - break; - - case 'revertTemporaryMatrix': - µm.tMatrix.assign(µm.pMatrix); - break; - - default: - return µm.messaging.defaultHandler(request, sender, callback); - } - - callback(response); -}; - -µm.messaging.listen('popup.js', onMessage); - -})(); - -/******************************************************************************/ -/******************************************************************************/ - -// content scripts - -(function() { - -var µm = µMatrix; - -/******************************************************************************/ - -var contentScriptSummaryHandler = function(tabId, details) { - // TODO: Investigate "Error in response to tabs.executeScript: TypeError: - // Cannot read property 'locationURL' of null" (2013-11-12). When can this - // happens? - if ( !details || !details.locationURL ) { - return; - } - var pageURL = µm.pageUrlFromTabId(tabId); - var pageStats = µm.pageStatsFromPageUrl(pageURL); - var µmuri = µm.URI.set(details.locationURL); - var frameURL = µmuri.normalizedURI(); - var frameHostname = µmuri.hostname; - var urls, url, r; - - // https://github.com/gorhill/httpswitchboard/issues/333 - // Look-up here whether inline scripting is blocked for the frame. - var inlineScriptBlocked = µm.mustBlock(µm.scopeFromURL(pageURL), frameHostname, 'script'); - - // scripts - // https://github.com/gorhill/httpswitchboard/issues/25 - if ( pageStats && inlineScriptBlocked ) { - urls = details.scriptSources; - for ( url in urls ) { - if ( !urls.hasOwnProperty(url) ) { - continue; - } - if ( url === '{inline_script}' ) { - url = frameURL + '{inline_script}'; - } - r = µm.filterRequest(pageURL, 'script', url); - pageStats.recordRequest('script', url, r !== false, r); - } - } - - // TODO: as of 2014-05-26, not sure this is needed anymore, since µMatrix - // no longer uses chrome.contentSettings API (I think that was the reason - // this code was put in). - // plugins - // https://github.com/gorhill/httpswitchboard/issues/25 - if ( pageStats ) { - urls = details.pluginSources; - for ( url in urls ) { - if ( !urls.hasOwnProperty(url) ) { - continue; - } - r = µm.filterRequest(pageURL, 'plugin', url); - pageStats.recordRequest('plugin', url, r !== false, r); - } - } - - // https://github.com/gorhill/httpswitchboard/issues/181 - µm.onPageLoadCompleted(pageURL); -}; - -/******************************************************************************/ - -var contentScriptLocalStorageHandler = function(pageURL) { - var µmuri = µm.URI.set(pageURL); - var response = µm.mustBlock(µm.scopeFromURL(pageURL), µmuri.hostname, 'cookie'); - µm.recordFromPageUrl( - pageURL, - 'cookie', - µmuri.rootURL() + '/{localStorage}', - response - ); - response = response && µm.userSettings.deleteLocalStorage; - if ( response ) { - µm.localStorageRemovedCounter++; - } - return response; -}; - -/******************************************************************************/ - -var onMessage = function(request, sender, callback) { - // Async - switch ( request.what ) { - default: - break; - } - - var tabId = sender.tab.id; - - // Sync - var response; - - switch ( request.what ) { - case 'contentScriptHasLocalStorage': - response = contentScriptLocalStorageHandler(request.url); - µm.updateBadgeAsync(tabId); - break; - - case 'contentScriptSummary': - contentScriptSummaryHandler(tabId, request); - µm.updateBadgeAsync(tabId); - break; - - case 'checkScriptBlacklisted': - response = { - scriptBlacklisted: µm.mustBlock( - µm.scopeFromURL(request.url), - µm.hostnameFromURL(request.url), - 'script' - ) - }; - break; - - case 'getUserAgentReplaceStr': - response = µm.tMatrix.evaluateSwitchZ('ua-spoof', request.hostname) ? - µm.userAgentReplaceStr : - undefined; - break; - - default: - return µm.messaging.defaultHandler(request, sender, callback); - } - - callback(response); -}; - -µMatrix.messaging.listen('contentscript-start.js', onMessage); -µMatrix.messaging.listen('contentscript-end.js', onMessage); - -/******************************************************************************/ - -})(); - -/******************************************************************************/ -/******************************************************************************/ - -// settings.js - -(function() { - -var onMessage = function(request, sender, callback) { - var µm = µMatrix; - - // Async - switch ( request.what ) { - default: - break; - } - - // Sync - var response; - - switch ( request.what ) { - default: - return µm.messaging.defaultHandler(request, sender, callback); - } - - callback(response); -}; - -µMatrix.messaging.listen('settings.js', onMessage); - -})(); - -/******************************************************************************/ -/******************************************************************************/ - -// privacy.js - -(function() { - -var onMessage = function(request, sender, callback) { - var µm = µMatrix; - - // Async - switch ( request.what ) { - default: - break; - } - - // Sync - var response; - - switch ( request.what ) { - case 'getPrivacySettings': - response = { - userSettings: µm.userSettings, - matrixSwitches: { - 'https-strict': µm.pMatrix.evaluateSwitch('https-strict', '*') === 1, - 'ua-spoof': µm.pMatrix.evaluateSwitch('ua-spoof', '*') === 1, - 'referrer-spoof': µm.pMatrix.evaluateSwitch('referrer-spoof', '*') === 1 - } - }; - break; - - case 'setMatrixSwitch': - µm.tMatrix.setSwitch(request.switchName, '*', request.state); - if ( µm.pMatrix.setSwitch(request.switchName, '*', request.state) ) { - µm.saveMatrix(); - } - break; - - default: - return µm.messaging.defaultHandler(request, sender, callback); - } - - callback(response); -}; - -µMatrix.messaging.listen('privacy.js', onMessage); - -})(); - -/******************************************************************************/ -/******************************************************************************/ - -// user-rules.js - -(function() { - -var µm = µMatrix; - -/******************************************************************************/ - -var onMessage = function(request, sender, callback) { - - // Async - switch ( request.what ) { - default: - break; - } - - // Sync - var response; - - switch ( request.what ) { - case 'getUserRules': - response = { - temporaryRules: µm.tMatrix.toString(), - permanentRules: µm.pMatrix.toString() - }; - break; - - case 'setUserRules': - if ( typeof request.temporaryRules === 'string' ) { - µm.tMatrix.fromString(request.temporaryRules); - } - if ( typeof request.permanentRules === 'string' ) { - µm.pMatrix.fromString(request.permanentRules); - µm.saveMatrix(); - } - response = { - temporaryRules: µm.tMatrix.toString(), - permanentRules: µm.pMatrix.toString() - }; - break; - - default: - return µm.messaging.defaultHandler(request, sender, callback); - } - - callback(response); -}; - -µMatrix.messaging.listen('user-rules.js', onMessage); - -})(); - -/******************************************************************************/ -/******************************************************************************/ - -// hosts-files.js - -(function() { - -var µm = µMatrix; - -/******************************************************************************/ - -var getLists = function(callback) { - var r = { - available: null, - cache: null, - current: µm.liveHostsFiles, - blockedHostnameCount: µm.ubiquitousBlacklist.count, - autoUpdate: µm.userSettings.autoUpdate - }; - var onMetadataReady = function(entries) { - r.cache = entries; - callback(r); - }; - var onAvailableHostsFilesReady = function(lists) { - r.available = lists; - µm.assets.metadata(onMetadataReady); - }; - µm.getAvailableHostsFiles(onAvailableHostsFilesReady); -}; - -/******************************************************************************/ - -var onMessage = function(request, sender, callback) { - var µm = µMatrix; - - // Async - switch ( request.what ) { - case 'getLists': - return getLists(callback); - - case 'purgeAllCaches': - return µm.assets.purgeAll(callback); - - default: - break; - } - - // Sync - var response; - - switch ( request.what ) { - case 'purgeCache': - µm.assets.purge(request.path); - break; - - default: - return µm.messaging.defaultHandler(request, sender, callback); - } - - callback(response); -}; - -µMatrix.messaging.listen('hosts-files.js', onMessage); - -})(); - -/******************************************************************************/ -/******************************************************************************/ - -// info.js - -(function() { - -/******************************************************************************/ - -// map(pageURL) => array of request log entries - -var getRequestLog = function(pageURL) { - var requestLogs = {}; - var pageStores = µMatrix.pageStats; - var pageURLs = pageURL ? [pageURL] : Object.keys(pageStores); - var pageStore, pageRequestLog, logEntries, j, logEntry; - - for ( var i = 0; i < pageURLs.length; i++ ) { - pageURL = pageURLs[i]; - pageStore = pageStores[pageURL]; - if ( !pageStore ) { - continue; - } - pageRequestLog = []; - logEntries = pageStore.requests.getLoggedRequests(); - j = logEntries.length; - while ( j-- ) { - // rhill 2013-12-04: `logEntry` can be null since a ring buffer is - // now used, and it might not have been filled yet. - if ( logEntry = logEntries[j] ) { - pageRequestLog.push(logEntry); - } - } - requestLogs[pageURL] = pageRequestLog; - } - - return requestLogs; -}; - -/******************************************************************************/ - -var clearRequestLog = function(pageURL) { - var pageStores = µMatrix.pageStats; - var pageURLs = pageURL ? [pageURL] : Object.keys(pageStores); - var pageStore; - - for ( var i = 0; i < pageURLs.length; i++ ) { - if ( pageStore = pageStores[pageURLs[i]] ) { - pageStore.requests.clearLogBuffer(); - } - } -}; - -/******************************************************************************/ - -var onMessage = function(request, sender, callback) { - var µm = µMatrix; - - // Async - switch ( request.what ) { - default: - break; - } - - // Sync - var response; - - switch ( request.what ) { - case 'getPageURLs': - response = { - pageURLs: Object.keys(µm.pageUrlToTabId), - behindTheSceneURL: µm.behindTheSceneURL - }; - break; - - case 'getStats': - var pageStore = µm.pageStats[request.pageURL]; - response = { - globalNetStats: µm.requestStats, - pageNetStats: pageStore ? pageStore.requestStats : null, - cookieHeaderFoiledCounter: µm.cookieHeaderFoiledCounter, - refererHeaderFoiledCounter: µm.refererHeaderFoiledCounter, - hyperlinkAuditingFoiledCounter: µm.hyperlinkAuditingFoiledCounter, - cookieRemovedCounter: µm.cookieRemovedCounter, - localStorageRemovedCounter: µm.localStorageRemovedCounter, - browserCacheClearedCounter: µm.browserCacheClearedCounter - }; - break; - - case 'getRequestLogs': - response = getRequestLog(request.pageURL); - break; - - case 'clearRequestLogs': - clearRequestLog(request.pageURL); - break; - - default: - return µm.messaging.defaultHandler(request, sender, callback); - } - - callback(response); -}; - -µMatrix.messaging.listen('info.js', onMessage); - -})(); - -/******************************************************************************/ -/******************************************************************************/ - -// about.js - -(function() { - -var µm = µMatrix; - -/******************************************************************************/ - -var restoreUserData = function(userData) { - var countdown = 3; - var onCountdown = function() { - countdown -= 1; - if ( countdown === 0 ) { - µm.XAL.restart(); - } - }; - - var onAllRemoved = function() { - // Be sure to adjust `countdown` if adding/removing anything below - µm.XAL.keyvalSetMany(userData.settings, onCountdown); - µm.XAL.keyvalSetOne('userMatrix', userData.rules, onCountdown); - µm.XAL.keyvalSetOne('liveHostsFiles', userData.hostsFiles, onCountdown); - }; - - // If we are going to restore all, might as well wipe out clean local - // storage - µm.XAL.keyvalRemoveAll(onAllRemoved); -}; - -/******************************************************************************/ - -var resetUserData = function() { - var onAllRemoved = function() { - µm.XAL.restart(); - }; - µm.XAL.keyvalRemoveAll(onAllRemoved); -}; - -/******************************************************************************/ - -var onMessage = function(request, sender, callback) { - // Async - switch ( request.what ) { - default: - break; - } - - // Sync - var response; - - switch ( request.what ) { - case 'getAllUserData': - response = { - app: 'µMatrix', - version: µm.manifest.version, - when: Date.now(), - settings: µm.userSettings, - rules: µm.pMatrix.toString(), - hostsFiles: µm.liveHostsFiles - }; - break; - - case 'getSomeStats': - response = { - version: µm.manifest.version, - storageQuota: µm.storageQuota, - storageUsed: µm.storageUsed - }; - break; - - case 'restoreAllUserData': - restoreUserData(request.userData); - break; - - case 'resetAllUserData': - resetUserData(); - break; - - default: - return µm.messaging.defaultHandler(request, sender, callback); - } - - callback(response); -}; - -µMatrix.messaging.listen('about.js', onMessage); - -/******************************************************************************/ - -})(); - -/******************************************************************************/ diff --git a/src/js/messaging.js b/src/js/messaging.js index 2becf2c..160e28f 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -19,163 +19,64 @@ Home: https://github.com/gorhill/uMatrix */ -// So there might be memory leaks related to the direct use of sendMessage(), -// as per https://code.google.com/p/chromium/issues/detail?id=320723. The issue -// is not marked as resolved, and the last message from chromium dev is: -// -// "You can construct Port objects (runtime.connect) and emulate sendMessage -// "behaviour. The bug is that sendMessage doesn't clean up its Ports." -// -// So the point here is to have an infrastructure which allows relying more on -// direct use of Port objects rather than going through sendMessage(). +/* global µMatrix, vAPI */ +/* jshint boss: true */ /******************************************************************************/ -/******************************************************************************* - -// Here this is the "server"-side implementation. -// -// Reference client-side implementation is found in: -// -// messaging-client.js -// -// For instance, it needs to be cut & pasted for content scripts since -// I can not include in a simple way js file content from another js file. - -*******************************************************************************/ /******************************************************************************/ -µMatrix.messaging = (function() { +// Default handler -/******************************************************************************/ +(function() { + +'use strict'; var µm = µMatrix; -var runtimeIdGenerator = 1; -var nameToPortMap = {}; -var nameToListenerMap = {}; -var nullFunc = function(){}; - -/******************************************************************************/ - -var listenerNameFromPortName = function(portName) { - var pos = portName.indexOf('/'); - if ( pos === -1 ) { - return ''; - } - return portName.slice(0, pos); -}; - -var listenerFromPortName = function(portName) { - return nameToListenerMap[listenerNameFromPortName(portName)]; -}; - -/******************************************************************************/ - -var listen = function(portName, callback) { - var listener = nameToListenerMap[portName]; - if ( listener && listener !== callback ) { - throw 'Only one listener allowed'; - } - nameToListenerMap[portName] = callback; -}; - -/******************************************************************************/ - -var tell = function(target, msg) { - target += '/'; - for ( var portName in nameToPortMap ) { - if ( nameToPortMap.hasOwnProperty(portName) === false ) { - continue; - } - if ( portName.indexOf(target) === 0 ) { - nameToPortMap[portName].postMessage({ id: -1, msg: msg }); - } - } -}; - -/******************************************************************************/ - -var announce = function(msg) { - // Background page handler - defaultHandler(msg, null, nullFunc); - - // Extension pages & content scripts handlers - for ( var portName in nameToPortMap ) { - if ( nameToPortMap.hasOwnProperty(portName) === false ) { - continue; - } - nameToPortMap[portName].postMessage({ id: -1, msg: msg }); - } -}; - -/******************************************************************************/ - -var onMessage = function(request, port) { - var reqId = request.id; - // Annoucement: dispatch everywhere. - if ( reqId < 0 ) { - announce(request.msg); - return; - } - var listener = listenerFromPortName(port.name) || defaultHandler; - // Being told - if ( reqId === 0 ) { - listener(request.msg, port.sender, nullFunc); - return; - } - // Being asked - listener(request.msg, port.sender, function(response) { - port.postMessage({ - id: reqId, - msg: response !== undefined ? response : null - }); - }); -}; /******************************************************************************/ // Default is for commonly used message. -function defaultHandler(request, sender, callback) { +function onMessage(request, sender, callback) { // Async switch ( request.what ) { - case 'getAssetContent': - return µm.assets.getLocal(request.url, callback); + case 'getAssetContent': + return µm.assets.getLocal(request.url, callback); - default: - break; + default: + break; } // Sync var response; switch ( request.what ) { - case 'forceReloadTab': - µm.forceReload(request.tabId); - break; + case 'forceReloadTab': + µm.forceReload(request.tabId); + break; - case 'getUserSettings': - response = µm.userSettings; - break; + case 'getUserSettings': + response = µm.userSettings; + break; - case 'gotoExtensionURL': - µm.utils.gotoExtensionURL(request.url); - break; + case 'gotoExtensionURL': + µm.utils.gotoExtensionURL(request.url); + break; - case 'gotoURL': - µm.utils.gotoURL(request); - break; + case 'gotoURL': + µm.utils.gotoURL(request); + break; - case 'reloadHostsFiles': - µm.reloadHostsFiles(request.switches, request.update); - break; + case 'reloadHostsFiles': + µm.reloadHostsFiles(request.switches, request.update); + break; - case 'userSettings': - response = µm.changeUserSettings(request.name, request.value); - break; + case 'userSettings': + response = µm.changeUserSettings(request.name, request.value); + break; - default: - // console.error('messaging.js / defaultHandler > unknown request: %o', request); - break; + default: + return vAPI.messaging.UNHANDLED; } callback(response); @@ -183,57 +84,814 @@ function defaultHandler(request, sender, callback) { /******************************************************************************/ -// Port disconnected, relay this information to apropriate listener. - -var onDisconnect = function(port) { - // Notify listener of the disconnection -- using a reserved message id. - var listener = listenerFromPortName(port.name) || defaultHandler; - var msg = { - 'what': 'disconnected', - 'which': listenerNameFromPortName(port.name) - }; - listener(msg, port.sender, nullFunc); - - // Cleanup port if no longer in use. - if ( nameToPortMap.hasOwnProperty(port.name) ) { - delete nameToPortMap[port.name]; - port.onMessage.removeListener(onMessage); - port.onDisconnect.removeListener(onDisconnect); - } -}; - -/******************************************************************************/ - -var onConnect = function(port) { - // We must have a port name. - if ( typeof port.name !== 'string' || port.name === '' ) { - console.error('messaging.js / onConnectHandler(): no port name!'); - return; - } - - // Ensure port name is unique - port.name += '/' + runtimeIdGenerator++; - - nameToPortMap[port.name] = port; - port.onMessage.addListener(onMessage); - port.onDisconnect.addListener(onDisconnect); -}; - -/******************************************************************************/ - -chrome.runtime.onConnect.addListener(onConnect); - -/******************************************************************************/ - -return { - listen: listen, - tell: tell, - announce: announce, - defaultHandler: defaultHandler -}; +vAPI.messaging.setup(onMessage); /******************************************************************************/ })(); /******************************************************************************/ +/******************************************************************************/ + +(function() { + +// popup.js + +var µm = µMatrix; + +/******************************************************************************/ + +var smartReload = function(tabs) { + var i = tabs.length; + while ( i-- ) { + µm.smartReloadTabs(µm.userSettings.smartAutoReload, tabs[i].id); + } +}; + +/******************************************************************************/ + +// Constructor is faster than object literal + +var RowSnapshot = function(srcHostname, desHostname, desDomain) { + this.domain = desDomain; + this.temporary = µm.tMatrix.evaluateRowZXY(srcHostname, desHostname); + this.permanent = µm.pMatrix.evaluateRowZXY(srcHostname, desHostname); + this.counts = RowSnapshot.counts.slice(); + this.totals = RowSnapshot.counts.slice(); +}; + +RowSnapshot.counts = (function() { + var i = Object.keys(µm.Matrix.getColumnHeaders()).length; + var aa = new Array(i); + while ( i-- ) { + aa[i] = 0; + } + return aa; +})(); + +/******************************************************************************/ + +var matrixSnapshot = function(tabId, details) { + var µmuser = µm.userSettings; + var r = { + tabId: tabId, + url: '', + hostname: '', + domain: '', + blockedCount: 0, + scope: '*', + headers: µm.Matrix.getColumnHeaders(), + tSwitches: {}, + pSwitches: {}, + rows: {}, + rowCount: 0, + diff: [], + userSettings: { + colorBlindFriendly: µmuser.colorBlindFriendly, + displayTextSize: µmuser.displayTextSize, + popupCollapseDomains: µmuser.popupCollapseDomains, + popupCollapseSpecificDomains: µmuser.popupCollapseSpecificDomains, + popupHideBlacklisted: µmuser.popupHideBlacklisted, + popupScopeLevel: µmuser.popupScopeLevel + } + }; + + // Allow examination of behind-the-scene requests + // TODO: Not portable + if ( details.tabURL ) { + if ( details.tabURL.lastIndexOf(vAPI.getURL(''), 0) === 0 ) { + tabId = µm.behindTheSceneTabId; + } else if ( details.tabURL === µm.behindTheSceneURL ) { + tabId = µm.behindTheSceneTabId; + } + } + + var pageStore = µm.pageStatsFromTabId(tabId); + if ( !pageStore ) { + return r; + } + + var headers = r.headers; + + r.url = pageStore.pageUrl; + r.hostname = pageStore.pageHostname; + r.domain = pageStore.pageDomain; + r.blockedCount = pageStore.requestStats.blocked.all; + + if ( µmuser.popupScopeLevel === 'site' ) { + r.scope = r.hostname; + } else if ( µmuser.popupScopeLevel === 'domain' ) { + r.scope = r.domain; + } + + var switchNames = µm.Matrix.getSwitchNames(); + for ( var switchName in switchNames ) { + if ( switchNames.hasOwnProperty(switchName) === false ) { + continue; + } + r.tSwitches[switchName] = µm.tMatrix.evaluateSwitchZ(switchName, r.scope); + r.pSwitches[switchName] = µm.pMatrix.evaluateSwitchZ(switchName, r.scope); + } + + // These rows always exist + r.rows['*'] = new RowSnapshot(r.scope, '*', '*'); + r.rows['1st-party'] = new RowSnapshot(r.scope, '1st-party', '1st-party'); + r.rowCount += 1; + + var µmuri = µm.URI; + var reqKey, reqType, reqHostname, reqDomain; + var desHostname; + var row, typeIndex; + var anyIndex = headers['*']; + + var pageRequests = pageStore.requests; + var reqKeys = pageRequests.getRequestKeys(); + var iReqKey = reqKeys.length; + var pos; + + while ( iReqKey-- ) { + reqKey = reqKeys[iReqKey]; + reqType = pageRequests.typeFromRequestKey(reqKey); + reqHostname = pageRequests.hostnameFromRequestKey(reqKey); + // rhill 2013-10-23: hostname can be empty if the request is a data url + // https://github.com/gorhill/httpswitchboard/issues/26 + if ( reqHostname === '' ) { + reqHostname = pageStore.pageHostname; + } + reqDomain = µmuri.domainFromHostname(reqHostname) || reqHostname; + + // We want rows of self and ancestors + desHostname = reqHostname; + for ( ;; ) { + // If row exists, ancestors exist + if ( r.rows.hasOwnProperty(desHostname) !== false ) { + break; + } + r.rows[desHostname] = new RowSnapshot(r.scope, desHostname, reqDomain); + r.rowCount += 1; + if ( desHostname === reqDomain ) { + break; + } + pos = desHostname.indexOf('.'); + if ( pos === -1 ) { + break; + } + desHostname = desHostname.slice(pos + 1); + } + + typeIndex = headers[reqType]; + + row = r.rows[reqHostname]; + row.counts[typeIndex] += 1; + row.counts[anyIndex] += 1; + + row = r.rows[reqDomain]; + row.totals[typeIndex] += 1; + row.totals[anyIndex] += 1; + + row = r.rows['*']; + row.totals[typeIndex] += 1; + row.totals[anyIndex] += 1; + } + + r.diff = µm.tMatrix.diff(µm.pMatrix, r.hostname, Object.keys(r.rows)); + + return r; +}; + +/******************************************************************************/ + +var matrixSnapshotFromTabId = function(details, callback) { + if ( details.targetTabId ) { + callback(matrixSnapshot(details.targetTabId, details)); + return; + } + + vAPI.tabs.get(null, function(tab) { + callback(matrixSnapshot(tab.id, details)); + }); +}; + +/******************************************************************************/ + +var onMessage = function(request, sender, callback) { + // Async + switch ( request.what ) { + case 'matrixSnapshot': + matrixSnapshotFromTabId(request, callback); + return; + + default: + break; + } + + // Sync + var response; + + switch ( request.what ) { + case 'disconnected': + // https://github.com/gorhill/httpswitchboard/issues/94 + if ( µm.userSettings.smartAutoReload ) { + vAPI.tabs.get(null, smartReload); + } + break; + + case 'toggleMatrixSwitch': + µm.tMatrix.setSwitchZ( + request.switchName, + request.srcHostname, + µm.tMatrix.evaluateSwitchZ(request.switchName, request.srcHostname) === false + ); + break; + + case 'blacklistMatrixCell': + µm.tMatrix.blacklistCell( + request.srcHostname, + request.desHostname, + request.type + ); + break; + + case 'whitelistMatrixCell': + µm.tMatrix.whitelistCell( + request.srcHostname, + request.desHostname, + request.type + ); + break; + + case 'graylistMatrixCell': + µm.tMatrix.graylistCell( + request.srcHostname, + request.desHostname, + request.type + ); + break; + + case 'applyDiffToPermanentMatrix': // aka "persist" + if ( µm.pMatrix.applyDiff(request.diff, µm.tMatrix) ) { + µm.saveMatrix(); + } + break; + + case 'applyDiffToTemporaryMatrix': // aka "revert" + µm.tMatrix.applyDiff(request.diff, µm.pMatrix); + break; + + case 'revertTemporaryMatrix': + µm.tMatrix.assign(µm.pMatrix); + break; + + default: + return vAPI.messaging.UNHANDLED; + } + + callback(response); +}; + +vAPI.messaging.listen('popup.js', onMessage); + +})(); + +/******************************************************************************/ +/******************************************************************************/ + +// content scripts + +(function() { + +var µm = µMatrix; + +/******************************************************************************/ + +var contentScriptSummaryHandler = function(tabId, details) { + // TODO: Investigate "Error in response to tabs.executeScript: TypeError: + // Cannot read property 'locationURL' of null" (2013-11-12). When can this + // happens? + if ( !details || !details.locationURL ) { + return; + } + var pageURL = µm.pageUrlFromTabId(tabId); + var pageStats = µm.pageStatsFromPageUrl(pageURL); + var µmuri = µm.URI.set(details.locationURL); + var frameURL = µmuri.normalizedURI(); + var frameHostname = µmuri.hostname; + var urls, url, r; + + // https://github.com/gorhill/httpswitchboard/issues/333 + // Look-up here whether inline scripting is blocked for the frame. + var inlineScriptBlocked = µm.mustBlock(µm.scopeFromURL(pageURL), frameHostname, 'script'); + + // scripts + // https://github.com/gorhill/httpswitchboard/issues/25 + if ( pageStats && inlineScriptBlocked ) { + urls = details.scriptSources; + for ( url in urls ) { + if ( !urls.hasOwnProperty(url) ) { + continue; + } + if ( url === '{inline_script}' ) { + url = frameURL + '{inline_script}'; + } + r = µm.filterRequest(pageURL, 'script', url); + pageStats.recordRequest('script', url, r !== false, r); + } + } + + // TODO: as of 2014-05-26, not sure this is needed anymore, since µMatrix + // no longer uses chrome.contentSettings API (I think that was the reason + // this code was put in). + // plugins + // https://github.com/gorhill/httpswitchboard/issues/25 + if ( pageStats ) { + urls = details.pluginSources; + for ( url in urls ) { + if ( !urls.hasOwnProperty(url) ) { + continue; + } + r = µm.filterRequest(pageURL, 'plugin', url); + pageStats.recordRequest('plugin', url, r !== false, r); + } + } + + // https://github.com/gorhill/httpswitchboard/issues/181 + µm.onPageLoadCompleted(pageURL); +}; + +/******************************************************************************/ + +var contentScriptLocalStorageHandler = function(pageURL) { + var µmuri = µm.URI.set(pageURL); + var response = µm.mustBlock(µm.scopeFromURL(pageURL), µmuri.hostname, 'cookie'); + µm.recordFromPageUrl( + pageURL, + 'cookie', + µmuri.rootURL() + '/{localStorage}', + response + ); + response = response && µm.userSettings.deleteLocalStorage; + if ( response ) { + µm.localStorageRemovedCounter++; + } + return response; +}; + +/******************************************************************************/ + +var onMessage = function(request, sender, callback) { + // Async + switch ( request.what ) { + default: + break; + } + + var tabId = sender.tab.id; + + // Sync + var response; + + switch ( request.what ) { + case 'contentScriptHasLocalStorage': + response = contentScriptLocalStorageHandler(request.url); + µm.updateBadgeAsync(tabId); + break; + + case 'contentScriptSummary': + contentScriptSummaryHandler(tabId, request); + µm.updateBadgeAsync(tabId); + break; + + case 'checkScriptBlacklisted': + response = { + scriptBlacklisted: µm.mustBlock( + µm.scopeFromURL(request.url), + µm.hostnameFromURL(request.url), + 'script' + ) + }; + break; + + case 'getUserAgentReplaceStr': + response = µm.tMatrix.evaluateSwitchZ('ua-spoof', request.hostname) ? + µm.userAgentReplaceStr : + undefined; + break; + + default: + return vAPI.messaging.UNHANDLED; + } + + callback(response); +}; + +vAPI.messaging.listen('contentscript-start.js', onMessage); +vAPI.messaging.listen('contentscript-end.js', onMessage); + +/******************************************************************************/ + +})(); + +/******************************************************************************/ +/******************************************************************************/ + +// settings.js + +(function() { + +var onMessage = function(request, sender, callback) { + var µm = µMatrix; + + // Async + switch ( request.what ) { + default: + break; + } + + // Sync + var response; + + switch ( request.what ) { + default: + return vAPI.messaging.UNHANDLED; + } + + callback(response); +}; + +vAPI.messaging.listen('settings.js', onMessage); + +})(); + +/******************************************************************************/ +/******************************************************************************/ + +// privacy.js + +(function() { + +var onMessage = function(request, sender, callback) { + var µm = µMatrix; + + // Async + switch ( request.what ) { + default: + break; + } + + // Sync + var response; + + switch ( request.what ) { + case 'getPrivacySettings': + response = { + userSettings: µm.userSettings, + matrixSwitches: { + 'https-strict': µm.pMatrix.evaluateSwitch('https-strict', '*') === 1, + 'ua-spoof': µm.pMatrix.evaluateSwitch('ua-spoof', '*') === 1, + 'referrer-spoof': µm.pMatrix.evaluateSwitch('referrer-spoof', '*') === 1 + } + }; + break; + + case 'setMatrixSwitch': + µm.tMatrix.setSwitch(request.switchName, '*', request.state); + if ( µm.pMatrix.setSwitch(request.switchName, '*', request.state) ) { + µm.saveMatrix(); + } + break; + + default: + return vAPI.messaging.UNHANDLED; + } + + callback(response); +}; + +vAPI.messaging.listen('privacy.js', onMessage); + +})(); + +/******************************************************************************/ +/******************************************************************************/ + +// user-rules.js + +(function() { + +var µm = µMatrix; + +/******************************************************************************/ + +var onMessage = function(request, sender, callback) { + + // Async + switch ( request.what ) { + default: + break; + } + + // Sync + var response; + + switch ( request.what ) { + case 'getUserRules': + response = { + temporaryRules: µm.tMatrix.toString(), + permanentRules: µm.pMatrix.toString() + }; + break; + + case 'setUserRules': + if ( typeof request.temporaryRules === 'string' ) { + µm.tMatrix.fromString(request.temporaryRules); + } + if ( typeof request.permanentRules === 'string' ) { + µm.pMatrix.fromString(request.permanentRules); + µm.saveMatrix(); + } + response = { + temporaryRules: µm.tMatrix.toString(), + permanentRules: µm.pMatrix.toString() + }; + break; + + default: + return vAPI.messaging.UNHANDLED; + } + + callback(response); +}; + +vAPI.messaging.listen('user-rules.js', onMessage); + +})(); + +/******************************************************************************/ +/******************************************************************************/ + +// hosts-files.js + +(function() { + +var µm = µMatrix; + +/******************************************************************************/ + +var getLists = function(callback) { + var r = { + available: null, + cache: null, + current: µm.liveHostsFiles, + blockedHostnameCount: µm.ubiquitousBlacklist.count, + autoUpdate: µm.userSettings.autoUpdate + }; + var onMetadataReady = function(entries) { + r.cache = entries; + callback(r); + }; + var onAvailableHostsFilesReady = function(lists) { + r.available = lists; + µm.assets.metadata(onMetadataReady); + }; + µm.getAvailableHostsFiles(onAvailableHostsFilesReady); +}; + +/******************************************************************************/ + +var onMessage = function(request, sender, callback) { + var µm = µMatrix; + + // Async + switch ( request.what ) { + case 'getLists': + return getLists(callback); + + case 'purgeAllCaches': + return µm.assets.purgeAll(callback); + + default: + break; + } + + // Sync + var response; + + switch ( request.what ) { + case 'purgeCache': + µm.assets.purge(request.path); + break; + + default: + return vAPI.messaging.UNHANDLED; + } + + callback(response); +}; + +vAPI.messaging.listen('hosts-files.js', onMessage); + +})(); + +/******************************************************************************/ +/******************************************************************************/ + +// info.js + +(function() { + +/******************************************************************************/ + +// map(pageURL) => array of request log entries + +var getRequestLog = function(pageURL) { + var requestLogs = {}; + var pageStores = µMatrix.pageStats; + var pageURLs = pageURL ? [pageURL] : Object.keys(pageStores); + var pageStore, pageRequestLog, logEntries, j, logEntry; + + for ( var i = 0; i < pageURLs.length; i++ ) { + pageURL = pageURLs[i]; + pageStore = pageStores[pageURL]; + if ( !pageStore ) { + continue; + } + pageRequestLog = []; + logEntries = pageStore.requests.getLoggedRequests(); + j = logEntries.length; + while ( j-- ) { + // rhill 2013-12-04: `logEntry` can be null since a ring buffer is + // now used, and it might not have been filled yet. + if ( logEntry = logEntries[j] ) { + pageRequestLog.push(logEntry); + } + } + requestLogs[pageURL] = pageRequestLog; + } + + return requestLogs; +}; + +/******************************************************************************/ + +var clearRequestLog = function(pageURL) { + var pageStores = µMatrix.pageStats; + var pageURLs = pageURL ? [pageURL] : Object.keys(pageStores); + var pageStore; + + for ( var i = 0; i < pageURLs.length; i++ ) { + if ( pageStore = pageStores[pageURLs[i]] ) { + pageStore.requests.clearLogBuffer(); + } + } +}; + +/******************************************************************************/ + +var onMessage = function(request, sender, callback) { + var µm = µMatrix; + + // Async + switch ( request.what ) { + default: + break; + } + + // Sync + var response; + + switch ( request.what ) { + case 'getPageURLs': + response = { + pageURLs: Object.keys(µm.pageUrlToTabId), + behindTheSceneURL: µm.behindTheSceneURL + }; + break; + + case 'getStats': + var pageStore = µm.pageStats[request.pageURL]; + response = { + globalNetStats: µm.requestStats, + pageNetStats: pageStore ? pageStore.requestStats : null, + cookieHeaderFoiledCounter: µm.cookieHeaderFoiledCounter, + refererHeaderFoiledCounter: µm.refererHeaderFoiledCounter, + hyperlinkAuditingFoiledCounter: µm.hyperlinkAuditingFoiledCounter, + cookieRemovedCounter: µm.cookieRemovedCounter, + localStorageRemovedCounter: µm.localStorageRemovedCounter, + browserCacheClearedCounter: µm.browserCacheClearedCounter + }; + break; + + case 'getRequestLogs': + response = getRequestLog(request.pageURL); + break; + + case 'clearRequestLogs': + clearRequestLog(request.pageURL); + break; + + default: + return vAPI.messaging.UNHANDLED; + } + + callback(response); +}; + +vAPI.messaging.listen('info.js', onMessage); + +})(); + +/******************************************************************************/ +/******************************************************************************/ + +// about.js + +(function() { + +var µm = µMatrix; + +/******************************************************************************/ + +var restoreUserData = function(userData) { + var countdown = 3; + var onCountdown = function() { + countdown -= 1; + if ( countdown === 0 ) { + vAPI.app.restart(); + } + }; + + var onAllRemoved = function() { + // Be sure to adjust `countdown` if adding/removing anything below + µm.XAL.keyvalSetMany(userData.settings, onCountdown); + µm.XAL.keyvalSetOne('userMatrix', userData.rules, onCountdown); + µm.XAL.keyvalSetOne('liveHostsFiles', userData.hostsFiles, onCountdown); + }; + + // If we are going to restore all, might as well wipe out clean local + // storage + µm.XAL.keyvalRemoveAll(onAllRemoved); +}; + +/******************************************************************************/ + +var resetUserData = function() { + var onAllRemoved = function() { + vAPI.app.restart(); + }; + µm.XAL.keyvalRemoveAll(onAllRemoved); +}; + +/******************************************************************************/ + +var onMessage = function(request, sender, callback) { + // Async + switch ( request.what ) { + default: + break; + } + + // Sync + var response; + + switch ( request.what ) { + case 'getAllUserData': + response = { + app: 'µMatrix', + version: vAPI.app.version, + when: Date.now(), + settings: µm.userSettings, + rules: µm.pMatrix.toString(), + hostsFiles: µm.liveHostsFiles + }; + break; + + case 'getSomeStats': + response = { + version: vAPI.app.version, + storageUsed: µm.storageUsed + }; + break; + + case 'restoreAllUserData': + restoreUserData(request.userData); + break; + + case 'resetAllUserData': + resetUserData(); + break; + + default: + return vAPI.messaging.UNHANDLED; + } + + callback(response); +}; + +vAPI.messaging.listen('about.js', onMessage); + +/******************************************************************************/ +/******************************************************************************/ + +})(); + +/******************************************************************************/ diff --git a/src/js/pagestats.js b/src/js/pagestats.js index 4adb1ea..76d00da 100644 --- a/src/js/pagestats.js +++ b/src/js/pagestats.js @@ -436,6 +436,8 @@ return { µMatrix.PageStore = (function() { +'use strict'; + /******************************************************************************/ var µm = µMatrix; @@ -555,20 +557,16 @@ PageStore.prototype.recordRequest = function(type, url, block) { // notifying me, and this causes internal cached state to be out of sync. PageStore.prototype.updateBadge = function(tabId) { - // Icon - var iconPath; + var iconId = null; var badgeStr = ''; var total = this.perLoadAllowedRequestCount + this.perLoadBlockedRequestCount; if ( total ) { var squareSize = 19; var greenSize = squareSize * Math.sqrt(this.perLoadAllowedRequestCount / total); - greenSize = greenSize < squareSize/2 ? Math.ceil(greenSize) : Math.floor(greenSize); - iconPath = 'img/browsericons/icon19-' + greenSize + '.png'; + iconId = greenSize < squareSize/2 ? Math.ceil(greenSize) : Math.floor(greenSize); badgeStr = µm.formatCount(this.distinctRequestCount); - } else { - iconPath = 'img/browsericons/icon19.png'; } - µm.XAL.setIcon(tabId, iconPath, badgeStr); + vAPI.setIcon(tabId, iconId, badgeStr); }; /******************************************************************************/ @@ -577,6 +575,8 @@ return { factory: pageStoreFactory }; +/******************************************************************************/ + })(); /******************************************************************************/ diff --git a/src/js/popup.js b/src/js/popup.js index 4fd42d5..bc4c742 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -19,7 +19,7 @@ Home: https://github.com/gorhill/uMatrix */ -/* global punycode, uDom, messaging */ +/* global punycode, vAPI, uDom */ /* jshint esnext: true, bitwise: false */ /******************************************************************************/ @@ -27,6 +27,8 @@ (function() { +'use strict'; + /******************************************************************************/ /******************************************************************************/ @@ -71,8 +73,6 @@ var blacklistedHostnamesLabel = ''; // https://github.com/gorhill/httpswitchboard/issues/345 -messaging.start('popup.js'); - var onMessage = function(msg) { if ( msg.what !== 'urlStatsChanged' ) { return; @@ -83,7 +83,7 @@ var onMessage = function(msg) { queryMatrixSnapshot(makeMenu); }; -messaging.listen(onMessage); +var messager = vAPI.messaging.channel('popup.js', onMessage); /******************************************************************************/ /******************************************************************************/ @@ -94,7 +94,7 @@ function getUserSetting(setting) { function setUserSetting(setting, value) { matrixSnapshot.userSettings[setting] = value; - messaging.tell({ + messager.send({ what: 'userSettings', name: setting, value: value @@ -405,7 +405,7 @@ function handleFilter(button, leaning) { desHostname: desHostname, type: type }; - messaging.ask(request, updateMatrixSnapshot); + messager.send(request, updateMatrixSnapshot); } function handleWhitelistFilter(button) { @@ -950,7 +950,7 @@ function initMenuEnvironment() { while ( i-- ) { key = keys[i]; cell = uDom('#matHead .matCell[data-req-type="'+ key +'"]'); - text = chrome.i18n.getMessage(key + 'PrettyName'); + text = vAPI.i18n(key + 'PrettyName'); cell.text(text); prettyNames[key] = text; } @@ -1043,7 +1043,7 @@ function toggleMatrixSwitch() { switchName: switchName, srcHostname: matrixSnapshot.scope }; - messaging.ask(request, updateMatrixSnapshot); + messager.send(request, updateMatrixSnapshot); } /******************************************************************************/ @@ -1068,7 +1068,7 @@ function persistMatrix() { what: 'applyDiffToPermanentMatrix', diff: matrixSnapshot.diff }; - messaging.ask(request, updateMatrixSnapshot); + messager.send(request, updateMatrixSnapshot); } /******************************************************************************/ @@ -1081,7 +1081,7 @@ function revertMatrix() { what: 'applyDiffToTemporaryMatrix', diff: matrixSnapshot.diff }; - messaging.ask(request, updateMatrixSnapshot); + messager.send(request, updateMatrixSnapshot); } /******************************************************************************/ @@ -1100,13 +1100,13 @@ function revertAll() { var request = { what: 'revertTemporaryMatrix' }; - messaging.ask(request, updateMatrixSnapshot); + messager.send(request, updateMatrixSnapshot); } /******************************************************************************/ function buttonReloadHandler() { - messaging.tell({ + messager.send({ what: 'forceReloadTab', tabId: targetTabId }); @@ -1125,9 +1125,9 @@ function mouseleaveMatrixCellHandler() { /******************************************************************************/ function gotoExtensionURL() { - var url = this.getAttribute('data-extension-url'); + var url = uDom(this).attr('data-extension-url'); if ( url ) { - messaging.tell({ what: 'gotoExtensionURL', url: url }); + messager.send({ what: 'gotoExtensionURL', url: url }); } } @@ -1136,7 +1136,7 @@ function gotoExtensionURL() { function gotoExternalURL() { var url = this.getAttribute('data-external-url'); if ( url ) { - messaging.tell({ what: 'gotoURL', url: url }); + messager.send({ what: 'gotoURL', url: url }); } } @@ -1152,8 +1152,6 @@ function dropDownMenuHide() { /******************************************************************************/ -// Because chrome.tabs.query() is async - var onMatrixSnapshotReady = function(response) { // Now that tabId and pageURL are set, we can build our menu initMenuEnvironment(); @@ -1165,7 +1163,7 @@ var onMatrixSnapshotReady = function(response) { uDom('#toolbarLeft').remove(); // https://github.com/gorhill/httpswitchboard/issues/191 - uDom('#noNetTrafficPrompt').text(chrome.i18n.getMessage('matrixNoNetTrafficPrompt')); + uDom('#noNetTrafficPrompt').text(vAPI.i18n('matrixNoNetTrafficPrompt')); uDom('#noNetTrafficPrompt').css('display', ''); } }; @@ -1183,20 +1181,7 @@ var queryMatrixSnapshot = function(callback) { matrixSnapshot = response; callback(); }; - var onTabsReceived = function(tabs) { - if ( tabs.length === 0 ) { - return; - } - var tab = tabs[0]; - request.tabId = targetTabId = tab.id; - request.tabURL = tab.url; - messaging.ask(request, snapshotReceived); - }; - if ( targetTabId === undefined ) { - chrome.tabs.query({ active: true, currentWindow: true }, onTabsReceived); - } else { - messaging.ask(request, snapshotReceived); - } + messager.send(request, snapshotReceived); }; /******************************************************************************/ diff --git a/src/js/privacy.js b/src/js/privacy.js index 4ffd6c5..f28151b 100644 --- a/src/js/privacy.js +++ b/src/js/privacy.js @@ -19,22 +19,24 @@ Home: https://github.com/gorhill/uMatrix */ -/* global messaging, uDom */ +/* global vAPI, uDom */ /******************************************************************************/ (function() { +'use strict'; + /******************************************************************************/ -messaging.start('privacy.js'); +var messager = vAPI.messaging.channel('privacy.js'); var cachedPrivacySettings = {}; /******************************************************************************/ function changeUserSettings(name, value) { - messaging.tell({ + messager.send({ what: 'userSettings', name: name, value: value @@ -44,7 +46,7 @@ function changeUserSettings(name, value) { /******************************************************************************/ function changeMatrixSwitch(name, state) { - messaging.tell({ + messager.send({ what: 'setMatrixSwitch', switchName: name, state: state @@ -141,7 +143,7 @@ uDom.onLoad(function() { installEventHandlers(); }; - messaging.ask({ what: 'getPrivacySettings' }, onSettingsReceived); + messager.send({ what: 'getPrivacySettings' }, onSettingsReceived); }); /******************************************************************************/ diff --git a/src/js/settings.js b/src/js/settings.js index 37ec23f..db0f358 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -19,16 +19,18 @@ Home: https://github.com/gorhill/uMatrix */ -/* global messaging, uDom */ +/* global vAPI, uDom */ /* jshint multistr: true */ /******************************************************************************/ (function() { +'use strict'; + /******************************************************************************/ -messaging.start('settings.js'); +var messager = vAPI.messaging.channel('settings.js'); var cachedUserSettings = {}; @@ -68,7 +70,7 @@ var onSubframeColorChanged = function() { /******************************************************************************/ function changeUserSettings(name, value) { - messaging.tell({ + messager.send({ what: 'userSettings', name: name, value: value @@ -126,7 +128,7 @@ uDom.onLoad(function() { installEventHandlers(); }; - messaging.ask({ what: 'getUserSettings' }, onUserSettingsReceived); + messager.send({ what: 'getUserSettings' }, onUserSettingsReceived); }); /******************************************************************************/ diff --git a/src/js/start.js b/src/js/start.js index 418a171..23c85fb 100644 --- a/src/js/start.js +++ b/src/js/start.js @@ -43,85 +43,6 @@ /******************************************************************************/ -function onTabCreated(tab) { - // Can this happen? - if ( tab.id < 0 || !tab.url || tab.url === '' ) { - return; - } - - // https://github.com/gorhill/httpswitchboard/issues/303 - // This takes care of rebinding the tab to the proper page store - // when the user navigate back in his history. - µMatrix.bindTabToPageStats(tab.id, tab.url); -} - -chrome.tabs.onCreated.addListener(onTabCreated); - -/******************************************************************************/ - -function onTabUpdated(tabId, changeInfo, tab) { - // Can this happen? - if ( !tab.url || tab.url === '' ) { - return; - } - - // https://github.com/gorhill/httpswitchboard/issues/303 - // This takes care of rebinding the tab to the proper page store - // when the user navigate back in his history. - if ( changeInfo.url ) { - µMatrix.bindTabToPageStats(tabId, tab.url, 'pageUpdated'); - } - - // rhill 2013-12-23: Compute state after whole page is loaded. This is - // better than building a state snapshot dynamically when requests are - // recorded, because here we are not afflicted by the browser cache - // mechanism. - - // rhill 2014-03-05: Use tab id instead of page URL: this allows a - // blocked page using µMatrix internal data URI-based page to be properly - // unblocked when user un-blacklist the hostname. - // https://github.com/gorhill/httpswitchboard/issues/198 - if ( changeInfo.status === 'complete' ) { - var pageStats = µMatrix.pageStatsFromTabId(tabId); - if ( pageStats ) { - pageStats.state = µMatrix.computeTabState(tabId); - } - } -} - -chrome.tabs.onUpdated.addListener(onTabUpdated); - -/******************************************************************************/ - -function onTabRemoved(tabId) { - // Can this happen? - if ( tabId < 0 ) { - return; - } - - µMatrix.unbindTabFromPageStats(tabId); -} - -chrome.tabs.onRemoved.addListener(onTabRemoved); - -/******************************************************************************/ - -// Bind a top URL to a specific tab - -function onBeforeNavigateCallback(details) { - // Don't bind to a subframe - if ( details.frameId > 0 ) { - return; - } - // console.debug('onBeforeNavigateCallback() > "%s" = %o', details.url, details); - - µMatrix.bindTabToPageStats(details.tabId, details.url); -} - -chrome.webNavigation.onBeforeNavigate.addListener(onBeforeNavigateCallback); - -/******************************************************************************/ - // Browser data jobs (function() { @@ -136,8 +57,8 @@ chrome.webNavigation.onBeforeNavigate.addListener(onBeforeNavigateCallback); } µm.clearBrowserCacheCycle = µm.userSettings.clearBrowserCacheAfter; µm.browserCacheClearedCounter++; - chrome.browsingData.removeCache({ since: 0 }); - // console.debug('clearBrowserCacheCallback()> chrome.browsingData.removeCache() called'); + vAPI.browserCache.clearByTime(0); + // console.debug('clearBrowserCacheCallback()> vAPI.browserCache.clearByTime() called'); }; µMatrix.asyncJobs.add('clearBrowserCache', null, jobCallback, 15 * 60 * 1000, true); @@ -177,13 +98,14 @@ chrome.webNavigation.onBeforeNavigate.addListener(onBeforeNavigateCallback); var i = tabs.length; // console.debug('start.js > binding %d tabs', i); while ( i-- ) { + µm.tabContextManager.commit(tabs[i].id, tabs[i].url); µm.bindTabToPageStats(tabs[i].id, tabs[i].url); } µm.webRequest.start(); }; var queryTabs = function() { - chrome.tabs.query({ url: '' }, bindTabs); + vAPI.tabs.getAll(bindTabs); }; µm.load(queryTabs); diff --git a/src/js/storage.js b/src/js/storage.js index 09a9d5f..83e3017 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -28,7 +28,7 @@ var getBytesInUseHandler = function(bytesInUse) { µm.storageUsed = bytesInUse; }; - chrome.storage.local.getBytesInUse(null, getBytesInUseHandler); + vAPI.storage.getBytesInUse(null, getBytesInUseHandler); }; /******************************************************************************/ @@ -71,7 +71,7 @@ callback(µm.userSettings); }; - chrome.storage.local.get(this.userSettings, settingsLoaded); + vAPI.storage.get(this.userSettings, settingsLoaded); }; /******************************************************************************/ @@ -157,7 +157,7 @@ } // Now get user's selection of lists - chrome.storage.local.get( + vAPI.storage.get( { 'liveHostsFiles': availableHostsFiles }, onSelectedHostsFilesLoaded ); @@ -208,8 +208,8 @@ var loadHostsFilesEnd = function() { µm.ubiquitousBlacklist.freeze(); - chrome.storage.local.set({ 'liveHostsFiles': µm.liveHostsFiles }); - µm.messaging.announce({ what: 'loadHostsFilesCompleted' }); + vAPI.storage.set({ 'liveHostsFiles': µm.liveHostsFiles }); + vAPI.messaging.broadcast({ what: 'loadHostsFilesCompleted' }); callback(); }; @@ -343,7 +343,7 @@ } // Save switch states - chrome.storage.local.set( + vAPI.storage.set( { 'liveHostsFiles': liveHostsFiles }, this.loadUpdatableAssets.bind(this, update) ); diff --git a/src/js/tab.js b/src/js/tab.js index d178a61..ff80fd0 100644 --- a/src/js/tab.js +++ b/src/js/tab.js @@ -21,11 +21,407 @@ /* global chrome, µMatrix */ +/******************************************************************************/ +/******************************************************************************/ + +(function() { + +'use strict'; + +/******************************************************************************/ + +var µm = µMatrix; + +// https://github.com/gorhill/httpswitchboard/issues/303 +// Some kind of trick going on here: +// Any scheme other than 'http' and 'https' is remapped into a fake +// URL which trick the rest of µMatrix into being able to process an +// otherwise unmanageable scheme. µMatrix needs web page to have a proper +// hostname to work properly, so just like the 'chromium-behind-the-scene' +// fake domain name, we map unknown schemes into a fake '{scheme}-scheme' +// hostname. This way, for a specific scheme you can create scope with +// rules which will apply only to that scheme. + +/******************************************************************************/ +/******************************************************************************/ + +µm.normalizePageURL = function(tabId, pageURL) { + if ( vAPI.isBehindTheSceneTabId(tabId) ) { + return 'http://behind-the-scene/'; + } + var uri = this.URI.set(pageURL); + var scheme = uri.scheme; + if ( scheme === 'https' || scheme === 'http' ) { + return uri.normalizedURI(); + } + + var url = 'http://' + scheme + '-scheme/'; + + if ( uri.hostname !== '' ) { + url += uri.hostname + '/'; + } + + return url; +}; + +/******************************************************************************/ +/****************************************************************************** + +To keep track from which context *exactly* network requests are made. This is +often tricky for various reasons, and the challenge is not specific to one +browser. + +The time at which a URL is assigned to a tab and the time when a network +request for a root document is made must be assumed to be unrelated: it's all +asynchronous. There is no guaranteed order in which the two events are fired. + +Also, other "anomalies" can occur: + +- a network request for a root document is fired without the corresponding +tab being really assigned a new URL + + +- a network request for a secondary resource is labeled with a tab id for +which no root document was pulled for that tab. + + +- a network request for a secondary resource is made without the root +document to which it belongs being formally bound yet to the proper tab id, +causing a bad scope to be used for filtering purpose. + + + +So the solution here is to keep a lightweight data structure which only +purpose is to keep track as accurately as possible of which root document +belongs to which tab. That's the only purpose, and because of this, there are +no restrictions for when the URL of a root document can be associated to a tab. + +Before, the PageStore object was trying to deal with this, but it had to +enforce some restrictions so as to not descend into one of the above issues, or +other issues. The PageStore object can only be associated with a tab for which +a definitive navigation event occurred, because it collects information about +what occurred in the tab (for example, the number of requests blocked for a +page). + +The TabContext objects do not suffer this restriction, and as a result they +offer the most reliable picture of which root document URL is really associated +to which tab. Moreover, the TabObject can undo an association from a root +document, and automatically re-associate with the next most recent. This takes +care of . + +The PageStore object no longer cache the various information about which +root document it is currently bound. When it needs to find out, it will always +defer to the TabContext object, which will provide the real answer. This takes +case of . In effect, the +master switch and dynamic filtering rules can be evaluated now properly even +in the absence of a PageStore object, this was not the case before. + +Also, the TabContext object will try its best to find a good candidate root +document URL for when none exists. This takes care of +. + +The TabContext manager is self-contained, and it takes care to properly +housekeep itself. + +*/ + +µm.tabContextManager = (function() { + var tabContexts = Object.create(null); + + // https://github.com/chrisaljoudi/uBlock/issues/1001 + // This is to be used as last-resort fallback in case a tab is found to not + // be bound while network requests are fired for the tab. + var mostRecentRootDocURL = ''; + var mostRecentRootDocURLTimestamp = 0; + + var gcPeriod = 10 * 60 * 1000; + + var TabContext = function(tabId) { + this.tabId = tabId; + this.stack = []; + this.rawURL = + this.normalURL = + this.rootHostname = + this.rootDomain = ''; + this.timer = null; + this.onTabCallback = null; + this.onTimerCallback = null; + + tabContexts[tabId] = this; + }; + + TabContext.prototype.destroy = function() { + if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { + return; + } + if ( this.timer !== null ) { + clearTimeout(this.timer); + this.timer = null; + } + delete tabContexts[this.tabId]; + }; + + TabContext.prototype.onTab = function(tab) { + if ( tab ) { + this.timer = setTimeout(this.onTimerCallback, gcPeriod); + } else { + this.destroy(); + } + }; + + TabContext.prototype.onTimer = function() { + this.timer = null; + if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { + return; + } + vAPI.tabs.get(this.tabId, this.onTabCallback); + }; + + // This takes care of orphanized tab contexts. Can't be started for all + // contexts, as the behind-the-scene context is permanent -- so we do not + // want to slush it. + TabContext.prototype.autodestroy = function() { + if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { + return; + } + this.onTabCallback = this.onTab.bind(this); + this.onTimerCallback = this.onTimer.bind(this); + this.timer = setTimeout(this.onTimerCallback, gcPeriod); + }; + + // Update just force all properties to be updated to match the most current + // root URL. + TabContext.prototype.update = function() { + if ( this.stack.length === 0 ) { + this.rawURL = this.normalURL = this.rootHostname = this.rootDomain = ''; + } else { + this.rawURL = this.stack[this.stack.length - 1]; + this.normalURL = µm.normalizePageURL(this.tabId, this.rawURL); + this.rootHostname = µm.URI.hostnameFromURI(this.normalURL); + this.rootDomain = µm.URI.domainFromHostname(this.rootHostname); + } + }; + + // Called whenever a candidate root URL is spotted for the tab. + TabContext.prototype.push = function(url) { + if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { + return; + } + this.stack.push(url); + this.update(); + }; + + // Called when a former push is a false positive: + // https://github.com/chrisaljoudi/uBlock/issues/516 + TabContext.prototype.unpush = function(url) { + if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { + return; + } + // We are not going to unpush if there is no other candidate, the + // point of unpush is to make space for a better candidate. + if ( this.stack.length === 1 ) { + return; + } + var pos = this.stack.indexOf(url); + if ( pos === -1 ) { + return; + } + this.stack.splice(pos, 1); + if ( this.stack.length === 0 ) { + this.destroy(); + return; + } + if ( pos !== this.stack.length ) { + return; + } + this.update(); + }; + + // This tells that the url is definitely the one to be associated with the + // tab, there is no longer any ambiguity about which root URL is really + // sitting in which tab. + TabContext.prototype.commit = function(url) { + if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { + return; + } + this.stack = [url]; + this.update(); + }; + + // These are to be used for the API of the tab context manager. + + var push = function(tabId, url) { + var entry = tabContexts[tabId]; + if ( entry === undefined ) { + entry = new TabContext(tabId); + entry.autodestroy(); + } + entry.push(url); + mostRecentRootDocURL = url; + mostRecentRootDocURLTimestamp = Date.now(); + return entry; + }; + + // Find a tab context for a specific tab. If none is found, attempt to + // fix this. When all fail, the behind-the-scene context is returned. + var lookup = function(tabId, url) { + var entry; + if ( url !== undefined ) { + entry = push(tabId, url); + } else { + entry = tabContexts[tabId]; + } + if ( entry !== undefined ) { + return entry; + } + // https://github.com/chrisaljoudi/uBlock/issues/1025 + // Google Hangout popup opens without a root frame. So for now we will + // just discard that best-guess root frame if it is too far in the + // future, at which point it ceases to be a "best guess". + if ( mostRecentRootDocURL !== '' && mostRecentRootDocURLTimestamp + 500 < Date.now() ) { + mostRecentRootDocURL = ''; + } + // https://github.com/chrisaljoudi/uBlock/issues/1001 + // Not a behind-the-scene request, yet no page store found for the + // tab id: we will thus bind the last-seen root document to the + // unbound tab. It's a guess, but better than ending up filtering + // nothing at all. + if ( mostRecentRootDocURL !== '' ) { + return push(tabId, mostRecentRootDocURL); + } + // If all else fail at finding a page store, re-categorize the + // request as behind-the-scene. At least this ensures that ultimately + // the user can still inspect/filter those net requests which were + // about to fall through the cracks. + // Example: Chromium + case #12 at + // http://raymondhill.net/ublock/popup.html + return tabContexts[vAPI.noTabId]; + }; + + var commit = function(tabId, url) { + var entry = tabContexts[tabId]; + if ( entry === undefined ) { + entry = push(tabId, url); + } else { + entry.commit(url); + } + return entry; + }; + + var unpush = function(tabId, url) { + var entry = tabContexts[tabId]; + if ( entry !== undefined ) { + entry.unpush(url); + } + }; + + var exists = function(tabId) { + return tabContexts[tabId] !== undefined; + }; + + // Behind-the-scene tab context + (function() { + var entry = new TabContext(vAPI.noTabId); + entry.stack.push(''); + entry.rawURL = ''; + entry.normalURL = µm.normalizePageURL(entry.tabId); + entry.rootHostname = µm.URI.hostnameFromURI(entry.normalURL); + entry.rootDomain = µm.URI.domainFromHostname(entry.rootHostname); + })(); + + // Context object, typically to be used to feed filtering engines. + var Context = function(tabId) { + var tabContext = lookup(tabId); + this.rootHostname = tabContext.rootHostname; + this.rootDomain = tabContext.rootDomain; + this.pageHostname = + this.pageDomain = + this.requestURL = + this.requestHostname = + this.requestDomain = ''; + }; + + var createContext = function(tabId) { + return new Context(tabId); + }; + + return { + push: push, + unpush: unpush, + commit: commit, + lookup: lookup, + exists: exists, + createContext: createContext + }; +})(); + +/******************************************************************************/ +/******************************************************************************/ + +// When the DOM content of root frame is loaded, this means the tab +// content has changed. + +vAPI.tabs.onNavigation = function(details) { + if ( details.frameId !== 0 ) { + return; + } + var tabContext = µm.tabContextManager.commit(details.tabId, details.url); + var pageStore = µm.bindTabToPageStats(details.tabId, 'afterNavigate'); + +}; + +/******************************************************************************/ + +// It may happen the URL in the tab changes, while the page's document +// stays the same (for instance, Google Maps). Without this listener, +// the extension icon won't be properly refreshed. + +vAPI.tabs.onUpdated = function(tabId, changeInfo, tab) { + if ( !tab.url || tab.url === '' ) { + return; + } + + if ( changeInfo.url ) { + µm.tabContextManager.commit(tabId, changeInfo.url); + µm.bindTabToPageStats(tabId, 'tabUpdated'); + } + + // rhill 2013-12-23: Compute state after whole page is loaded. This is + // better than building a state snapshot dynamically when requests are + // recorded, because here we are not afflicted by the browser cache + // mechanism. + + // rhill 2014-03-05: Use tab id instead of page URL: this allows a + // blocked page using µMatrix internal data URI-based page to be properly + // unblocked when user un-blacklist the hostname. + // https://github.com/gorhill/httpswitchboard/issues/198 + if ( changeInfo.status === 'complete' ) { + var pageStats = µm.pageStatsFromTabId(tabId); + if ( pageStats ) { + pageStats.state = µm.computeTabState(tabId); + } + } +}; + +/******************************************************************************/ + +vAPI.tabs.onClosed = function(tabId) { + if ( vAPI.isBehindTheSceneTabId(tabId) ) { + return; + } + µm.unbindTabFromPageStats(tabId); +}; + +/******************************************************************************/ + +vAPI.tabs.registerListeners(); + +/******************************************************************************/ /******************************************************************************/ // Create a new page url stats store (if not already present) -µMatrix.createPageStore = function(pageURL) { +µm.createPageStore = function(pageURL) { // https://github.com/gorhill/httpswitchboard/issues/303 // At this point, the URL has been page-URL-normalized @@ -57,46 +453,31 @@ /******************************************************************************/ -// https://github.com/gorhill/httpswitchboard/issues/303 -// Some kind of trick going on here: -// Any scheme other than 'http' and 'https' is remapped into a fake -// URL which trick the rest of µMatrix into being able to process an -// otherwise unmanageable scheme. µMatrix needs web pages to have a proper -// hostname to work properly, so just like the 'chromium-behind-the-scene' -// fake domain name, we map unknown schemes into a fake '{scheme}-scheme' -// hostname. This way, for a specific scheme you can create scope with -// rules which will apply only to that scheme. - -µMatrix.normalizePageURL = function(pageURL) { - var uri = this.URI.set(pageURL); - if ( uri.scheme === 'https' || uri.scheme === 'http' ) { - return uri.normalizedURI(); - } - // If it is a scheme-based page URL, it is important it is crafted as a - // normalized URL just like above. - if ( uri.scheme !== '' ) { - return 'http://' + uri.scheme + '-scheme/'; - } - return ''; -}; - -/******************************************************************************/ - // Create an entry for the tab if it doesn't exist -µMatrix.bindTabToPageStats = function(tabId, pageURL, context) { +µm.bindTabToPageStats = function(tabId, context) { + if ( vAPI.isBehindTheSceneTabId(tabId) === false ) { + this.updateBadgeAsync(tabId); + } + + // Do not create a page store for URLs which are of no interests + if ( µm.tabContextManager.exists(tabId) === false ) { + this.unbindTabFromPageStats(tabId); + return null; + } + + var tabContext = µm.tabContextManager.lookup(tabId); + var rawURL = tabContext.rawURL; + // https://github.com/gorhill/httpswitchboard/issues/303 // Don't rebind pages blocked by µMatrix. var blockedRootFramePrefix = this.webRequest.blockedRootFramePrefix; - if ( pageURL.slice(0, blockedRootFramePrefix.length) === blockedRootFramePrefix ) { + if ( rawURL.lastIndexOf(blockedRootFramePrefix, 0) === 0 ) { return null; } var pageStore; - - // https://github.com/gorhill/httpswitchboard/issues/303 - // Normalize to a page-URL. - pageURL = this.normalizePageURL(pageURL); + var pageURL = tabContext.normalURL; // The previous page URL, if any, associated with the tab if ( this.tabIdToPageUrl.hasOwnProperty(tabId) ) { @@ -159,7 +540,7 @@ /******************************************************************************/ -µMatrix.unbindTabFromPageStats = function(tabId) { +µm.unbindTabFromPageStats = function(tabId) { if ( this.tabIdToPageUrl.hasOwnProperty(tabId) === false ) { return; } @@ -179,7 +560,7 @@ // Log a request -µMatrix.recordFromTabId = function(tabId, type, url, blocked) { +µm.recordFromTabId = function(tabId, type, url, blocked) { var pageStats = this.pageStatsFromTabId(tabId); if ( pageStats ) { pageStats.recordRequest(type, url, blocked); @@ -187,7 +568,7 @@ } }; -µMatrix.recordFromPageUrl = function(pageUrl, type, url, blocked, reason) { +µm.recordFromPageUrl = function(pageUrl, type, url, blocked, reason) { var pageStats = this.pageStatsFromPageUrl(pageUrl); if ( pageStats ) { pageStats.recordRequest(type, url, blocked, reason); @@ -196,7 +577,7 @@ /******************************************************************************/ -µMatrix.onPageLoadCompleted = function(pageURL) { +µm.onPageLoadCompleted = function(pageURL) { var pageStats = this.pageStatsFromPageUrl(pageURL); if ( !pageStats ) { return; @@ -210,9 +591,9 @@ /******************************************************************************/ -// Reload content of a tabs. +// Reload content of one or more tabs. -µMatrix.smartReloadTabs = function(which, tabId) { +µm.smartReloadTabs = function(which, tabId) { if ( which === 'none' ) { return; } @@ -224,7 +605,6 @@ // which === 'all' var reloadTabs = function(chromeTabs) { - var µm = µMatrix; var tabId; var i = chromeTabs.length; while ( i-- ) { @@ -236,7 +616,7 @@ }; var getTabs = function() { - chrome.tabs.query({ status: 'complete' }, reloadTabs); + vAPI.tabs.getAll(reloadTabs); }; this.asyncJobs.add('smartReloadTabs', null, getTabs, 500); @@ -246,7 +626,7 @@ // Reload content of a tab -µMatrix.smartReloadTab = function(tabId) { +µm.smartReloadTab = function(tabId) { var pageStats = this.pageStatsFromTabId(tabId); if ( !pageStats ) { //console.error('HTTP Switchboard> µMatrix.smartReloadTab(): page stats for tab id %d not found', tabId); @@ -304,7 +684,7 @@ // console.log('old state: %o\nnew state: %o', oldState, newState); if ( mustReload ) { - chrome.tabs.reload(tabId); + vAPI.tabs.reload(tabId); } // pageStats.state = newState; }; @@ -317,13 +697,13 @@ // `chrome-devtools://devtools/devtools.html` // etc. -µMatrix.tabExists = function(tabId) { +µm.tabExists = function(tabId) { return !!this.pageUrlFromTabId(tabId); }; /******************************************************************************/ -µMatrix.computeTabState = function(tabId) { +µm.computeTabState = function(tabId) { var pageStats = this.pageStatsFromTabId(tabId); if ( !pageStats ) { //console.error('tab.js > µMatrix.computeTabState(): page stats for tab id %d not found', tabId); @@ -358,18 +738,18 @@ /******************************************************************************/ -µMatrix.pageUrlFromTabId = function(tabId) { +µm.pageUrlFromTabId = function(tabId) { return this.tabIdToPageUrl[tabId]; }; -µMatrix.pageUrlFromPageStats = function(pageStats) { +µm.pageUrlFromPageStats = function(pageStats) { if ( pageStats ) { return pageStats.pageUrl; } return undefined; }; -µMatrix.pageStatsFromTabId = function(tabId) { +µm.pageStatsFromTabId = function(tabId) { var pageUrl = this.tabIdToPageUrl[tabId]; if ( pageUrl ) { return this.pageStats[pageUrl]; @@ -377,7 +757,7 @@ return undefined; }; -µMatrix.pageStatsFromPageUrl = function(pageURL) { +µm.pageStatsFromPageUrl = function(pageURL) { if ( pageURL ) { return this.pageStats[this.normalizePageURL(pageURL)]; } @@ -386,7 +766,7 @@ /******************************************************************************/ -µMatrix.resizeLogBuffers = function(size) { +µm.resizeLogBuffers = function(size) { var pageStores = this.pageStats; for ( var pageURL in pageStores ) { if ( pageStores.hasOwnProperty(pageURL) ) { @@ -397,15 +777,14 @@ /******************************************************************************/ -µMatrix.forceReload = function(tabId) { - chrome.tabs.reload(tabId, { bypassCache: true }); +µm.forceReload = function(tabId) { + vAPI.tabs.reload(tabId, { bypassCache: true }); }; /******************************************************************************/ // Garbage collect stale url stats entries (function() { - var µm = µMatrix; var gcPageStats = function() { var pageStore; var now = Date.now(); @@ -447,7 +826,7 @@ // Time somewhat arbitrary: If a web page has not been in a tab // for some time minutes, flush its stats. - µMatrix.asyncJobs.add( + µm.asyncJobs.add( 'gcPageStats', null, gcPageStats, @@ -455,3 +834,7 @@ true ); })(); + +/******************************************************************************/ + +})(); diff --git a/src/js/traffic.js b/src/js/traffic.js index a00b506..20952e0 100644 --- a/src/js/traffic.js +++ b/src/js/traffic.js @@ -197,15 +197,15 @@ var onBeforeChromeExtensionRequestHandler = function(details) { var onBeforeRootFrameRequestHandler = function(details) { var µm = µMatrix; - - // Do not ignore traffic outside tabs + var requestURL = details.url; var tabId = details.tabId; - if ( tabId < 0 ) { + + µm.tabContextManager.push(tabId, requestURL); + + if ( vAPI.isBehindTheSceneTabId(tabId) ) { tabId = µm.behindTheSceneTabId; - } - // It's a root frame, bind to a new page store - else { - µm.bindTabToPageStats(tabId, details.url); + } else { + µm.bindTabToPageStats(tabId); } var uri = µm.URI.set(details.url); @@ -213,7 +213,6 @@ var onBeforeRootFrameRequestHandler = function(details) { return; } - var requestURL = uri.normalizedURI(); var requestHostname = uri.hostname; var pageStore = µm.pageStatsFromTabId(tabId); @@ -730,10 +729,6 @@ var onSubDocHeadersReceived = function(details) { /******************************************************************************/ -// As per Chrome API doc, webRequest.onErrorOccurred event is the last -// one called in the sequence of webRequest events. -// http://developer.chrome.com/extensions/webRequest.html - var onErrorOccurredHandler = function(details) { // console.debug('onErrorOccurred()> "%s": %o', details.url, details); var requestType = requestTypeNormalizer[details.type]; @@ -798,68 +793,54 @@ var requestTypeNormalizer = { /******************************************************************************/ +vAPI.net.onBeforeRequest = { + urls: [ + "http://*/*", + "https://*/*", + "chrome-extension://*/*" + ], + extra: [ 'blocking' ], + callback: onBeforeRequestHandler +}; + +vAPI.net.onBeforeSendHeaders = { + urls: [ + "http://*/*", + "https://*/*" + ], + types: [ + "main_frame", + "sub_frame" + ], + extra: [ 'blocking', 'requestHeaders' ], + callback: onBeforeSendHeadersHandler +}; + +vAPI.net.onHeadersReceived = { + urls: [ + "http://*/*", + "https://*/*" + ], + types: [ + "main_frame", + "sub_frame" + ], + extra: [ 'blocking', 'responseHeaders' ], + callback: onHeadersReceived +}; + +vAPI.net.onErrorOccurred = { + urls: [ + "http://*/*", + "https://*/*" + ], + callback: onErrorOccurredHandler +}; + +/******************************************************************************/ + var start = function() { - chrome.webRequest.onBeforeRequest.addListener( - //function(details) { - // quickProfiler.start('onBeforeRequest'); - // var r = onBeforeRequestHandler(details); - // quickProfiler.stop(); - // return r; - //}, - onBeforeRequestHandler, - { - "urls": [ - "http://*/*", - "https://*/*", - "chrome-extension://*/*" - ], - "types": [ - "main_frame", - "sub_frame", - 'stylesheet', - "script", - "image", - "object", - "xmlhttprequest", - "other" - ] - }, - [ "blocking" ] - ); - - //console.log('µMatrix > Beginning to intercept net requests at %s', (new Date()).toISOString()); - - chrome.webRequest.onBeforeSendHeaders.addListener( - onBeforeSendHeadersHandler, - { - 'urls': [ - "http://*/*", - "https://*/*" - ] - }, - ['blocking', 'requestHeaders'] - ); - - chrome.webRequest.onHeadersReceived.addListener( - onHeadersReceived, - { - 'urls': [ - "http://*/*", - "https://*/*" - ] - }, - ['blocking', 'responseHeaders'] - ); - - chrome.webRequest.onErrorOccurred.addListener( - onErrorOccurredHandler, - { - 'urls': [ - "http://*/*", - "https://*/*" - ] - } - ); + vAPI.net.registerListeners(); }; /******************************************************************************/ diff --git a/src/js/udom.js b/src/js/udom.js index 8adf2be..8d5b6a0 100644 --- a/src/js/udom.js +++ b/src/js/udom.js @@ -1,6 +1,6 @@ /******************************************************************************* - µBlock - a Chromium browser extension to block requests. + µBlock - a browser extension to block requests. Copyright (C) 2014 Raymond Hill This program is free software: you can redistribute it and/or modify @@ -19,7 +19,9 @@ Home: https://github.com/gorhill/uBlock */ -/******************************************************************************/ +/* global DOMTokenList */ +/* exported uDom */ + /******************************************************************************/ // It's just a silly, minimalist DOM framework: this allows me to not rely @@ -30,6 +32,8 @@ var uDom = (function() { +'use strict'; + /******************************************************************************/ var DOMList = function() { @@ -136,7 +140,7 @@ var addHTMLToList = function(list, html) { var cTag = matches[1]; var pTag = pTagOfChildTag[cTag] || 'div'; var p = document.createElement(pTag); - p.innerHTML = html; + vAPI.insertHTML(p, html); // Find real parent var c = p.querySelector(cTag); p = c.parentNode; @@ -222,6 +226,12 @@ DOMList.prototype.toArray = function() { /******************************************************************************/ +DOMList.prototype.pop = function() { + return addNodeToList(new DOMList(), this.nodes.pop()); +}; + +/******************************************************************************/ + DOMList.prototype.forEach = function(fn) { var n = this.nodes.length; for ( var i = 0; i < n; i++ ) { @@ -479,6 +489,28 @@ DOMList.prototype.clone = function(notDeep) { /******************************************************************************/ +DOMList.prototype.nthOfType = function() { + if ( this.nodes.length === 0 ) { + return 0; + } + var node = this.nodes[0]; + var tagName = node.tagName; + var i = 1; + while ( node.previousElementSibling !== null ) { + node = node.previousElementSibling; + if ( typeof node.tagName !== 'string' ) { + continue; + } + if ( node.tagName !== tagName ) { + continue; + } + i++; + } + return i; +}; + +/******************************************************************************/ + DOMList.prototype.attr = function(attr, value) { var i = this.nodes.length; if ( value === undefined && typeof attr !== 'object' ) { @@ -543,7 +575,7 @@ DOMList.prototype.html = function(html) { return i ? this.nodes[0].innerHTML : ''; } while ( i-- ) { - this.nodes[i].innerHTML = html; + vAPI.insertHTML(this.nodes[i], html); } return this; }; @@ -637,6 +669,24 @@ DOMList.prototype.toggleClasses = function(classNames, targetState) { /******************************************************************************/ +var listenerEntries = []; + +var ListenerEntry = function(target, type, capture, callback) { + this.target = target; + this.type = type; + this.capture = capture; + this.callback = callback; + target.addEventListener(type, callback, capture); +}; + +ListenerEntry.prototype.dispose = function() { + this.target.removeEventListener(this.type, this.callback, this.capture); + this.target = null; + this.callback = null; +}; + +/******************************************************************************/ + var makeEventHandler = function(selector, callback) { return function(event) { var dispatcher = event.currentTarget; @@ -660,7 +710,7 @@ DOMList.prototype.on = function(etype, selector, callback) { var i = this.nodes.length; while ( i-- ) { - this.nodes[i].addEventListener(etype, callback, selector !== undefined); + listenerEntries.push(new ListenerEntry(this.nodes[i], etype, selector !== undefined, callback)); } return this; }; @@ -691,6 +741,20 @@ DOMList.prototype.trigger = function(etype) { /******************************************************************************/ +// Cleanup + +var onBeforeUnload = function() { + var entry; + while ( entry = listenerEntries.pop() ) { + entry.dispose(); + } + window.removeEventListener('beforeunload', onBeforeUnload); +}; + +window.addEventListener('beforeunload', onBeforeUnload); + +/******************************************************************************/ + return DOMListFactory; })(); diff --git a/src/js/uritools.js b/src/js/uritools.js index acfd240..35f94bc 100644 --- a/src/js/uritools.js +++ b/src/js/uritools.js @@ -31,10 +31,10 @@ Naming convention from https://en.wikipedia.org/wiki/URI_scheme#Examples /******************************************************************************/ -// This will inserted as a module in the µMatrix object. - µMatrix.URI = (function() { +'use strict'; + /******************************************************************************/ // Favorite regex tool: http://regex101.com/ @@ -49,6 +49,7 @@ var reRFC3986 = /^([^:\/?#]+:)?(\/\/[^\/?#]*)?([^?#]*)(\?[^#]*)?(#.*)?/; // Derived var reSchemeFromURI = /^[^:\/?#]+:/; var reAuthorityFromURI = /^(?:[^:\/?#]+:)?(\/\/[^\/?#]+)/; +var reCommonHostnameFromURL = /^https?:\/\/([0-9a-z_][0-9a-z._-]*[0-9a-z])\//; // These are to parse authority field, not parsed by above official regex // IPv6 is seen as an exception: a non-compatible IPv6 is first tried, and @@ -248,7 +249,11 @@ URI.authorityFromURI = function(uri) { // The most used function, so it better be fast. URI.hostnameFromURI = function(uri) { - var matches = reAuthorityFromURI.exec(uri); + var matches = reCommonHostnameFromURL.exec(uri); + if ( matches ) { + return matches[1]; + } + matches = reAuthorityFromURI.exec(uri); if ( !matches ) { return ''; } @@ -274,21 +279,98 @@ URI.hostnameFromURI = function(uri) { /******************************************************************************/ -// It is expected that there is higher-scoped `publicSuffixList` lingering -// somewhere. Cache it. See . -var psl = publicSuffixList; - URI.domainFromHostname = function(hostname) { - if ( !reIPAddressNaive.test(hostname) ) { - return psl.getDomain(hostname); + // Try to skip looking up the PSL database + if ( domainCache.hasOwnProperty(hostname) ) { + var entry = domainCache[hostname]; + entry.tstamp = Date.now(); + return entry.domain; } - return hostname; + // Meh.. will have to search it + if ( reIPAddressNaive.test(hostname) === false ) { + return domainCacheAdd(hostname, psl.getDomain(hostname)); + } + return domainCacheAdd(hostname, hostname); }; URI.domain = function() { return this.domainFromHostname(this.hostname); }; +// It is expected that there is higher-scoped `publicSuffixList` lingering +// somewhere. Cache it. See . +var psl = publicSuffixList; + +/******************************************************************************/ + +// Trying to alleviate the worries of looking up too often the domain name from +// a hostname. With a cache, uBlock benefits given that it deals with a +// specific set of hostnames within a narrow time span -- in other words, I +// believe probability of cache hit are high in uBlock. + +var DomainCacheEntry = function(domain) { + this.init(domain); +}; + +DomainCacheEntry.prototype.init = function(domain) { + this.domain = domain; + this.tstamp = Date.now(); + return this; +}; + +DomainCacheEntry.prototype.dispose = function() { + this.domain = ''; + if ( domainCacheEntryJunkyard.length < 25 ) { + domainCacheEntryJunkyard.push(this); + } +}; + +var domainCacheEntryFactory = function(domain) { + var entry = domainCacheEntryJunkyard.pop(); + if ( entry ) { + return entry.init(domain); + } + return new DomainCacheEntry(domain); +}; + +var domainCacheEntryJunkyard = []; + +var domainCacheAdd = function(hostname, domain) { + if ( domainCache.hasOwnProperty(hostname) ) { + domainCache[hostname].tstamp = Date.now(); + } else { + domainCache[hostname] = domainCacheEntryFactory(domain); + domainCacheCount += 1; + if ( domainCacheCount === domainCacheCountHighWaterMark ) { + domainCachePrune(); + } + } + return domain; +}; + +var domainCacheEntrySort = function(a, b) { + return b.tstamp - a.tstamp; +}; + +var domainCachePrune = function() { + var hostnames = Object.keys(domainCache) + .sort(domainCacheEntrySort) + .slice(domainCacheCountLowWaterMark); + var i = hostnames.length; + domainCacheCount -= i; + var hostname; + while ( i-- ) { + hostname = hostnames[i]; + domainCache[hostname].dispose(); + delete domainCache[hostname]; + } +}; + +var domainCache = {}; +var domainCacheCount = 0; +var domainCacheCountLowWaterMark = 75; +var domainCacheCountHighWaterMark = 100; + /******************************************************************************/ URI.domainFromURI = function(uri) { diff --git a/src/js/user-rules.js b/src/js/user-rules.js index 3a8fa6b..cfd0362 100644 --- a/src/js/user-rules.js +++ b/src/js/user-rules.js @@ -19,15 +19,17 @@ Home: https://github.com/gorhill/uMatrix */ -/* global chrome, messaging, uDom */ +/* global vAPI, uDom */ /******************************************************************************/ (function() { +'use strict'; + /******************************************************************************/ -messaging.start('user-rules.js'); +var messager = vAPI.messaging.channel('user-rules.js'); /******************************************************************************/ @@ -102,7 +104,7 @@ function handleImportFilePicker() { 'what': 'setUserRules', 'temporaryRules': rulesFromHTML('#diff .right li') + '\n' + this.result }; - messaging.ask(request, processUserRules); + messager.send(request, processUserRules); }; var file = this.files[0]; if ( file === undefined || file.name === '' ) { @@ -130,10 +132,9 @@ var startImportFilePicker = function() { /******************************************************************************/ function exportUserRulesToFile() { - chrome.downloads.download({ + vAPI.download({ 'url': 'data:text/plain,' + encodeURIComponent(rulesFromHTML('#diff .left li')), - 'filename': uDom('[data-i18n="userRulesDefaultFileName"]').text(), - 'saveAs': true + 'filename': uDom('[data-i18n="userRulesDefaultFileName"]').text() }); } @@ -161,7 +162,7 @@ var revertHandler = function() { 'what': 'setUserRules', 'temporaryRules': rulesFromHTML('#diff .left li') }; - messaging.ask(request, processUserRules); + messager.send(request, processUserRules); }; /******************************************************************************/ @@ -171,7 +172,7 @@ var commitHandler = function() { 'what': 'setUserRules', 'permanentRules': rulesFromHTML('#diff .right li') }; - messaging.ask(request, processUserRules); + messager.send(request, processUserRules); }; /******************************************************************************/ @@ -191,7 +192,7 @@ var editStopHandler = function(ev) { 'what': 'setUserRules', 'temporaryRules': uDom('#diff .right textarea').val() }; - messaging.ask(request, processUserRules); + messager.send(request, processUserRules); }; /******************************************************************************/ @@ -210,7 +211,7 @@ var temporaryRulesToggler = function(ev) { 'what': 'setUserRules', 'temporaryRules': rulesFromHTML('#diff .right li') }; - messaging.ask(request, processUserRules); + messager.send(request, processUserRules); }; /******************************************************************************/ @@ -221,14 +222,14 @@ uDom.onLoad(function() { uDom('#importFilePicker').on('change', handleImportFilePicker); uDom('#exportButton').on('click', exportUserRulesToFile); - uDom('#revertButton').on('click', revertHandler) - uDom('#commitButton').on('click', commitHandler) - uDom('#editEnterButton').on('click', editStartHandler) - uDom('#editStopButton').on('click', editStopHandler) - uDom('#editCancelButton').on('click', editCancelHandler) - uDom('#diff > .right > ul').on('click', 'li', temporaryRulesToggler) + uDom('#revertButton').on('click', revertHandler); + uDom('#commitButton').on('click', commitHandler); + uDom('#editEnterButton').on('click', editStartHandler); + uDom('#editStopButton').on('click', editStopHandler); + uDom('#editCancelButton').on('click', editCancelHandler); + uDom('#diff > .right > ul').on('click', 'li', temporaryRulesToggler); - messaging.ask({ what: 'getUserRules' }, processUserRules); + messager.send({ what: 'getUserRules' }, processUserRules); }); /******************************************************************************/ diff --git a/src/js/utils.js b/src/js/utils.js index 2cd2229..6d63458 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -30,69 +30,17 @@ /******************************************************************************/ var gotoURL = function(details) { - if ( details.tabId ) { - chrome.tabs.update(details.tabId, { url: details.url }); - } else { - chrome.tabs.create({ url: details.url }); - } + vAPI.tabs.open(details); }; /******************************************************************************/ var gotoExtensionURL = function(url) { - var hasFragment = function(url) { - return url.indexOf('#') >= 0; - }; - - var removeFragment = function(url) { - var pos = url.indexOf('#'); - if ( pos < 0 ) { - return url; - } - return url.slice(0, pos); - }; - - var tabIndex = 9999; - var targetUrl = chrome.extension.getURL(url); - var urlToFind = removeFragment(targetUrl); - - var currentWindow = function(tabs) { - var updateProperties = { active: true }; - var i = tabs.length; - while ( i-- ) { - if ( removeFragment(tabs[i].url) !== urlToFind ) { - continue; - } - // If current tab in dashboard is different, force the new one, if - // there is one, to be activated. - if ( tabs[i].url !== targetUrl ) { - if ( hasFragment(targetUrl) ) { - updateProperties.url = targetUrl; - } - } - // Activate found matching tab - // Commented out as per: - // https://github.com/gorhill/httpswitchboard/issues/150#issuecomment-32683726 - // chrome.tabs.move(tabs[0].id, { index: index + 1 }); - chrome.tabs.update(tabs[i].id, updateProperties); - return; - } - chrome.tabs.create({ 'url': targetUrl, index: tabIndex + 1 }); - }; - - var currentTab = function(tabs) { - if ( tabs.length ) { - tabIndex = tabs[0].index; - } - chrome.tabs.query({ currentWindow: true }, currentWindow); - }; - - // https://github.com/gorhill/httpswitchboard/issues/150 - // Logic: - // - If URL is already opened in a tab, just activate tab - // - Otherwise find the current active tab and open in a tab immediately - // to the right of the active tab - chrome.tabs.query({ active: true }, currentTab); + vAPI.tabs.open({ + url: url, + index: -1, + select: true + }); }; /******************************************************************************/ diff --git a/src/js/xal.js b/src/js/xal.js index 251e705..10e980e 100644 --- a/src/js/xal.js +++ b/src/js/xal.js @@ -32,73 +32,34 @@ var noopFunc = function(){}; /******************************************************************************/ -// Must read: https://code.google.com/p/chromium/issues/detail?id=410868#c8 - -// https://github.com/gorhill/uBlock/issues/19 -// https://github.com/gorhill/uBlock/issues/207 -// Since we may be called asynchronously, the tab id may not exist -// anymore, so this ensures it does still exist. - -exports.setIcon = function(id, imgDict, overlayStr) { - var onIconReady = function() { - if ( chrome.runtime.lastError ) { - return; - } - chrome.browserAction.setBadgeText({ tabId: id, text: overlayStr }); - if ( overlayStr !== '' ) { - chrome.browserAction.setBadgeBackgroundColor({ tabId: id, color: '#666' }); - } - }; - chrome.browserAction.setIcon({ tabId: id, path: imgDict }, onIconReady); -}; - -/******************************************************************************/ - -exports.injectScript = function(id, details) { - chrome.tabs.executeScript(id, details); -}; - -/******************************************************************************/ - exports.keyvalSetOne = function(key, val, callback) { var bin = {}; bin[key] = val; - chrome.storage.local.set(bin, callback || noopFunc); + vAPI.storage.set(bin, callback || noopFunc); }; /******************************************************************************/ exports.keyvalGetOne = function(key, callback) { - chrome.storage.local.get(key, callback); + vAPI.storage.get(key, callback); }; /******************************************************************************/ exports.keyvalSetMany = function(dict, callback) { - chrome.storage.local.set(dict, callback || noopFunc); + vAPI.storage.set(dict, callback || noopFunc); }; /******************************************************************************/ exports.keyvalRemoveOne = function(key, callback) { - chrome.storage.local.remove(key, callback || noopFunc); + vAPI.storage.remove(key, callback || noopFunc); }; /******************************************************************************/ exports.keyvalRemoveAll = function(callback) { - chrome.storage.local.clear(callback || noopFunc); -}; - -/******************************************************************************/ - -exports.restart = function() { - // https://github.com/gorhill/uMatrix/issues/40 - // I don't know if that helps workaround whatever Chromium bug causes - // the browser to crash. - chrome.runtime.sendMessage({ what: 'restart' }, function() { - chrome.runtime.reload(); - }); + vAPI.storage.clear(callback || noopFunc); }; /******************************************************************************/ diff --git a/src/popup.html b/src/popup.html index 4ed403f..a35ad04 100644 --- a/src/popup.html +++ b/src/popup.html @@ -91,9 +91,10 @@ + + - diff --git a/src/privacy.html b/src/privacy.html index 41ddf55..c36e041 100644 --- a/src/privacy.html +++ b/src/privacy.html @@ -104,10 +104,11 @@ html.rtl #spoof-user-agent-with {

    + + - diff --git a/src/settings.html b/src/settings.html index 2c0b74c..f504d25 100644 --- a/src/settings.html +++ b/src/settings.html @@ -66,10 +66,11 @@ ul > li { + + - diff --git a/src/user-rules.html b/src/user-rules.html index 5ff88ce..a425cf5 100644 --- a/src/user-rules.html +++ b/src/user-rules.html @@ -37,10 +37,11 @@ + + -