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 | |
parent | 56ed6b9f4e65efb8b80b7b1ad93931484c54dde0 (diff) | |
download | uBlock-1deae3bfe0c1379efb869b94a30d1a731292cfab.zip uBlock-1deae3bfe0c1379efb869b94a30d1a731292cfab.tar.gz uBlock-1deae3bfe0c1379efb869b94a30d1a731292cfab.tar.bz2 |
this fixes #138, next: thorough code review
-rw-r--r-- | 3p-filters.html | 58 | ||||
-rw-r--r-- | _locales/de/messages.json | 22 | ||||
-rw-r--r-- | _locales/en/messages.json | 22 | ||||
-rw-r--r-- | _locales/fr/messages.json | 24 | ||||
-rw-r--r-- | about.html | 74 | ||||
-rw-r--r-- | assets/user/filters.txt | 1 | ||||
-rw-r--r-- | background.html | 1 | ||||
-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 | ||||
-rw-r--r-- | manifest.json | 2 |
20 files changed, 893 insertions, 792 deletions
diff --git a/3p-filters.html b/3p-filters.html index c2706f5..b404fcd 100644 --- a/3p-filters.html +++ b/3p-filters.html @@ -36,18 +36,59 @@ ul > li > ul > li { .dim { color: #888; } -button.purge { +/* I designed the button with: http://charliepark.org/bootstrap_buttons/ */ +button.reloadAll { + border: 1px solid transparent; + border-radius: 3px; + border-color: #dddddd #dddddd hsl(36, 0%, 85%); + padding: 5px; + background-color: hsl(36, 0%, 72%) !important; + background-repeat: repeat-x; + background-image: linear-gradient(#f2f2f2, #dddddd); + color: #aaa; + } +button.reloadAll.enabled { + border-color: #ffcc7f #ffcc7f hsl(36, 100%, 73%); + color: #222; + background-color: hsl(36, 100%, 75%) !important; + background-image: linear-gradient(#ffdca8, #ffcc7f); + cursor: pointer; + opacity: 0.8; + } +button.reloadAll:hover { + opacity: 1.0; + } +#buttonApply { + display: none; + position: fixed; + right: 1em; + top: 1em; + } +#buttonApply.enabled { + display: initial; + } +span.status { margin: 0; - border: 1px solid #ccc; + border: 1px solid transparent; padding: 1px 2px; + display: inline-block; + font-size: 11px; + opacity: 0.7; +} +span.purge { + border-color: #ddd; color: #444; background-color: #eee; - font-size: 11px; - opacity: 0.6; + cursor: pointer; } -button.purge:hover { +span.purge:hover { opacity: 1; } +span.obsolete { + border-color: hsl(36, 100%, 73%); + color: #222; + background-color: hsl(36, 100%, 75%); + } #externalLists { font-size: smaller; width: 48em; @@ -59,10 +100,11 @@ button.purge:hover { <body> +<button id="buttonApply" class="reloadAll" data-i18n="3pApplyChanges"></button> <ul id="options"> - <li><button id="blacklistsApply" disabled="true" data-i18n="3pApplyChanges"></button> - <li style="margin-top:0.75em"><input type="checkbox" id="parseAllABPHideFilters"><label data-i18n="3pParseAllABPHideFiltersPrompt1"></label> - <span class="dim" id="3pParseAllABPHideFiltersPrompt2"></span> + <li><input type="checkbox" id="autoUpdate"><label data-i18n="3pAutoUpdatePrompt1"></label> + <button class="reloadAll" id="buttonUpdate" data-i18n="3pUpdateNow"></button> + <li><input type="checkbox" id="parseCosmeticFilters"><label data-i18n="3pParseAllABPHideFiltersPrompt1"></label> <button class="whatisthis"></button> <div class="whatisthis-expandable para" data-i18n="3pParseAllABPHideFiltersInfo"></div> <li style="margin-top:0.75em"><p id="listsOfBlockedHostsPrompt"></p> diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 5506866..ffb62a5 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -99,13 +99,17 @@ "message":"{{used}} benutzt aus {{total}}", "description":"English: {{used}} used out of {{total}}" }, + "3pAutoUpdatePrompt1":{ + "message":"Auto-update filter lists.", + "description":"English: Auto-update filter lists." + }, + "3pUpdateNow":{ + "message":"Update now", + "description":"English: Update now" + }, "3pParseAllABPHideFiltersPrompt1":{ "message":"Adblock+ Filter zum Verstecken von Elementen analysieren und anwenden.", - "description":"English: Parse and enforce Adblock+ element hiding filters." - }, - "3pParseAllABPHideFiltersPrompt2":{ - "message":"{{abpHideFilterCount}} Filter zum Verstecken von Elementen genutzt.", - "description":"English: {{abpHideFilterCount}} element hiding filters used." + "description":"English: Parse and enforce cosmetic filters." }, "3pParseAllABPHideFiltersInfo":{ "message":"<p>Diese Option ermöglicht die Analyse und Anwendung von <a href=\"https:\/\/adblockplus.org\/de\/faq_internal#elemhide\">Adblock Plus-kompatiblen Filtern zum “Verstecken von Elementen” <\/a>. Diese Filter sind in erster Linie kosmetischer Natur und dienen zum Verstecken von Elementen auf einer Webseite, die als optische Belästigung wahrgenommen werden und nicht von den vorhandenen Filtern geblockt werden können.<\/p><p>Das Aktivieren dieser Option erhöht den Speicherbedarf von <i>µBlock<\/i>.<\/p>", @@ -159,6 +163,14 @@ "message":"Leere den Cache", "description":"English: purge cache" }, + "3pExternalListNew":{ + "message":"new version available", + "description":"English: new version available" + }, + "3pExternalListObsolete":{ + "message":"may be obsolete", + "description":"English: may be obsolete" + }, "1pFormatHint":{ "message":"Eine Regel pro Zeile. Eine Regel kann ein einfacher Host-Name sein oder ein Adblock Plus-kompatibler Filter. Zeilen mit vorangestelltem ‘!’ werden ignoriert.", "description":"English: One filter per line. A filter can be a plain hostname, or an Adblock Plus-compatible filter. Lines prefixed with ‘!’ will be ignored." diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 033aa2f..9d6a101 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -99,14 +99,18 @@ "message":"{{used}} used out of {{total}}", "description":"English: {{used}} used out of {{total}}" }, + "3pAutoUpdatePrompt1":{ + "message":"Auto-update filter lists.", + "description":"English: Auto-update filter lists." + }, + "3pUpdateNow":{ + "message":"Update now", + "description":"English: Update now" + }, "3pParseAllABPHideFiltersPrompt1":{ - "message":"Parse and enforce Adblock+ element hiding filters.", + "message":"Parse and enforce cosmetic filters.", "description":"English: Parse and enforce Adblock+ element hiding filters." }, - "3pParseAllABPHideFiltersPrompt2":{ - "message":"{{abpHideFilterCount}} element hiding filters used.", - "description":"English: {{abpHideFilterCount}} element hiding filters used." - }, "3pParseAllABPHideFiltersInfo":{ "message":"<p>This option enables the parsing and enforcing of <a href=\"https:\/\/adblockplus.org\/en\/faq_internal#elemhide\">Adblock Plus-compatible “element hiding” filters<\/a>. These filters are essentially cosmetic, they serve to hide elements in a web page which are deemed to be a visual nuisance, and which can't be blocked by the net request-based filtering engine.<\/p><p>Enabling this feature increases <i>µBlock<\/i>'s memory footprint.<\/p>", "description":"English: see English messages.json" @@ -159,6 +163,14 @@ "message":"purge cache", "description":"English: purge cache" }, + "3pExternalListNew":{ + "message":"new version available", + "description":"English: new version available" + }, + "3pExternalListObsolete":{ + "message":"may be obsolete", + "description":"English: may be obsolete" + }, "1pFormatHint":{ "message":"One filter per line. A filter can be a plain hostname, or an Adblock Plus-compatible filter. Lines prefixed with ‘!’ will be ignored.", "description":"English: One filter per line. A filter can be a plain hostname, or an Adblock Plus-compatible filter. Lines prefixed with ‘!’ will be ignored." diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 7bb11b7..53b9e2b 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -99,13 +99,17 @@ "message":"{{used}} utilisé(s) sur un total de {{total}}", "description":"English: {{used}} used out of {{total}}" }, - "3pParseAllABPHideFiltersPrompt1":{ - "message":"Utiliser en plus les règles Adblock Plus esthétiques", - "description":"English: Parse and enforce Adblock+ element hiding filters." + "3pAutoUpdatePrompt1":{ + "message":"Mise à jour automatique des listes", + "description":"English: Auto-update filter lists." + }, + "3pUpdateNow":{ + "message":"Mettre à jour", + "description":"English: Update now" }, - "3pParseAllABPHideFiltersPrompt2":{ - "message":"{{abpHideFilterCount}} règle(s) Adblock Plus esthétique(s) utilisée(s)", - "description":"English: {{abpHideFilterCount}} element hiding filters used." + "3pParseAllABPHideFiltersPrompt1":{ + "message":"Utiliser en plus les règles esthétiques", + "description":"English: Parse and enforce cosmetic filters." }, "3pParseAllABPHideFiltersInfo":{ "message":"<p>Cette option permet de prendre en charge les filtres AdblockPlus de type <a href=\"https:\/\/adblockplus.org\/en\/faq_internal#elemhide\">“element hiding”<\/a>. Ces filtres ont principalement un impact visuel, servant à dissimuler des éléments nuisibles d'une page Web et qui ne sont pas blocables par le filtrage standard.<\/p><p>L'activation de cette fonctionnalité augmente l'empreinte mémoire de <i>µBlock<\/i><\/p>", @@ -159,6 +163,14 @@ "message":"vider le cache", "description":"English: purge cache" }, + "3pExternalListNew":{ + "message":"nouvelle version disponible", + "description":"English: new version available" + }, + "3pExternalListObsolete":{ + "message":"potentiellement désuet", + "description":"English: may be obsolete" + }, "1pFormatHint":{ "message":"Une règle par ligne. Une règle peut être un simple nom d'hôte, ou encore un filtre respectant la syntaxe des filtres Adblock Plus. Les lignes débutant par ‘!’ seront ignorées.", "description":"English: One filter per line. A filter can be a plain hostname, or an Adblock Plus-compatible filter. Lines prefixed with ‘!’ will be ignored." @@ -6,46 +6,8 @@ <link rel="stylesheet" type="text/css" href="css/common.css"> <link rel="stylesheet" type="text/css" href="css/dashboard-common.css"> <style> -table th { - text-align: left; - } -table td:first-child { - padding-right: 2em; - } -#allLocalAssetsUpdated { - margin-left: 1em; - color: green; - } -#assetList table tr.unchanged { - color: gray; - } -#assetList table tr td:nth-of-type(2) { - font-weight: bold; - white-space: nowrap; - } -#assetList table tr.unchanged td:nth-of-type(2) { - font-weight: normal; - } - -#assetList.ooo > table { - display: none; - } -#assetList.oox > table, -#assetList.oxx > table, -#assetList.xxx > table { - display: block; - } -#assetList > p { - display: none; - } -#assetList.ooo > p.ooo { - display: block; - } -#assetList.oxx > p.oxx { - display: block; - } -#assetList.xxx > p.xxx { - display: block; +ul { + padding-left: 1em; } </style> </head> @@ -53,38 +15,14 @@ table td:first-child { <body> <h2>µBlock <span id="aboutVersion"></span></h2> -<div> - <a href="https://github.com/gorhill/uBlock/releases" data-i18n="aboutChangelog"></a><br> - <a href="https://github.com/gorhill/ublock" data-i18n="aboutCode"></a><br> -</div> - -<h2 data-i18n="aboutExtensionDataHeader"></h2> -<div> - <p class="para" data-i18n="aboutAssetsUpdatePrompt"></p> - <div id="assetList"> - <!-- - Let's define 'abc' as bit 0 to 2 - Where bit can be '0' or '1' - Bit 0: list => o=absent, x=present - Bit 1: list => o=clean, x=dirty - Bit 2: update => o=idle, x=updating - Therefore: - List visible: oox oxx xox - Etc. - --> - <table class="ooo"> - <tr><th data-i18n="aboutAssetsUpdateColPath"><th data-i18n="aboutAssetsUpdateColStatus"> - </table> - <p class="ooo" style="color:red" data-i18n="aboutAssetsUpdateGetListError"></p> - <p class="oxx"><button type="button" id="aboutAssetsUpdateButton" data-i18n="aboutAssetsUpdateButton"></button></p> - <p class="xxx"><button type="button" disabled data-i18n="aboutAssetsUpdatingButton"></button></p> - </div> -</div> +<ul> + <li><a href="https://github.com/gorhill/uBlock/releases" data-i18n="aboutChangelog"></a><br> + <li><a href="https://github.com/gorhill/ublock" data-i18n="aboutCode"></a><br> +</ul> <script src="js/udom.js"></script> <script src="js/i18n.js"></script> <script src="js/dashboard-common.js"></script> -<script src="js/messaging-client.js"></script> <script src="js/about.js"></script> </body> diff --git a/assets/user/filters.txt b/assets/user/filters.txt deleted file mode 100644 index e9e3def..0000000 --- a/assets/user/filters.txt +++ /dev/null @@ -1 +0,0 @@ -# your custom filters
\ No newline at end of file diff --git a/background.html b/background.html index 90adffa..ce82bbe 100644 --- a/background.html +++ b/background.html @@ -13,7 +13,6 @@ <script src="js/liquid-dict.js"></script> <script src="js/utils.js"></script> <script src="js/assets.js"></script> -<script src="js/asset-updater.js"></script> <script src="js/abp-filters.js"></script> <script src="js/abp-hide-filters.js"></script> <script src="js/ublock.js"></script> 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; + /******************************************************************************/ })(); diff --git a/manifest.json b/manifest.json index 4a926ee..4bfcbb8 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "µBlock", - "version": "0.4.0.1", + "version": "0.4.0.2", "description": "__MSG_extShortDesc__", "icons": { "16": "img/icon_16.png", |