diff options
author | gorhill <rhill@raymondhill.net> | 2014-08-19 20:41:52 -0400 |
---|---|---|
committer | gorhill <rhill@raymondhill.net> | 2014-08-19 20:41:52 -0400 |
commit | 1deae3bfe0c1379efb869b94a30d1a731292cfab (patch) | |
tree | 2154673232cac0c2bd58398c20c2bb12b42181dc /js | |
parent | 56ed6b9f4e65efb8b80b7b1ad93931484c54dde0 (diff) | |
download | uBlock-1deae3bfe0c1379efb869b94a30d1a731292cfab.zip uBlock-1deae3bfe0c1379efb869b94a30d1a731292cfab.tar.gz uBlock-1deae3bfe0c1379efb869b94a30d1a731292cfab.tar.bz2 |
this fixes #138, next: thorough code review
Diffstat (limited to 'js')
-rw-r--r-- | js/3p-filters.js | 109 | ||||
-rw-r--r-- | js/about.js | 88 | ||||
-rw-r--r-- | js/asset-updater.js | 227 | ||||
-rw-r--r-- | js/assets.js | 909 | ||||
-rw-r--r-- | js/background.js | 1 | ||||
-rw-r--r-- | js/messaging-handlers.js | 35 | ||||
-rw-r--r-- | js/messaging.js | 9 | ||||
-rw-r--r-- | js/pagestore.js | 4 | ||||
-rw-r--r-- | js/start.js | 13 | ||||
-rw-r--r-- | js/storage.js | 29 | ||||
-rw-r--r-- | js/ublock.js | 21 | ||||
-rw-r--r-- | js/utils.js | 36 |
12 files changed, 784 insertions, 697 deletions
diff --git a/js/3p-filters.js b/js/3p-filters.js index ac18763..cb3da21 100644 --- a/js/3p-filters.js +++ b/js/3p-filters.js @@ -31,6 +31,7 @@ var userListName = chrome.i18n.getMessage('1pPageName'); var listDetails = {}; var externalLists = ''; var cacheWasPurged = false; +var needUpdate = false; /******************************************************************************/ @@ -40,7 +41,6 @@ var onMessage = function(msg) { switch ( msg.what ) { case 'loadUbiquitousBlacklistCompleted': renderBlacklists(); - selectedBlacklistsChanged(); break; default: @@ -109,6 +109,8 @@ var renderBlacklists = function() { var listStatsTemplate = chrome.i18n.getMessage('3pListsOfBlockedHostsPerListStats'); var purgeButtontext = chrome.i18n.getMessage('3pExternalListPurge'); + var updateButtontext = chrome.i18n.getMessage('3pExternalListNew'); + var obsoleteButtontext = chrome.i18n.getMessage('3pExternalListObsolete'); var htmlFromBranch = function(groupKey, listKeys, lists) { var html = [ @@ -134,7 +136,7 @@ var renderBlacklists = function() { listStatsTemplate, '</span>' ].join(''); - var listKey, list, listEntry; + var listKey, list, listEntry, entryDetails; for ( var i = 0; i < listKeys.length; i++ ) { listKey = listKeys[i]; list = lists[listKey]; @@ -146,12 +148,27 @@ var renderBlacklists = function() { .replace('{{total}}', !isNaN(+list.entryCount) ? renderNumber(list.entryCount) : '?'); html.push(listEntry); // https://github.com/gorhill/uBlock/issues/104 - if ( /^https?:\/\/.+/.test(listKey) && listDetails.cache[listKey] ) { + entryDetails = listDetails.cache[listKey]; + if ( entryDetails === undefined ) { + continue; + } + // Update status + if ( !list.off && (entryDetails.repoObsolete || entryDetails.cacheObsolete) ) { + html.push( + ' ', + '<span class="status obsolete">', + entryDetails.repoObsolete ? updateButtontext : obsoleteButtontext, + '</span>' + ); + needUpdate = true; + } + // In cache + else if ( entryDetails.cached ) { html.push( ' ', - '<button type="button" class="purge">', + '<span class="status purge">', purgeButtontext, - '</button>' + '</span>' ); } } @@ -159,6 +176,8 @@ var renderBlacklists = function() { return html.join(''); }; + // https://www.youtube.com/watch?v=unCVi4hYRlY#t=30m18s + var groupsFromLists = function(lists) { var groups = {}; var listKeys = Object.keys(lists); @@ -178,6 +197,7 @@ var renderBlacklists = function() { var onListsReceived = function(details) { listDetails = details; + needUpdate = false; var lists = details.available; var html = []; @@ -208,13 +228,10 @@ var renderBlacklists = function() { uDom('#lists .listDetails').remove(); uDom('#lists').html(html.join('')); - uDom('#parseAllABPHideFilters').prop('checked', listDetails.cosmetic === true); - uDom('#ubiquitousParseAllABPHideFiltersPrompt2').text( - chrome.i18n.getMessage("listsParseAllABPHideFiltersPrompt2") - .replace('{{abpHideFilterCount}}', renderNumber(µb.abpHideFilters.getFilterCount())) - ); + uDom('#autoUpdate').prop('checked', listDetails.autoUpdate === true); + uDom('#parseCosmeticFilters').prop('checked', listDetails.cosmetic === true); uDom('a').attr('target', '_blank'); - selectedBlacklistsChanged(); + updateApplyButtons(); }; messaging.ask({ what: 'getLists' }, onListsReceived); @@ -222,9 +239,9 @@ var renderBlacklists = function() { /******************************************************************************/ -// Check whether lists need reloading. +// Return whether selection of lists changed. -var needToReload = function() { +var listsSelectionChanged = function() { if ( listDetails.cosmetic !== getµb().userSettings.parseAllABPHideFilters ) { return true; } @@ -261,10 +278,19 @@ var needToReload = function() { /******************************************************************************/ +// Return whether content need update. + +var listsContentChanged = function() { + return needUpdate; +}; + +/******************************************************************************/ + // This is to give a visual hint that the selection of blacklists has changed. -var selectedBlacklistsChanged = function() { - uDom('#blacklistsApply').prop('disabled', !needToReload()); +var updateApplyButtons = function() { + uDom('#buttonApply').toggleClass('enabled', listsSelectionChanged()); + uDom('#buttonUpdate').toggleClass('enabled', listsContentChanged()); }; /******************************************************************************/ @@ -278,7 +304,7 @@ var onListCheckboxChanged = function() { return; } listDetails.available[href].off = !this.checked; - selectedBlacklistsChanged(); + updateApplyButtons(); }; /******************************************************************************/ @@ -295,22 +321,22 @@ var onListLinkClicked = function(ev) { var onPurgeClicked = function(ev) { var button = uDom(this); - var href = button.parent().find('a').first().attr('href'); + var li = button.parent(); + var href = li.find('a').first().attr('href'); if ( !href ) { return; } messaging.tell({ what: 'purgeCache', path: href }); button.remove(); - cacheWasPurged = true; - selectedBlacklistsChanged(); + if ( li.find('input').first().prop('checked') ) { + cacheWasPurged = true; + updateApplyButtons(); + } }; /******************************************************************************/ -var blacklistsApplyHandler = function() { - if ( !needToReload() ) { - return; - } +var reloadAll = function(update) { // Reload blacklists messaging.tell({ what: 'userSettings', @@ -334,17 +360,40 @@ var blacklistsApplyHandler = function() { } messaging.tell({ what: 'reloadAllFilters', - switches: switches + switches: switches, + update: update }); cacheWasPurged = false; - uDom('#blacklistsApply').prop('disabled', true); +}; + +/******************************************************************************/ + +var buttonApplyHandler = function() { + reloadAll(); + uDom('#buttonApply').toggleClass('enabled', false); +}; + +/******************************************************************************/ + +var buttonUpdateHandler = function() { + reloadAll(true); +}; + +/******************************************************************************/ + +var autoUpdateCheckboxChanged = function() { + messaging.tell({ + what: 'userSettings', + name: 'autoUpdate', + value: this.checked + }); }; /******************************************************************************/ var abpHideFiltersCheckboxChanged = function() { listDetails.cosmetic = this.checked; - selectedBlacklistsChanged(); + updateApplyButtons(); }; /******************************************************************************/ @@ -383,11 +432,13 @@ var externalListsApplyHandler = function() { uDom.onLoad(function() { // Handle user interaction - uDom('#parseAllABPHideFilters').on('change', abpHideFiltersCheckboxChanged); - uDom('#blacklistsApply').on('click', blacklistsApplyHandler); + uDom('#autoUpdate').on('change', autoUpdateCheckboxChanged); + uDom('#parseCosmeticFilters').on('change', abpHideFiltersCheckboxChanged); + uDom('#buttonApply').on('click', buttonApplyHandler); + uDom('#buttonUpdate').on('click', buttonUpdateHandler); uDom('#lists').on('change', '.listDetails > input', onListCheckboxChanged); uDom('#lists').on('click', '.listDetails > a:nth-of-type(1)', onListLinkClicked); - uDom('#lists').on('click', 'button.purge', onPurgeClicked); + uDom('#lists').on('click', 'span.purge', onPurgeClicked); uDom('#externalLists').on('input', externalListsChangeHandler); uDom('#externalListsApply').on('click', externalListsApplyHandler); diff --git a/js/about.js b/js/about.js index b27cb7e..3203b98 100644 --- a/js/about.js +++ b/js/about.js @@ -27,95 +27,7 @@ uDom.onLoad(function() { /******************************************************************************/ -var updateList = {}; -var assetListSwitches = ['o', 'o', 'o']; -var commitHistoryURLPrefix = 'https://github.com/gorhill/ublock/commits/master/'; - -/******************************************************************************/ - -var setAssetListClassBit = function(bit, state) { - assetListSwitches[assetListSwitches.length-1-bit] = !state ? 'o' : 'x'; - uDom('#assetList') - .removeClass() - .addClass(assetListSwitches.join('')); -}; - -/******************************************************************************/ - -var renderAssetList = function(details) { - var dirty = false; - var paths = Object.keys(details.list).sort(); - if ( paths.length > 0 ) { - uDom('#assetList .assetEntry').remove(); - var i = 0; - var path, status, html = []; - while ( path = paths[i++] ) { - status = details.list[path].status; - dirty = dirty || status !== 'Unchanged'; - html.push( - '<tr class="assetEntry ' + status.toLowerCase().replace(/ +/g, '-') + '">', - '<td>', - '<a href="' + commitHistoryURLPrefix + path + '">', - path.replace(/^(assets\/[^/]+\/)(.+)$/, '$1<b>$2</b>'), - '</a>', - '<td>', - chrome.i18n.getMessage('aboutAssetsUpdateStatus' + status) - ); - } - uDom('#assetList table tBody').append(html.join('')); - uDom('#assetList a').attr('target', '_blank'); - updateList = details.list; - } - setAssetListClassBit(0, paths.length !== 0); - setAssetListClassBit(1, dirty); - setAssetListClassBit(2, false); -}; - -/******************************************************************************/ - -var updateAssets = function() { - setAssetListClassBit(2, true); - var onDone = function(details) { - if ( details.changedCount !== 0 ) { - messaging.tell({ what: 'loadUpdatableAssets' }); - } - }; - messaging.ask({ what: 'launchAssetUpdater', list: updateList }, onDone); -}; - -/******************************************************************************/ - -var updateAssetsList = function() { - messaging.ask({ what: 'getAssetUpdaterList' }, renderAssetList); -}; - -/******************************************************************************/ - -// Updating all assets could be done from elsewhere and if so the -// list here needs to be updated. - -var onAnnounce = function(msg) { - switch ( msg.what ) { - case 'allLocalAssetsUpdated': - updateAssetsList(); - break; - - default: - break; - } -}; - -messaging.start('about.js'); -messaging.listen(onAnnounce); - -/******************************************************************************/ - uDom('#aboutVersion').html(chrome.runtime.getManifest().version); -uDom('#aboutAssetsUpdateButton').on('click', updateAssets); - -/******************************************************************************/ - -updateAssetsList(); /******************************************************************************/ diff --git a/js/asset-updater.js b/js/asset-updater.js deleted file mode 100644 index 546682b..0000000 --- a/js/asset-updater.js +++ /dev/null @@ -1,227 +0,0 @@ -/******************************************************************************* - - µBlock - a Chromium browser extension to block 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/uBlock -*/ - -/* global chrome, µBlock */ - -/******************************************************************************/ - -// Asset update manager - -µBlock.assetUpdater = (function() { - -/******************************************************************************/ - -var getUpdateList = function(callback) { - var localChecksumsText = ''; - var remoteChecksumsText = ''; - - var compareChecksums = function() { - var parseChecksumsText = function(text) { - var result = {}; - var lines = text.split(/\n+/); - var i = lines.length; - var fields; - while ( i-- ) { - fields = lines[i].trim().split(/\s+/); - if ( fields.length !== 2 ) { - continue; - } - result[fields[1]] = fields[0]; - } - return result; - }; - if ( remoteChecksumsText === 'Error' || localChecksumsText === 'Error' ) { - remoteChecksumsText = localChecksumsText = ''; - } - var localAssetChecksums = parseChecksumsText(localChecksumsText); - var remoteAssetChecksums = parseChecksumsText(remoteChecksumsText); - - var toUpdate = {}; - var path; - for ( path in remoteAssetChecksums ) { - if ( !remoteAssetChecksums.hasOwnProperty(path) ) { - continue; - } - if ( localAssetChecksums[path] === undefined ) { - toUpdate[path] = { - status: 'Added', - remoteChecksum: remoteAssetChecksums[path], - localChecksum: '' - }; - continue; - } - if ( localAssetChecksums[path] === remoteAssetChecksums[path] ) { - toUpdate[path] = { - status: 'Unchanged', - remoteChecksum: remoteAssetChecksums[path], - localChecksum: localAssetChecksums[path] - }; - continue; - } - toUpdate[path] = { - status: 'Changed', - remoteChecksum: remoteAssetChecksums[path], - localChecksum: localAssetChecksums[path] - }; - } - for ( path in localAssetChecksums ) { - if ( !localAssetChecksums.hasOwnProperty(path) ) { - continue; - } - if ( remoteAssetChecksums[path] === undefined ) { - toUpdate[path] = { - status: 'Removed', - remoteChecksum: '', - localChecksum: localAssetChecksums[path] - }; - } - } - - callback({ 'list': toUpdate }); - }; - - var validateChecksums = function(details) { - if ( details.error || details.content === '' ) { - return 'Error'; - } - if ( /^(?:[0-9a-f]{32}\s+\S+(\s+|$))+/.test(details.content) ) { - return details.content; - } - return 'Error'; - }; - - var onLocalChecksumsLoaded = function(details) { - localChecksumsText = validateChecksums(details); - if ( remoteChecksumsText !== '' ) { - compareChecksums(); - } - }; - - var onRemoteChecksumsLoaded = function(details) { - remoteChecksumsText = validateChecksums(details); - if ( localChecksumsText !== '' ) { - compareChecksums(); - } - }; - - µBlock.assets.getRepo('assets/checksums.txt', onRemoteChecksumsLoaded); - µBlock.assets.get('assets/checksums.txt', onLocalChecksumsLoaded); -}; - -/******************************************************************************/ - -// If `list` is null, it will be fetched internally. - -var update = function(list, callback) { - var assetChangedCount = 0; - var assetProcessedCount; - var updatedAssetChecksums = []; - - var onCompleted = function() { - var details = { - what: 'allLocalAssetsUpdated', - changedCount: assetChangedCount - }; - callback(details); - µBlock.messaging.announce(details); - }; - - var doCountdown = function() { - assetProcessedCount -= 1; - if ( assetProcessedCount > 0 ) { - return; - } - µBlock.assets.put( - 'assets/checksums.txt', - updatedAssetChecksums.join('\n'), - onCompleted - ); - chrome.storage.local.set({ 'assetsUpdateTimestamp': Date.now() }); - }; - - var assetUpdated = function(details) { - var path = details.path; - var entry = list[path]; - if ( details.error ) { - updatedAssetChecksums.push(entry.localChecksum + ' ' + path); - } else { - updatedAssetChecksums.push(entry.remoteChecksum + ' ' + path); - assetChangedCount += 1; - } - doCountdown(); - }; - - var processList = function() { - assetProcessedCount = Object.keys(list).length; - if ( assetProcessedCount === 0 ) { - onCompleted(); - return; - } - var entry; - var details = { path: '', md5: '' }; - for ( var path in list ) { - if ( list.hasOwnProperty(path) === false ) { - continue; - } - entry = list[path]; - if ( entry.status === 'Added' || entry.status === 'Changed' ) { - details.path = path; - details.md5 = entry.remoteChecksum; - µBlock.assets.update(details, assetUpdated); - continue; - } - if ( entry.status === 'Unchanged' ) { - updatedAssetChecksums.push(entry.localChecksum + ' ' + path); - } - doCountdown(); - } - }; - - var listLoaded = function(details) { - list = details.list; - processList(); - }; - - // Purge obsolete external assets - µBlock.assets.purge(/^https?:\/\/.+$/, Date.now() - µBlock.updateAssetsEvery); - - if ( list ) { - processList(); - } else { - getUpdateList(listLoaded); - } -}; - -/******************************************************************************/ - -// Export API - -return { - 'getList': getUpdateList, - 'update': update -}; - -/******************************************************************************/ - -})(); - -/******************************************************************************/ - diff --git a/js/assets.js b/js/assets.js index 3f202d1..e57247c 100644 --- a/js/assets.js +++ b/js/assets.js @@ -23,20 +23,6 @@ /******************************************************************************* -Assets - Read: - If in cache - Use cache - If not in cache - Use local - Update: - Use remote - Save in cache - - Import: - Use textarea - Save in cache [user directory] - File system structure: assets ublock @@ -44,14 +30,11 @@ File system structure: thirdparties ... user - blacklisted-hosts.txt - ... + filters.txt + ... */ -// Ref: http://www.w3.org/TR/2012/WD-file-system-api-20120417/ -// Ref: http://www.html5rocks.com/en/tutorials/file/filesystem/ - /******************************************************************************/ // Low-level asset files manager @@ -60,11 +43,31 @@ File system structure: /******************************************************************************/ -var fileSystem; -var fileSystemQuota = 40 * 1024 * 1024; var repositoryRoot = µBlock.projectServerRoot; var nullFunc = function() {}; -var thirdpartyHomeURLs = null; +var reIsExternalPath = /^https?:\/\/[a-z0-9]/; +var reIsUserPath = /^assets\/user\//; + +var exports = { + autoUpdate: true, + autoUpdateDelay: 2 * 24 * 60 * 60 * 1000 +}; + +/******************************************************************************/ + +var AssetEntry = function() { + this.localChecksum = ''; + this.repoChecksum = ''; + this.expireTimestamp = 0; + this.homeURL = ''; +}; + +var RepoMetadata = function() { + this.entries = {}; + this.waiting = []; +}; + +var repoMeta = null; /******************************************************************************/ @@ -78,6 +81,18 @@ var cachedAssetsManager = (function() { callback(entries); return; } + // 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 lastVersion = store.extensionLastVersion || '0.0.0.0'; + if ( currentVersion !== lastVersion ) { + chrome.storage.local.set({ 'extensionLastVersion': currentVersion }); + exports.remove(/^assets\/(ublock|thirdparties)\//); + exports.remove('assets/checksums.txt'); + } + callback(entries); + }; var onLoaded = function(bin) { // https://github.com/gorhill/httpswitchboard/issues/381 // Maybe the index was requested multiple times and already @@ -91,7 +106,7 @@ var cachedAssetsManager = (function() { } entries = bin.cached_asset_entries || {}; } - callback(entries); + chrome.storage.local.get('extensionLastVersion', onLastVersionRead); }; chrome.storage.local.get('cached_asset_entries', onLoaded); }; @@ -146,10 +161,8 @@ var cachedAssetsManager = (function() { } }; var onEntries = function(entries) { - if ( entries[path] === undefined ) { - entries[path] = Date.now(); - bin.cached_asset_entries = entries; - } + entries[path] = Date.now(); + bin.cached_asset_entries = entries; chrome.storage.local.set(bin, onSaved); }; getEntries(onEntries); @@ -202,173 +215,177 @@ var getTextFileFromURL = function(url, onLoad, onError) { /******************************************************************************/ -// https://github.com/gorhill/uBlock/issues/89 -// Remove when I am confident everybody moved to the new storage - -// Useful to avoid having to manage a directory tree - -var cachePathFromPath = function(path) { - return path.replace(/\//g, '___'); +var updateLocalChecksums = function() { + var localChecksums = []; + var entries = repoMeta.entries; + var entry; + for ( var path in entries ) { + if ( entries.hasOwnProperty(path) === false ) { + continue; + } + entry = entries[path]; + if ( entry.localChecksum !== '' ) { + localChecksums.push(entry.localChecksum + ' ' + path); + } + } + cachedAssetsManager.save('assets/checksums.txt', localChecksums.join('\n')); }; /******************************************************************************/ -// https://github.com/gorhill/uBlock/issues/89 -// Remove when I am confident everybody moved to the new storage +// Gather meta data of all assets. + +var getRepoMeta = function(callback, update) { + callback = callback || nullFunc; -var requestFileSystem = function(onSuccess, onError) { - if ( fileSystem ) { - onSuccess(fileSystem); + if ( update ) { + repoMeta = null; + } + if ( repoMeta !== null ) { + if ( repoMeta.waiting.length !== 0 ) { + repoMeta.waiting.push(callback); + } else { + callback(repoMeta); + } return; } - var onRequestFileSystem = function(fs) { - fileSystem = fs; - onSuccess(fs); - }; - - var onRequestQuota = function(grantedBytes) { - window.webkitRequestFileSystem(window.PERSISTENT, grantedBytes, onRequestFileSystem, onError); - }; - - navigator.webkitPersistentStorage.requestQuota(fileSystemQuota, onRequestQuota, onError); -}; - -/******************************************************************************/ - -// https://github.com/gorhill/uBlock/issues/89 -// Remove when I am confident everybody moved to the new storage - -var oldReadCachedFile = function(path, callback) { - var reportBack = function(content, err) { - var details = { - 'path': path, - 'content': content, - 'error': err - }; - callback(details); - }; - - var onCacheFileLoaded = function() { - // console.log('µBlock> readLocalFile() / onCacheFileLoaded()'); - reportBack(this.responseText); - this.onload = this.onerror = null; + // https://github.com/gorhill/uBlock/issues/84 + // First try to load from the actual home server of a third-party. + var parseHomeURLs = function(text) { + var entries = repoMeta.entries; + var urlPairs = text.split(/\n\n+/); + var i = urlPairs.length; + var pair, pos, k, v; + while ( i-- ) { + pair = urlPairs[i]; + pos = pair.indexOf('\n'); + if ( pos === -1 ) { + continue; + } + k = 'assets/thirdparties/' + pair.slice(0, pos).trim(); + v = pair.slice(pos).trim(); + if ( k === '' || v === '' ) { + continue; + } + if ( entries[k] === undefined ) { + entries[k] = new AssetEntry(); + } + entries[k].homeURL = v; + } + while ( callback = repoMeta.waiting.pop() ) { + callback(repoMeta); + } }; - var onCacheFileError = function(ev) { - // This handler may be called under normal circumstances: it appears - // the entry may still be present even after the file was removed. - // console.error('µBlock> readLocalFile() / onCacheFileError("%s")', path); - reportBack('', 'Error'); - this.onload = this.onerror = null; - }; + var pathToHomeURLs = 'assets/ublock/thirdparty-lists.txt'; - var onCacheEntryFound = function(entry) { - // console.log('µBlock> readLocalFile() / onCacheEntryFound():', entry.toURL()); - // rhill 2014-04-18: `ublock` query parameter is added to ensure - // the browser cache is bypassed. - getTextFileFromURL(entry.toURL() + '?ublock=' + Date.now(), onCacheFileLoaded, onCacheFileError); + var onLocalHomeURLsLoaded = function(details) { + parseHomeURLs(details.content); }; - var onCacheEntryError = function(err) { - if ( err.name !== 'NotFoundError' ) { - console.error('µBlock> readLocalFile() / onCacheEntryError("%s"):', path, err.name); + var onRepoHomeURLsLoaded = function(details) { + var entries = repoMeta.entries; + var entry = entries[pathToHomeURLs]; + if ( YaMD5.hashStr(details.content) !== entry.repoChecksum ) { + entry.repoChecksum = entry.localChecksum; + readLocalFile(pathToHomeURLs, onLocalHomeURLsLoaded); + return; } - reportBack('', 'Error'); - }; - - var onRequestFileSystemSuccess = function(fs) { - fs.root.getFile(cachePathFromPath(path), null, onCacheEntryFound, onCacheEntryError); + cachedAssetsManager.save(pathToHomeURLs, details.content, onLocalHomeURLsLoaded); }; - var onRequestFileSystemError = function(err) { - console.error('µBlock> readLocalFile() / onRequestFileSystemError():', err.name); - reportBack('', 'Error'); - }; - - requestFileSystem(onRequestFileSystemSuccess, onRequestFileSystemError); -}; - -/******************************************************************************/ - -// Flush cached non-user assets if these are from a prior version. -// https://github.com/gorhill/httpswitchboard/issues/212 - -var cacheSynchronized = false; - -var synchronizeCache = function() { - if ( cacheSynchronized ) { - return; - } - cacheSynchronized = true; - - // https://github.com/gorhill/uBlock/issues/89 - // Remove when I am confident everybody moved to the new storage + var checksumCountdown = 2; + var localChecksums = ''; + var repoChecksums = ''; - var directoryReader; - var done = function() { - directoryReader = null; - }; - - var onReadEntries = function(entries) { - var n = entries.length; - if ( !n ) { - return done(); + var checksumsReceived = function() { + checksumCountdown -= 1; + if ( checksumCountdown !== 0 ) { + return; } + // Remove from cache assets which no longer exist + var entries = repoMeta.entries; + var checksumsChanged = false; var entry; - for ( var i = 0; i < n; i++ ) { - entry = entries[i]; - entry.remove(nullFunc); + for ( var path in entries ) { + if ( entries.hasOwnProperty(path) === false ) { + continue; + } + entry = entries[path]; + // If repo checksums could not be fetched, assume no change + if ( repoChecksums === '' ) { + entry.repoChecksum = entry.localChecksum; + } + if ( entry.repoChecksum !== '' || entry.localChecksum === '' ) { + continue; + } + checksumsChanged = true; + cachedAssetsManager.remove(path); + entry.localChecksum = ''; + } + if ( checksumsChanged ) { + updateLocalChecksums(); + } + // Fetch and store homeURL associations + entry = entries[pathToHomeURLs]; + if ( entry.localChecksum !== entry.repoChecksum ) { + readRepoFile(pathToHomeURLs, onRepoHomeURLsLoaded); + } else { + readLocalFile(pathToHomeURLs, onLocalHomeURLsLoaded); } - // Read entries until none returned. - directoryReader.readEntries(onReadEntries, onReadEntriesError); - }; - - var onReadEntriesError = function(err) { - console.error('µBlock> synchronizeCache() / onReadEntriesError("%s"):', err.name); - done(); - }; - - var onRequestFileSystemSuccess = function(fs) { - directoryReader = fs.root.createReader(); - directoryReader.readEntries(onReadEntries, onReadEntriesError); }; - var onRequestFileSystemError = function(err) { - console.error('µBlock> synchronizeCache() / onRequestFileSystemError():', err.name); - done(); + var validateChecksums = function(details) { + if ( details.error || details.content === '' ) { + return ''; + } + if ( /^(?:[0-9a-f]{32}\s+\S+(?:\s+|$))+/.test(details.content) === false ) { + return ''; + } + return details.content; }; - var onLastVersionRead = function(store) { - var currentVersion = chrome.runtime.getManifest().version; - var lastVersion = store.extensionLastVersion || '0.0.0.0'; - if ( currentVersion === lastVersion ) { - return done(); + var parseChecksums = function(text, which) { + var entries = repoMeta.entries; + var lines = text.split(/\n+/); + var i = lines.length; + var fields, assetPath; + while ( i-- ) { + fields = lines[i].trim().split(/\s+/); + if ( fields.length !== 2 ) { + continue; + } + assetPath = fields[1]; + if ( entries[assetPath] === undefined ) { + entries[assetPath] = new AssetEntry(); + } + entries[assetPath][which + 'Checksum'] = fields[0]; } - chrome.storage.local.set({ 'extensionLastVersion': currentVersion }); - cachedAssetsManager.remove(/^assets\/(ublock|thirdparties)\//); - cachedAssetsManager.remove('assets/checksums.txt'); - requestFileSystem(onRequestFileSystemSuccess, onRequestFileSystemError); }; - // https://github.com/gorhill/uBlock/issues/89 - // Backward compatiblity. - - var onUserFiltersSaved = function() { - chrome.storage.local.get('extensionLastVersion', onLastVersionRead); + var onLocalChecksumsLoaded = function(details) { + if ( localChecksums = validateChecksums(details) ) { + parseChecksums(localChecksums, 'local'); + } + checksumsReceived(); }; - var onUserFiltersLoaded = function(details) { - if ( details.content !== '' ) { - cachedAssetsManager.save(details.path, details.content, onUserFiltersSaved); - } else { - chrome.storage.local.get('extensionLastVersion', onLastVersionRead); + var onRepoChecksumsLoaded = function(details) { + if ( repoChecksums = validateChecksums(details) ) { + parseChecksums(repoChecksums, 'repo'); } + checksumsReceived(); }; - oldReadCachedFile('assets/user/filters.txt', onUserFiltersLoaded); + repoMeta = new RepoMetadata(); + repoMeta.waiting.push(callback); + readRepoFile('assets/checksums.txt', onRepoChecksumsLoaded); + readLocalFile('assets/checksums.txt', onLocalChecksumsLoaded); }; +// https://www.youtube.com/watch?v=-t3WYfgM4x8 + /******************************************************************************/ var readLocalFile = function(path, callback) { @@ -389,7 +406,7 @@ var readLocalFile = function(path, callback) { this.onload = this.onerror = null; }; - var onLocalFileError = function(ev) { + var onLocalFileError = function() { console.error('µBlock> readLocalFile() / onLocalFileError("%s")', path); reportBack('', 'Error'); this.onload = this.onerror = null; @@ -423,6 +440,7 @@ var readRepoFile = function(path, callback) { }; var onRepoFileLoaded = function() { + this.onload = this.onerror = null; // console.log('µBlock> readRepoFile() / onRepoFileLoaded()'); // https://github.com/gorhill/httpswitchboard/issues/263 if ( this.status === 200 ) { @@ -430,13 +448,12 @@ var readRepoFile = function(path, callback) { } else { reportBack('', 'Error ' + this.statusText); } - this.onload = this.onerror = null; }; - var onRepoFileError = function(ev) { + var onRepoFileError = function() { + this.onload = this.onerror = null; console.error('µBlock> readRepoFile() / onRepoFileError("%s")', path); reportBack('', 'Error'); - this.onload = this.onerror = null; }; // 'ublock=...' is to skip browser cache @@ -473,19 +490,19 @@ var readExternalFile = function(path, callback) { cachedAssetsManager.save(path, this.responseText, onExternalFileCached); }; - var onExternalFileError = function(ev) { + var onExternalFileError = function() { console.error('µBlock> onExternalFileError() / onLocalFileError("%s")', path); reportBack('', 'Error'); this.onload = this.onerror = null; }; var onCachedContentLoaded = function(details) { - // console.log('µBlock> readLocalFile() / onCacheFileLoaded()'); + // console.log('µBlock> readExternalFile() / onCacheFileLoaded()'); reportBack(details.content); }; var onCachedContentError = function(details) { - // console.error('µBlock> readLocalFile() / onCacheFileError("%s")', path); + // console.error('µBlock> readExternalFile() / onCacheFileError("%s")', path); getTextFileFromURL(details.path, onExternalFileLoaded, onExternalFileError); }; @@ -494,165 +511,521 @@ var readExternalFile = function(path, callback) { /******************************************************************************/ -var purgeCache = function(pattern, before) { - cachedAssetsManager.remove(pattern, before); -}; +// An asset from an external source with a copy shipped with the extension: +// Path --> starts with 'assets/(thirdparties|ublock)/', with a home URL +// 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 writeLocalFile = function(path, content, callback) { - cachedAssetsManager.save(path, content, callback); -}; + var reportBack = function(content, err) { + var details = { + 'path': path, + 'content': content + }; + if ( err ) { + details.error = err; + } + callback(details); + }; -/******************************************************************************/ + var updateChecksum = function() { + if ( assetEntry !== undefined ) { + if ( assetEntry.repoChecksum !== assetEntry.localChecksum ) { + assetEntry.localChecksum = assetEntry.repoChecksum; + updateLocalChecksums(); + } + } + }; -var updateFromRemote = function(details, callback) { - // 'ublock=...' is to skip browser cache - var targetPath = details.path; - // https://github.com/gorhill/uBlock/issues/84 - var homeURL = ''; - var repositoryURL = repositoryRoot + targetPath + '?ublock=' + Date.now(); - var targetMd5 = details.md5 || ''; + var onInstallFileLoaded = function() { + this.onload = this.onerror = null; + console.log('µBlock> readRepoCopyAsset("%s") / onInstallFileLoaded()', path); + reportBack(this.responseText); + }; + + var onInstallFileError = function() { + this.onload = this.onerror = null; + console.error('µBlock> readRepoCopyAsset("%s") / onInstallFileError():', path, this.statusText); + reportBack('', 'Error'); + }; + + var onCachedContentLoaded = function(details) { + console.log('µBlock> readRepoCopyAsset("%s") / onCacheFileLoaded()', path); + reportBack(details.content); + }; - var reportBackError = function() { - callback({ - 'path': targetPath, - 'error': 'Error' - }); + var onCachedContentError = function(details) { + console.log('µBlock> readRepoCopyAsset("%s") / onCacheFileError()', path); + getTextFileFromURL(chrome.runtime.getURL(details.path), onInstallFileLoaded, onInstallFileError); }; + var repositoryURL = repositoryRoot + path + '?ublock=' + Date.now(); + var onRepoFileLoaded = function() { this.onload = this.onerror = null; - if ( typeof this.responseText !== 'string' ) { - console.error('µBlock> updateFromRemote("%s") / onRepoFileLoaded(): no response', repositoryURL); - reportBackError(); + if ( typeof this.responseText !== 'string' || this.responseText === '' ) { + console.error('µBlock> readRepoCopyAsset("%s") / onRepoFileLoaded("%s"): no response', path, repositoryURL); + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); return; } - if ( targetMd5 !== '' && YaMD5.hashStr(this.responseText) !== targetMd5 ) { - console.error('µBlock> updateFromRemote("%s") / onRepoFileLoaded(): bad md5 checksum', repositoryURL); - reportBackError(); - return; - } - // console.debug('µBlock> updateFromRemote("%s") / onRepoFileLoaded()', repositoryURL); - writeLocalFile(targetPath, this.responseText, callback); + console.debug('µBlock> readRepoCopyAsset("%s") / onRepoFileLoaded("%s")', path, repositoryURL); + updateChecksum(); + cachedAssetsManager.save(path, this.responseText, callback); }; - var onRepoFileError = function(ev) { + var onRepoFileError = function() { this.onload = this.onerror = null; - console.error('µBlock> updateFromRemote() / onRepoFileError("%s"):', repositoryURL, this.statusText); - reportBackError(); + console.error('µBlock> readRepoCopyAsset("%s") / onRepoFileError("%s"):', path, repositoryURL, this.statusText); + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); }; var onHomeFileLoaded = function() { this.onload = this.onerror = null; - if ( typeof this.responseText !== 'string' ) { - console.error('µBlock> updateFromRemote("%s") / onHomeFileLoaded(): no response', homeURL); - getTextFileFromURL(repositoryURL, onRepoFileLoaded, onRepoFileError); + if ( typeof this.responseText !== 'string' || this.responseText === '' ) { + console.error('µBlock> readRepoCopyAsset("%s") / onHomeFileLoaded("%s"): no response', path, assetEntry.homeURL); + // Fetch from repo only if obsolescence was due to repo checksum + if ( assetEntry.localChecksum !== assetEntry.repoChecksum ) { + getTextFileFromURL(repositoryURL, onRepoFileLoaded, onRepoFileError); + } else { + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); + } return; } - if ( targetMd5 !== '' && YaMD5.hashStr(this.responseText) !== targetMd5 ) { - console.error('µBlock> updateFromRemote("%s") / onHomeFileLoaded(): bad md5 checksum', homeURL); + console.debug('µBlock> readRepoCopyAsset("%s") / onHomeFileLoaded("%s")', path, assetEntry.homeURL); + updateChecksum(); + cachedAssetsManager.save(path, this.responseText, callback); + }; + + var onHomeFileError = function() { + this.onload = this.onerror = null; + console.error('µBlock> readRepoCopyAsset("%s") / onHomeFileError("%s"):', path, assetEntry.homeURL, this.statusText); + // Fetch from repo only if obsolescence was due to repo checksum + if ( assetEntry.localChecksum !== assetEntry.repoChecksum ) { getTextFileFromURL(repositoryURL, onRepoFileLoaded, onRepoFileError); + } else { + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); + } + }; + + var onCacheMetaReady = function(entries) { + var timestamp = entries[path]; + // In cache + if ( typeof timestamp === 'number' ) { + // Verify obsolescence + if ( exports.autoUpdate ) { + var obsolete = Date.now() - exports.autoUpdateDelay; + if ( timestamp <= obsolete ) { + console.log('µBlock> readRepoCopyAsset("%s") / onCacheMetaReady(): cache is obsolete', path); + getTextFileFromURL(assetEntry.homeURL, onHomeFileLoaded, onHomeFileError); + return; + } + } + // Not obsolete + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); return; } - // console.debug('µBlock> updateFromRemote("%s") / onHomeFileLoaded()', homeURL); - writeLocalFile(targetPath, this.responseText, callback); + // Not in cache + getTextFileFromURL(chrome.runtime.getURL(path), onInstallFileLoaded, onInstallFileError); }; - var onHomeFileError = function(ev) { + var onRepoMetaReady = function(meta) { + assetEntry = meta.entries[path]; + + // Asset doesn't exist + if ( assetEntry === undefined ) { + reportBack('', 'Error: Asset not found'); + return; + } + + // Repo copy changed: fetch from home URL + if ( exports.autoUpdate ) { + if ( assetEntry.localChecksum !== assetEntry.repoChecksum ) { + console.log('µBlock> readRepoCopyAsset("%s") / onRepoMetaReady(): repo has newer version', path); + getTextFileFromURL(assetEntry.homeURL, onHomeFileLoaded, onHomeFileError); + return; + } + } + + // No change, we will inspect the cached version for obsolescence + cachedAssetsManager.entries(onCacheMetaReady); + }; + + getRepoMeta(onRepoMetaReady); +}; + +// https://www.youtube.com/watch?v=uvUW4ozs7pY + +/******************************************************************************/ + +// An important asset shipped with the extension -- typically small, or +// doesn't change often: +// Path --> starts with 'assets/(thirdparties|ublock)/', without a home URL +// Repository --> has checksum (to detect need for update and corruption) +// Cache --> whatever from above +// Local --> install time version + +var readRepoOnlyAsset = function(path, callback) { + + var assetEntry; + + var reportBack = function(content, err) { + var details = { + 'path': path, + 'content': content + }; + if ( err ) { + details.error = err; + } + callback(details); + }; + + var onInstallFileLoaded = function() { this.onload = this.onerror = null; - console.error('µBlock> updateFromRemote() / onHomeFileError("%s"):', homeURL, this.statusText); - getTextFileFromURL(repositoryURL, onRepoFileLoaded, onRepoFileError); + console.log('µBlock> readRepoOnlyAsset("%s") / onInstallFileLoaded()', path); + reportBack(this.responseText); }; - // https://github.com/gorhill/uBlock/issues/84 - // Create a URL from where the asset needs to be downloaded. It can be: - // - a home server URL - // - a github repo URL - var getThirdpartyHomeURL = function() { - // If it is a 3rd-party, look-up home server URL, if any. - if ( thirdpartyHomeURLs && targetPath.indexOf('assets/thirdparties/') === 0 ) { - var k = targetPath.replace('assets/thirdparties/', ''); - if ( thirdpartyHomeURLs[k] ) { - homeURL = thirdpartyHomeURLs[k]; - } + var onInstallFileError = function() { + this.onload = this.onerror = null; + console.error('µBlock> readRepoOnlyAsset("%s") / onInstallFileError()', path); + reportBack('', 'Error'); + }; + + var onCachedContentLoaded = function(details) { + console.log('µBlock> readRepoOnlyAsset("%s") / onCachedContentLoaded()', path); + reportBack(details.content); + }; + + var onCachedContentError = function() { + console.log('µBlock> readRepoOnlyAsset("%s") / onCachedContentError()', path); + getTextFileFromURL(chrome.runtime.getURL(path), onInstallFileLoaded, onInstallFileError); + }; + + var repositoryURL = repositoryRoot + path + '?ublock=' + Date.now(); + + var onRepoFileLoaded = function() { + this.onload = this.onerror = null; + if ( typeof this.responseText !== 'string' ) { + console.error('µBlock> readRepoOnlyAsset("%s") / onRepoFileLoaded("%s"): no response', path, repositoryURL); + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); + return; } - // If there is a home server, disregard checksum: the file is assumed - // most up to date at the home server. - if ( homeURL !== '' ) { - targetMd5 = ''; - getTextFileFromURL(homeURL, onHomeFileLoaded, onHomeFileError); + if ( YaMD5.hashStr(this.responseText) !== assetEntry.repoChecksum ) { + console.error('µBlock> readRepoOnlyAsset("%s") / onRepoFileLoaded("%s"): bad md5 checksum', path, repositoryURL); + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); return; } - // The resource will be pulled from Github repo. It's reserved for - // more important assets, so we keep and use the checksum. - getTextFileFromURL(repositoryURL, onRepoFileLoaded, onRepoFileError); + console.debug('µBlock> readRepoOnlyAsset("%s") / onRepoFileLoaded("%s")', path, repositoryURL); + assetEntry.localChecksum = assetEntry.repoChecksum; + updateLocalChecksums(); + cachedAssetsManager.save(path, this.responseText, callback); }; - // https://github.com/gorhill/uBlock/issues/84 - // First try to load from the actual home server of a third-party. - var onThirdpartyHomeURLsLoaded = function(details) { - thirdpartyHomeURLs = {}; - if ( details.error ) { - getThirdpartyHomeURL(); + var onRepoFileError = function() { + this.onload = this.onerror = null; + console.error('µBlock> readRepoOnlyAsset("%s") / onRepoFileError("%s"):', path, repositoryURL, this.statusText); + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); + }; + + var onRepoMetaReady = function(meta) { + assetEntry = meta.entries[path]; + + // Asset doesn't exist + if ( assetEntry === undefined ) { + reportBack('', 'Error: Asset not found'); return; } - var urlPairs = details.content.split(/\n\n+/); - var i = urlPairs.length; - var pair, pos, k, v; - while ( i-- ) { - pair = urlPairs[i]; - pos = pair.indexOf('\n'); - if ( pos === -1 ) { - continue; + + // Asset added or changed: load from repo URL and then cache result + if ( exports.autoUpdate ) { + if ( assetEntry.localChecksum !== assetEntry.repoChecksum ) { + console.log('µBlock> readRepoOnlyAsset("%s") / onRepoMetaReady(): repo has newer version', path); + getTextFileFromURL(repositoryURL, onRepoFileLoaded, onRepoFileError); + return; } - k = pair.slice(0, pos).trim(); - v = pair.slice(pos).trim(); - if ( k === '' || v === '' ) { - continue; + } + + // Asset unchanged: load from cache + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); + }; + + getRepoMeta(onRepoMetaReady); +}; + +/******************************************************************************/ + +// Asset doesn't exist. Just for symmetry purpose. + +var readNilAsset = function(path, callback) { + callback({ + 'path': path, + 'content': '', + 'error': 'Error: asset not found' + }); +}; + +/******************************************************************************/ + +// An external asset: +// Path --> starts with 'http' +// External --> https://..., http://... +// Cache --> has expiration timestamp (in cache) + +var readExternalAsset = function(path, callback) { + var reportBack = function(content, err) { + var details = { + 'path': path, + 'content': content + }; + if ( err ) { + details.error = err; + } + callback(details); + }; + + var onCachedContentLoaded = function(details) { + console.log('µBlock> readExternalAsset("%s") / onCachedContentLoaded()', path); + reportBack(details.content); + }; + + var onCachedContentError = function() { + console.error('µBlock> readExternalAsset("%s") / onCachedContentError()', path); + reportBack('', 'Error'); + }; + + var onExternalFileLoaded = function() { + this.onload = this.onerror = null; + console.log('µBlock> readExternalAsset("%s") / onExternalFileLoaded1()', path); + cachedAssetsManager.save(path, this.responseText); + reportBack(this.responseText); + }; + + var onExternalFileError2 = function() { + this.onload = this.onerror = null; + console.error('µBlock> readExternalAsset("%s") / onExternalFileError2()', path); + reportBack('', 'Error'); + }; + + var onExternalFileError1 = function() { + this.onload = this.onerror = null; + console.error('µBlock> readExternalAsset("%s") / onExternalFileError1()', path); + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); + }; + + var entriesReady = function(entries) { + var timestamp = entries[path]; + // In cache + if ( typeof timestamp === 'number' ) { + // Obsolete? + if ( exports.autoUpdate ) { + var obsolete = Date.now() - exports.autoUpdateDelay; + if ( timestamp <= obsolete ) { + getTextFileFromURL(path, onExternalFileLoaded, onExternalFileError1); + return; + } } - thirdpartyHomeURLs[k] = v; + // Not obsolete + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); + return; } - getThirdpartyHomeURL(); + // Not in cache + getTextFileFromURL(path, onExternalFileLoaded, onExternalFileError2); + }; + + cachedAssetsManager.entries(entriesReady); +}; + +/******************************************************************************/ + +// User data: +// Path --> starts with 'assets/user/' +// Cache --> whatever user saved + +var readUserAsset = function(path, callback) { + var onCachedContentLoaded = function(details) { + console.log('µBlock> readUserAsset("%s") / onCachedContentLoaded()', path); + callback({ 'path': path, 'content': details.content }); + }; + + var onCachedContentError = function() { + console.log('µBlock> readUserAsset("%s") / onCachedContentError()', path); + callback({ 'path': path, 'content': '' }); }; - // Get home servers if not done yet. - if ( thirdpartyHomeURLs === null ) { - readLocalFile('assets/ublock/thirdparty-lists.txt', onThirdpartyHomeURLsLoaded); - } else { - getThirdpartyHomeURL(); + cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); +}; + +/******************************************************************************/ + +// Assets +// +// A copy of an asset from an external source shipped with the extension: +// Path --> starts with 'assets/(thirdparties|ublock)/', with a home URL +// External --> +// Repository --> has checksum (to detect obsolescence) +// Cache --> has expiration timestamp (to detect obsolescence) +// Local --> install time version +// +// An important asset shipped with the extension (usually small, or doesn't +// change often): +// Path --> starts with 'assets/(thirdparties|ublock)/', without a home URL +// Repository --> has checksum (to detect obsolescence or data corruption) +// Cache --> whatever from above +// Local --> install time version +// +// An external filter list: +// Path --> starts with 'http' +// External --> +// Cache --> has expiration timestamp (to detect obsolescence) +// +// User data: +// Path --> starts with 'assets/user/' +// Cache --> whatever user saved +// +// When a checksum is present, it is used to determine whether the asset +// needs to be updated. +// When an expiration timestamp is present, it is used to determine whether +// 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 +// an asset shipped with the extension, external for an asset not shipped +// with the extension. + +exports.get = function(path, callback) { + + if ( reIsUserPath.test(path) ) { + readUserAsset(path, callback); + return; } + + if ( reIsExternalPath.test(path) ) { + readExternalAsset(path, callback); + return; + } + + var onRepoMetaReady = function(meta) { + var assetEntry = meta.entries[path]; + + // Asset doesn't exist + if ( assetEntry === undefined ) { + readNilAsset(path, callback); + return; + } + + // Asset is repo copy of external content + if ( assetEntry.homeURL !== '' ) { + readRepoCopyAsset(path, callback); + return; + } + + // Asset is repo only + readRepoOnlyAsset(path, callback); + }; + + getRepoMeta(onRepoMetaReady); }; +// https://www.youtube.com/watch?v=98y0Q7nLGWk + /******************************************************************************/ -var cacheEntries = function(callback) { - return cachedAssetsManager.entries(callback); +exports.put = function(path, content, callback) { + cachedAssetsManager.save(path, content, callback); }; /******************************************************************************/ -// Flush cached assets if cache content is from an older version: the extension -// always ships with the most up-to-date assets. +exports.purge = function(pattern, before) { + cachedAssetsManager.remove(pattern, before); +}; + +/******************************************************************************/ + +exports.metadata = function(callback) { + var out = {}; + + var onRepoMetaReady = function(meta) { + var entries = meta.entries; + var entryRepo, entryOut; + for ( var path in entries ) { + if ( entries.hasOwnProperty(path) === false ) { + continue; + } + entryRepo = entries[path]; + entryOut = out[path]; + if ( entryOut === undefined ) { + entryOut = out[path] = {}; + } + entryOut.localChecksum = entryRepo.localChecksum; + entryOut.repoChecksum = entryRepo.repoChecksum; + entryOut.homeURL = entryRepo.homeURL; + entryOut.repoObsolete = entryOut.localChecksum !== entryOut.repoChecksum; + } + callback(out); + }; + + var onCacheMetaReady = function(entries) { + var obsolete = Date.now() - exports.autoUpdateDelay; + var entryOut; + for ( var path in entries ) { + if ( entries.hasOwnProperty(path) === false ) { + continue; + } + entryOut = out[path]; + if ( entryOut === undefined ) { + entryOut = out[path] = {}; + } + entryOut.lastModified = entries[path]; + // User data is not literally cache data + if ( reIsUserPath.test(path) ) { + continue; + } + entryOut.cached = true; + entryOut.cacheObsolete = entryOut.lastModified <= obsolete; + if ( reIsExternalPath.test(path) ) { + entryOut.homeURL = path; + } + } + getRepoMeta(onRepoMetaReady); + }; -synchronizeCache(); + cachedAssetsManager.entries(onCacheMetaReady); +}; /******************************************************************************/ -// Export API +exports.purgeAll = function(callback) { + var onMetaDataReady = function(entries) { + var out = {}; + var entry; + for ( var path in entries ) { + if ( entries.hasOwnProperty(path) === false ) { + continue; + } + entry = entries[path]; + if ( !entry.cacheObsolete && !entry.repoObsolete ) { + continue; + } + cachedAssetsManager.remove(path); + out[path] = true; + } + callback(out); + }; -return { - 'get': readLocalFile, - 'getExternal': readExternalFile, - 'getRepo': readRepoFile, - 'purge': purgeCache, - 'put': writeLocalFile, - 'update': updateFromRemote, - 'entries': cacheEntries + exports.metadata(onMetaDataReady); }; /******************************************************************************/ +return exports; + +/******************************************************************************/ + })(); /******************************************************************************/ diff --git a/js/background.js b/js/background.js index 7c966fc..ff72b98 100644 --- a/js/background.js +++ b/js/background.js @@ -31,6 +31,7 @@ return { manifest: chrome.runtime.getManifest(), userSettings: { + autoUpdate: true, collapseBlocked: true, externalLists: '', logBlockedRequests: false, diff --git a/js/messaging-handlers.js b/js/messaging-handlers.js index 3a93f81..460896f 100644 --- a/js/messaging-handlers.js +++ b/js/messaging-handlers.js @@ -250,9 +250,10 @@ var getLists = function(callback) { available: null, current: µb.remoteBlacklists, cosmetic: µb.userSettings.parseAllABPHideFilters, + autoUpdate: µb.userSettings.autoUpdate, cache: null }; - var onEntries = function(entries) { + var onMetadataReady = function(entries) { r.cache = entries; if ( r.available ) { callback(r); @@ -265,7 +266,7 @@ var getLists = function(callback) { } }; µb.getAvailableLists(onLists); - µb.assets.entries(onEntries); + µb.assets.metadata(onMetadataReady); }; /******************************************************************************/ @@ -475,20 +476,6 @@ var onMessage = function(request, sender, callback) { // Async switch ( request.what ) { - case 'getAssetUpdaterList': - return µb.assetUpdater.getList(callback); - - case 'launchAssetUpdater': - return µb.assetUpdater.update(request.list, callback); - - case 'readUserSettings': - return chrome.storage.local.get(µb.userSettings, callback); - - case 'readUserFilters': - return µb.assets.get(µb.userFiltersPath, callback); - - case 'writeUserFilters': - return µb.assets.put(µb.userFiltersPath, request.content, callback); default: break; @@ -498,20 +485,6 @@ var onMessage = function(request, sender, callback) { var response; switch ( request.what ) { - case 'loadUpdatableAssets': - response = µb.loadUpdatableAssets(); - break; - - case 'readFilterListSelection': - response = µb.remoteBlacklists; - break; - - case 'getSomeStats': - response = { - storageQuota: µb.storageQuota, - storageUsed: µb.storageUsed - }; - break; default: return µb.messaging.defaultHandler(request, sender, callback); @@ -524,4 +497,6 @@ var onMessage = function(request, sender, callback) { })(); +// https://www.youtube.com/watch?v=3_WcygKJP1k + /******************************************************************************/ diff --git a/js/messaging.js b/js/messaging.js index bc5d4e7..86c2309 100644 --- a/js/messaging.js +++ b/js/messaging.js @@ -132,15 +132,12 @@ var onMessage = function(request, port) { /******************************************************************************/ -// Default is for commonly used message. +// Default is for commonly used messages. function defaultHandler(request, sender, callback) { // Async switch ( request.what ) { case 'getAssetContent': - if ( /^https?:\/\//.test(request.url) ) { - return µBlock.assets.getExternal(request.url, callback); - } return µBlock.assets.get(request.url, callback); case 'loadUbiquitousAllowRules': @@ -171,7 +168,7 @@ function defaultHandler(request, sender, callback) { break; case 'reloadAllFilters': - µBlock.reloadPresetBlacklists(request.switches); + µBlock.reloadPresetBlacklists(request.switches, request.update); break; case 'userSettings': @@ -186,6 +183,8 @@ function defaultHandler(request, sender, callback) { callback(response); } +// https://www.youtube.com/watch?v=rrzRgUAHqc8 + /******************************************************************************/ // Port disconnected, relay this information to apropriate listener. diff --git a/js/pagestore.js b/js/pagestore.js index d541305..1302155 100644 --- a/js/pagestore.js +++ b/js/pagestore.js @@ -218,7 +218,7 @@ PageStore.prototype.updateBadgeFromTab = function(tab) { var iconStr = ''; if ( µb.userSettings.showIconBadge && netFiltering && this.perLoadBlockedRequestCount ) { - iconStr = this.perLoadBlockedRequestCount.toLocaleString(); + iconStr = µb.utils.formatCount(this.perLoadBlockedRequestCount); } chrome.browserAction.setBadgeText({ tabId: tab.id, text: iconStr }); @@ -234,6 +234,8 @@ PageStore.prototype.updateBadge = function() { chrome.tabs.get(this.tabId, this.updateBadgeFromTab.bind(this)); }; +// https://www.youtube.com/watch?v=drW8p_dTLD4 + /******************************************************************************/ return { diff --git a/js/start.js b/js/start.js index 2ab08ba..aef9f4a 100644 --- a/js/start.js +++ b/js/start.js @@ -35,18 +35,17 @@ (function() { var µb = µBlock; - var jobDone = function(details) { - if ( details.changedCount === 0 ) { + var jobCallback = function() { + if ( µb.userSettings.autoUpdate !== true ) { return; } - µb.loadUpdatableAssets(); - }; - - var jobCallback = function() { - µb.assetUpdater.update(null, jobDone); + // TODO: need smarter update, currently blindly reloading all. + µb.loadUpdatableAssets(true); }; µb.asyncJobs.add('autoUpdateAssets', null, jobCallback, µb.updateAssetsEvery, true); })(); +// https://www.youtube.com/watch?v=cIrGQD84F1g + /******************************************************************************/ diff --git a/js/storage.js b/js/storage.js index 1073d2c..a8f1840 100644 --- a/js/storage.js +++ b/js/storage.js @@ -70,7 +70,7 @@ /******************************************************************************/ -µBlock.loadUserSettings = function() { +µBlock.loadUserSettings = function(callback) { var settingsLoaded = function(store) { var µb = µBlock; // Backward compatibility after fix to #5 @@ -88,6 +88,8 @@ µb.userSettings = store; µb.netWhitelist = µb.whitelistFromString(store.netWhitelist); µb.netWhitelistModifyTime = Date.now(); + + callback(); }; chrome.storage.local.get(this.userSettings, settingsLoaded); @@ -265,11 +267,7 @@ blacklistLoadCount -= 1; continue; } - if ( /^https?:\/\/.+$/.test(location) ) { - µb.assets.getExternal(location, mergeBlacklist); - } else { - µb.assets.get(location, mergeBlacklist); - } + µb.assets.get(location, mergeBlacklist); } }; @@ -400,7 +398,7 @@ // `switches` contains the preset blacklists for which the switch must be // revisited. -µBlock.reloadPresetBlacklists = function(switches) { +µBlock.reloadPresetBlacklists = function(switches, update) { var presetBlacklists = this.remoteBlacklists; // Toggle switches, if any @@ -420,7 +418,7 @@ } // Now force reload - this.loadUbiquitousBlacklists(); + this.loadUpdatableAssets(update); }; /******************************************************************************/ @@ -443,9 +441,11 @@ // Load updatable assets -µBlock.loadUpdatableAssets = function() { - this.loadUbiquitousBlacklists(); +µBlock.loadUpdatableAssets = function(update) { + this.assets.autoUpdate = update; + this.assets.autoUpdateDelay = this.updateAssetsEvery; this.loadPublicSuffixList(); + this.loadUbiquitousBlacklists(); }; /******************************************************************************/ @@ -453,11 +453,8 @@ // Load all µBlock.load = function() { - this.loadLocalSettings(); - this.loadUserSettings(); - - // load updatable assets -- after updating them if needed - this.assetUpdater.update(null, this.loadUpdatableAssets.bind(this)); - + // User settings need to be available for this because we need + // µBlock.userSettings.externalLists + this.loadUserSettings(this.loadUpdatableAssets.bind(this, this.userSettings.autoUpdate)); this.getBytesInUse(); }; diff --git a/js/ublock.js b/js/ublock.js index 169efbb..cce9aa5 100644 --- a/js/ublock.js +++ b/js/ublock.js @@ -233,24 +233,3 @@ }; /******************************************************************************/ - -µBlock.formatCount = function(count) { - if ( typeof count !== 'number' ) { - return ''; - } - var s = count.toFixed(0); - if ( count >= 1000 ) { - if ( count < 10000 ) { - s = '>' + s.slice(0,1) + 'K'; - } else if ( count < 100000 ) { - s = s.slice(0,2) + 'K'; - } else if ( count < 1000000 ) { - s = s.slice(0,3) + 'K'; - } else if ( count < 10000000 ) { - s = s.slice(0,1) + 'M'; - } else { - s = s.slice(0,-6) + 'M'; - } - } - return s; -}; diff --git a/js/utils.js b/js/utils.js index 0b534de..65cc997 100644 --- a/js/utils.js +++ b/js/utils.js @@ -29,7 +29,11 @@ /******************************************************************************/ -var gotoURL = function(details) { +var exports = {}; + +/******************************************************************************/ + +exports.gotoURL = function(details) { if ( details.tabId ) { chrome.tabs.update(details.tabId, { url: details.url }); } else { @@ -39,7 +43,7 @@ var gotoURL = function(details) { /******************************************************************************/ -var gotoExtensionURL = function(url) { +exports.gotoExtensionURL = function(url) { var hasQuery = function(url) { return url.indexOf('?') >= 0; @@ -98,11 +102,33 @@ var gotoExtensionURL = function(url) { /******************************************************************************/ -return { - gotoURL: gotoURL, - gotoExtensionURL: gotoExtensionURL +exports.formatCount = function(count) { + if ( typeof count !== 'number' ) { + return ''; + } + var s = count.toFixed(0); + if ( count >= 1000 ) { + if ( count < 10000 ) { + s = '>' + s.slice(0,1) + 'K'; + } else if ( count < 100000 ) { + s = s.slice(0,2) + 'K'; + } else if ( count < 1000000 ) { + s = s.slice(0,3) + 'K'; + } else if ( count < 10000000 ) { + s = s.slice(0,1) + 'M'; + } else { + s = s.slice(0,-6) + 'M'; + } + } + return s; }; +// https://www.youtube.com/watch?v=uvUW4ozs7pY + +/******************************************************************************/ + +return exports; + /******************************************************************************/ })(); |