diff --git a/.travis.yml b/.travis.yml index 5f15a9782e3b0..a46f7f9c2113a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ env: script: ./tools/make-${BROWSER}.sh all deploy: provider: releases + prerelease: true # https://github.com/travis-ci/travis-ci/issues/6772 edge: branch: releases-fix diff --git a/assets/assets.json b/assets/assets.json new file mode 100644 index 0000000000000..305d1b6cdd658 --- /dev/null +++ b/assets/assets.json @@ -0,0 +1,597 @@ +{ + "assets.json": { + "content": "internal", + "updateAfter": 13, + "contentURL": [ + "https://raw.githubusercontent.com/gorhill/uBlock/master/assets/assets.json", + "assets/assets.json" + ] + }, + "public_suffix_list.dat": { + "content": "internal", + "updateAfter": 19, + "contentURL": [ + "https://publicsuffix.org/list/public_suffix_list.dat", + "assets/thirdparties/publicsuffix.org/list/effective_tld_names.dat" + ] + }, + "ublock-resources": { + "content": "internal", + "updateAfter": 7, + "contentURL": [ + "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/resources.txt", + "assets/ublock/resources.txt" + ] + }, + "ublock-filters": { + "content": "filters", + "group": "default", + "title": "uBlock filters", + "contentURL": [ + "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt", + "assets/ublock/filters.txt" + ], + "supportURL": "https://github.com/uBlockOrigin/uAssets/issues" + }, + "ublock-badware": { + "content": "filters", + "group": "default", + "title": "uBlock filters – Badware risks", + "contentURL": [ + "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/badware.txt", + "assets/ublock/badware.txt" + ], + "supportURL": "https://github.com/gorhill/uBlock/wiki/Badware-risks", + "instructionURL": "https://github.com/gorhill/uBlock/wiki/Badware-risks" + }, + "ublock-experimental": { + "content": "filters", + "group": "default", + "title": "uBlock filters – Experimental", + "off": true, + "contentURL": [ + "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/experimental.txt", + "assets/ublock/experimental.txt" + ], + "supportURL": "https://github.com/gorhill/uBlock/wiki/Experimental-filters", + "instructionURL": "https://github.com/gorhill/uBlock/wiki/Experimental-filters" + }, + "ublock-privacy": { + "content": "filters", + "group": "default", + "title": "uBlock filters – Privacy", + "contentURL": [ + "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/privacy.txt", + "assets/ublock/privacy.txt" + ] + }, + "ublock-unbreak": { + "content": "filters", + "group": "default", + "title": "uBlock filters – Unbreak", + "contentURL": [ + "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/unbreak.txt", + "assets/ublock/unbreak.txt" + ] + }, + "awrl-0": { + "content": "filters", + "group": "ads", + "off": true, + "title": "Adblock Warning Removal List", + "contentURL": "https://easylist-downloads.adblockplus.org/antiadblockfilters.txt", + "supportURL": "https://forums.lanik.us/" + }, + "reek-0": { + "content": "filters", + "group": "ads", + "off": true, + "title": "Anti-Adblock Killer | Reek", + "contentURL": "https://raw.githubusercontent.com/reek/anti-adblock-killer/master/anti-adblock-killer-filters.txt", + "supportURL": "https://github.com/reek/anti-adblock-killer", + "instructionURL": "https://github.com/reek/anti-adblock-killer#instruction" + }, + "easylist": { + "content": "filters", + "group": "ads", + "title": "EasyList", + "contentURL": [ + "https://easylist.to/easylist/easylist.txt", + "https://easylist-downloads.adblockplus.org/easylist.txt", + "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/thirdparties/easylist-downloads.adblockplus.org/easylist.txt", + "assets/thirdparties/easylist-downloads.adblockplus.org/easylist.txt" + ], + "supportURL": "https://forums.lanik.us/" + }, + "easylist-nocosmetic": { + "content": "filters", + "group": "ads", + "off": true, + "title": "EasyList without element hiding rules", + "contentURL": "https://easylist-downloads.adblockplus.org/easylist_noelemhide.txt", + "supportURL": "https://forums.lanik.us/" + }, + "disconnect-tracking": { + "content": "filters", + "group": "privacy", + "off": true, + "title": "Basic tracking list by Disconnect", + "contentURL": "https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt" + }, + "easyprivacy": { + "content": "filters", + "group": "privacy", + "title": "EasyPrivacy", + "contentURL": [ + "https://easylist.to/easylist/easyprivacy.txt", + "https://easylist-downloads.adblockplus.org/easyprivacy.txt", + "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/thirdparties/easylist-downloads.adblockplus.org/easyprivacy.txt", + "assets/thirdparties/easylist-downloads.adblockplus.org/easyprivacy.txt" + ], + "supportURL": "https://forums.lanik.us/" + }, + "fanboy-enhanced": { + "content": "filters", + "group": "privacy", + "off": true, + "title": "Fanboy’s Enhanced Tracking List", + "contentURL": "https://www.fanboy.co.nz/enhancedstats.txt", + "supportURL": "https://forums.lanik.us/" + }, + "disconnect-malvertising": { + "content": "filters", + "group": "malware", + "off": true, + "title": "Malvertising filter list by Disconnect", + "contentURL": "https://s3.amazonaws.com/lists.disconnect.me/simple_malvertising.txt" + }, + "malware-0": { + "content": "filters", + "group": "malware", + "title": "Malware Domain List", + "contentURL": [ + "https://www.malwaredomainlist.com/hostslist/hosts.txt", + "assets/thirdparties/www.malwaredomainlist.com/hostslist/hosts.txt" + ] + }, + "malware-1": { + "content": "filters", + "group": "malware", + "title": "Malware domains", + "contentURL": [ + "https://mirror.cedia.org.ec/malwaredomains/justdomains", + "https://mirror1.malwaredomains.com/files/justdomains", + "assets/thirdparties/mirror1.malwaredomains.com/files/justdomains", + "assets/thirdparties/mirror1.malwaredomains.com/files/justdomains.txt" + ], + "supportURL": "http://www.malwaredomains.com/" + }, + "malware-2": { + "content": "filters", + "group": "malware", + "off": true, + "title": "Malware domains (long-lived)", + "contentURL": [ + "https://mirror1.malwaredomains.com/files/immortal_domains.txt", + "https://mirror.cedia.org.ec/malwaredomains/immortal_domains.txt" + ], + "supportURL": "http://www.malwaredomains.com/" + }, + "disconnect-malware": { + "content": "filters", + "group": "malware", + "off": true, + "title": "Malware filter list by Disconnect", + "contentURL": "https://s3.amazonaws.com/lists.disconnect.me/simple_malware.txt" + }, + "spam404-0": { + "content": "filters", + "group": "malware", + "off": true, + "title": "Spam404", + "contentURL": "https://raw.githubusercontent.com/Dawsey21/Lists/master/adblock-list.txt", + "supportURL": "http://www.spam404.com/" + }, + "fanboy-thirdparty_social": { + "content": "filters", + "group": "social", + "off": true, + "title": "Anti-ThirdpartySocial (see warning inside list)", + "contentURL": "https://fanboy.co.nz/fanboy-antifacebook.txt", + "supportURL": "https://forums.lanik.us/" + }, + "fanboy-annoyance": { + "content": "filters", + "group": "social", + "off": true, + "title": "Fanboy’s Annoyance List", + "contentURL": [ + "https://easylist.to/easylist/fanboy-annoyance.txt", + "https://fanboy.co.nz/fanboy-annoyance.txt", + "https://easylist-downloads.adblockplus.org/fanboy-annoyance.txt" + ], + "supportURL": "https://forums.lanik.us/" + }, + "fanboy-social": { + "content": "filters", + "group": "social", + "off": true, + "title": "Fanboy’s Social Blocking List", + "contentURL": [ + "https://easylist.to/easylist/fanboy-social.txt", + "https://fanboy.co.nz/fanboy-social.txt", + "https://easylist-downloads.adblockplus.org/fanboy-social.txt" + ], + "supportURL": "https://forums.lanik.us/" + }, + "dpollock-0": { + "content": "filters", + "group": "multipurpose", + "updateAfter": 11, + "off": true, + "title": "Dan Pollock’s hosts file", + "contentURL": "http://someonewhocares.org/hosts/hosts", + "supportURL": "http://someonewhocares.org/hosts/" + }, + "fanboy-ultimate": { + "content": "filters", + "group": "multipurpose", + "off": true, + "title": "Fanboy+Easylist-Merged Ultimate List", + "contentURL": "https://www.fanboy.co.nz/r/fanboy-ultimate.txt", + "supportURL": "https://forums.lanik.us/" + }, + "hphosts": { + "content": "filters", + "group": "multipurpose", + "updateAfter": 11, + "off": true, + "title": "hpHosts’ Ad and tracking servers", + "contentURL": "https://hosts-file.net/.%5Cad_servers.txt", + "supportURL": "https://hosts-file.net/" + }, + "mvps-0": { + "content": "filters", + "group": "multipurpose", + "updateAfter": 11, + "off": true, + "title": "MVPS HOSTS", + "contentURL": "http://winhelp2002.mvps.org/hosts.txt", + "supportURL": "http://winhelp2002.mvps.org/" + }, + "plowe-0": { + "content": "filters", + "group": "multipurpose", + "updateAfter": 13, + "title": "Peter Lowe’s Ad and tracking server list", + "contentURL": [ + "https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=1&mimetype=plaintext", + "assets/thirdparties/pgl.yoyo.org/as/serverlist", + "assets/thirdparties/pgl.yoyo.org/as/serverlist.txt" + ], + "supportURL": "https://pgl.yoyo.org/adservers/" + }, + "ara-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "ara: Liste AR", + "lang": "ar", + "contentURL": "https://easylist-downloads.adblockplus.org/Liste_AR.txt", + "supportURL": "https://forums.lanik.us/viewforum.php?f=98" + }, + "BGR-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "BGR: Bulgarian Adblock list", + "lang": "bg", + "contentURL": "https://stanev.org/abp/adblock_bg.txt", + "supportURL": "https://stanev.org/abp/" + }, + "CHN-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "CHN: EasyList China (中文)", + "lang": "zh", + "contentURL": "https://easylist-downloads.adblockplus.org/easylistchina.txt", + "supportURL": "http://abpchina.org/forum/forum.php" + }, + "CHN-1": { + "content": "filters", + "group": "regions", + "off": true, + "title": "CHN: CJX's EasyList Lite (main focus on Chinese sites)", + "contentURL": "https://raw.githubusercontent.com/cjx82630/cjxlist/master/cjxlist.txt", + "supportURL": "https://github.com/cjx82630/cjxlist" + }, + "CHN-2": { + "content": "filters", + "group": "regions", + "off": true, + "title": "CHN: CJX's Annoyance List", + "contentURL": "https://raw.githubusercontent.com/cjx82630/cjxlist/master/cjx-annoyance.txt", + "supportURL": "https://github.com/cjx82630/cjxlist" + }, + "CZE-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "CZE, SVK: EasyList Czech and Slovak", + "lang": "cs", + "contentURL": "https://raw.githubusercontent.com/tomasko126/easylistczechandslovak/master/filters.txt", + "supportURL": "https://github.com/tomasko126/easylistczechandslovak" + }, + "DEU-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "DEU: EasyList Germany", + "lang": "de", + "contentURL": [ + "https://easylist.to/easylistgermany/easylistgermany.txt", + "https://easylist-downloads.adblockplus.org/easylistgermany.txt" + ], + "supportURL": "https://forums.lanik.us/viewforum.php?f=90" + }, + "DNK-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "DNK: Schacks Adblock Plus liste", + "lang": "da", + "contentURL": "https://adblock.dk/block.csv", + "supportURL": "https://henrik.schack.dk/adblock/" + }, + "EST-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "EST: Eesti saitidele kohandatud filter", + "lang": "et", + "contentURL": "http://adblock.ee/list.php", + "supportURL": "http://adblock.ee/" + }, + "EU-prebake": { + "content": "filters", + "group": "regions", + "off": true, + "title": "EU: Prebake - Filter Obtrusive Cookie Notices", + "contentURL": "https://raw.githubusercontent.com/liamja/Prebake/master/obtrusive.txt", + "supportURL": "https://github.com/liamja/Prebake" + }, + "FIN-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "FIN: Finnish Addition to Easylist", + "lang": "fi", + "contentURL": "http://adb.juvander.net/Finland_adb.txt", + "supportURL": "http://www.juvander.fi/AdblockFinland" + }, + "FRA-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "FRA: EasyList Liste FR", + "lang": "fr", + "contentURL": "https://easylist-downloads.adblockplus.org/liste_fr.txt", + "supportURL": "https://forums.lanik.us/viewforum.php?f=91" + }, + "GRC-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "GRC: Greek AdBlock Filter", + "lang": "el", + "contentURL": "https://www.void.gr/kargig/void-gr-filters.txt", + "supportURL": "https://github.com/kargig/greek-adblockplus-filter" + }, + "HUN-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "HUN: hufilter", + "lang": "hu", + "contentURL": "https://raw.githubusercontent.com/szpeter80/hufilter/master/hufilter.txt", + "supportURL": "https://github.com/szpeter80/hufilter" + }, + "IDN-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "IDN: ABPindo", + "lang": "id", + "contentURL": [ + "https://raw.githubusercontent.com/ABPindo/indonesianadblockrules/master/subscriptions/abpindo.txt", + "https://raw.githubusercontent.com/heradhis/indonesianadblockrules/master/subscriptions/abpindo.txt" + ], + "supportURL": "https://github.com/ABPindo/indonesianadblockrules" + }, + "ISL-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "ISL: Icelandic ABP List", + "lang": "is", + "contentURL": "http://adblock.gardar.net/is.abp.txt", + "supportURL": "http://adblock.gardar.net/" + }, + "ISR-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "ISR: EasyList Hebrew", + "lang": "he", + "contentURL": "https://raw.githubusercontent.com/easylist/EasyListHebrew/master/EasyListHebrew.txt", + "supportURL": "https://github.com/easylist/EasyListHebrew" + }, + "ITA-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "ITA: EasyList Italy", + "lang": "it", + "contentURL": "https://easylist-downloads.adblockplus.org/easylistitaly.txt", + "supportURL": "https://forums.lanik.us/viewforum.php?f=96" + }, + "ITA-1": { + "content": "filters", + "group": "regions", + "off": true, + "title": "ITA: ABP X Files", + "contentURL": "https://raw.githubusercontent.com/gioxx/xfiles/master/filtri.txt", + "supportURL": "http://noads.it/" + }, + "JPN-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "JPN: ABP Japanese filters (日本用フィルタ)", + "lang": "ja", + "contentURL": "https://raw.githubusercontent.com/k2jp/abp-japanese-filters/master/abpjf.txt", + "supportURL": "https://github.com/k2jp/abp-japanese-filters/wiki/Support_Policy" + }, + "KOR-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "KOR: Korean Adblock List", + "lang": "ko", + "contentURL": "https://raw.githubusercontent.com/gfmaster/adblock-korea-contrib/master/filter.txt", + "supportURL": "https://github.com/gfmaster/adblock-korea-contrib" + }, + "KOR-1": { + "content": "filters", + "group": "regions", + "off": true, + "title": "KOR: YousList", + "lang": "ko", + "contentURL": "https://raw.githubusercontent.com/yous/YousList/master/youslist.txt", + "supportURL": "https://github.com/yous/YousList" + }, + "KOR-2": { + "content": "filters", + "group": "regions", + "off": true, + "title": "KOR: Fanboy's Korean", + "contentURL": "https://www.fanboy.co.nz/fanboy-korean.txt", + "supportURL": "https://forums.lanik.us/" + }, + "LTU-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "LTU: Adblock Plus Lithuania", + "lang": "lt", + "contentURL": "http://margevicius.lt/easylistlithuania.txt", + "supportURL": "http://margevicius.lt/easylist_lithuania/" + }, + "LVA-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "LVA: Latvian List", + "lang": "lv", + "contentURL": "https://notabug.org/latvian-list/adblock-latvian/raw/master/lists/latvian-list.txt", + "supportURL": "https://notabug.org/latvian-list/adblock-latvian" + }, + "NLD-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "NLD: EasyList Dutch", + "lang": "nl", + "contentURL": "https://easylist-downloads.adblockplus.org/easylistdutch.txt", + "supportURL": "https://forums.lanik.us/viewforum.php?f=100" + }, + "POL-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "POL: polskie filtry do Adblocka i uBlocka", + "lang": "pl", + "contentURL": "https://raw.githubusercontent.com/MajkiIT/polish-ads-filter/master/polish-adblock-filters/adblock.txt", + "supportURL": "https://www.certyficate.it/adblock-ublock-polish-filters/" + }, + "POL-1": { + "content": "filters", + "group": "regions", + "off": true, + "title": "POL: polskie filtry do uBlocka uzupelnienie", + "lang": "pl", + "contentURL": "https://raw.githubusercontent.com/MajkiIT/polish-ads-filter/master/polish-adblock-filters/adblock_ublock.txt", + "supportURL": "https://www.certyficate.it/adblock-ublock-polish-filters/" + }, + "RUS-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "RUS: RU AdList (Дополнительная региональная подписка)", + "lang": "be ru uk", + "contentURL": "https://easylist-downloads.adblockplus.org/advblock.txt", + "supportURL": "https://forums.lanik.us/viewforum.php?f=102" + }, + "RUS-1": { + "content": "filters", + "group": "regions", + "off": true, + "title": "RUS: BitBlock List (Дополнительная подписка фильтров)", + "contentURL": "https://easylist-downloads.adblockplus.org/bitblock.txt", + "supportURL": "https://forums.lanik.us/viewforum.php?f=102" + }, + "RUS-2": { + "content": "filters", + "group": "regions", + "off": true, + "title": "RUS: Adguard Russian Filter", + "contentURL": "https://filters.adtidy.org/extension/chromium/filters/1.txt", + "supportURL": "https://forum.adguard.com/forumdisplay.php?69-%D0%A4%D0%B8%D0%BB%D1%8C%D1%82%D1%80%D1%8B-Adguard" + }, + "spa-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "spa: EasyList Spanish", + "lang": "es", + "contentURL": "https://easylist-downloads.adblockplus.org/easylistspanish.txt", + "supportURL": "https://forums.lanik.us/viewforum.php?f=103" + }, + "SVN-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "SVN: Slovenian List", + "lang": "sl", + "contentURL": "https://raw.githubusercontent.com/betterwebleon/slovenian-list/master/filters.txt", + "supportURL": "https://github.com/betterwebleon/slovenian-list" + }, + "SWE-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "SWE: Fanboy's Swedish", + "lang": "sv", + "contentURL": "https://www.fanboy.co.nz/fanboy-swedish.txt", + "supportURL": "https://forums.lanik.us/" + }, + "TUR-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "TUR: Adguard Turkish Filter", + "lang": "tr", + "contentURL": "https://filters.adtidy.org/extension/chromium/filters/13.txt", + "supportURL": "https://forum.adguard.com/forumdisplay.php?51-Filter-Rules" + }, + "VIE-0": { + "content": "filters", + "group": "regions", + "off": true, + "title": "VIE: Fanboy's Vietnamese", + "lang": "vi", + "contentURL": "https://www.fanboy.co.nz/fanboy-vietnam.txt", + "supportURL": "https://forums.lanik.us/" + } +} diff --git a/dist/description/description-hi.txt b/dist/description/description-hi.txt index 6bb418ca3f6f3..33caf12a383ed 100644 --- a/dist/description/description-hi.txt +++ b/dist/description/description-hi.txt @@ -1,28 +1,28 @@ -An efficient blocker: easy on memory and CPU footprint, and yet can load and enforce thousands more filters than other popular blockers out there. +एक कुशल अवरोधक: स्मृति और सीपीयू पदचिह्न पर आसान है, और अभी तक लोड और हजारों लागू वहाँ से बाहर अन्य लोकप्रिय ब्लॉकर्स और अधिक से अधिक फिल्टर कर सकते हैं। अपनी क्षमता का सचित्र अवलोकन: https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared -Usage: The big power button in the popup is to permanently disable/enable uBlock for the current web site. It applies to the current web site only, it is not a global power button. +उपयोग: पॉपअप में बड़ी शक्ति बटन स्थायी रूप से अक्षम / वर्तमान वेब साइट uBlock लिए सक्षम है। यह केवल मौजूदा वेब साइट पर लागू होता है, यह एक वैश्विक शक्ति बटन नहीं है। *** -Flexible, it's more than an "ad blocker": it can also read and create filters from hosts files. +लचीले, यह एक केवल "विज्ञापन अवरोधक" की तुलना से अधिक है: यह भी पढ़ सकता हैं और मेजबान फाइलों से फिल्टर बना सकते हैं। Out of the box, these lists of filters are loaded and enforced: -- EasyList -- Peter Lowe’s Ad server list -- EasyPrivacy -- Malware domains +- आसान सूची +- Peter Lowe's विज्ञापन सर्वर सूची +- आसान गुप्तता +- मैलवेयर डोमेन -More lists are available for you to select if you wish: +यदि आप चाहें तो आप का चयन करने के लिए और अधिक सूची उपलब्ध हैं -- Fanboy’s Enhanced Tracking List -- Dan Pollock’s hosts file -- hpHosts’s Ad and tracking servers -- MVPS HOSTS -- Spam404 -- And many others +- Fanboy's बढ़ी ट्रैकिंग सूची +- Dan Pollock's मेजबान फ़ाइल +- hpHosts's विज्ञापन और ट्रैकिंग सर्वर +- MVPS मेज़बान +-स्पैम404 +- और बहुत सारे Of course, the more filters enabled, the higher the memory footprint. Yet, even after adding Fanboy's two extra lists, hpHosts’s Ad and tracking servers, uBlock still has a lower memory footprint than other very popular blockers out there. diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index e63d49aad9fb8..b17c8120e87d7 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "uBlock Origin", - "version": "1.10.5.9", + "version": "1.11.0", "default_locale": "en", "description": "__MSG_extShortDesc__", diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index db6b0305ec29c..63290d47b6ab0 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -978,15 +978,20 @@ vAPI.net.registerListeners = function() { // search for "https://github.com/gorhill/uBlock/issues/1497". var onBeforeWebsocketRequest = function(details) { details.type = 'websocket'; - var matches = /url=([^&]+)/.exec(details.url); + var requestURL = details.url; + var matches = /[?&]url=([^&]+)/.exec(requestURL); details.url = decodeURIComponent(matches[1]); var r = onBeforeRequestClient(details); // Blocked? - if ( r && r.cancel ) { - return r; - } - // Returning a 1x1 transparent pixel means "not blocked". - return { redirectUrl: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' }; + if ( r && r.cancel ) { return r; } + // Try to redirect to the URL of an image already present in the + // document, or a 1x1 data: URL if none is present. + matches = /[?&]r=([^&]+)/.exec(requestURL); + return { + redirectUrl: matches !== null ? + decodeURIComponent(matches[1]) : + 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' + }; }; var onBeforeRequestClient = this.onBeforeRequest.callback; diff --git a/platform/edge/manifest.json b/platform/edge/manifest.json index 99c59d1ff8b88..336ab6fe603a8 100644 --- a/platform/edge/manifest.json +++ b/platform/edge/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "uBlock Origin", - "version": "1.10.5.9", + "version": "1.11.0", "default_locale": "en", "description": "__MSG_extShortDesc__", diff --git a/platform/edge/vapi-background.js b/platform/edge/vapi-background.js index c653e0d1ce2d0..243bd2e1d53c3 100644 --- a/platform/edge/vapi-background.js +++ b/platform/edge/vapi-background.js @@ -1186,15 +1186,20 @@ vAPI.net.registerListeners = function() { // search for "https://github.com/gorhill/uBlock/issues/1497". var onBeforeWebsocketRequest = function(details) { details.type = 'websocket'; - var matches = /url=([^&]+)/.exec(details.url); + var requestURL = details.url; + var matches = /[?&]url=([^&]+)/.exec(requestURL); details.url = decodeURIComponent(matches[1]); var r = onBeforeRequestClient(details); // Blocked? - if ( r && r.cancel ) { - return r; - } - // Returning a 1x1 transparent pixel means "not blocked". - return { redirectUrl: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' }; + if ( r && r.cancel ) { return r; } + // Try to redirect to the URL of an image already present in the + // document, or a 1x1 data: URL if none is present. + matches = /[?&]r=([^&]+)/.exec(requestURL); + return { + redirectUrl: matches !== null ? + decodeURIComponent(matches[1]) : + 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' + }; }; var onBeforeRequestClient = this.onBeforeRequest.callback; diff --git a/platform/firefox/vapi-background.js b/platform/firefox/vapi-background.js index 742a99258f2ff..c9cc54b83c031 100644 --- a/platform/firefox/vapi-background.js +++ b/platform/firefox/vapi-background.js @@ -274,7 +274,11 @@ vAPI.browserSettings = { // has a `media.peerconnection.ice.default_address_only` pref which // purpose is to prevent local IP address leakage. case 'webrtcIPAddress': - if ( this.getValue('media.peerconnection', 'ice.default_address_only') !== undefined ) { + // https://github.com/gorhill/uBlock/issues/2337 + if ( this.getValue('media.peerconnection', 'ice.no_host') !== undefined ) { + prefName = 'ice.no_host'; + prefVal = true; + } else if ( this.getValue('media.peerconnection', 'ice.default_address_only') !== undefined ) { prefName = 'ice.default_address_only'; prefVal = true; } else { diff --git a/src/1p-filters.html b/src/1p-filters.html index 37e81610eab3e..b1146a09d1d52 100644 --- a/src/1p-filters.html +++ b/src/1p-filters.html @@ -2,6 +2,7 @@ + uBlock — Your filters diff --git a/src/3p-filters.html b/src/3p-filters.html index 8bce9d01aaab4..64931d5693134 100644 --- a/src/3p-filters.html +++ b/src/3p-filters.html @@ -2,7 +2,8 @@ -uBlock — Ubiquitous rules + +uBlock — Filter lists @@ -14,53 +15,44 @@
- +

-

- - -

-

- -

-
- -
-
- -
-
+

+ + + +

diff --git a/src/_locales/bg/messages.json b/src/_locales/bg/messages.json index 782775f42dbc6..9a607eb4f3682 100644 --- a/src/_locales/bg/messages.json +++ b/src/_locales/bg/messages.json @@ -680,7 +680,7 @@ "description":"used as a prompt for the user to provide a custom device name" }, "advancedSettingsWarning":{ - "message":"Внимание! Променяте тези настройки на свой собствен риск.", + "message":"Внимание! Променяте настройките на свой собствен риск.", "description":"A warning to users at the top of 'Advanced settings' page" }, "genericSubmit":{ diff --git a/src/_locales/el/messages.json b/src/_locales/el/messages.json index ad9818a10af18..1cea6f934bc98 100644 --- a/src/_locales/el/messages.json +++ b/src/_locales/el/messages.json @@ -292,7 +292,7 @@ "description":"This will cause uBO to ignore all generic cosmetic filters." }, "3pIgnoreGenericCosmeticFiltersInfo":{ - "message":"

Generic cosmetic filters are those cosmetic filters which are meant to apply on all web sites.

Though handled efficiently by uBlock₀, generic cosmetic filters may still contribute measurable memory and CPU overhead on some web pages, especially for large and long-lived ones.

Enabling this option will eliminate the memory and CPU overhead added to web pages as a result of handling generic cosmetic filters, and also lower the memory footprint of uBlock₀ itself.

It is recommended to enable this option on less powerful devices.", + "message":"

Τα γενικά κοσμητικά φίλτρα είναι εκείνα τα κοσμητικά φίλτρα που εφαρμόζονται σε όλες τις ιστοσελίδες.

Αν και γίνεται αποτελεσματική διαχείρισή τους από το uBlock₀, τα γενικά κοσμητικά φίλτρα ενδέχεται να καταναλώσουν σημαντική μνήμη και να υπερφορτώσουν τη CPU σε μερικές ιστοσελίδες, ειδικά για μεγάλες μακροχρόνιες.

Η ενεργοποίηση αυτής της επιλογής θα εξαλείψει την υπερφόρτωση μνήμης και CPU στις ιστοσελίδες ως αποτέλεσμα της διαχείρισης γενικών κοσμητικών φίλτρων, ενώ ενδέχεται να μειώσει την κατανάλωση μνήμης του ίδιου του uBlock₀.

Προτείνεται η ενεργοποίηση αυτής της επιλογής στις λιγότερο ισχυρές συσκευές.", "description":"Describes the purpose of the 'Ignore generic cosmetic filters' feature." }, "3pListsOfBlockedHostsHeader":{ diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index b31059c90154c..653cf75c40c94 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -580,8 +580,8 @@ "description": "Message asking user to confirm reset" }, "errorCantConnectTo":{ - "message":"Unable to connect to {{url}}", - "description":"English: Network error: unable to connect to {{url}}" + "message":"Network error: {{msg}}", + "description":"English: Network error: {{msg}}" }, "subscriberConfirm":{ "message":"uBlock₀: Add the following URL to your custom filter lists?\n\nTitle: \"{{title}}\"\nURL: {{url}}", diff --git a/src/_locales/et/messages.json b/src/_locales/et/messages.json index edd521c2f94f1..de3eade6be014 100644 --- a/src/_locales/et/messages.json +++ b/src/_locales/et/messages.json @@ -424,7 +424,7 @@ "description":"English: dynamic rule syntax and full documentation." }, "whitelistPrompt":{ - "message":"Nimekiri domeenidest, millel uBlock₀ keelatakse. Üks domeen rea kohta, vigased domeeninimed eiratakse vaikimisi ning kommenteeritakse välja.", + "message":"Valge nimekirja direktiivid määravad, millistel veebilehtedel peaks uBlock Origin keelatud olema. Üks domeen rea kohta. Sobimatuid direktiive vaikselt ignoreeritakse ja kommenteeritakse välja.", "description":"English: An overview of the content of the dashboard's Whitelist pane." }, "whitelistImport":{ diff --git a/src/_locales/fi/messages.json b/src/_locales/fi/messages.json index 6678f3b5e9c2f..efd1f28cc400a 100644 --- a/src/_locales/fi/messages.json +++ b/src/_locales/fi/messages.json @@ -616,7 +616,7 @@ "description":"Firefox\/Fennec-specific: Show Dashboard" }, "showNetworkLogButton":{ - "message":"Näytä Verkkopyyntöjen Loki", + "message":"Näytä loki", "description":"Firefox\/Fennec-specific: Show Logger" }, "fennecMenuItemBlockingOff":{ diff --git a/src/_locales/fy/messages.json b/src/_locales/fy/messages.json index 059edde5c7c16..d4cdd3a6bf802 100644 --- a/src/_locales/fy/messages.json +++ b/src/_locales/fy/messages.json @@ -40,7 +40,7 @@ "description":"appears as tab name in dashboard" }, "advancedSettingsPageName":{ - "message":"Advanced settings", + "message":"Avansearre ynstellingen", "description":"Title for the advanced settings page" }, "popupPowerSwitchInfo":{ @@ -212,7 +212,7 @@ "description":"" }, "settingsAdvancedUserSettings":{ - "message":"advanced settings", + "message":"avansearre ynstellingen", "description":"For the tooltip of a link which gives access to advanced settings" }, "settingsPrefetchingDisabledPrompt":{ @@ -424,7 +424,7 @@ "description":"English: dynamic rule syntax and full documentation." }, "whitelistPrompt":{ - "message":"Jo list fan hostnammen wêrop uBlock₀ útskeakele is. Ien per rigel ynjaan. Unjildige hostnammen wurde stil negearre.", + "message":"De whitelist-ynstruksjes skriuwe foar op hokker websiden uBlock Origin útskeakele wurde moat. Ien fermelding per rigel. Unjildige ynstruksjes wurde sûnder meidieling negearre en útskeakele.", "description":"English: An overview of the content of the dashboard's Whitelist pane." }, "whitelistImport":{ @@ -680,7 +680,7 @@ "description":"used as a prompt for the user to provide a custom device name" }, "advancedSettingsWarning":{ - "message":"Warning! Change these advanced settings at your own risk.", + "message":"Warskôging! Wizigje dizze avansearre ynstellingen op eigen risiko.", "description":"A warning to users at the top of 'Advanced settings' page" }, "genericSubmit":{ @@ -688,7 +688,7 @@ "description":"for generic 'Submit' buttons" }, "genericApplyChanges":{ - "message":"Apply changes", + "message":"Wizigingen tapasse", "description":"for generic 'Apply changes' buttons" }, "genericRevert":{ diff --git a/src/_locales/hi/messages.json b/src/_locales/hi/messages.json index 2777ccbb282bc..d52d1536616d9 100644 --- a/src/_locales/hi/messages.json +++ b/src/_locales/hi/messages.json @@ -40,7 +40,7 @@ "description":"appears as tab name in dashboard" }, "advancedSettingsPageName":{ - "message":"Advanced settings", + "message":"उन्नत सेटिंग्स", "description":"Title for the advanced settings page" }, "popupPowerSwitchInfo":{ diff --git a/src/_locales/id/messages.json b/src/_locales/id/messages.json index e17a97b95e0b2..6f9362d2940a0 100644 --- a/src/_locales/id/messages.json +++ b/src/_locales/id/messages.json @@ -424,7 +424,7 @@ "description":"English: dynamic rule syntax and full documentation." }, "whitelistPrompt":{ - "message":"Daftar nama host yang mana uBlock₀ akan dinonfungsikan. Satu entri per baris. Nama host yang tidak valid akan diabaikan tanpa peringatan.", + "message":"Daftar putih domain atau halaman, uBlock Origin akan dinonaktifkan. Satu entri per baris. Entri yang tidak valid akan diabaikan dan dijadikan komentar.", "description":"English: An overview of the content of the dashboard's Whitelist pane." }, "whitelistImport":{ diff --git a/src/_locales/it/messages.json b/src/_locales/it/messages.json index 22b31136dd3a6..a8bf2552f51a9 100644 --- a/src/_locales/it/messages.json +++ b/src/_locales/it/messages.json @@ -680,7 +680,7 @@ "description":"used as a prompt for the user to provide a custom device name" }, "advancedSettingsWarning":{ - "message":"Attenzione! Cambia queste impostazioni a tuo rischio.", + "message":"Attenzione! Modifica queste impostazioni avanzate a tuo rischio e pericolo.", "description":"A warning to users at the top of 'Advanced settings' page" }, "genericSubmit":{ diff --git a/src/_locales/ko/messages.json b/src/_locales/ko/messages.json index f8466f176dd2f..83ed0caa5514f 100644 --- a/src/_locales/ko/messages.json +++ b/src/_locales/ko/messages.json @@ -424,7 +424,7 @@ "description":"English: dynamic rule syntax and full documentation." }, "whitelistPrompt":{ - "message":"목록에 있는 호스트들은 uBlock₀에서 비활성화됩니다. 한 줄에 한 개씩 입력하세요. 존재하지 않는 호스트는 자동으로 무시됩니다.", + "message":"목록에 있는 호스트들은 uBlock₀에서 비활성화됩니다. 한 줄에 한 개씩 입력하세요. 존재하지 않는 호스트는 무시 및 주석 처리됩니다.", "description":"English: An overview of the content of the dashboard's Whitelist pane." }, "whitelistImport":{ diff --git a/src/_locales/lv/messages.json b/src/_locales/lv/messages.json index d137cd2decd3a..b7b96f754a7dd 100644 --- a/src/_locales/lv/messages.json +++ b/src/_locales/lv/messages.json @@ -40,7 +40,7 @@ "description":"appears as tab name in dashboard" }, "advancedSettingsPageName":{ - "message":"Advanced settings", + "message":"Papildu iestatījumi", "description":"Title for the advanced settings page" }, "popupPowerSwitchInfo":{ @@ -212,7 +212,7 @@ "description":"" }, "settingsAdvancedUserSettings":{ - "message":"advanced settings", + "message":"papildu iestatījumi", "description":"For the tooltip of a link which gives access to advanced settings" }, "settingsPrefetchingDisabledPrompt":{ @@ -288,7 +288,7 @@ "description":"Describes the purpose of the 'Parse and enforce cosmetic filters' feature." }, "3pIgnoreGenericCosmeticFilters":{ - "message":"Ignore generic cosmetic filters", + "message":"Ignorēt vispārīgos kosmētiskos filtrus", "description":"This will cause uBO to ignore all generic cosmetic filters." }, "3pIgnoreGenericCosmeticFiltersInfo":{ @@ -540,7 +540,7 @@ "description":"English: project' wiki on Github" }, "aboutSupport":{ - "message":"Support", + "message":"Atbalsts", "description":"A link for where to get support" }, "aboutCode":{ diff --git a/src/_locales/pt_BR/messages.json b/src/_locales/pt_BR/messages.json index a642797772981..42fcae42696b7 100644 --- a/src/_locales/pt_BR/messages.json +++ b/src/_locales/pt_BR/messages.json @@ -8,7 +8,7 @@ "description":"this will be in the chrome web store: must be 132 characters or less" }, "dashboardName":{ - "message":"uBlock₀ — Painel", + "message":"uBlock₀ — Painel de controle", "description":"English: uBlock₀ — Dashboard" }, "settingsPageName":{ @@ -200,7 +200,7 @@ "description":"English: Make use of context menu where appropriate" }, "settingsColorBlindPrompt":{ - "message":"Cores amigáveis para daltônicos", + "message":"Modo Daltonismo", "description":"English: Color-blind friendly" }, "settingsCloudStorageEnabledPrompt":{ @@ -424,7 +424,7 @@ "description":"English: dynamic rule syntax and full documentation." }, "whitelistPrompt":{ - "message":"Sua lista de exceções de servidores para qual o uBlock₀ será desativado. Uma entrada por linha. Servidores inválidos serão silenciosamente ignorados.", + "message":"Sua lista branca de servidores para qual o uBlock Origin será desativado. Uma regra por linha. Servidores inválidos serão silenciosamente ignorados.", "description":"English: An overview of the content of the dashboard's Whitelist pane." }, "whitelistImport":{ @@ -612,7 +612,7 @@ "description":"English: {{value}} days ago" }, "showDashboardButton":{ - "message":"Mostrar Painel", + "message":"Mostrar Painel de Controle", "description":"Firefox\/Fennec-specific: Show Dashboard" }, "showNetworkLogButton":{ diff --git a/src/_locales/pt_PT/messages.json b/src/_locales/pt_PT/messages.json index b0bd15256d2e2..e2d63b466d109 100644 --- a/src/_locales/pt_PT/messages.json +++ b/src/_locales/pt_PT/messages.json @@ -84,7 +84,7 @@ "description":"Tooltip for the no-popups per-site switch" }, "popupTipNoLargeMedia":{ - "message":"Alternar o bloqueio de grandes elementos multimédia para este site", + "message":"Alternar o bloqueio de elementos multimédia grandes para este site", "description":"Tooltip for the no-large-media per-site switch" }, "popupTipNoCosmeticFiltering":{ @@ -144,7 +144,7 @@ "description":"" }, "popupHitDomainCountPrompt":{ - "message":"domínios conectados", + "message":"domínios ligados", "description":"appears in popup" }, "popupHitDomainCount":{ @@ -640,7 +640,7 @@ "description":"English: List of filter list names follows" }, "docblockedBack":{ - "message":"Recuar", + "message":"Retroceder", "description":"English: Go back" }, "docblockedClose":{ @@ -700,7 +700,7 @@ "description":"" }, "contextMenuTemporarilyAllowLargeMediaElements":{ - "message":"Permitir temporariamente grandes elementos multimédia", + "message":"Permitir temporariamente elementos multimédia grandes", "description":"A context menu entry, present when large media elements have been blocked on the current site" }, "dummy":{ diff --git a/src/_locales/sr/messages.json b/src/_locales/sr/messages.json index 9e060b20bb341..aef2d92d94501 100644 --- a/src/_locales/sr/messages.json +++ b/src/_locales/sr/messages.json @@ -152,7 +152,7 @@ "description":"appears in popup" }, "pickerCreate":{ - "message":"Направи", + "message":"Креирај", "description":"English: Create" }, "pickerPick":{ diff --git a/src/_locales/uk/messages.json b/src/_locales/uk/messages.json index 5757874f75717..7f0ae5f73f0fb 100644 --- a/src/_locales/uk/messages.json +++ b/src/_locales/uk/messages.json @@ -84,7 +84,7 @@ "description":"Tooltip for the no-popups per-site switch" }, "popupTipNoLargeMedia":{ - "message":"Перемкнути блокування великих медіа елементів на цьому сайті", + "message":"Увімк\/Вимк блокування великих медіа елементів на цьому сайті", "description":"Tooltip for the no-large-media per-site switch" }, "popupTipNoCosmeticFiltering":{ @@ -292,7 +292,7 @@ "description":"This will cause uBO to ignore all generic cosmetic filters." }, "3pIgnoreGenericCosmeticFiltersInfo":{ - "message":"

Загальні косметичні фільтри — це косметичні фільтри, які повинні застосовуватися до всіх веб-сторінок.

Хоча uBlock і звертається з ними ефективно, вони все одно можуть вимагати значну кількість ресурсів на деяких, особливо великих, сторінках.

Увімкнення цього параметра знизить споживання ресурсів таких сторінок від застосування загальних косметичних фільтрів, а також знизить споживання пам'яті самого uBlock.

Рекомендується увімкнути цей параметр на слабких пристроях.", + "message":"

Загальні косметичні фільтри — це косметичні фільтри, які застосовуються до всіх веб-сторінок.

Хоча й uBlock обробує фільтри ефективно, вони все одно можуть вимагати значну кількість ресурсів на деяких, особливо навантаженних сторінках.

Увімкнення цього параметра знизить споживання ресурсів на таких сторінках від застосування загальних косметичних фільтрів, а також знизить споживання пам'яті самого uBlock.

Рекомендується увімкнути цей параметр на слабких пристроях.", "description":"Describes the purpose of the 'Ignore generic cosmetic filters' feature." }, "3pListsOfBlockedHostsHeader":{ @@ -424,7 +424,7 @@ "description":"English: dynamic rule syntax and full documentation." }, "whitelistPrompt":{ - "message":"Список тих доменів, для яких µBlock буде вимикатись. Один запис на рядок. Недопустимі назви будуть ігноруватись.", + "message":"Ваш список адрес сайтів, для яких µBlock буде неактивним. Додайте по одному запису на рядок. Невірні адреси будуть проігноровані без зауважень.", "description":"English: An overview of the content of the dashboard's Whitelist pane." }, "whitelistImport":{ diff --git a/src/_locales/vi/messages.json b/src/_locales/vi/messages.json index f112d6f706eeb..b4d2248e7bb0e 100644 --- a/src/_locales/vi/messages.json +++ b/src/_locales/vi/messages.json @@ -424,7 +424,7 @@ "description":"English: dynamic rule syntax and full documentation." }, "whitelistPrompt":{ - "message":"Danh sách tên các máy chủ mà uBlock₀ sẽ bị chặn. Một mục nhập trên mỗi dòng. Tên máy chủ không hợp lệ sẽ được tự động bỏ qua.", + "message":"Danh sách tên các máy chủ mà µBlock₀ sẽ bị vô hiệu. Một mục nhập trên mỗi dòng. Tên máy chủ không hợp lệ sẽ được tự động bỏ qua.", "description":"English: An overview of the content of the dashboard's Whitelist pane." }, "whitelistImport":{ diff --git a/src/_locales/zh_CN/messages.json b/src/_locales/zh_CN/messages.json index 9a753bad9c379..7cd9edb6802fc 100644 --- a/src/_locales/zh_CN/messages.json +++ b/src/_locales/zh_CN/messages.json @@ -224,7 +224,7 @@ "description":"English: " }, "settingsWebRTCIPAddressHiddenPrompt":{ - "message":"防止 WebRTC 泄露本地IP地址", + "message":"避免 WebRTC 泄露本地IP地址", "description":"English: " }, "settingPerSiteSwitchGroup":{ @@ -424,7 +424,7 @@ "description":"English: dynamic rule syntax and full documentation." }, "whitelistPrompt":{ - "message":"uBlock₀ 在列表里的城名将会停用。一行一条规则。无效的城名将直接被忽略。", + "message":"您的列表中针对 µBlock 的主机名将被禁用。每行一条规则。无效的主机名将直接被忽略。", "description":"English: An overview of the content of the dashboard's Whitelist pane." }, "whitelistImport":{ diff --git a/src/about.html b/src/about.html index 855170e966206..842088d956f13 100644 --- a/src/about.html +++ b/src/about.html @@ -2,6 +2,7 @@ + uBlock — About diff --git a/src/background.html b/src/background.html index a8463068e76c3..0d56942fc2b0e 100644 --- a/src/background.html +++ b/src/background.html @@ -8,7 +8,6 @@ - diff --git a/src/css/3p-filters.css b/src/css/3p-filters.css index 99f87cfd48235..359ab709e5b61 100644 --- a/src/css/3p-filters.css +++ b/src/css/3p-filters.css @@ -1,3 +1,6 @@ +@keyframes spin { + 100% { transform: rotate(360deg); -webkit-transform: rotate(360deg); } + } ul { padding: 0; list-style-type: none; @@ -5,14 +8,24 @@ ul { #options li { margin-bottom: 0.5em; } +#listsOfBlockedHostsPrompt { + cursor: pointer; + } +#listsOfBlockedHostsPrompt:before { + color: #aaa; + content: '\2212 '; + } +body.hideUnused #listsOfBlockedHostsPrompt:before { + content: '+ '; + } #lists { margin: 0.5em 0 0 0; - padding-left: 1em; + padding-left: 0.5em; padding-right: 0em; } -body[dir=rtl] #lists { +body[dir="rtl"] #lists { padding-left: 0em; - padding-right: 1em; + padding-right: 0.5em; } #lists > li { margin: 0.5em 0 0 0; @@ -41,36 +54,62 @@ body[dir=rtl] #lists { display: none; } li.listEntry { + line-height: 150%; margin: 0 auto 0 auto; - margin-left: 3em; - margin-right: 0em; + margin-left: 2.5em; + margin-right: 0; text-indent: -2em; } -body[dir=rtl] li.listEntry { +body[dir="rtl"] li.listEntry { margin-left: 0em; - margin-right: 1em; + margin-right: 2.5em; } li.listEntry > * { + margin-right: 0.5em; text-indent: 0; unicode-bidi: embed; } -li.listEntry > a:nth-of-type(2) { - font-size: 16px; - opacity: 0.7; +li.listEntry.toRemove > input[type="checkbox"] { + visibility: hidden; } -li.listEntry > a:nth-of-type(2), -li.listEntry > a:nth-of-type(2):visited { - color: mediumblue; +li.listEntry.toRemove > a.content { + text-decoration: line-through; } -li.listEntry > a:nth-of-type(2):hover { +li.listEntry > .fa { + color: inherit; + display: none; + font-size: 110%; + opacity: 0.5; + vertical-align: baseline; + } +li.listEntry > a.fa:hover { opacity: 1; } -li.listEntry > a:nth-of-type(3) { - font-size: smaller; - opacity: 0.5; +li.listEntry.support > a.support { + display: inline-block; + } +li.listEntry > a.remove, +li.listEntry > a.remove:visited { + color: darkred; + } +li.listEntry.external > a.remove { + display: inline-block; + } +li.listEntry.mustread > a.mustread { + display: inline-block; } +li.listEntry.mustread > a.mustread:hover { + color: mediumblue; + } +li.listEntry > .counts { + display: none; + font-size: smaller; +} +li.listEntry > input[type="checkbox"]:checked ~ .counts { + display: inline; +} .dim { - opacity: 0.5; + opacity: 0.6; } #buttonApply { display: initial; @@ -85,104 +124,56 @@ body[dir=rtl] #buttonApply { #buttonApply.disabled { display: none; } -span.status { - border: 1px solid transparent; +li.listEntry span.status { color: #444; - display: inline-block; - font-size: smaller; - line-height: 1; - margin: 0 0 0 0.5em; - opacity: 0.8; - padding: 1px 2px; + cursor: default; + display: none; } -span.unsecure { - background-color: hsl(0, 100%, 88%); - border-color: hsl(0, 100%, 83%); +li.listEntry span.status:hover { + opacity: 1; + } +li.listEntry span.unsecure { + color: darkred; + } +li.listEntry.unsecure > input[type="checkbox"]:checked ~ span.unsecure { + display: inline-block; + } +li.listEntry span.failed { + color: darkred; + } +li.listEntry.failed span.failed { + display: inline-block; } -span.purge { - border-color: #ddd; - background-color: #eee; +li.listEntry span.cache { cursor: pointer; } -span.purge:hover { - opacity: 1; +li.listEntry.cached:not(.obsolete) > input[type="checkbox"]:checked ~ span.cache { + display: inline-block; } -span.obsolete, -span.new { - background-color: hsl(36, 100%, 80%); - border-color: hsl(36, 100%, 75%); +li.listEntry span.obsolete { + color: hsl(36, 100%, 40%); + } +body:not(.updating) li.listEntry.obsolete > input[type="checkbox"]:checked ~ span.obsolete { + display: inline-block; + } +li.listEntry span.updating { + transform-origin: 50% 46%; + } +body.updating li.listEntry.obsolete > input[type="checkbox"]:checked ~ span.updating { + animation: spin 2s linear infinite; + display: inline-block; } #externalListsDiv { - margin: 2em auto 0 2em; + margin: 1.5em auto 0 1.5em; } body[dir=rtl] #externalListsDiv { - margin: 2em 2em 0; + margin: 1.5em 1.5em 0 auto; } #externalLists { box-sizing: border-box; - height: 10em; + height: 8em; + margin-top: 0.25em; white-space: pre; width: 100%; word-wrap: normal; } -body #busyOverlay { - background-color: transparent; - bottom: 0; - cursor: wait; - display: none; - left: 0; - position: fixed; - right: 0; - top: 0; - z-index: 1000; - } -body.busy #busyOverlay { - display: block; - } -#busyOverlay > div:nth-of-type(1) { - background-color: white; - bottom: 0; - left: 0; - opacity: 0.75; - position: absolute; - right: 0; - top: 0; - } -#busyOverlay > div:nth-of-type(2) { - background-color: #eee; - border: 1px solid transparent; - border-color: #80b3ff #80b3ff hsl(216, 100%, 75%); - border-radius: 3px; - box-sizing: border-box; - height: 3em; - left: 10%; - position: absolute; - bottom: 75%; - width: 80%; - } -#busyOverlay > div:nth-of-type(2) > div:nth-of-type(1) { - background-color: hsl(216, 100%, 75%); - background-image: linear-gradient(#a8cbff, #80b3ff); - background-repeat: repeat-x; - border: 0; - box-sizing: border-box; - color: #222; - height: 100%; - left: 0; - padding: 0; - position: absolute; - width: 25%; - } -#busyOverlay > div:nth-of-type(2) > div:nth-of-type(2) { - background-color: transparent; - border: 0; - box-sizing: border-box; - height: 100%; - left: 0; - line-height: 3em; - overflow: hidden; - position: absolute; - text-align: center; - top: 0; - width: 100%; - } diff --git a/src/css/dashboard-common.css b/src/css/dashboard-common.css index dd0a8c19c75cf..bc8f3ddd28e3a 100644 --- a/src/css/dashboard-common.css +++ b/src/css/dashboard-common.css @@ -2,6 +2,11 @@ body { margin: 0; padding: 0 0.5em 0.5em 0.5em; } +@media screen and (max-device-width: 960px) { + body { + zoom: 1.4; + } + } h2, h3 { margin: 1em 0; font-family: sans-serif; diff --git a/src/css/dashboard.css b/src/css/dashboard.css index 1b34a7e7df50a..ea8f0573ecce1 100644 --- a/src/css/dashboard.css +++ b/src/css/dashboard.css @@ -7,6 +7,11 @@ html, body { height: 100%; overflow: hidden; } +@media screen and (max-device-width: 960px) { + #dashboard-nav { + zoom: 1.2; + } + } #dashboard-nav { border: 0; margin: 0; diff --git a/src/dashboard.html b/src/dashboard.html index 893d7d3644555..8826d40869efb 100644 --- a/src/dashboard.html +++ b/src/dashboard.html @@ -2,8 +2,7 @@ - - + diff --git a/src/dyna-rules.html b/src/dyna-rules.html index cb81f99efc575..fac16dd28b652 100644 --- a/src/dyna-rules.html +++ b/src/dyna-rules.html @@ -2,6 +2,7 @@ + uBlock — Dynamic filtering rules diff --git a/src/js/3p-filters.js b/src/js/3p-filters.js index 0b960e81c0911..57cccf2f6ae16 100644 --- a/src/js/3p-filters.js +++ b/src/js/3p-filters.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2014-2016 Raymond Hill + Copyright (C) 2014-2017 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 @@ -21,38 +21,32 @@ /* global uDom */ +'use strict'; + /******************************************************************************/ (function() { -'use strict'; - /******************************************************************************/ -var userListName = vAPI.i18n('1pPageName'); -var listDetails = {}; -var parseCosmeticFilters = true; -var ignoreGenericCosmeticFilters = false; -var externalLists = ''; -var cacheWasPurged = false; -var needUpdate = false; -var hasCachedContent = false; +var listDetails = {}, + filteringSettingsHash = '', + lastUpdateTemplateString = vAPI.i18n('3pLastUpdate'), + reValidExternalList = /[a-z-]+:\/\/\S*\/\S+/; /******************************************************************************/ var onMessage = function(msg) { switch ( msg.what ) { + case 'assetUpdated': + updateAssetStatus(msg); + break; + case 'assetsUpdated': + document.body.classList.remove('updating'); + break; case 'staticFilteringDataChanged': renderFilterLists(); break; - - case 'forceUpdateAssetsProgress': - renderBusyOverlay(true, msg.progress); - if ( msg.done ) { - messaging.send('dashboard', { what: 'reloadAllFilters' }); - } - break; - default: break; } @@ -69,102 +63,93 @@ var renderNumber = function(value) { /******************************************************************************/ -// TODO: get rid of background page dependencies - -var renderFilterLists = function() { - var listGroupTemplate = uDom('#templates .groupEntry'); - var listEntryTemplate = uDom('#templates .listEntry'); - var listStatsTemplate = vAPI.i18n('3pListsOfBlockedHostsPerListStats'); - var renderElapsedTimeToString = vAPI.i18n.renderElapsedTimeToString; - var lastUpdateString = vAPI.i18n('3pLastUpdate'); +var renderFilterLists = function(soft) { + var listGroupTemplate = uDom('#templates .groupEntry'), + listEntryTemplate = uDom('#templates .listEntry'), + listStatsTemplate = vAPI.i18n('3pListsOfBlockedHostsPerListStats'), + renderElapsedTimeToString = vAPI.i18n.renderElapsedTimeToString, + hideUnusedLists = document.body.classList.contains('hideUnused'); - // Assemble a pretty blacklist name if possible + // Assemble a pretty list name if possible var listNameFromListKey = function(listKey) { - if ( listKey === listDetails.userFiltersPath ) { - return userListName; - } var list = listDetails.current[listKey] || listDetails.available[listKey]; var listTitle = list ? list.title : ''; - if ( listTitle === '' ) { - return listKey; - } + if ( listTitle === '' ) { return listKey; } return listTitle; }; - var liFromListEntry = function(listKey) { - var entry = listDetails.available[listKey]; - var li = listEntryTemplate.clone(); - - if ( entry.off !== true ) { - li.descendants('input').attr('checked', ''); - } - - var elem = li.descendants('a:nth-of-type(1)'); - elem.attr('href', 'asset-viewer.html?url=' + encodeURI(listKey)); - elem.attr('type', 'text/html'); - elem.attr('data-listkey', listKey); - elem.text(listNameFromListKey(listKey) + '\u200E'); - - if ( entry.instructionURL ) { - elem = li.descendants('a:nth-of-type(2)'); - elem.attr('href', entry.instructionURL); - elem.css('display', ''); + var liFromListEntry = function(listKey, li) { + var entry = listDetails.available[listKey], + elem; + if ( !li ) { + li = listEntryTemplate.clone().nodeAt(0); } - - if ( entry.supportName ) { - elem = li.descendants('a:nth-of-type(3)'); - elem.attr('href', entry.supportURL); - elem.text('(' + entry.supportName + ')'); - elem.css('display', ''); + if ( li.getAttribute('data-listkey') !== listKey ) { + li.setAttribute('data-listkey', listKey); + elem = li.querySelector('input[type="checkbox"]'); + elem.checked = entry.off !== true; + elem = li.querySelector('a:nth-of-type(1)'); + elem.setAttribute('href', 'asset-viewer.html?url=' + encodeURI(listKey)); + elem.setAttribute('type', 'text/html'); + elem.textContent = listNameFromListKey(listKey); + li.classList.remove('toRemove'); + if ( entry.supportName ) { + li.classList.add('support'); + elem = li.querySelector('a.support'); + elem.setAttribute('href', entry.supportURL); + elem.setAttribute('title', entry.supportName); + } else { + li.classList.remove('support'); + } + if ( entry.external ) { + li.classList.add('external'); + } else { + li.classList.remove('external'); + } + if ( entry.instructionURL ) { + li.classList.add('mustread'); + elem = li.querySelector('a.mustread'); + elem.setAttribute('href', entry.instructionURL); + } else { + li.classList.remove('mustread'); + } } - - elem = li.descendants('span.counts'); - var text = listStatsTemplate - .replace('{{used}}', renderNumber(!entry.off && !isNaN(+entry.entryUsedCount) ? entry.entryUsedCount : 0)) - .replace('{{total}}', !isNaN(+entry.entryCount) ? renderNumber(entry.entryCount) : '?'); - elem.text(text); - - // https://github.com/gorhill/uBlock/issues/78 - // Badge for non-secure connection - var remoteURL = listKey; - if ( remoteURL.lastIndexOf('http:', 0) !== 0 ) { - remoteURL = entry.homeURL || ''; + // https://github.com/gorhill/uBlock/issues/1429 + if ( !soft ) { + elem = li.querySelector('input[type="checkbox"]'); + elem.checked = entry.off !== true; } - if ( remoteURL.lastIndexOf('http:', 0) === 0 ) { - li.descendants('span.status.unsecure').css('display', ''); + li.style.setProperty('display', hideUnusedLists && entry.off === true ? 'none' : ''); + elem = li.querySelector('span.counts'); + var text = ''; + if ( !isNaN(+entry.entryUsedCount) && !isNaN(+entry.entryCount) ) { + text = listStatsTemplate + .replace('{{used}}', renderNumber(entry.off ? 0 : entry.entryUsedCount)) + .replace('{{total}}', renderNumber(entry.entryCount)); } - + elem.textContent = text; // https://github.com/chrisaljoudi/uBlock/issues/104 var asset = listDetails.cache[listKey] || {}; - - // Badge for update status - if ( entry.off !== true ) { - if ( asset.repoObsolete ) { - li.descendants('span.status.new').css('display', ''); - needUpdate = true; - } else if ( asset.cacheObsolete ) { - li.descendants('span.status.obsolete').css('display', ''); - needUpdate = true; - } else if ( entry.external && !asset.cached ) { - li.descendants('span.status.obsolete').css('display', ''); - needUpdate = true; - } - } - - // In cache + var remoteURL = asset.remoteURL; + li.classList.toggle( + 'unsecure', + typeof remoteURL === 'string' && remoteURL.lastIndexOf('http:', 0) === 0 + ); + li.classList.toggle('failed', asset.error !== undefined); + li.classList.toggle('obsolete', asset.obsolete === true); + li.classList.toggle('cached', asset.cached === true && asset.writeTime > 0); if ( asset.cached ) { - elem = li.descendants('span.status.purge'); - elem.css('display', ''); - elem.attr('title', lastUpdateString.replace('{{ago}}', renderElapsedTimeToString(asset.lastModified))); - hasCachedContent = true; + li.querySelector('.status.cache').setAttribute( + 'title', + lastUpdateTemplateString.replace('{{ago}}', renderElapsedTimeToString(asset.writeTime)) + ); } + li.classList.remove('discard'); return li; }; var listEntryCountFromGroup = function(listKeys) { - if ( Array.isArray(listKeys) === false ) { - return ''; - } + if ( Array.isArray(listKeys) === false ) { return ''; } var count = 0; var i = listKeys.length; while ( i-- ) { @@ -176,27 +161,31 @@ var renderFilterLists = function() { }; var liFromListGroup = function(groupKey, listKeys) { - var liGroup = listGroupTemplate.clone(); - var groupName = vAPI.i18n('3pGroup' + groupKey.charAt(0).toUpperCase() + groupKey.slice(1)); - if ( groupName !== '' ) { - liGroup.descendants('span.geName').text(groupName); - liGroup.descendants('span.geCount').text(listEntryCountFromGroup(listKeys)); + var liGroup = document.querySelector('#lists > .groupEntry[data-groupkey="' + groupKey + '"]'); + if ( liGroup === null ) { + liGroup = listGroupTemplate.clone().nodeAt(0); + var groupName = vAPI.i18n('3pGroup' + groupKey.charAt(0).toUpperCase() + groupKey.slice(1)); + if ( groupName !== '' ) { + liGroup.querySelector('.geName').textContent = groupName; + } } - var ulGroup = liGroup.descendants('ul'); - if ( !listKeys ) { - return liGroup; + if ( liGroup.querySelector('.geName:empty') === null ) { + liGroup.querySelector('.geCount').textContent = listEntryCountFromGroup(listKeys); } + var ulGroup = liGroup.querySelector('.listEntries'); + if ( !listKeys ) { return liGroup; } listKeys.sort(function(a, b) { return (listDetails.available[a].title || '').localeCompare(listDetails.available[b].title || ''); }); for ( var i = 0; i < listKeys.length; i++ ) { - ulGroup.append(liFromListEntry(listKeys[i])); + var liEntry = liFromListEntry(listKeys[i], ulGroup.children[i]); + if ( liEntry.parentElement === null ) { + ulGroup.appendChild(liEntry); + } } return liGroup; }; - // https://www.youtube.com/watch?v=unCVi4hYRlY#t=30m18s - var groupsFromLists = function(lists) { var groups = {}; var listKeys = Object.keys(lists); @@ -217,16 +206,16 @@ var renderFilterLists = function() { var onListsReceived = function(details) { // Before all, set context vars listDetails = details; - parseCosmeticFilters = details.parseCosmeticFilters; - ignoreGenericCosmeticFilters = details.ignoreGenericCosmeticFilters; - needUpdate = false; - hasCachedContent = false; + + // Incremental rendering: this will allow us to easily discard unused + // DOM list entries. + uDom('#lists .listEntries .listEntry').addClass('discard'); // Visually split the filter lists in purpose-based groups - var ulLists = uDom('#lists').empty(), liGroup; - var groups = groupsFromLists(details.available); - var groupKey, i; - var groupKeys = [ + var ulLists = document.querySelector('#lists'), + groups = groupsFromLists(details.available), + liGroup, i, groupKey, + groupKeys = [ 'default', 'ads', 'privacy', @@ -239,31 +228,39 @@ var renderFilterLists = function() { for ( i = 0; i < groupKeys.length; i++ ) { groupKey = groupKeys[i]; liGroup = liFromListGroup(groupKey, groups[groupKey]); - liGroup.toggleClass( + liGroup.setAttribute('data-groupkey', groupKey); + liGroup.classList.toggle( 'collapsed', vAPI.localStorage.getItem('collapseGroup' + (i + 1)) === 'y' ); - ulLists.append(liGroup); + if ( liGroup.parentElement === null ) { + ulLists.appendChild(liGroup); + } delete groups[groupKey]; } // For all groups not covered above (if any left) groupKeys = Object.keys(groups); for ( i = 0; i < groupKeys.length; i++ ) { groupKey = groupKeys[i]; - ulLists.append(liFromListGroup(groupKey, groups[groupKey])); + ulLists.appendChild(liFromListGroup(groupKey, groups[groupKey])); } + uDom('#lists .listEntries .listEntry.discard').remove(); + uDom('#autoUpdate').prop('checked', listDetails.autoUpdate === true); uDom('#listsOfBlockedHostsPrompt').text( vAPI.i18n('3pListsOfBlockedHostsPrompt') .replace('{{netFilterCount}}', renderNumber(details.netFilterCount)) .replace('{{cosmeticFilterCount}}', renderNumber(details.cosmeticFilterCount)) ); - uDom('#autoUpdate').prop('checked', listDetails.autoUpdate === true); + + // Compute a hash of the settings so that we can keep track of changes + // affecting the loading of filter lists. uDom('#parseCosmeticFilters').prop('checked', listDetails.parseCosmeticFilters === true); uDom('#ignoreGenericCosmeticFilters').prop('checked', listDetails.ignoreGenericCosmeticFilters === true); - + if ( !soft ) { + filteringSettingsHash = hashFromCurrentFromSettings(); + } renderWidgets(); - renderBusyOverlay(details.manualUpdate, details.manualUpdateProgress); }; messaging.send('dashboard', { what: 'getLists' }, onListsReceived); @@ -271,134 +268,99 @@ var renderFilterLists = function() { /******************************************************************************/ -// Progress must be normalized to [0, 1], or can be undefined. - -var renderBusyOverlay = function(state, progress) { - progress = progress || {}; - var showProgress = typeof progress.value === 'number'; - if ( showProgress ) { - uDom('#busyOverlay > div:nth-of-type(2) > div:first-child').css( - 'width', - (progress.value * 100).toFixed(1) + '%' - ); - var text = progress.text || ''; - if ( text !== '' ) { - uDom('#busyOverlay > div:nth-of-type(2) > div:last-child').text(text); - } - } - uDom('#busyOverlay > div:nth-of-type(2)').css('display', showProgress ? '' : 'none'); - uDom('body').toggleClass('busy', !!state); -}; - -/******************************************************************************/ - -// This is to give a visual hint that the selection of blacklists has changed. - var renderWidgets = function() { - uDom('#buttonApply').toggleClass('disabled', !listsSelectionChanged()); - uDom('#buttonUpdate').toggleClass('disabled', !listsContentChanged()); - uDom('#buttonPurgeAll').toggleClass('disabled', !hasCachedContent); + uDom('#buttonApply').toggleClass('disabled', filteringSettingsHash === hashFromCurrentFromSettings()); + uDom('#buttonPurgeAll').toggleClass('disabled', document.querySelector('#lists .listEntry.cached') === null); + uDom('#buttonUpdate').toggleClass('disabled', document.querySelector('body:not(.updating) #lists .listEntry.obsolete > input[type="checkbox"]:checked') === null); }; /******************************************************************************/ -// Return whether selection of lists changed. - -var listsSelectionChanged = function() { - if ( - listDetails.parseCosmeticFilters !== parseCosmeticFilters || - listDetails.parseCosmeticFilters && listDetails.ignoreGenericCosmeticFilters !== ignoreGenericCosmeticFilters - ) { - return true; +var updateAssetStatus = function(details) { + var li = document.querySelector('#lists .listEntry[data-listkey="' + details.key + '"]'); + if ( li === null ) { return; } + li.classList.toggle('failed', !!details.failed); + li.classList.toggle('obsolete', !details.cached); + li.classList.toggle('cached', !!details.cached); + if ( details.cached ) { + li.querySelector('.status.cache').setAttribute( + 'title', + lastUpdateTemplateString.replace( + '{{ago}}', + vAPI.i18n.renderElapsedTimeToString(Date.now()) + ) + ); } + renderWidgets(); +}; - if ( cacheWasPurged ) { - return true; - } +/******************************************************************************* - var availableLists = listDetails.available; - var currentLists = listDetails.current; - var location, availableOff, currentOff; - - // This check existing entries - for ( location in availableLists ) { - if ( availableLists.hasOwnProperty(location) === false ) { - continue; - } - availableOff = availableLists[location].off === true; - currentOff = currentLists[location] === undefined || currentLists[location].off === true; - if ( availableOff !== currentOff ) { - return true; - } - } + Compute a hash from all the settings affecting how filter lists are loaded + in memory. - // This check removed entries - for ( location in currentLists ) { - if ( currentLists.hasOwnProperty(location) === false ) { - continue; - } - currentOff = currentLists[location].off === true; - availableOff = availableLists[location] === undefined || availableLists[location].off === true; - if ( availableOff !== currentOff ) { - return true; +**/ + +var hashFromCurrentFromSettings = function() { + var hash = [ + document.getElementById('parseCosmeticFilters').checked, + document.getElementById('ignoreGenericCosmeticFilters').checked + ]; + var listHash = [], + listEntries = document.querySelectorAll('#lists .listEntry[data-listkey]:not(.toRemove)'), + liEntry, + i = listEntries.length; + while ( i-- ) { + liEntry = listEntries[i]; + if ( liEntry.querySelector('input[type="checkbox"]:checked') !== null ) { + listHash.push(liEntry.getAttribute('data-listkey')); } } - - return false; + hash.push( + listHash.sort().join(), + reValidExternalList.test(document.getElementById('externalLists').value), + document.querySelector('#lists .listEntry.toRemove') !== null + ); + return hash.join(); }; /******************************************************************************/ -// Return whether content need update. - -var listsContentChanged = function() { - return needUpdate; +var onFilteringSettingsChanged = function() { + renderWidgets(); }; /******************************************************************************/ -var onListCheckboxChanged = function() { - var href = uDom(this).parent().descendants('a').first().attr('data-listkey'); - if ( typeof href !== 'string' ) { - return; - } - if ( listDetails.available[href] === undefined ) { - return; +var onRemoveExternalList = function(ev) { + var liEntry = uDom(this).ancestors('[data-listkey]'), + listKey = liEntry.attr('data-listkey'); + if ( listKey ) { + liEntry.toggleClass('toRemove'); + renderWidgets(); } - listDetails.available[href].off = !this.checked; - renderWidgets(); + ev.preventDefault(); }; /******************************************************************************/ var onPurgeClicked = function() { - var button = uDom(this); - var li = button.parent(); - var href = li.descendants('a').first().attr('data-listkey'); - if ( !href ) { - return; - } + var button = uDom(this), + liEntry = button.ancestors('[data-listkey]'), + listKey = liEntry.attr('data-listkey'); + if ( !listKey ) { return; } - messaging.send('dashboard', { what: 'purgeCache', path: href }); - button.remove(); + messaging.send('dashboard', { what: 'purgeCache', assetKey: listKey }); // If the cached version is purged, the installed version must be assumed // to be obsolete. // https://github.com/gorhill/uBlock/issues/1733 - // An external filter list must not be marked as obsolete, they will always - // be fetched anyways if there is no cached copy. - var entry = listDetails.current && listDetails.current[href]; - if ( entry && entry.off !== true && /^[a-z]+:\/\//.test(href) === false ) { - if ( typeof entry.homeURL !== 'string' || entry.homeURL === '' ) { - li.descendants('span.status.new').css('display', ''); - } else { - li.descendants('span.status.obsolete').css('display', ''); - } - needUpdate = true; - } + // An external filter list must not be marked as obsolete, they will + // always be fetched anyways if there is no cached copy. + liEntry.addClass('obsolete'); + liEntry.removeClass('cached'); - if ( li.descendants('input').first().prop('checked') ) { - cacheWasPurged = true; + if ( liEntry.descendants('input').first().prop('checked') ) { renderWidgets(); } }; @@ -410,83 +372,87 @@ var selectFilterLists = function(callback) { messaging.send('dashboard', { what: 'userSettings', name: 'parseAllABPHideFilters', - value: listDetails.parseCosmeticFilters + value: document.getElementById('parseCosmeticFilters').checked }); messaging.send('dashboard', { what: 'userSettings', name: 'ignoreGenericCosmeticFilters', - value: listDetails.ignoreGenericCosmeticFilters + value: document.getElementById('ignoreGenericCosmeticFilters').checked }); - // Filter lists - var switches = []; - var lis = uDom('#lists .listEntry'), li; - var i = lis.length; + // Filter lists to select + var toSelect = [], + liEntries = document.querySelectorAll('#lists .listEntry[data-listkey]:not(.toRemove)'), + i = liEntries.length, + liEntry; while ( i-- ) { - li = lis.at(i); - switches.push({ - location: li.descendants('a').attr('data-listkey'), - off: li.descendants('input').prop('checked') === false - }); + liEntry = liEntries[i]; + if ( liEntry.querySelector('input[type="checkbox"]:checked') !== null ) { + toSelect.push(liEntry.getAttribute('data-listkey')); + } } + // External filter lists to remove + var toRemove = []; + liEntries = document.querySelectorAll('#lists .listEntry.toRemove[data-listkey]'); + i = liEntries.length; + while ( i-- ) { + toRemove.push(liEntries[i].getAttribute('data-listkey')); + } + + // External filter lists to import + var externalListsElem = document.getElementById('externalLists'), + toImport = externalListsElem.value.trim(); + externalListsElem.value = ''; + messaging.send( 'dashboard', { - what: 'selectFilterLists', - switches: switches + what: 'applyFilterListSelection', + toSelect: toSelect, + toImport: toImport, + toRemove: toRemove }, callback ); + filteringSettingsHash = hashFromCurrentFromSettings(); }; /******************************************************************************/ var buttonApplyHandler = function() { uDom('#buttonApply').removeClass('enabled'); - - renderBusyOverlay(true); - var onSelectionDone = function() { messaging.send('dashboard', { what: 'reloadAllFilters' }); }; - selectFilterLists(onSelectionDone); - - cacheWasPurged = false; + renderWidgets(); }; /******************************************************************************/ var buttonUpdateHandler = function() { - uDom('#buttonUpdate').removeClass('enabled'); - - if ( needUpdate ) { - renderBusyOverlay(true); - - var onSelectionDone = function() { - messaging.send('dashboard', { what: 'forceUpdateAssets' }); - }; - - selectFilterLists(onSelectionDone); - - cacheWasPurged = false; - } + var onSelectionDone = function() { + document.body.classList.add('updating'); + messaging.send('dashboard', { what: 'forceUpdateAssets' }); + renderWidgets(); + }; + selectFilterLists(onSelectionDone); + renderWidgets(); }; /******************************************************************************/ -var buttonPurgeAllHandler = function() { +var buttonPurgeAllHandler = function(ev) { uDom('#buttonPurgeAll').removeClass('enabled'); - - renderBusyOverlay(true); - - var onCompleted = function() { - cacheWasPurged = true; - renderFilterLists(); - }; - - messaging.send('dashboard', { what: 'purgeAllCaches' }, onCompleted); + messaging.send( + 'dashboard', + { + what: 'purgeAllCaches', + hard: ev.ctrlKey && ev.shiftKey + }, + function() { renderFilterLists(true); } + ); }; /******************************************************************************/ @@ -504,47 +470,13 @@ var autoUpdateCheckboxChanged = function() { /******************************************************************************/ -var cosmeticSwitchChanged = function() { - listDetails.parseCosmeticFilters = uDom.nodeFromId('parseCosmeticFilters').checked; - listDetails.ignoreGenericCosmeticFilters = uDom.nodeFromId('ignoreGenericCosmeticFilters').checked; - renderWidgets(); -}; - -/******************************************************************************/ - -var renderExternalLists = function() { - var onReceived = function(details) { - uDom('#externalLists').val(details); - externalLists = details; - }; - messaging.send( - 'dashboard', - { what: 'userSettings', name: 'externalLists' }, - onReceived - ); -}; - -/******************************************************************************/ - -var externalListsChangeHandler = function() { - uDom.nodeFromId('externalListsApply').disabled = - uDom.nodeFromId('externalLists').value.trim() === externalLists.trim(); -}; - -/******************************************************************************/ - -var externalListsApplyHandler = function() { - externalLists = uDom.nodeFromId('externalLists').value; - messaging.send( - 'dashboard', - { - what: 'userSettings', - name: 'externalLists', - value: externalLists - } - ); - renderFilterLists(); - uDom('#externalListsApply').prop('disabled', true); +var toggleUnusedLists = function() { + document.body.classList.toggle('hideUnused'); + var hide = document.body.classList.contains('hideUnused'); + uDom('#lists li.listEntry > input[type="checkbox"]:not(:checked)') + .ancestors('li.listEntry[data-listkey]') + .css('display', hide ? 'none' : ''); + vAPI.localStorage.setItem('hideUnusedFilterLists', hide ? '1' : '0'); }; /******************************************************************************/ @@ -562,32 +494,30 @@ var groupEntryClickHandler = function() { /******************************************************************************/ -var getCloudData = function() { +var toCloudData = function() { var bin = { parseCosmeticFilters: uDom.nodeFromId('parseCosmeticFilters').checked, ignoreGenericCosmeticFilters: uDom.nodeFromId('ignoreGenericCosmeticFilters').checked, selectedLists: [], - externalLists: externalLists + externalLists: listDetails.externalLists }; - var lis = uDom('#lists .listEntry'), li; - var i = lis.length; + var liEntries = uDom('#lists .listEntry'), liEntry; + var i = liEntries.length; while ( i-- ) { - li = lis.at(i); - if ( li.descendants('input').prop('checked') ) { - bin.selectedLists.push(li.descendants('a').attr('data-listkey')); + liEntry = liEntries.at(i); + if ( liEntry.descendants('input').prop('checked') ) { + bin.selectedLists.push(liEntry.attr('data-listkey')); } } return bin; }; -var setCloudData = function(data, append) { - if ( typeof data !== 'object' || data === null ) { - return; - } +var fromCloudData = function(data, append) { + if ( typeof data !== 'object' || data === null ) { return; } - var elem, checked; + var elem, checked, i, n; elem = uDom.nodeFromId('parseCosmeticFilters'); checked = data.parseCosmeticFilters === true || append && elem.checked; @@ -597,47 +527,55 @@ var setCloudData = function(data, append) { checked = data.ignoreGenericCosmeticFilters === true || append && elem.checked; elem.checked = listDetails.ignoreGenericCosmeticFilters = checked; - var lis = uDom('#lists .listEntry'), li, listKey; - var i = lis.length; - while ( i-- ) { - li = lis.at(i); - elem = li.descendants('input'); - listKey = li.descendants('a').attr('data-listkey'); - checked = data.selectedLists.indexOf(listKey) !== -1 || - append && elem.prop('checked'); - elem.prop('checked', checked); - listDetails.available[listKey].off = !checked; + var listKey; + for ( i = 0, n = data.selectedLists.length; i < n; i++ ) { + listKey = data.selectedLists[i]; + if ( listDetails.aliases[listKey] ) { + data.selectedLists[i] = listDetails.aliases[listKey]; + } + } + var selectedSet = new Set(data.selectedLists), + listEntries = uDom('#lists .listEntry'), + listEntry, input; + for ( i = 0, n = listEntries.length; i < n; i++ ) { + listEntry = listEntries.at(i); + listKey = listEntry.attr('data-listkey'); + input = listEntry.descendants('input').first(); + if ( append && input.prop('checked') ) { continue; } + input.prop('checked', selectedSet.has(listKey) ); } elem = uDom.nodeFromId('externalLists'); - if ( !append ) { - elem.value = ''; - } + if ( !append ) { elem.value = ''; } elem.value += data.externalLists || ''; renderWidgets(); - externalListsChangeHandler(); }; -self.cloud.onPush = getCloudData; -self.cloud.onPull = setCloudData; +self.cloud.onPush = toCloudData; +self.cloud.onPull = fromCloudData; /******************************************************************************/ +document.body.classList.toggle( + 'hideUnused', + vAPI.localStorage.getItem('hideUnusedFilterLists') === '1' +); + uDom('#autoUpdate').on('change', autoUpdateCheckboxChanged); -uDom('#parseCosmeticFilters').on('change', cosmeticSwitchChanged); -uDom('#ignoreGenericCosmeticFilters').on('change', cosmeticSwitchChanged); +uDom('#parseCosmeticFilters').on('change', onFilteringSettingsChanged); +uDom('#ignoreGenericCosmeticFilters').on('change', onFilteringSettingsChanged); uDom('#buttonApply').on('click', buttonApplyHandler); uDom('#buttonUpdate').on('click', buttonUpdateHandler); uDom('#buttonPurgeAll').on('click', buttonPurgeAllHandler); -uDom('#lists').on('change', '.listEntry > input', onListCheckboxChanged); -uDom('#lists').on('click', 'span.purge', onPurgeClicked); -uDom('#externalLists').on('input', externalListsChangeHandler); -uDom('#externalListsApply').on('click', externalListsApplyHandler); +uDom('#listsOfBlockedHostsPrompt').on('click', toggleUnusedLists); uDom('#lists').on('click', '.groupEntry > span', groupEntryClickHandler); +uDom('#lists').on('change', '.listEntry > input', onFilteringSettingsChanged); +uDom('#lists').on('click', '.listEntry > a.remove', onRemoveExternalList); +uDom('#lists').on('click', 'span.cache', onPurgeClicked); +uDom('#externalLists').on('input', onFilteringSettingsChanged); renderFilterLists(); -renderExternalLists(); /******************************************************************************/ diff --git a/src/js/assets.js b/src/js/assets.js index c134f9e778808..d420d8b7fae5c 100644 --- a/src/js/assets.js +++ b/src/js/assets.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2014-2016 Raymond Hill + Copyright (C) 2014-2017 Raymond Hill This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,311 +19,55 @@ Home: https://github.com/gorhill/uBlock */ -/* global YaMD5 */ - 'use strict'; -/******************************************************************************* - -File system structure: - assets - ublock - ... - thirdparties - ... - user - filters.txt - ... - -*/ - /******************************************************************************/ -// Low-level asset files manager - µBlock.assets = (function() { /******************************************************************************/ -var oneSecond = 1000; -var oneMinute = 60 * oneSecond; -var oneHour = 60 * oneMinute; -var oneDay = 24 * oneHour; - -/******************************************************************************/ +var reIsExternalPath = /^(?:[a-z-]+):\/\//, + reIsUserAsset = /^user-/, + errorCantConnectTo = vAPI.i18n('errorCantConnectTo'), + noopfunc = function(){}; -var projectRepositoryRoot = µBlock.projectServerRoot; -var assetsRepositoryRoot = 'https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/'; -var nullFunc = function() {}; -var reIsExternalPath = /^(file|ftps?|https?|resource):\/\//; -var reIsUserPath = /^assets\/user\//; -var reIsCachePath = /^cache:\/\//; -var lastRepoMetaTimestamp = 0; -var lastRepoMetaIsRemote = false; -var refreshRepoMetaPeriod = 5 * oneHour; -var errorCantConnectTo = vAPI.i18n('errorCantConnectTo'); -var xhrTimeout = vAPI.localStorage.getItem('xhrTimeout') || 30000; -var onAssetRemovedListener = null; - -var exports = { - autoUpdate: true, - autoUpdateDelay: 4 * oneDay, - - // https://github.com/chrisaljoudi/uBlock/issues/426 - remoteFetchBarrier: 0 +var api = { }; /******************************************************************************/ -var AssetEntry = function() { - this.localChecksum = ''; - this.repoChecksum = ''; - this.expireTimestamp = 0; -}; - -var RepoMetadata = function() { - this.entries = {}; - this.waiting = []; -}; - -var repoMetadata = null; - -// We need these to persist beyond repoMetaData -var homeURLs = {}; - -/******************************************************************************/ +var observers = []; -var stringIsNotEmpty = function(s) { - return typeof s === 'string' && s !== ''; +api.addObserver = function(observer) { + if ( observers.indexOf(observer) === -1 ) { + observers.push(observer); + } }; -/******************************************************************************/ - -var cacheIsObsolete = function(t) { - return typeof t !== 'number' || (Date.now() - t) >= exports.autoUpdateDelay; +api.removeObserver = function(observer) { + var pos; + while ( (pos = observers.indexOf(observer)) !== -1 ) { + observers.splice(pos, 1); + } }; -/******************************************************************************/ - -var cachedAssetsManager = (function() { - var exports = {}; - var entries = null; - var cachedAssetPathPrefix = 'cached_asset_content://'; - - var getEntries = function(callback) { - if ( entries !== null ) { - 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 = vAPI.app.version; - var lastVersion = store.extensionLastVersion || '0.0.0.0'; - if ( currentVersion !== lastVersion ) { - vAPI.cacheStorage.set({ 'extensionLastVersion': currentVersion }); - } - callback(entries); - }; - var onLoaded = function(bin) { - // https://github.com/gorhill/httpswitchboard/issues/381 - // Maybe the index was requested multiple times and already - // fetched by one of the occurrences. - if ( entries === null ) { - var lastError = vAPI.lastError(); - if ( lastError ) { - console.error( - 'µBlock> cachedAssetsManager> getEntries():', - lastError.message - ); - } - entries = bin.cached_asset_entries || {}; - } - vAPI.cacheStorage.get('extensionLastVersion', onLastVersionRead); - }; - vAPI.cacheStorage.get('cached_asset_entries', onLoaded); - }; - exports.entries = getEntries; - - exports.load = function(path, cbSuccess, cbError) { - cbSuccess = cbSuccess || nullFunc; - cbError = cbError || cbSuccess; - var details = { - 'path': path, - 'content': '' - }; - var cachedContentPath = cachedAssetPathPrefix + path; - var onLoaded = function(bin) { - var lastError = vAPI.lastError(); - if ( lastError ) { - details.error = 'Error: ' + lastError.message; - console.error('µBlock> cachedAssetsManager.load():', details.error); - cbError(details); - return; - } - // Not sure how this can happen, but I've seen it happen. It could - // be because the save occurred while I was stepping in the code - // though, which means it would not occur during normal operation. - // Still, just to be safe. - if ( stringIsNotEmpty(bin[cachedContentPath]) === false ) { - exports.remove(path); - details.error = 'Error: not found'; - cbError(details); - return; - } - details.content = bin[cachedContentPath]; - cbSuccess(details); - }; - var onEntries = function(entries) { - if ( entries[path] === undefined ) { - details.error = 'Error: not found'; - cbError(details); - return; - } - vAPI.cacheStorage.get(cachedContentPath, onLoaded); - }; - getEntries(onEntries); - }; - - exports.save = function(path, content, cbSuccess, cbError) { - cbSuccess = cbSuccess || nullFunc; - cbError = cbError || cbSuccess; - var details = { - path: path, - content: content - }; - if ( content === '' ) { - exports.remove(path); - cbSuccess(details); - return; +var fireNotification = function(topic, details) { + var result; + for ( var i = 0; i < observers.length; i++ ) { + if ( observers[i](topic, details) === false ) { + result = false; } - var cachedContentPath = cachedAssetPathPrefix + path; - var bin = {}; - bin[cachedContentPath] = content; - var removedItems = []; - var onSaved = function() { - var lastError = vAPI.lastError(); - if ( lastError ) { - details.error = 'Error: ' + lastError.message; - console.error('µBlock> cachedAssetsManager.save():', details.error); - cbError(details); - return; - } - // Saving over an existing item must be seen as removing an - // existing item and adding a new one. - if ( onAssetRemovedListener instanceof Function ) { - onAssetRemovedListener(removedItems); - } - cbSuccess(details); - }; - var onEntries = function(entries) { - if ( entries.hasOwnProperty(path) ) { - removedItems.push(path); - } - entries[path] = Date.now(); - bin.cached_asset_entries = entries; - vAPI.cacheStorage.set(bin, onSaved); - }; - getEntries(onEntries); - }; - - exports.remove = function(pattern, before) { - var onEntries = function(entries) { - var keystoRemove = []; - var removedItems = []; - var paths = Object.keys(entries); - var i = paths.length; - var path; - while ( i-- ) { - path = paths[i]; - if ( typeof pattern === 'string' && path !== pattern ) { - continue; - } - if ( pattern instanceof RegExp && !pattern.test(path) ) { - continue; - } - if ( typeof before === 'number' && entries[path] >= before ) { - continue; - } - removedItems.push(path); - keystoRemove.push(cachedAssetPathPrefix + path); - delete entries[path]; - } - if ( keystoRemove.length ) { - vAPI.cacheStorage.remove(keystoRemove); - vAPI.cacheStorage.set({ 'cached_asset_entries': entries }); - if ( onAssetRemovedListener instanceof Function ) { - onAssetRemovedListener(removedItems); - } - } - }; - getEntries(onEntries); - }; - - exports.removeAll = function(callback) { - var onEntries = function() { - // Careful! do not remove 'assets/user/' - exports.remove(/^https?:\/\/[a-z0-9]+/); - exports.remove(/^assets\/(ublock|thirdparties)\//); - exports.remove(/^cache:\/\//); - exports.remove('assets/checksums.txt'); - if ( typeof callback === 'function' ) { - callback(null); - } - }; - getEntries(onEntries); - }; - - exports.rmrf = function() { - exports.remove(/./); - }; - - exports.exists = function(path) { - return entries !== null && entries.hasOwnProperty(path); - }; - - getEntries(function(){}); - - return exports; -})(); - -/******************************************************************************/ - -var toRepoURL = function(path) { - if ( path.startsWith('assets/ublock/filter-lists.json') ) { - return projectRepositoryRoot + path; - } - - if ( path.startsWith('assets/checksums.txt') ) { - return path.replace( - /^assets\/checksums.txt/, - assetsRepositoryRoot + 'checksums/ublock0.txt' - ); - } - - if ( path.startsWith('assets/thirdparties/') ) { - return path.replace( - /^assets\/thirdparties\//, - assetsRepositoryRoot + 'thirdparties/' - ); - } - - if ( path.startsWith('assets/ublock/') ) { - return path.replace( - /^assets\/ublock\//, - assetsRepositoryRoot + 'filters/' - ); } - - // At this point, `path` is assumed to point to a resource specific to - // this project. - return projectRepositoryRoot + path; + return result; }; /******************************************************************************/ var getTextFileFromURL = function(url, onLoad, onError) { - // console.log('µBlock.assets/getTextFileFromURL("%s"):', url); + if ( reIsExternalPath.test(url) === false ) { + url = vAPI.getURL(url); + } if ( typeof onError !== 'function' ) { onError = onLoad; @@ -353,6 +97,7 @@ var getTextFileFromURL = function(url, onLoad, onError) { var onErrorReceived = function() { this.onload = this.onerror = this.ontimeout = null; + µBlock.logger.writeOne('', 'error', errorCantConnectTo.replace('{{msg}}', url)); onError.call(this); }; @@ -362,7 +107,7 @@ var getTextFileFromURL = function(url, onLoad, onError) { var xhr = new XMLHttpRequest(); try { xhr.open('get', url, true); - xhr.timeout = xhrTimeout; + xhr.timeout = µBlock.hiddenSettings.assetFetchTimeout * 1000 || 30000; xhr.onload = onResponseReceived; xhr.onerror = onErrorReceived; xhr.ontimeout = onErrorReceived; @@ -373,1289 +118,930 @@ var getTextFileFromURL = function(url, onLoad, onError) { } }; -/******************************************************************************/ +/******************************************************************************* -var updateLocalChecksums = function() { - var localChecksums = []; - var entries = repoMetadata.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')); + TODO(seamless migration): + This block of code will be removed when I am confident all users have + moved to a version of uBO which does not require the old way of caching + assets. + + api.listKeyAliases: a map of old asset keys to new asset keys. + + migrate(): to seamlessly migrate the old cache manager to the new one: + - attempt to preserve and move content of cached assets to new locations; + - removes all traces of now obsolete cache manager entries in cacheStorage. + + This code will typically execute only once, when the newer version of uBO + is first installed and executed. + +**/ + +api.listKeyAliases = { + "assets/thirdparties/publicsuffix.org/list/effective_tld_names.dat": "public_suffix_list.dat", + "assets/user/filters.txt": "user-filters", + "assets/ublock/resources.txt": "ublock-resources", + "assets/ublock/filters.txt": "ublock-filters", + "assets/ublock/privacy.txt": "ublock-privacy", + "assets/ublock/unbreak.txt": "ublock-unbreak", + "assets/ublock/badware.txt": "ublock-badware", + "assets/ublock/experimental.txt": "ublock-experimental", + "https://easylist-downloads.adblockplus.org/easylistchina.txt": "CHN-0", + "https://raw.githubusercontent.com/cjx82630/cjxlist/master/cjxlist.txt": "CHN-1", + "https://raw.githubusercontent.com/cjx82630/cjxlist/master/cjx-annoyance.txt": "CHN-2", + "https://easylist-downloads.adblockplus.org/easylistgermany.txt": "DEU-0", + "https://adblock.dk/block.csv": "DNK-0", + "assets/thirdparties/easylist-downloads.adblockplus.org/easylist.txt": "easylist", + "https://easylist-downloads.adblockplus.org/easylist_noelemhide.txt": "easylist-nocosmetic", + "assets/thirdparties/easylist-downloads.adblockplus.org/easyprivacy.txt": "easyprivacy", + "https://easylist-downloads.adblockplus.org/fanboy-annoyance.txt": "fanboy-annoyance", + "https://easylist-downloads.adblockplus.org/fanboy-social.txt": "fanboy-social", + "https://easylist-downloads.adblockplus.org/liste_fr.txt": "FRA-0", + "http://adblock.gardar.net/is.abp.txt": "ISL-0", + "https://easylist-downloads.adblockplus.org/easylistitaly.txt": "ITA-0", + "https://dl.dropboxusercontent.com/u/1289327/abpxfiles/filtri.txt": "ITA-1", + "https://easylist-downloads.adblockplus.org/advblock.txt": "RUS-0", + "https://easylist-downloads.adblockplus.org/bitblock.txt": "RUS-1", + "https://filters.adtidy.org/extension/chromium/filters/1.txt": "RUS-2", + "https://adguard.com/en/filter-rules.html?id=1": "RUS-2", + "https://easylist-downloads.adblockplus.org/easylistdutch.txt": "NLD-0", + "https://notabug.org/latvian-list/adblock-latvian/raw/master/lists/latvian-list.txt": "LVA-0", + "http://hosts-file.net/.%5Cad_servers.txt": "hphosts", + "http://adblock.ee/list.php": "EST-0", + "https://s3.amazonaws.com/lists.disconnect.me/simple_malvertising.txt": "disconnect-malvertising", + "https://s3.amazonaws.com/lists.disconnect.me/simple_malware.txt": "disconnect-malware", + "https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt": "disconnect-tracking", + "https://www.certyficate.it/adblock/adblock.txt": "POL-0", + "https://raw.githubusercontent.com/MajkiIT/polish-ads-filter/master/polish-adblock-filters/adblock.txt": "POL-0", + "https://easylist-downloads.adblockplus.org/antiadblockfilters.txt": "awrl-0", + "http://adb.juvander.net/Finland_adb.txt": "FIN-0", + "https://raw.githubusercontent.com/gfmaster/adblock-korea-contrib/master/filter.txt": "KOR-0", + "https://raw.githubusercontent.com/yous/YousList/master/youslist.txt": "KOR-1", + "https://www.fanboy.co.nz/fanboy-korean.txt": "KOR-2", + "https://raw.githubusercontent.com/heradhis/indonesianadblockrules/master/subscriptions/abpindo.txt": "IDN-0", + "https://raw.githubusercontent.com/ABPindo/indonesianadblockrules/master/subscriptions/abpindo.txt": "IDN-0", + "https://raw.githubusercontent.com/k2jp/abp-japanese-filters/master/abpjf.txt": "JPN-0", + "https://raw.githubusercontent.com/liamja/Prebake/master/obtrusive.txt": "EU-prebake", + "https://easylist-downloads.adblockplus.org/Liste_AR.txt": "ara-0", + "http://margevicius.lt/easylistlithuania.txt": "LTU-0", + "assets/thirdparties/www.malwaredomainlist.com/hostslist/hosts.txt": "malware-0", + "assets/thirdparties/mirror1.malwaredomains.com/files/justdomains": "malware-1", + "http://malwaredomains.lehigh.edu/files/immortal_domains.txt": "malware-2", + "assets/thirdparties/pgl.yoyo.org/as/serverlist": "plowe-0", + "https://raw.githubusercontent.com/easylist/EasyListHebrew/master/EasyListHebrew.txt": "ISR-0", + "https://raw.githubusercontent.com/reek/anti-adblock-killer/master/anti-adblock-killer-filters.txt": "reek-0", + "https://raw.githubusercontent.com/szpeter80/hufilter/master/hufilter.txt": "HUN-0", + "https://raw.githubusercontent.com/tomasko126/easylistczechandslovak/master/filters.txt": "CZE-0", + "http://someonewhocares.org/hosts/hosts": "dpollock-0", + "https://raw.githubusercontent.com/Dawsey21/Lists/master/adblock-list.txt": "spam404-0", + "http://stanev.org/abp/adblock_bg.txt": "BGR-0", + "http://winhelp2002.mvps.org/hosts.txt": "mvps-0", + "https://www.fanboy.co.nz/enhancedstats.txt": "fanboy-enhanced", + "https://www.fanboy.co.nz/fanboy-antifacebook.txt": "fanboy-thirdparty_social", + "https://easylist-downloads.adblockplus.org/easylistspanish.txt": "spa-0", + "https://www.fanboy.co.nz/fanboy-swedish.txt": "SWE-0", + "https://www.fanboy.co.nz/r/fanboy-ultimate.txt": "fanboy-ultimate", + "https://filters.adtidy.org/extension/chromium/filters/13.txt": "TUR-0", + "https://adguard.com/filter-rules.html?id=13": "TUR-0", + "https://www.fanboy.co.nz/fanboy-vietnam.txt": "VIE-0", + "https://www.void.gr/kargig/void-gr-filters.txt": "GRC-0", + "https://raw.githubusercontent.com/betterwebleon/slovenian-list/master/filters.txt": "SVN-0" }; -/******************************************************************************/ - -// Gather meta data of all assets. - -var getRepoMetadata = function(callback) { - callback = callback || nullFunc; - - // https://github.com/chrisaljoudi/uBlock/issues/515 - // Handle re-entrancy here, i.e. we MUST NOT tamper with the waiting list - // of callers, if any, except to add one at the end of the list. - if ( repoMetadata !== null && repoMetadata.waiting.length !== 0 ) { - repoMetadata.waiting.push(callback); - return; - } - - if ( exports.remoteFetchBarrier === 0 && lastRepoMetaIsRemote === false ) { - lastRepoMetaTimestamp = 0; - } - if ( (Date.now() - lastRepoMetaTimestamp) >= refreshRepoMetaPeriod ) { - repoMetadata = null; - } - if ( repoMetadata !== null ) { - callback(repoMetadata); - return; - } - - lastRepoMetaTimestamp = Date.now(); - lastRepoMetaIsRemote = exports.remoteFetchBarrier === 0; - - var defaultChecksums; - var localChecksums; - var repoChecksums; +var migrate = function(callback) { + var entries, + moveCount = 0, + toRemove = []; + + var countdown = function(change) { + moveCount -= (change || 0); + if ( moveCount !== 0 ) { return; } + vAPI.cacheStorage.remove(toRemove); + saveAssetCacheRegistry(); + callback(); + }; + + var onContentRead = function(oldKey, newKey, bin) { + var content = bin && bin['cached_asset_content://' + oldKey] || undefined; + if ( content ) { + assetCacheRegistry[newKey] = { + readTime: Date.now(), + writeTime: entries[oldKey] + }; + if ( reIsExternalPath.test(oldKey) ) { + assetCacheRegistry[newKey].remoteURL = oldKey; + } + bin = {}; + bin['cache/' + newKey] = content; + vAPI.cacheStorage.set(bin); + } + countdown(1); + }; - var checksumsReceived = function() { - if ( - defaultChecksums === undefined || - localChecksums === undefined || - repoChecksums === undefined - ) { - return; + var onEntries = function(bin) { + entries = bin && bin['cached_asset_entries']; + if ( !entries ) { return callback(); } + if ( bin && bin['assetCacheRegistry'] ) { + assetCacheRegistry = bin['assetCacheRegistry']; } - // Remove from cache assets which no longer exist in the repo - var entries = repoMetadata.entries; - var checksumsChanged = false; - var entry; - for ( var path in entries ) { - if ( entries.hasOwnProperty(path) === false ) { - continue; - } - entry = entries[path]; - // https://github.com/gorhill/uBlock/issues/760 - // If the resource does not have a cached instance, we must reset - // the checksum to its value at install time. - if ( - stringIsNotEmpty(defaultChecksums[path]) && - entry.localChecksum !== defaultChecksums[path] && - cachedAssetsManager.exists(path) === false - ) { - entry.localChecksum = defaultChecksums[path]; - checksumsChanged = true; + var aliases = api.listKeyAliases; + for ( var oldKey in entries ) { + if ( oldKey.endsWith('assets/user/filters.txt') ) { continue; } + var newKey = aliases[oldKey]; + if ( !newKey && /^https?:\/\//.test(oldKey) ) { + newKey = oldKey; } - // If repo checksums could not be fetched, assume no change. - // https://github.com/gorhill/uBlock/issues/602 - // Added: if repo checksum is that of the empty string, - // assume no change - if ( - repoChecksums === '' || - entry.repoChecksum === 'd41d8cd98f00b204e9800998ecf8427e' - ) { - entry.repoChecksum = entry.localChecksum; - } - if ( entry.repoChecksum !== '' || entry.localChecksum === '' ) { - continue; + if ( newKey ) { + vAPI.cacheStorage.get( + 'cached_asset_content://' + oldKey, + onContentRead.bind(null, oldKey, newKey) + ); + moveCount += 1; } - checksumsChanged = true; - cachedAssetsManager.remove(path); - entry.localChecksum = ''; + toRemove.push('cached_asset_content://' + oldKey); } - if ( checksumsChanged ) { - updateLocalChecksums(); - } - // Notify all waiting callers - // https://github.com/chrisaljoudi/uBlock/issues/515 - // VERY IMPORTANT: because of re-entrancy, we MUST: - // - process the waiting callers in a FIFO manner - // - not cache repoMetadata.waiting.length, we MUST use the live - // value, because it can change while looping - // - not change the waiting list until they are all processed - for ( var i = 0; i < repoMetadata.waiting.length; i++ ) { - repoMetadata.waiting[i](repoMetadata); - } - repoMetadata.waiting.length = 0; + toRemove.push('cached_asset_entries', 'extensionLastVersion'); + countdown(); }; - var validateChecksums = function(details) { - if ( details.error || details.content === '' ) { - return ''; + vAPI.cacheStorage.get( + [ 'cached_asset_entries', 'assetCacheRegistry' ], + onEntries + ); +}; + +/******************************************************************************* + + The purpose of the asset source registry is to keep key detail information + about an asset: + - Where to load it from: this may consist of one or more URLs, either local + or remote. + - After how many days an asset should be deemed obsolete -- i.e. in need of + an update. + - The origin and type of an asset. + - The last time an asset was registered. + +**/ + +var assetSourceRegistryStatus, + assetSourceRegistry = Object.create(null); + +var registerAssetSource = function(assetKey, dict) { + var entry = assetSourceRegistry[assetKey] || {}; + for ( var prop in dict ) { + if ( dict.hasOwnProperty(prop) === false ) { continue; } + if ( dict[prop] === undefined ) { + delete entry[prop]; + } else { + entry[prop] = dict[prop]; } - if ( /^(?:[0-9a-f]{32}\s+\S+(?:\s+|$))+/.test(details.content) === false ) { - return ''; + } + var contentURL = dict.contentURL; + if ( contentURL !== undefined ) { + if ( typeof contentURL === 'string' ) { + contentURL = entry.contentURL = [ contentURL ]; + } else if ( Array.isArray(contentURL) === false ) { + contentURL = entry.contentURL = []; } - // https://github.com/gorhill/uBlock/issues/602 - // External filter lists are not meant to appear in checksums.txt. - // TODO: remove this code once v1.1.0.0 is everywhere. - var out = []; - var listMap = µBlock.oldListToNewListMap; - var lines = details.content.split(/\s*\n\s*/); - var line, matches; - for ( var i = 0; i < lines.length; i++ ) { - line = lines[i]; - matches = line.match(/^[0-9a-f]+ (.+)$/); - if ( matches === null || listMap.hasOwnProperty(matches[1]) ) { - continue; + var remoteURLCount = 0; + for ( var i = 0; i < contentURL.length; i++ ) { + if ( reIsExternalPath.test(contentURL[i]) ) { + remoteURLCount += 1; } - out.push(line); } - return out.join('\n'); - }; + entry.hasLocalURL = remoteURLCount !== contentURL.length; + entry.hasRemoteURL = remoteURLCount !== 0; + } else if ( entry.contentURL === undefined ) { + entry.contentURL = []; + } + if ( typeof entry.updateAfter !== 'number' ) { + entry.updateAfter = 5; + } + if ( entry.submitter ) { + entry.submitTime = Date.now(); // To detect stale entries + } + assetSourceRegistry[assetKey] = entry; +}; - var parseChecksums = function(text, eachFn) { - var lines = text.split(/\n+/); - var i = lines.length; - var fields; - while ( i-- ) { - fields = lines[i].trim().split(/\s+/); - if ( fields.length !== 2 ) { - continue; - } - eachFn(fields[1], fields[0]); - } - }; +var unregisterAssetSource = function(assetKey) { + assetCacheRemove(assetKey); + delete assetSourceRegistry[assetKey]; +}; - var onLocalChecksumsLoaded = function(details) { - var entries = repoMetadata.entries; - var processChecksum = function(path, checksum) { - if ( entries.hasOwnProperty(path) === false ) { - entries[path] = new AssetEntry(); - } - entries[path].localChecksum = checksum; - }; - if ( (localChecksums = validateChecksums(details)) ) { - parseChecksums(localChecksums, processChecksum); - } - checksumsReceived(); +var saveAssetSourceRegistry = (function() { + var timer; + var save = function() { + timer = undefined; + vAPI.cacheStorage.set({ assetSourceRegistry: assetSourceRegistry }); }; - - var onRepoChecksumsLoaded = function(details) { - var entries = repoMetadata.entries; - var processChecksum = function(path, checksum) { - if ( entries.hasOwnProperty(path) === false ) { - entries[path] = new AssetEntry(); - } - entries[path].repoChecksum = checksum; - }; - if ( (repoChecksums = validateChecksums(details)) ) { - parseChecksums(repoChecksums, processChecksum); + return function(lazily) { + if ( timer !== undefined ) { + clearTimeout(timer); + } + if ( lazily ) { + timer = vAPI.setTimeout(save, 500); + } else { + save(); } - checksumsReceived(); - }; - - // https://github.com/gorhill/uBlock/issues/760 - // We need the checksum values at install time, because some resources - // may have been purged, in which case the checksum must be reset to the - // value at install time. - var onDefaultChecksumsLoaded = function() { - defaultChecksums = Object.create(null); - var processChecksum = function(path, checksum) { - defaultChecksums[path] = checksum; - }; - parseChecksums(this.responseText || '', processChecksum); - checksumsReceived(); }; +})(); - repoMetadata = new RepoMetadata(); - repoMetadata.waiting.push(callback); - readRepoFile('assets/checksums.txt', onRepoChecksumsLoaded); - getTextFileFromURL(vAPI.getURL('assets/checksums.txt'), onDefaultChecksumsLoaded); - readLocalFile('assets/checksums.txt', onLocalChecksumsLoaded); -}; +var updateAssetSourceRegistry = function(json, silent) { + var newDict; + try { + newDict = JSON.parse(json); + } catch (ex) { + } + if ( newDict instanceof Object === false ) { return; } -// https://www.youtube.com/watch?v=-t3WYfgM4x8 + var oldDict = assetSourceRegistry, + assetKey; -/******************************************************************************/ + // Remove obsolete entries (only those which were built-in). + for ( assetKey in oldDict ) { + if ( + newDict[assetKey] === undefined && + oldDict[assetKey].submitter === undefined + ) { + unregisterAssetSource(assetKey); + } + } + // Add/update existing entries. Notify of new asset sources. + for ( assetKey in newDict ) { + if ( oldDict[assetKey] === undefined && !silent ) { + fireNotification( + 'builtin-asset-source-added', + { assetKey: assetKey, entry: newDict[assetKey] } + ); + } + registerAssetSource(assetKey, newDict[assetKey]); + } + saveAssetSourceRegistry(); +}; -exports.setHomeURL = function(path, homeURL) { - if ( typeof homeURL !== 'string' || homeURL === '' ) { +var getAssetSourceRegistry = function(callback) { + // Already loaded. + if ( assetSourceRegistryStatus === 'ready' ) { + callback(assetSourceRegistry); return; } - homeURLs[path] = homeURL; -}; -/******************************************************************************/ + // Being loaded. + if ( Array.isArray(assetSourceRegistryStatus) ) { + assetSourceRegistryStatus.push(callback); + return; + } -// Get a local asset, do not look-up repo or remote location if local asset -// is not found. + // Not loaded: load it. + assetSourceRegistryStatus = [ callback ]; -var readLocalFile = function(path, callback) { - var reportBack = function(content, err) { - var details = { - 'path': path, - 'content': content - }; - if ( err ) { - details.error = err; + var registryReady = function() { + var callers = assetSourceRegistryStatus; + assetSourceRegistryStatus = 'ready'; + var fn; + while ( (fn = callers.shift()) ) { + fn(assetSourceRegistry); } - callback(details); - }; - - var onInstallFileLoaded = function() { - //console.log('µBlock> readLocalFile("%s") / onInstallFileLoaded()', path); - reportBack(this.responseText); - }; - - var onInstallFileError = function() { - console.error('µBlock> readLocalFile("%s") / onInstallFileError()', path); - reportBack('', 'Error'); }; - var onCachedContentLoaded = function(details) { - //console.log('µBlock> readLocalFile("%s") / onCachedContentLoaded()', path); - reportBack(details.content); + // First-install case. + var createRegistry = function() { + getTextFileFromURL( + µBlock.assetsBootstrapLocation || 'assets/assets.json', + function() { + updateAssetSourceRegistry(this.responseText, true); + registryReady(); + } + ); }; - var onCachedContentError = function(details) { - //console.error('µBlock> readLocalFile("%s") / onCachedContentError()', path); - if ( reIsExternalPath.test(path) ) { - reportBack('', 'Error: asset not found'); - return; - } - // It's ok for user data to not be found - if ( reIsUserPath.test(path) ) { - reportBack(''); + vAPI.cacheStorage.get('assetSourceRegistry', function(bin) { + if ( !bin || !bin.assetSourceRegistry ) { + createRegistry(); return; } - getTextFileFromURL(vAPI.getURL(details.path), onInstallFileLoaded, onInstallFileError); - }; + assetSourceRegistry = bin.assetSourceRegistry; + registryReady(); + }); +}; - cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); +api.registerAssetSource = function(assetKey, details) { + getAssetSourceRegistry(function() { + registerAssetSource(assetKey, details); + saveAssetSourceRegistry(true); + }); }; -// https://www.youtube.com/watch?v=r9KVpuFPtHc +api.unregisterAssetSource = function(assetKey) { + getAssetSourceRegistry(function() { + unregisterAssetSource(assetKey); + saveAssetSourceRegistry(true); + }); +}; -/******************************************************************************/ +/******************************************************************************* -// Get the repository copy of a built-in asset. + The purpose of the asset cache registry is to keep track of all assets + which have been persisted into the local cache. -var readRepoFile = function(path, callback) { - // https://github.com/chrisaljoudi/uBlock/issues/426 - if ( exports.remoteFetchBarrier !== 0 ) { - readLocalFile(path, callback); +**/ + +var assetCacheRegistryStatus, + assetCacheRegistryStartTime = Date.now(), + assetCacheRegistry = {}; + +var getAssetCacheRegistry = function(callback) { + // Already loaded. + if ( assetCacheRegistryStatus === 'ready' ) { + callback(assetCacheRegistry); return; } - var reportBack = function(content, err) { - var details = { - 'path': path, - 'content': content, - 'error': err - }; - callback(details); - }; + // Being loaded. + if ( Array.isArray(assetCacheRegistryStatus) ) { + assetCacheRegistryStatus.push(callback); + return; + } - var repositoryURL = toRepoURL(path); + // Not loaded: load it. + assetCacheRegistryStatus = [ callback ]; - var onRepoFileLoaded = function() { - //console.log('µBlock> readRepoFile("%s") / onRepoFileLoaded()', path); - // https://github.com/gorhill/httpswitchboard/issues/263 - if ( this.status === 200 ) { - reportBack(this.responseText); - } else { - reportBack('', 'Error: ' + this.statusText); + var registryReady = function() { + var callers = assetCacheRegistryStatus; + assetCacheRegistryStatus = 'ready'; + var fn; + while ( (fn = callers.shift()) ) { + fn(assetCacheRegistry); } }; - var onRepoFileError = function() { - console.error(errorCantConnectTo.replace('{{url}}', repositoryURL)); - reportBack('', 'Error'); + var migrationDone = function() { + vAPI.cacheStorage.get('assetCacheRegistry', function(bin) { + if ( bin && bin.assetCacheRegistry ) { + assetCacheRegistry = bin.assetCacheRegistry; + } + registryReady(); + }); }; - // '_=...' is to skip browser cache - getTextFileFromURL( - repositoryURL + '?_=' + Date.now(), - onRepoFileLoaded, - onRepoFileError - ); + migrate(migrationDone); }; -/******************************************************************************/ - -// 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 saveAssetCacheRegistry = (function() { + var timer; + var save = function() { + timer = undefined; + vAPI.cacheStorage.set({ assetCacheRegistry: assetCacheRegistry }); + }; + return function(lazily) { + if ( timer !== undefined ) { clearTimeout(timer); } + if ( lazily ) { + timer = vAPI.setTimeout(save, 500); + } else { + save(); + } + }; +})(); -var readRepoCopyAsset = function(path, callback) { - var assetEntry; - var homeURL = homeURLs[path]; +var assetCacheRead = function(assetKey, callback) { + var internalKey = 'cache/' + assetKey; var reportBack = function(content, err) { - var details = { - 'path': path, - 'content': content - }; - if ( err ) { - details.error = err; - } + var details = { assetKey: assetKey, content: content }; + if ( err ) { details.error = err; } callback(details); }; - var updateChecksum = function() { - if ( assetEntry !== undefined && assetEntry.repoChecksum !== assetEntry.localChecksum ) { - assetEntry.localChecksum = assetEntry.repoChecksum; - updateLocalChecksums(); + var onAssetRead = function(bin) { + if ( !bin || !bin[internalKey] ) { + return reportBack('', 'E_NOTFOUND'); } + var entry = assetCacheRegistry[assetKey]; + if ( entry === undefined ) { + return reportBack('', 'E_NOTFOUND'); + } + entry.readTime = Date.now(); + saveAssetCacheRegistry(true); + reportBack(bin[internalKey]); }; - var onInstallFileLoaded = function() { - //console.log('µBlock> readRepoCopyAsset("%s") / onInstallFileLoaded()', path); - reportBack(this.responseText); - }; - - var onInstallFileError = function() { - console.error('µBlock> readRepoCopyAsset("%s") / onInstallFileError():', path, this.statusText); - reportBack('', 'Error'); + var onReady = function() { + vAPI.cacheStorage.get(internalKey, onAssetRead); }; - var onCachedContentLoaded = function(details) { - //console.log('µBlock> readRepoCopyAsset("%s") / onCacheFileLoaded()', path); - reportBack(details.content); - }; + getAssetCacheRegistry(onReady); +}; - var onCachedContentError = function(details) { - //console.log('µBlock> readRepoCopyAsset("%s") / onCacheFileError()', path); - getTextFileFromURL(vAPI.getURL(details.path), onInstallFileLoaded, onInstallFileError); - }; +var assetCacheWrite = function(assetKey, details, callback) { + var internalKey = 'cache/' + assetKey; + var content = ''; + if ( typeof details === 'string' ) { + content = details; + } else if ( details instanceof Object ) { + content = details.content || ''; + } - var repositoryURL = toRepoURL(path); - var repositoryURLSkipCache = repositoryURL + '?_=' + Date.now(); + if ( content === '' ) { + return assetCacheRemove(assetKey, callback); + } - var onRepoFileLoaded = function() { - if ( stringIsNotEmpty(this.responseText) === false ) { - console.error('µBlock> readRepoCopyAsset("%s") / onRepoFileLoaded("%s"): error', path, repositoryURL); - cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); - return; + var reportBack = function(content) { + var details = { assetKey: assetKey, content: content }; + if ( typeof callback === 'function' ) { + callback(details); } - //console.log('µBlock> readRepoCopyAsset("%s") / onRepoFileLoaded("%s")', path, repositoryURL); - updateChecksum(); - cachedAssetsManager.save(path, this.responseText, callback); + fireNotification('after-asset-updated', details); }; - var onRepoFileError = function() { - console.error(errorCantConnectTo.replace('{{url}}', repositoryURL)); - cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); + var onReady = function() { + var entry = assetCacheRegistry[assetKey]; + if ( entry === undefined ) { + entry = assetCacheRegistry[assetKey] = {}; + } + entry.writeTime = entry.readTime = Date.now(); + if ( details instanceof Object && typeof details.url === 'string' ) { + entry.remoteURL = details.url; + } + var bin = { assetCacheRegistry: assetCacheRegistry }; + bin[internalKey] = content; + vAPI.cacheStorage.set(bin); + reportBack(content); }; + getAssetCacheRegistry(onReady); +}; - var onHomeFileLoaded = function() { - if ( stringIsNotEmpty(this.responseText) === false ) { - console.error('µBlock> readRepoCopyAsset("%s") / onHomeFileLoaded("%s"): no response', path, homeURL); - // Fetch from repo only if obsolescence was due to repo checksum - if ( assetEntry.localChecksum !== assetEntry.repoChecksum ) { - getTextFileFromURL(repositoryURLSkipCache, onRepoFileLoaded, onRepoFileError); - } else { - cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); +var assetCacheRemove = function(pattern, callback) { + var onReady = function() { + var cacheDict = assetCacheRegistry, + removedEntries = [], + removedContent = []; + for ( var assetKey in cacheDict ) { + if ( pattern instanceof RegExp && !pattern.test(assetKey) ) { + continue; } - return; + if ( typeof pattern === 'string' && assetKey !== pattern ) { + continue; + } + removedEntries.push(assetKey); + removedContent.push('cache/' + assetKey); + delete cacheDict[assetKey]; } - //console.log('µBlock> readRepoCopyAsset("%s") / onHomeFileLoaded("%s")', path, homeURL); - updateChecksum(); - cachedAssetsManager.save(path, this.responseText, callback); - }; - - var onHomeFileError = function() { - console.error(errorCantConnectTo.replace('{{url}}', homeURL)); - // Fetch from repo only if obsolescence was due to repo checksum - if ( assetEntry.localChecksum !== assetEntry.repoChecksum ) { - getTextFileFromURL(repositoryURLSkipCache, onRepoFileLoaded, onRepoFileError); - } else { - cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); + if ( removedContent.length !== 0 ) { + vAPI.cacheStorage.remove(removedContent); + var bin = { assetCacheRegistry: assetCacheRegistry }; + vAPI.cacheStorage.set(bin); + } + if ( typeof callback === 'function' ) { + callback(); + } + for ( var i = 0; i < removedEntries.length; i++ ) { + fireNotification('after-asset-updated', { assetKey: removedEntries[i] }); } }; - var onCacheMetaReady = function(entries) { - // Fetch from remote if: - // - Auto-update enabled AND (not in cache OR in cache but obsolete) - var timestamp = entries[path]; - var inCache = typeof timestamp === 'number'; - if ( - exports.remoteFetchBarrier === 0 && - exports.autoUpdate && stringIsNotEmpty(homeURL) - ) { - if ( inCache === false || cacheIsObsolete(timestamp) ) { - //console.log('µBlock> readRepoCopyAsset("%s") / onCacheMetaReady(): not cached or obsolete', path); - getTextFileFromURL(homeURL, onHomeFileLoaded, onHomeFileError); - return; + getAssetCacheRegistry(onReady); +}; + +var assetCacheMarkAsDirty = function(pattern, exclude, callback) { + var onReady = function() { + var cacheDict = assetCacheRegistry, + cacheEntry, + mustSave = false; + for ( var assetKey in cacheDict ) { + if ( pattern instanceof RegExp ) { + if ( pattern.test(assetKey) === false ) { continue; } + } else if ( typeof pattern === 'string' ) { + if ( assetKey !== pattern ) { continue; } + } else if ( Array.isArray(pattern) ) { + if ( pattern.indexOf(assetKey) === -1 ) { continue; } } + if ( exclude instanceof RegExp ) { + if ( exclude.test(assetKey) ) { continue; } + } else if ( typeof exclude === 'string' ) { + if ( assetKey === exclude ) { continue; } + } else if ( Array.isArray(exclude) ) { + if ( exclude.indexOf(assetKey) !== -1 ) { continue; } + } + cacheEntry = cacheDict[assetKey]; + if ( !cacheEntry.writeTime ) { continue; } + cacheDict[assetKey].writeTime = 0; + mustSave = true; } - - // In cache - if ( inCache ) { - cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); - return; + if ( mustSave ) { + var bin = { assetCacheRegistry: assetCacheRegistry }; + vAPI.cacheStorage.set(bin); + } + if ( typeof callback === 'function' ) { + callback(); } - - // Not in cache - getTextFileFromURL(vAPI.getURL(path), onInstallFileLoaded, onInstallFileError); }; + if ( typeof exclude === 'function' ) { + callback = exclude; + exclude = undefined; + } + getAssetCacheRegistry(onReady); +}; - var onRepoMetaReady = function(meta) { - assetEntry = meta.entries[path]; +/******************************************************************************/ - // Asset doesn't exist - if ( assetEntry === undefined ) { - reportBack('', 'Error: asset not found'); - return; - } +var stringIsNotEmpty = function(s) { + return typeof s === 'string' && s !== ''; +}; - // Repo copy changed: fetch from home URL - if ( - exports.remoteFetchBarrier === 0 && - exports.autoUpdate && - assetEntry.localChecksum !== assetEntry.repoChecksum - ) { - //console.log('µBlock> readRepoCopyAsset("%s") / onRepoMetaReady(): repo has newer version', path); - if ( stringIsNotEmpty(homeURL) ) { - getTextFileFromURL(homeURL, onHomeFileLoaded, onHomeFileError); - } else { - getTextFileFromURL(repositoryURLSkipCache, onRepoFileLoaded, onRepoFileError); - } - return; - } +/******************************************************************************* + + User assets are NOT persisted in the cache storage. User assets are + recognized by the asset key which always starts with 'user-'. + + TODO(seamless migration): + Can remove instances of old user asset keys when I am confident all users + are using uBO v1.11 and beyond. - // Load from cache - cachedAssetsManager.entries(onCacheMetaReady); +**/ + +var readUserAsset = function(assetKey, callback) { + var reportBack = function(content) { + callback({ assetKey: assetKey, content: content }); }; - getRepoMetadata(onRepoMetaReady); + var onLoaded = function(bin) { + if ( !bin ) { return reportBack(''); } + var content = ''; + if ( typeof bin['cached_asset_content://assets/user/filters.txt'] === 'string' ) { + content = bin['cached_asset_content://assets/user/filters.txt']; + vAPI.cacheStorage.remove('cached_asset_content://assets/user/filters.txt'); + } + if ( typeof bin['assets/user/filters.txt'] === 'string' ) { + content = bin['assets/user/filters.txt']; + // TODO(seamless migration): + // Uncomment once all moved to v1.11+. + //vAPI.storage.remove('assets/user/filters.txt'); + } + if ( typeof bin[assetKey] === 'string' ) { + // TODO(seamless migration): + // Replace conditional with assignment once all moved to v1.11+ + if ( content !== bin[assetKey] ) { + saveUserAsset(assetKey, content); + } + } else if ( content !== '' ) { + saveUserAsset(assetKey, content); + } + return reportBack(content); + }; + var toRead = assetKey; + if ( assetKey === µBlock.userFiltersPath ) { + toRead = [ + assetKey, + 'assets/user/filters.txt', + 'cached_asset_content://assets/user/filters.txt' + ]; + } + vAPI.storage.get(toRead, onLoaded); }; -// https://www.youtube.com/watch?v=uvUW4ozs7pY +var saveUserAsset = function(assetKey, content, callback) { + var bin = {}; + bin[assetKey] = content; + // TODO(seamless migration): + // This is for forward compatibility. Only for a limited time. Remove when + // everybody moved to 1.11.0 and beyond. + // >>>>>>>> + if ( assetKey === µBlock.userFiltersPath ) { + bin['assets/user/filters.txt'] = content; + } + // <<<<<<<< + var onSaved = function() { + if ( callback instanceof Function ) { + callback({ assetKey: assetKey, content: content }); + } + }; + vAPI.storage.set(bin, onSaved); +}; /******************************************************************************/ -// 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 +api.get = function(assetKey, options, callback) { + if ( typeof options === 'function' ) { + callback = options; + options = {}; + } else if ( typeof callback !== 'function' ) { + callback = noopfunc; + } -var readRepoOnlyAsset = function(path, callback) { + if ( assetKey === µBlock.userFiltersPath ) { + readUserAsset(assetKey, callback); + return; + } - var assetEntry; + var assetDetails = {}, + contentURLs, + contentURL; var reportBack = function(content, err) { - var details = { - 'path': path, - 'content': content - }; + var details = { assetKey: assetKey, content: content }; if ( err ) { - details.error = err; + details.error = assetDetails.lastError = err; + } else { + assetDetails.lastError = undefined; } callback(details); }; - var onInstallFileLoaded = function() { - //console.log('µBlock> readRepoOnlyAsset("%s") / onInstallFileLoaded()', path); - reportBack(this.responseText); - }; - - var onInstallFileError = function() { - 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(vAPI.getURL(path), onInstallFileLoaded, onInstallFileError); - }; - - var repositoryURL = toRepoURL(path + '?_=' + Date.now()); - - var onRepoFileLoaded = function() { - if ( typeof this.responseText !== 'string' ) { - console.error('µBlock> readRepoOnlyAsset("%s") / onRepoFileLoaded("%s"): no response', path, repositoryURL); - cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); - return; + var onContentNotLoaded = function() { + var isExternal; + while ( (contentURL = contentURLs.shift()) ) { + isExternal = reIsExternalPath.test(contentURL); + if ( isExternal === false || assetDetails.hasLocalURL !== true ) { + break; + } } - 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; + if ( !contentURL ) { + return reportBack('', 'E_NOTFOUND'); } - //console.log('µBlock> readRepoOnlyAsset("%s") / onRepoFileLoaded("%s")', path, repositoryURL); - assetEntry.localChecksum = assetEntry.repoChecksum; - updateLocalChecksums(); - cachedAssetsManager.save(path, this.responseText, callback); + getTextFileFromURL(contentURL, onContentLoaded, onContentNotLoaded); }; - var onRepoFileError = function() { - console.error(errorCantConnectTo.replace('{{url}}', repositoryURL)); - 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'); + var onContentLoaded = function() { + if ( stringIsNotEmpty(this.responseText) === false ) { + onContentNotLoaded(); return; } - - // Asset added or changed: load from repo URL and then cache result - if ( - exports.remoteFetchBarrier === 0 && - exports.autoUpdate && - assetEntry.localChecksum !== assetEntry.repoChecksum - ) { - //console.log('µBlock> readRepoOnlyAsset("%s") / onRepoMetaReady(): repo has newer version', path); - getTextFileFromURL(repositoryURL, onRepoFileLoaded, onRepoFileError); - return; + if ( reIsExternalPath.test(contentURL) && options.dontCache !== true ) { + assetCacheWrite(assetKey, { + content: this.responseText, + url: contentURL + }); } - - // Load from cache - cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); + reportBack(this.responseText); }; - getRepoMetadata(onRepoMetaReady); -}; - -/******************************************************************************/ - -// Asset doesn't exist. Just for symmetry purpose. + var onCachedContentLoaded = function(details) { + if ( details.content !== '' ) { + return reportBack(details.content); + } + getAssetSourceRegistry(function(registry) { + assetDetails = registry[assetKey] || {}; + if ( typeof assetDetails.contentURL === 'string' ) { + contentURLs = [ assetDetails.contentURL ]; + } else if ( Array.isArray(assetDetails.contentURL) ) { + contentURLs = assetDetails.contentURL.slice(0); + } else { + contentURLs = []; + } + onContentNotLoaded(); + }); + }; -var readNilAsset = function(path, callback) { - callback({ - 'path': path, - 'content': '', - 'error': 'Error: asset not found' - }); + assetCacheRead(assetKey, onCachedContentLoaded); }; /******************************************************************************/ -// An external asset: -// Path --> starts with 'http' -// External --> https://..., http://... -// Cache --> has expiration timestamp (in cache) +var getRemote = function(assetKey, callback) { + var assetDetails = {}, + contentURLs, + contentURL; -var readExternalAsset = function(path, callback) { var reportBack = function(content, err) { - var details = { - 'path': path, - 'content': content - }; + var details = { assetKey: assetKey, content: content }; if ( err ) { - details.error = err; + details.error = assetDetails.lastError = err; + } else { + assetDetails.lastError = undefined; } 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() { - // https://github.com/chrisaljoudi/uBlock/issues/708 - // A successful download should never return an empty file: turn this - // into an error condition. + var onRemoteContentLoaded = function() { if ( stringIsNotEmpty(this.responseText) === false ) { - onExternalFileError(); + registerAssetSource(assetKey, { error: { time: Date.now(), error: 'No content' } }); + tryLoading(); return; } - //console.log('µBlock> readExternalAsset("%s") / onExternalFileLoaded1()', path); - cachedAssetsManager.save(path, this.responseText); + assetCacheWrite(assetKey, { + content: this.responseText, + url: contentURL + }); + registerAssetSource(assetKey, { error: undefined }); reportBack(this.responseText); }; - var onExternalFileError = function() { - console.error(errorCantConnectTo.replace('{{url}}', path)); - cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); - }; - - var onCacheMetaReady = function(entries) { - // Fetch from remote if: - // - Not in cache OR - // - // - Auto-update enabled AND in cache but obsolete - var timestamp = entries[path]; - var notInCache = typeof timestamp !== 'number'; - var updateCache = exports.remoteFetchBarrier === 0 && - exports.autoUpdate && - cacheIsObsolete(timestamp); - if ( notInCache || updateCache ) { - getTextFileFromURL(path, onExternalFileLoaded, onExternalFileError); - return; + var onRemoteContentError = function() { + var text = this.statusText; + if ( this.status === 0 ) { + text = 'network error'; } - - // In cache - cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); + registerAssetSource(assetKey, { error: { time: Date.now(), error: text } }); + tryLoading(); }; - cachedAssetsManager.entries(onCacheMetaReady); -}; - -/******************************************************************************/ - -// User data: -// Path --> starts with 'assets/user/' -// Cache --> whatever user saved - -var readUserAsset = function(path, callback) { - // TODO: remove when confident all users no longer have their custom - // filters saved into vAPI.cacheStorage. - var onCachedContentLoaded = function(details) { - saveUserAsset(path, details.content); - //console.log('µBlock.assets/readUserAsset("%s")/onCachedContentLoaded()', path); - callback({ 'path': path, 'content': details.content }); - }; - - var onCachedContentError = function() { - saveUserAsset(path, ''); - //console.log('µBlock.assets/readUserAsset("%s")/onCachedContentError()', path); - callback({ 'path': path, 'content': '' }); - }; - - var onLoaded = function(bin) { - var content = bin && bin[path]; - if ( typeof content === 'string' ) { - callback({ 'path': path, 'content': content }); - return; + var tryLoading = function() { + while ( (contentURL = contentURLs.shift()) ) { + if ( reIsExternalPath.test(contentURL) ) { break; } } - cachedAssetsManager.load(path, onCachedContentLoaded, onCachedContentError); - }; - - vAPI.storage.get(path, onLoaded); -}; - -var saveUserAsset = function(path, content, callback) { - var bin = {}; - bin[path] = content; - var onSaved = function() { - // Saving over an existing asset must be seen as removing an - // existing asset and adding a new one. - if ( onAssetRemovedListener instanceof Function ) { - onAssetRemovedListener([ path ]); + if ( !contentURL ) { + return reportBack('', 'E_NOTFOUND'); } - if ( callback instanceof Function ) { - callback({ path: path, content: content }); - } - }; - vAPI.storage.set(bin, onSaved); -}; - -/******************************************************************************/ - -// Asset available only from the cache. -// Cache data: -// Path --> starts with 'cache://' -// Cache --> whatever - -var readCacheAsset = function(path, callback) { - var onCachedContentLoaded = function(details) { - //console.log('µBlock.assets/readCacheAsset("%s")/onCachedContentLoaded()', path); - callback({ 'path': path, 'content': details.content }); + getTextFileFromURL(contentURL, onRemoteContentLoaded, onRemoteContentError); }; - var onCachedContentError = function() { - //console.log('µBlock.assets/readCacheAsset("%s")/onCachedContentError()', path); - callback({ 'path': path, 'content': '' }); - }; - - 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 ( reIsCachePath.test(path) ) { - readCacheAsset(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 ( stringIsNotEmpty(homeURLs[path]) ) { - readRepoCopyAsset(path, callback); - return; + getAssetSourceRegistry(function(registry) { + assetDetails = registry[assetKey] || {}; + if ( typeof assetDetails.contentURL === 'string' ) { + contentURLs = [ assetDetails.contentURL ]; + } else if ( Array.isArray(assetDetails.contentURL) ) { + contentURLs = assetDetails.contentURL.slice(0); + } else { + contentURLs = []; } - - // Asset is repo only - readRepoOnlyAsset(path, callback); - }; - - getRepoMetadata(onRepoMetaReady); + tryLoading(); + }); }; -// https://www.youtube.com/watch?v=98y0Q7nLGWk - /******************************************************************************/ -exports.getLocal = readLocalFile; - -/******************************************************************************/ - -exports.put = function(path, content, callback) { - if ( reIsUserPath.test(path) ) { - saveUserAsset(path, content, callback); - return; +api.put = function(assetKey, content, callback) { + if ( reIsUserAsset.test(assetKey) ) { + return saveUserAsset(assetKey, content, callback); } - - cachedAssetsManager.save(path, content, callback); + assetCacheWrite(assetKey, content, callback); }; /******************************************************************************/ -exports.rmrf = function() { - cachedAssetsManager.rmrf(); -}; - -/******************************************************************************/ - -exports.rename = function(from, to, callback) { - var done = function() { - if ( typeof callback === 'function' ) { - callback(); - } - }; - - var fromLoaded = function(details) { - cachedAssetsManager.remove(from); - cachedAssetsManager.save(to, details.content, callback); - done(); - }; - - var toLoaded = function(details) { - // `to` already exists: do nothing - if ( details.content !== '' ) { - return done(); - } - cachedAssetsManager.load(from, fromLoaded); - }; - - // If `to` content already exists, do nothing. - cachedAssetsManager.load(to, toLoaded); -}; - -/******************************************************************************/ - -exports.metadata = function(callback) { - var out = {}; - - // https://github.com/chrisaljoudi/uBlock/issues/186 - // We need to check cache obsolescence when both cache and repo meta data - // has been gathered. - var checkCacheObsolescence = function() { - var entry, homeURL; - for ( var path in out ) { - if ( out.hasOwnProperty(path) === false ) { - continue; - } - entry = out[path]; - // https://github.com/gorhill/uBlock/issues/528 - // Not having a homeURL property does not mean the filter list - // is not external. - homeURL = reIsExternalPath.test(path) ? path : homeURLs[path]; - entry.cacheObsolete = stringIsNotEmpty(homeURL) && - cacheIsObsolete(entry.lastModified); - } - callback(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] = {}; +api.metadata = function(callback) { + var assetRegistryReady = false, + cacheRegistryReady = false; + + var onReady = function() { + var assetDict = JSON.parse(JSON.stringify(assetSourceRegistry)), + cacheDict = assetCacheRegistry, + assetEntry, cacheEntry, + now = Date.now(), obsoleteAfter; + for ( var assetKey in assetDict ) { + assetEntry = assetDict[assetKey]; + cacheEntry = cacheDict[assetKey]; + if ( cacheEntry ) { + assetEntry.cached = true; + assetEntry.writeTime = cacheEntry.writeTime; + obsoleteAfter = cacheEntry.writeTime + assetEntry.updateAfter * 86400000; + assetEntry.obsolete = obsoleteAfter < now; + assetEntry.remoteURL = cacheEntry.remoteURL; + } else { + assetEntry.writeTime = 0; + obsoleteAfter = 0; + assetEntry.obsolete = true; } - entryOut.localChecksum = entryRepo.localChecksum; - entryOut.repoChecksum = entryRepo.repoChecksum; - entryOut.homeURL = homeURLs[path] || ''; - entryOut.supportURL = entryRepo.supportURL || ''; - entryOut.repoObsolete = entryOut.localChecksum !== entryOut.repoChecksum; } - checkCacheObsolescence(); + callback(assetDict); }; - var onCacheMetaReady = function(entries) { - 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; - if ( reIsExternalPath.test(path) ) { - entryOut.homeURL = path; - } - } - getRepoMetadata(onRepoMetaReady); - }; + getAssetSourceRegistry(function() { + assetRegistryReady = true; + if ( cacheRegistryReady ) { onReady(); } + }); - cachedAssetsManager.entries(onCacheMetaReady); + getAssetCacheRegistry(function() { + cacheRegistryReady = true; + if ( assetRegistryReady ) { onReady(); } + }); }; /******************************************************************************/ -exports.purge = function(pattern, before) { - cachedAssetsManager.remove(pattern, before); -}; - -exports.purgeCacheableAsset = function(pattern, before) { - cachedAssetsManager.remove(pattern, before); - lastRepoMetaTimestamp = 0; -}; +api.purge = assetCacheMarkAsDirty; -exports.purgeAll = function(callback) { - cachedAssetsManager.removeAll(callback); - lastRepoMetaTimestamp = 0; +api.remove = function(pattern, callback) { + assetCacheRemove(pattern, callback); }; -/******************************************************************************/ - -exports.onAssetRemoved = { - addListener: function(callback) { - onAssetRemovedListener = callback instanceof Function ? callback : null; - } -}; - -/******************************************************************************/ - -return exports; - -})(); - -/******************************************************************************/ -/******************************************************************************/ - -µBlock.assetUpdater = (function() { - -/******************************************************************************/ - -var µb = µBlock; - -var updateDaemonTimer = null; -var autoUpdateDaemonTimerPeriod = 11 * 60 * 1000; // 11 minutes -var manualUpdateDaemonTimerPeriod = 5 * 1000; // 5 seconds - -var updateCycleFirstPeriod = 7 * 60 * 1000; // 7 minutes -var updateCycleNextPeriod = 11 * 60 * 60 * 1000; // 11 hours -var updateCycleTime = 0; - -var toUpdate = {}; -var toUpdateCount = 0; -var updated = {}; -var updatedCount = 0; -var metadata = null; - -var onStartListener = null; -var onCompletedListener = null; -var onAssetUpdatedListener = null; - -var exports = { - manualUpdate: false, - manualUpdateProgress: { - value: 0, - text: null - } +api.rmrf = function() { + assetCacheRemove(/./); }; /******************************************************************************/ -var onOneUpdated = function(details) { - // Resource fetched, we can safely restart the daemon. - scheduleUpdateDaemon(); - - var path = details.path; - if ( details.error ) { - manualUpdateNotify(false, updatedCount / (updatedCount + toUpdateCount)); - //console.debug('µBlock.assetUpdater/onOneUpdated: "%s" failed', path); - return; - } - - //console.debug('µBlock.assetUpdater/onOneUpdated: "%s"', path); - updated[path] = true; - updatedCount += 1; - - if ( typeof onAssetUpdatedListener === 'function' ) { - onAssetUpdatedListener(details); - } - - manualUpdateNotify(false, updatedCount / (updatedCount + toUpdateCount + 1)); +// Asset updater area. +var updaterStatus, + updaterTimer, + updaterAssetDelayDefault = 120000, + updaterAssetDelay = updaterAssetDelayDefault, + updaterUpdated = [], + updaterFetched = new Set(); + +var updateFirst = function() { + updaterStatus = 'updating'; + updaterFetched.clear(); + updaterUpdated = []; + fireNotification('before-assets-updated'); + updateNext(); }; -/******************************************************************************/ - -var updateOne = function() { - // Because this can be called from outside the daemon's main loop - µb.assets.autoUpdate = µb.userSettings.autoUpdate || exports.manualUpdate; +var updateNext = function() { + var assetDict, cacheDict; - var metaEntry; - var updatingCount = 0; - var updatingText = null; - - for ( var path in toUpdate ) { - if ( toUpdate.hasOwnProperty(path) === false ) { - continue; - } - if ( toUpdate[path] !== true ) { - continue; + // This will remove a cached asset when it's no longer in use. + var garbageCollectOne = function(assetKey) { + var cacheEntry = cacheDict[assetKey]; + if ( cacheEntry && cacheEntry.readTime < assetCacheRegistryStartTime ) { + assetCacheRemove(assetKey); } - toUpdate[path] = false; - toUpdateCount -= 1; - if ( metadata.hasOwnProperty(path) === false ) { - continue; - } - metaEntry = metadata[path]; - if ( !metaEntry.cacheObsolete && !metaEntry.repoObsolete ) { - continue; - } - - // Will restart the update daemon once the resource is received: the - // fetching of a resource may take some time, possibly beyond the - // next scheduled daemon cycle, so this ensure the daemon won't do - // anything else before the resource is fetched (or times out). - suspendUpdateDaemon(); - - //console.debug('µBlock.assetUpdater/updateOne: assets.get("%s")', path); - µb.assets.get(path, onOneUpdated); - updatingCount = 1; - updatingText = metaEntry.homeURL || path; - break; - } - - manualUpdateNotify( - false, - (updatedCount + updatingCount/2) / (updatedCount + toUpdateCount + updatingCount + 1), - updatingText - ); -}; - -/******************************************************************************/ - -// Update one asset, fetch metadata if not done yet. - -var safeUpdateOne = function() { - if ( metadata !== null ) { - updateOne(); - return; - } - - // Because this can be called from outside the daemon's main loop - µb.assets.autoUpdate = µb.userSettings.autoUpdate || exports.manualUpdate; - - var onMetadataReady = function(response) { - scheduleUpdateDaemon(); - metadata = response; - updateOne(); }; - suspendUpdateDaemon(); - µb.assets.metadata(onMetadataReady); -}; - -/******************************************************************************/ - -var safeStartListener = function(callback) { - // Because this can be called from outside the daemon's main loop - µb.assets.autoUpdate = µb.userSettings.autoUpdate || exports.manualUpdate; - - var onStartListenerDone = function(assets) { - scheduleUpdateDaemon(); - assets = assets || {}; - for ( var path in assets ) { - if ( assets.hasOwnProperty(path) === false ) { + var findOne = function() { + var now = Date.now(), + assetEntry, cacheEntry; + for ( var assetKey in assetDict ) { + assetEntry = assetDict[assetKey]; + if ( assetEntry.hasRemoteURL !== true ) { continue; } + if ( updaterFetched.has(assetKey) ) { continue; } + cacheEntry = cacheDict[assetKey]; + if ( cacheEntry && (cacheEntry.writeTime + assetEntry.updateAfter * 86400000) > now ) { continue; } - if ( toUpdate.hasOwnProperty(path) ) { - continue; + if ( fireNotification('before-asset-updated', { assetKey: assetKey }) !== false ) { + return assetKey; } - //console.debug('assets.js > µBlock.assetUpdater/safeStartListener: "%s"', path); - toUpdate[path] = true; - toUpdateCount += 1; - } - if ( typeof callback === 'function' ) { - callback(); + garbageCollectOne(assetKey); } }; - if ( typeof onStartListener === 'function' ) { - suspendUpdateDaemon(); - onStartListener(onStartListenerDone); - } else { - onStartListenerDone(null); - } -}; - -/******************************************************************************/ - -var updateDaemon = function() { - updateDaemonTimer = null; - scheduleUpdateDaemon(); - - µb.assets.autoUpdate = µb.userSettings.autoUpdate || exports.manualUpdate; - - if ( µb.assets.autoUpdate !== true ) { - return; - } - - // Start an update cycle? - if ( updateCycleTime !== 0 ) { - if ( Date.now() >= updateCycleTime ) { - //console.debug('µBlock.assetUpdater/updateDaemon: update cycle started'); - reset(); - safeStartListener(); + var updatedOne = function(details) { + if ( details.content !== '' ) { + updaterUpdated.push(details.assetKey); + if ( details.assetKey === 'assets.json' ) { + updateAssetSourceRegistry(details.content); + } + } else { + fireNotification('asset-update-failed', { assetKey: details.assetKey }); } - return; - } - - // Any asset to update? - if ( toUpdateCount !== 0 ) { - safeUpdateOne(); - return; - } - // Nothing left to update - - // In case of manual update, fire progress notifications - manualUpdateNotify(true, 1, ''); - - // If anything was updated, notify listener - if ( updatedCount !== 0 ) { - if ( typeof onCompletedListener === 'function' ) { - //console.debug('µBlock.assetUpdater/updateDaemon: update cycle completed'); - onCompletedListener({ - updated: JSON.parse(JSON.stringify(updated)), // give callee its own safe copy - updatedCount: updatedCount - }); + if ( findOne() !== undefined ) { + vAPI.setTimeout(updateNext, updaterAssetDelay); + } else { + updateDone(); } - } - - // Schedule next update cycle - if ( updateCycleTime === 0 ) { - reset(); - //console.debug('µBlock.assetUpdater/updateDaemon: update cycle re-scheduled'); - updateCycleTime = Date.now() + updateCycleNextPeriod; - } -}; - -/******************************************************************************/ - -var scheduleUpdateDaemon = function() { - if ( updateDaemonTimer !== null ) { - clearTimeout(updateDaemonTimer); - } - updateDaemonTimer = vAPI.setTimeout( - updateDaemon, - exports.manualUpdate ? manualUpdateDaemonTimerPeriod : autoUpdateDaemonTimerPeriod - ); -}; - -var suspendUpdateDaemon = function() { - if ( updateDaemonTimer !== null ) { - clearTimeout(updateDaemonTimer); - updateDaemonTimer = null; - } -}; - -scheduleUpdateDaemon(); - -/******************************************************************************/ - -var reset = function() { - toUpdate = {}; - toUpdateCount = 0; - updated = {}; - updatedCount = 0; - updateCycleTime = 0; - metadata = null; -}; - -/******************************************************************************/ - -var manualUpdateNotify = function(done, value, text) { - if ( exports.manualUpdate === false ) { - return; - } + }; - exports.manualUpdate = !done; - exports.manualUpdateProgress.value = value || 0; - if ( typeof text === 'string' ) { - exports.manualUpdateProgress.text = text; - } + var updateOne = function() { + var assetKey = findOne(); + if ( assetKey === undefined ) { + return updateDone(); + } + updaterFetched.add(assetKey); + getRemote(assetKey, updatedOne); + }; - vAPI.messaging.broadcast({ - what: 'forceUpdateAssetsProgress', - done: !exports.manualUpdate, - progress: exports.manualUpdateProgress, - updatedCount: updatedCount + getAssetSourceRegistry(function(dict) { + assetDict = dict; + if ( !cacheDict ) { return; } + updateOne(); }); - // When manually updating, whatever launched the manual update is - // responsible to launch a reload of the filter lists. - if ( exports.manualUpdate !== true ) { - reset(); - } + getAssetCacheRegistry(function(dict) { + cacheDict = dict; + if ( !assetDict ) { return; } + updateOne(); + }); }; -/******************************************************************************/ - -// Manual update: just a matter of forcing the update daemon to work on a -// tighter schedule. - -exports.force = function() { - if ( exports.manualUpdate ) { - return; - } - - reset(); - - exports.manualUpdate = true; - - var onStartListenerDone = function() { - if ( toUpdateCount === 0 ) { - updateCycleTime = Date.now() + updateCycleNextPeriod; - manualUpdateNotify(true, 1); - } else { - manualUpdateNotify(false, 0); - safeUpdateOne(); - } - }; - - safeStartListener(onStartListenerDone); +var updateDone = function() { + var assetKeys = updaterUpdated.slice(0); + updaterFetched.clear(); + updaterUpdated = []; + updaterStatus = undefined; + updaterAssetDelay = updaterAssetDelayDefault; + fireNotification('after-assets-updated', { assetKeys: assetKeys }); }; -/******************************************************************************/ - -exports.onStart = { - addEventListener: function(callback) { - onStartListener = callback || null; - if ( typeof onStartListener === 'function' ) { - updateCycleTime = Date.now() + updateCycleFirstPeriod; +api.updateStart = function(details) { + var oldUpdateDelay = updaterAssetDelay, + newUpdateDelay = details.delay || updaterAssetDelayDefault; + updaterAssetDelay = Math.min(oldUpdateDelay, newUpdateDelay); + if ( updaterStatus !== undefined ) { + if ( newUpdateDelay < oldUpdateDelay ) { + clearTimeout(updaterTimer); + updaterTimer = vAPI.setTimeout(updateNext, updaterAssetDelay); } + return; } + updateFirst(); }; -/******************************************************************************/ - -exports.onAssetUpdated = { - addEventListener: function(callback) { - onAssetUpdatedListener = callback || null; +api.updateStop = function() { + if ( updaterTimer ) { + clearTimeout(updaterTimer); + updaterTimer = undefined; } -}; - -/******************************************************************************/ - -exports.onCompleted = { - addEventListener: function(callback) { - onCompletedListener = callback || null; + if ( updaterStatus !== undefined ) { + updateDone(); } }; /******************************************************************************/ -// Typically called when an update has been forced. - -exports.restart = function() { - reset(); - updateCycleTime = Date.now() + updateCycleNextPeriod; -}; - -/******************************************************************************/ - -// Call when disabling uBlock, to ensure it doesn't stick around as a detached -// window object in Firefox. - -exports.shutdown = function() { - suspendUpdateDaemon(); - reset(); -}; +return api; /******************************************************************************/ -return exports; - })(); /******************************************************************************/ diff --git a/src/js/background.js b/src/js/background.js index 00b82a1c64997..5cc6d8915a601 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2014-2016 Raymond Hill + Copyright (C) 2014-2017 Raymond Hill This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,20 +19,16 @@ Home: https://github.com/gorhill/uBlock */ -/* exported µBlock */ - 'use strict'; /******************************************************************************/ -var µBlock = (function() { +var µBlock = (function() { // jshint ignore:line /******************************************************************************/ var oneSecond = 1000; var oneMinute = 60 * oneSecond; -var oneHour = 60 * oneMinute; -// var oneDay = 24 * oneHour; /******************************************************************************/ @@ -71,8 +67,12 @@ return { }, hiddenSettingsDefault: { + assetFetchTimeout: 30, + autoUpdateAssetFetchPeriod: 120, + autoUpdatePeriod: 7, ignoreRedirectFilters: false, ignoreScriptInjectFilters: false, + manualUpdateAssetFetchPeriod: 2000, popupFontSize: 'unset', suspendTabsUntilReady: false }, @@ -108,8 +108,8 @@ return { // read-only systemSettings: { - compiledMagic: 'zelhzxrhkfjr', - selfieMagic: 'zelhzxrhkfjr' + compiledMagic: 'fxtcjjhbhyiw', + selfieMagic: 'fxtcjjhbhyiw' }, restoreBackupSettings: { @@ -119,92 +119,16 @@ return { lastBackupTime: 0 }, - // EasyList, EasyPrivacy and many others have an 4-day update period, - // as per list headers. - updateAssetsEvery: 97 * oneHour, - projectServerRoot: 'https://raw.githubusercontent.com/gorhill/uBlock/master/', - userFiltersPath: 'assets/user/filters.txt', - pslPath: 'assets/thirdparties/publicsuffix.org/list/effective_tld_names.dat', - - // permanent lists - permanentLists: { - // User - 'assets/user/filters.txt': { - group: 'default' - }, - // uBlock - 'assets/ublock/filters.txt': { - title: 'uBlock filters', - group: 'default' - }, - 'assets/ublock/privacy.txt': { - title: 'uBlock filters – Privacy', - group: 'default' - }, - 'assets/ublock/unbreak.txt': { - title: 'uBlock filters – Unbreak', - group: 'default' - }, - 'assets/ublock/badware.txt': { - title: 'uBlock filters – Badware risks', - group: 'default', - supportURL: 'https://github.com/gorhill/uBlock/wiki/Badware-risks', - instructionURL: 'https://github.com/gorhill/uBlock/wiki/Badware-risks' - }, - 'assets/ublock/experimental.txt': { - title: 'uBlock filters – Experimental', - group: 'default', - off: true, - supportURL: 'https://github.com/gorhill/uBlock/wiki/Experimental-filters', - instructionURL: 'https://github.com/gorhill/uBlock/wiki/Experimental-filters' - } - }, + // Allows to fully customize uBO's assets, typically set through admin + // settings. The content of 'assets.json' will also tell which filter + // lists to enable by default when uBO is first installed. + assetsBootstrapLocation: 'assets/assets.json', - // current lists - remoteBlacklists: {}, - oldListToNewListMap: { - "assets/thirdparties/adblock.gardar.net/is.abp.txt": "http://adblock.gardar.net/is.abp.txt", - "assets/thirdparties/adblock.schack.dk/block.txt": "https://adblock.dk/block.csv", - "https://adblock.schack.dk/block.txt": "https://adblock.dk/block.csv", - "assets/thirdparties/dl.dropboxusercontent.com/u/1289327/abpxfiles/filtri.txt": "https://dl.dropboxusercontent.com/u/1289327/abpxfiles/filtri.txt", - "assets/thirdparties/easylist-downloads.adblockplus.org/advblock.txt": "https://easylist-downloads.adblockplus.org/advblock.txt", - "assets/thirdparties/easylist-downloads.adblockplus.org/bitblock.txt": "https://easylist-downloads.adblockplus.org/bitblock.txt", - "assets/thirdparties/easylist-downloads.adblockplus.org/easylist_noelemhide.txt": "https://easylist-downloads.adblockplus.org/easylist_noelemhide.txt", - "assets/thirdparties/easylist-downloads.adblockplus.org/easylistchina.txt": "https://easylist-downloads.adblockplus.org/easylistchina.txt", - "assets/thirdparties/easylist-downloads.adblockplus.org/easylistdutch.txt": "https://easylist-downloads.adblockplus.org/easylistdutch.txt", - "assets/thirdparties/easylist-downloads.adblockplus.org/easylistgermany.txt": "https://easylist-downloads.adblockplus.org/easylistgermany.txt", - "assets/thirdparties/easylist-downloads.adblockplus.org/easylistitaly.txt": "https://easylist-downloads.adblockplus.org/easylistitaly.txt", - "assets/thirdparties/easylist-downloads.adblockplus.org/fanboy-annoyance.txt": "https://easylist-downloads.adblockplus.org/fanboy-annoyance.txt", - "assets/thirdparties/easylist-downloads.adblockplus.org/fanboy-social.txt": "https://easylist-downloads.adblockplus.org/fanboy-social.txt", - "assets/thirdparties/easylist-downloads.adblockplus.org/liste_fr.txt": "https://easylist-downloads.adblockplus.org/liste_fr.txt", - "assets/thirdparties/gitorious.org/adblock-latvian/adblock-latvian/raw/master_lists/latvian-list.txt": "https://notabug.org/latvian-list/adblock-latvian/raw/master/lists/latvian-list.txt", - "assets/thirdparties/home.fredfiber.no/langsholt/adblock.txt": "http://home.fredfiber.no/langsholt/adblock.txt", - "assets/thirdparties/hosts-file.net/ad-servers": "http://hosts-file.net/.%5Cad_servers.txt", - "assets/thirdparties/http://www.certyficate.it/adblock/adblock.txt": "https://raw.githubusercontent.com/MajkiIT/polish-ads-filter/master/polish-adblock-filters/adblock.txt", - "assets/thirdparties/liste-ar-adblock.googlecode.com/hg/Liste_AR.txt": "https://liste-ar-adblock.googlecode.com/hg/Liste_AR.txt", - "assets/thirdparties/margevicius.lt/easylistlithuania.txt": "http://margevicius.lt/easylistlithuania.txt", - "assets/thirdparties/mirror1.malwaredomains.com/files/immortal_domains.txt": "http://malwaredomains.lehigh.edu/files/immortal_domains.txt", - "assets/thirdparties/raw.githubusercontent.com/AdBlockPlusIsrael/EasyListHebrew/master/EasyListHebrew.txt": "https://raw.githubusercontent.com/AdBlockPlusIsrael/EasyListHebrew/master/EasyListHebrew.txt", - "assets/thirdparties/raw.githubusercontent.com/cjx82630/cjxlist/master/cjxlist.txt": "https://raw.githubusercontent.com/cjx82630/cjxlist/master/cjxlist.txt", - "assets/thirdparties/raw.githubusercontent.com/reek/anti-adblock-killer/master/anti-adblock-killer-filters.txt": "https://raw.githubusercontent.com/reek/anti-adblock-killer/master/anti-adblock-killer-filters.txt", - "assets/thirdparties/raw.githubusercontent.com/szpeter80/hufilter/master/hufilter.txt": "https://raw.githubusercontent.com/szpeter80/hufilter/master/hufilter.txt", - "assets/thirdparties/raw.githubusercontent.com/tomasko126/easylistczechandslovak/master/filters.txt": "https://raw.githubusercontent.com/tomasko126/easylistczechandslovak/master/filters.txt", - "assets/thirdparties/someonewhocares.org/hosts/hosts": "http://someonewhocares.org/hosts/hosts", - "assets/thirdparties/spam404bl.com/spam404scamlist.txt": "https://spam404bl.com/spam404scamlist.txt", - "assets/thirdparties/stanev.org/abp/adblock_bg.txt": "http://stanev.org/abp/adblock_bg.txt", - "assets/thirdparties/winhelp2002.mvps.org/hosts.txt": "http://winhelp2002.mvps.org/hosts.txt", - "assets/thirdparties/www.fanboy.co.nz/enhancedstats.txt": "https://www.fanboy.co.nz/enhancedstats.txt", - "assets/thirdparties/www.fanboy.co.nz/fanboy-antifacebook.txt": "https://www.fanboy.co.nz/fanboy-antifacebook.txt", - "assets/thirdparties/www.fanboy.co.nz/fanboy-korean.txt": "https://www.fanboy.co.nz/fanboy-korean.txt", - "assets/thirdparties/www.fanboy.co.nz/fanboy-swedish.txt": "https://www.fanboy.co.nz/fanboy-swedish.txt", - "assets/thirdparties/www.fanboy.co.nz/fanboy-ultimate.txt": "https://www.fanboy.co.nz/r/fanboy-ultimate.txt", - "assets/thirdparties/www.fanboy.co.nz/fanboy-vietnam.txt": "https://www.fanboy.co.nz/fanboy-vietnam.txt", - "assets/thirdparties/www.void.gr/kargig/void-gr-filters.txt": "https://www.void.gr/kargig/void-gr-filters.txt", - "assets/thirdparties/www.zoso.ro/pages/rolist.txt": "", - "https://iadb.azurewebsites.net/Finland_adb.txt": "http://adb.juvander.net/Finland_adb.txt", - "https://www.certyficate.it/adblock/adblock.txt": "https://raw.githubusercontent.com/MajkiIT/polish-ads-filter/master/polish-adblock-filters/adblock.txt", - "https://raw.githubusercontent.com/heradhis/indonesianadblockrules/master/subscriptions/abpindo.txt": "https://raw.githubusercontent.com/ABPindo/indonesianadblockrules/master/subscriptions/abpindo.txt" - }, + userFiltersPath: 'user-filters', + pslAssetKey: 'public_suffix_list.dat', + + selectedFilterLists: [], + availableFilterLists: {}, selfieAfter: 23 * oneMinute, diff --git a/src/js/logger-ui.js b/src/js/logger-ui.js index ba05a6bd19b30..ad398f17352e0 100644 --- a/src/js/logger-ui.js +++ b/src/js/logger-ui.js @@ -543,7 +543,7 @@ var renderLogEntry = function(entry) { var time = logDate; time.setTime(entry.tstamp - logDateTimezoneOffset); tr.cells[0].textContent = padTo2(time.getUTCHours()) + ':' + - padTo2(time.getMinutes()) + ':' + + padTo2(time.getUTCMinutes()) + ':' + padTo2(time.getSeconds()); if ( entry.tab ) { diff --git a/src/js/logger.js b/src/js/logger.js index b9a7bee328748..700521ab5eada 100644 --- a/src/js/logger.js +++ b/src/js/logger.js @@ -19,15 +19,13 @@ Home: https://github.com/gorhill/uBlock */ -/* global µBlock */ +'use strict'; /******************************************************************************/ /******************************************************************************/ µBlock.logger = (function() { -'use strict'; - /******************************************************************************/ /******************************************************************************/ diff --git a/src/js/messaging.js b/src/js/messaging.js index 360fc7e0f39fd..20ec0e3ca11c4 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2014-2016 Raymond Hill + Copyright (C) 2014-2017 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 @@ -57,7 +57,7 @@ var onMessage = function(request, sender, callback) { switch ( request.what ) { case 'getAssetContent': // https://github.com/chrisaljoudi/uBlock/issues/417 - µb.assets.get(request.url, callback); + µb.assets.get(request.url, { dontCache: true }, callback); return; case 'listsFromNetFilter': @@ -77,7 +77,7 @@ var onMessage = function(request, sender, callback) { return; case 'reloadAllFilters': - µb.reloadAllFilters(callback); + µb.loadFilterLists(); return; case 'scriptlet': @@ -94,10 +94,8 @@ var onMessage = function(request, sender, callback) { var response; switch ( request.what ) { - case 'mouseClick': - µb.mouseX = request.x; - µb.mouseY = request.y; - µb.mouseURL = request.url; + case 'applyFilterListSelection': + response = µb.applyFilterListSelection(request); break; case 'compileCosmeticFilterSelector': @@ -121,7 +119,8 @@ var onMessage = function(request, sender, callback) { break; case 'forceUpdateAssets': - µb.assetUpdater.force(); + µb.scheduleAssetUpdater(0); + µb.assets.updateStart({ delay: µb.hiddenSettings.manualUpdateAssetFetchPeriod || 2000 }); break; case 'getAppData': @@ -146,6 +145,12 @@ var onMessage = function(request, sender, callback) { µb.openNewTab(request.details); break; + case 'mouseClick': + µb.mouseX = request.x; + µb.mouseY = request.y; + µb.mouseURL = request.url; + break; + case 'reloadTab': if ( vAPI.isBehindTheSceneTabId(request.tabId) === false ) { vAPI.tabs.reload(request.tabId); @@ -159,10 +164,6 @@ var onMessage = function(request, sender, callback) { µb.scriptlets.report(tabId, request.scriptlet, request.response); break; - case 'selectFilterLists': - µb.selectFilterLists(request.switches); - break; - case 'setWhitelist': µb.netWhitelist = µb.whitelistFromString(request.whitelist); µb.saveWhitelist(); @@ -753,18 +754,25 @@ var backupUserData = function(callback) { timeStamp: Date.now(), version: vAPI.app.version, userSettings: µb.userSettings, - filterLists: {}, + selectedFilterLists: µb.selectedFilterLists, hiddenSettingsString: µb.stringFromHiddenSettings(), netWhitelist: µb.stringFromWhitelist(µb.netWhitelist), dynamicFilteringString: µb.permanentFirewall.toString(), urlFilteringString: µb.permanentURLFiltering.toString(), hostnameSwitchesString: µb.hnSwitches.toString(), - userFilters: '' + userFilters: '', + // TODO(seamless migration): + // The following is strictly for convenience, to be minimally + // forward-compatible. This will definitely be removed in the + // short term, as I do not expect the need to install an older + // version of uBO to ever be needed beyond the short term. + // >>>>>>>> + filterLists: µb.oldDataFromNewListKeys(µb.selectedFilterLists) + // <<<<<<<< }; - var onSelectedListsReady = function(filterLists) { - userData.filterLists = filterLists; - + var onUserFiltersReady = function(details) { + userData.userFilters = details.content; var filename = vAPI.i18n('aboutBackupFilename') .replace('{{datetime}}', µb.dateNowToSensibleString()) .replace(/ +/g, '_'); @@ -773,50 +781,51 @@ var backupUserData = function(callback) { 'url': 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(userData, null, ' ')), 'filename': filename }); - µb.restoreBackupSettings.lastBackupFile = filename; µb.restoreBackupSettings.lastBackupTime = Date.now(); vAPI.storage.set(µb.restoreBackupSettings); - getLocalData(callback); }; - var onUserFiltersReady = function(details) { - userData.userFilters = details.content; - µb.extractSelectedFilterLists(onSelectedListsReady); - }; - µb.assets.get(µb.userFiltersPath, onUserFiltersReady); }; var restoreUserData = function(request) { var userData = request.userData; - var countdown = 8; - var onCountdown = function() { - countdown -= 1; - if ( countdown === 0 ) { - vAPI.app.restart(); - } + + var restart = function() { + vAPI.app.restart(); }; var onAllRemoved = function() { - // Be sure to adjust `countdown` if adding/removing anything below - µb.keyvalSetOne('version', userData.version); µBlock.saveLocalSettings(); - vAPI.storage.set(userData.userSettings, onCountdown); - µb.keyvalSetOne('remoteBlacklists', userData.filterLists, onCountdown); + vAPI.storage.set(userData.userSettings); µb.hiddenSettingsFromString(userData.hiddenSettingsString || ''); - µb.keyvalSetOne('netWhitelist', userData.netWhitelist || '', onCountdown); - µb.keyvalSetOne('dynamicFilteringString', userData.dynamicFilteringString || '', onCountdown); - µb.keyvalSetOne('urlFilteringString', userData.urlFilteringString || '', onCountdown); - µb.keyvalSetOne('hostnameSwitchesString', userData.hostnameSwitchesString || '', onCountdown); - µb.assets.put(µb.userFiltersPath, userData.userFilters, onCountdown); vAPI.storage.set({ + netWhitelist: userData.netWhitelist || '', + dynamicFilteringString: userData.dynamicFilteringString || '', + urlFilteringString: userData.urlFilteringString || '', + hostnameSwitchesString: userData.hostnameSwitchesString || '', lastRestoreFile: request.file || '', lastRestoreTime: Date.now(), lastBackupFile: '', lastBackupTime: 0 - }, onCountdown); + }); + µb.assets.put(µb.userFiltersPath, userData.userFilters); + + // 'filterLists' is available up to uBO v1.10.4, not beyond. + // 'selectedFilterLists' is available from uBO v1.11 and beyond. + var listKeys; + if ( Array.isArray(userData.selectedFilterLists) ) { + listKeys = userData.selectedFilterLists; + } else if ( userData.filterLists instanceof Object ) { + listKeys = µb.newListKeysFromOldData(userData.filterLists); + } + if ( listKeys !== undefined ) { + µb.saveSelectedFilterLists(listKeys, restart); + } else { + restart(); + } }; // https://github.com/chrisaljoudi/uBlock/issues/1102 @@ -848,9 +857,7 @@ var prepListEntries = function(entries) { var µburi = µb.URI; var entry, hn; for ( var k in entries ) { - if ( entries.hasOwnProperty(k) === false ) { - continue; - } + if ( entries.hasOwnProperty(k) === false ) { continue; } entry = entries[k]; if ( typeof entry.supportURL === 'string' && entry.supportURL !== '' ) { entry.supportName = µburi.hostnameFromURI(entry.supportURL); @@ -867,18 +874,17 @@ var getLists = function(callback) { autoUpdate: µb.userSettings.autoUpdate, available: null, cache: null, - parseCosmeticFilters: µb.userSettings.parseAllABPHideFilters, cosmeticFilterCount: µb.cosmeticFilteringEngine.getFilterCount(), - current: µb.remoteBlacklists, + current: µb.availableFilterLists, + externalLists: µb.userSettings.externalLists, ignoreGenericCosmeticFilters: µb.userSettings.ignoreGenericCosmeticFilters, - manualUpdate: false, netFilterCount: µb.staticNetFilteringEngine.getFilterCount(), - userFiltersPath: µb.userFiltersPath + parseCosmeticFilters: µb.userSettings.parseAllABPHideFilters, + userFiltersPath: µb.userFiltersPath, + aliases: µb.assets.listKeyAliases }; var onMetadataReady = function(entries) { r.cache = entries; - r.manualUpdate = µb.assetUpdater.manualUpdate; - r.manualUpdateProgress = µb.assetUpdater.manualUpdateProgress; prepListEntries(r.cache); callback(r); }; @@ -952,9 +958,6 @@ var onMessage = function(request, sender, callback) { case 'getLocalData': return getLocalData(callback); - case 'purgeAllCaches': - return µb.assets.purgeAll(callback); - case 'readUserFilters': return µb.loadUserFilters(callback); @@ -973,8 +976,17 @@ var onMessage = function(request, sender, callback) { response = getRules(); break; + case 'purgeAllCaches': + if ( request.hard ) { + µb.assets.remove(/./); + } else { + µb.assets.purge(/./, 'public_suffix_list.dat'); + } + break; + case 'purgeCache': - µb.assets.purgeCacheableAsset(request.path); + µb.assets.purge(request.assetKey); + µb.assets.remove('compiled/' + request.assetKey); break; case 'readHiddenSettings': @@ -1276,8 +1288,7 @@ var onMessage = function(request, sender, callback) { case 'subscriberData': response = { - confirmStr: vAPI.i18n('subscriberConfirm'), - externalLists: µBlock.userSettings.externalLists + confirmStr: vAPI.i18n('subscriberConfirm') }; break; diff --git a/src/js/redirect-engine.js b/src/js/redirect-engine.js index 473bffa79a372..dfae64ffd158d 100644 --- a/src/js/redirect-engine.js +++ b/src/js/redirect-engine.js @@ -402,27 +402,15 @@ RedirectEngine.prototype.resourceContentFromName = function(name, mime) { // TODO: combine same key-redirect pairs into a single regex. RedirectEngine.prototype.resourcesFromString = function(text) { - var textEnd = text.length; - var lineBeg = 0, lineEnd; - var line, fields, encoded; - var reNonEmptyLine = /\S/; + var line, fields, encoded, + reNonEmptyLine = /\S/, + lineIter = new µBlock.LineIterator(text); this.resources = new Map(); - while ( lineBeg < textEnd ) { - lineEnd = text.indexOf('\n', lineBeg); - if ( lineEnd < 0 ) { - lineEnd = text.indexOf('\r', lineBeg); - if ( lineEnd < 0 ) { - lineEnd = textEnd; - } - } - line = text.slice(lineBeg, lineEnd); - lineBeg = lineEnd + 1; - - if ( line.startsWith('#') ) { - continue; - } + while ( lineIter.eot() === false ) { + line = lineIter.next(); + if ( line.startsWith('#') ) { continue; } if ( fields === undefined ) { fields = line.trim().split(/\s+/); diff --git a/src/js/reverselookup-worker.js b/src/js/reverselookup-worker.js index f52002e676750..17e8f99151af5 100644 --- a/src/js/reverselookup-worker.js +++ b/src/js/reverselookup-worker.js @@ -1,7 +1,7 @@ /******************************************************************************* - uBlock - a browser extension to block requests. - Copyright (C) 2015 Raymond Hill + uBlock Origin - a browser extension to block requests. + Copyright (C) 2015-2017 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 @@ -43,8 +43,8 @@ var fromNetFilter = function(details) { var lists = []; var compiledFilter = details.compiledFilter; var entry, content, pos, c; - for ( var path in listEntries ) { - entry = listEntries[path]; + for ( var assetKey in listEntries ) { + entry = listEntries[assetKey]; if ( entry === undefined ) { continue; } @@ -173,11 +173,11 @@ var fromCosmeticFilter = function(details) { ); } - var re, path, entry; + var re, assetKey, entry; for ( var candidate in candidates ) { re = candidates[candidate]; - for ( path in listEntries ) { - entry = listEntries[path]; + for ( assetKey in listEntries ) { + entry = listEntries[assetKey]; if ( entry === undefined ) { continue; } @@ -206,7 +206,7 @@ var reHighMedium = /^\[href\^="https?:\/\/([^"]{8})[^"]*"\]$/; /******************************************************************************/ -onmessage = function(e) { +onmessage = function(e) { // jshint ignore:line var msg = e.data; switch ( msg.what ) { @@ -215,7 +215,7 @@ onmessage = function(e) { break; case 'setList': - listEntries[msg.details.path] = msg.details; + listEntries[msg.details.assetKey] = msg.details; break; case 'fromNetFilter': diff --git a/src/js/reverselookup.js b/src/js/reverselookup.js index 79af14e7c571e..c18bfba6662b7 100644 --- a/src/js/reverselookup.js +++ b/src/js/reverselookup.js @@ -1,7 +1,7 @@ /******************************************************************************* - uBlock - a browser extension to block requests. - Copyright (C) 2015 Raymond Hill + uBlock Origin - a browser extension to block requests. + Copyright (C) 2015-2017 Raymond Hill This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,14 +19,12 @@ Home: https://github.com/gorhill/uBlock */ -/* global µBlock */ +'use strict'; /******************************************************************************/ µBlock.staticFilteringReverseLookup = (function() { -'use strict'; - /******************************************************************************/ var worker = null; @@ -77,16 +75,16 @@ var initWorker = function(callback) { var countdown = 0; var onListLoaded = function(details) { - var entry = entries[details.path]; + var entry = entries[details.assetKey]; // https://github.com/gorhill/uBlock/issues/536 - // Use path string when there is no filter list title. + // Use assetKey when there is no filter list title. worker.postMessage({ what: 'setList', details: { - path: details.path, - title: entry.title || details.path, + assetKey: details.assetKey, + title: entry.title || details.assetKey, supportURL: entry.supportURL, content: details.content } @@ -99,18 +97,18 @@ var initWorker = function(callback) { }; var µb = µBlock; - var path, entry; + var listKey, entry; - for ( path in µb.remoteBlacklists ) { - if ( µb.remoteBlacklists.hasOwnProperty(path) === false ) { - continue; - } - entry = µb.remoteBlacklists[path]; - if ( entry.off === true ) { + for ( listKey in µb.availableFilterLists ) { + if ( µb.availableFilterLists.hasOwnProperty(listKey) === false ) { continue; } - entries[path] = { - title: path !== µb.userFiltersPath ? entry.title : vAPI.i18n('1pPageName'), + entry = µb.availableFilterLists[listKey]; + if ( entry.off === true ) { continue; } + entries[listKey] = { + title: listKey !== µb.userFiltersPath ? + entry.title : + vAPI.i18n('1pPageName'), supportURL: entry.supportURL || '' }; countdown += 1; @@ -121,8 +119,8 @@ var initWorker = function(callback) { return; } - for ( path in entries ) { - µb.getCompiledFilterList(path, onListLoaded); + for ( listKey in entries ) { + µb.getCompiledFilterList(listKey, onListLoaded); } }; diff --git a/src/js/scriptlets/subscriber.js b/src/js/scriptlets/subscriber.js index 9c525de0d9b36..4250c5ff10518 100644 --- a/src/js/scriptlets/subscriber.js +++ b/src/js/scriptlets/subscriber.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2015-2016 Raymond Hill + Copyright (C) 2015-2017 Raymond Hill This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,7 +19,9 @@ Home: https://github.com/gorhill/uBlock */ -/* global vAPI, HTMLDocument */ +/* global HTMLDocument */ + +'use strict'; /******************************************************************************/ @@ -30,8 +32,6 @@ (function() { -'use strict'; - /******************************************************************************/ // https://github.com/chrisaljoudi/uBlock/issues/464 @@ -79,9 +79,7 @@ var onAbpLinkClicked = function(ev) { var matches = /^abp:\/*subscribe\/*\?location=([^&]+).*title=([^&]+)/.exec(href); if ( matches === null ) { matches = /^https?:\/\/.*?[&?]location=([^&]+).*?&title=([^&]+)/.exec(href); - if ( matches === null ) { - return; - } + if ( matches === null ) { return; } } var location = decodeURIComponent(matches[1]); @@ -95,43 +93,18 @@ var onAbpLinkClicked = function(ev) { messaging.send('scriptlets', { what: 'reloadAllFilters' }); }; - var onExternalListsSaved = function() { - messaging.send( - 'scriptlets', - { - what: 'selectFilterLists', - switches: [ { location: location, off: false } ] - }, - onListsSelectionDone - ); - }; - var onSubscriberDataReady = function(details) { var confirmStr = details.confirmStr .replace('{{url}}', location) .replace('{{title}}', title); - if ( !window.confirm(confirmStr) ) { - return; - } - - // List already subscribed to? - // https://github.com/chrisaljoudi/uBlock/issues/1033 - // Split on line separators, not whitespaces. - var text = details.externalLists.trim(); - var lines = text !== '' ? text.split(/\s*[\n\r]+\s*/) : []; - if ( lines.indexOf(location) !== -1 ) { - return; - } - lines.push(location, ''); - + if ( !window.confirm(confirmStr) ) { return; } messaging.send( 'scriptlets', { - what: 'userSettings', - name: 'externalLists', - value: lines.join('\n') + what: 'applyFilterListSelection', + toImport: location }, - onExternalListsSaved + onListsSelectionDone ); }; diff --git a/src/js/settings.js b/src/js/settings.js index 6e75aa1584e30..16582955916ed 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -56,7 +56,10 @@ var handleImportFilePicker = function() { if ( typeof userData.netWhitelist !== 'string' ) { throw 'Invalid'; } - if ( typeof userData.filterLists !== 'object' ) { + if ( + typeof userData.filterLists !== 'object' && + Array.isArray(userData.selectedFilterLists) === false + ) { throw 'Invalid'; } } diff --git a/src/js/start.js b/src/js/start.js index 12a583db48137..8c37217f9b439 100644 --- a/src/js/start.js +++ b/src/js/start.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2014-2016 Raymond Hill + Copyright (C) 2014-2017 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 @@ -39,7 +39,7 @@ var µb = µBlock; vAPI.app.onShutdown = function() { µb.staticFilteringReverseLookup.shutdown(); - µb.assetUpdater.shutdown(); + µb.assets.updateStop(); µb.staticNetFilteringEngine.reset(); µb.cosmeticFilteringEngine.reset(); µb.sessionFirewall.reset(); @@ -58,14 +58,8 @@ vAPI.app.onShutdown = function() { var onAllReady = function() { // https://github.com/chrisaljoudi/uBlock/issues/184 // Check for updates not too far in the future. - µb.assetUpdater.onStart.addEventListener(µb.updateStartHandler.bind(µb)); - µb.assetUpdater.onCompleted.addEventListener(µb.updateCompleteHandler.bind(µb)); - µb.assetUpdater.onAssetUpdated.addEventListener(µb.assetUpdatedHandler.bind(µb)); - µb.assets.onAssetRemoved.addListener(µb.assetCacheRemovedHandler.bind(µb)); - - // Important: remove barrier to remote fetching, this was useful only - // for launch time. - µb.assets.remoteFetchBarrier -= 1; + µb.assets.addObserver(µb.assetObserver.bind(µb)); + µb.scheduleAssetUpdater(µb.userSettings.autoUpdate ? 7 * 60 * 1000 : 0); // vAPI.cloud is optional. if ( µb.cloudStorageSupported ) { @@ -129,7 +123,7 @@ var onSelfieReady = function(selfie) { return false; } - µb.remoteBlacklists = selfie.filterLists; + µb.availableFilterLists = selfie.availableFilterLists; µb.staticNetFilteringEngine.fromSelfie(selfie.staticNetFilteringEngine); µb.redirectEngine.fromSelfie(selfie.redirectEngine); µb.cosmeticFilteringEngine.fromSelfie(selfie.cosmeticFilteringEngine); @@ -157,12 +151,6 @@ var onUserSettingsReady = function(fetched) { fromFetch(userSettings, fetched); - // https://github.com/chrisaljoudi/uBlock/issues/426 - // Important: block remote fetching for when loading assets at launch - // time. - µb.assets.autoUpdate = userSettings.autoUpdate; - µb.assets.autoUpdateDelay = µb.updateAssetsEvery; - if ( µb.privacySettingsSupported ) { vAPI.browserSettings.set({ 'hyperlinkAuditing': !userSettings.hyperlinkAuditingDisabled, @@ -192,7 +180,7 @@ var onUserSettingsReady = function(fetched) { var onSystemSettingsReady = function(fetched) { var mustSaveSystemSettings = false; if ( fetched.compiledMagic !== µb.systemSettings.compiledMagic ) { - µb.assets.purge(/^cache:\/\/compiled-/); + µb.assets.remove(/^compiled\//); mustSaveSystemSettings = true; } if ( fetched.selfieMagic !== µb.systemSettings.selfieMagic ) { @@ -253,10 +241,7 @@ var fromFetch = function(to, fetched) { /******************************************************************************/ -var onAdminSettingsRestored = function() { - // Forbid remote fetching of assets - µb.assets.remoteFetchBarrier += 1; - +var onSelectedFilterListsLoaded = function() { var fetchableProps = { 'compiledMagic': '', 'dynamicFilteringString': 'behind-the-scene * 3p noop\nbehind-the-scene * 3p-frame noop', @@ -281,6 +266,18 @@ var onAdminSettingsRestored = function() { /******************************************************************************/ +// TODO(seamless migration): +// Eventually selected filter list keys will be loaded as a fetchable +// property. Until then we need to handle backward and forward +// compatibility, this means a special asynchronous call to load selected +// filter lists. + +var onAdminSettingsRestored = function() { + µb.loadSelectedFilterLists(onSelectedFilterListsLoaded); +}; + +/******************************************************************************/ + µb.hiddenSettings = (function() { var out = objectAssign({}, µb.hiddenSettingsDefault), json = vAPI.localStorage.getItem('hiddenSettings'); diff --git a/src/js/storage.js b/src/js/storage.js index db079f5ac1b8f..bfba9d2ea560c 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2014-2016 Raymond Hill + Copyright (C) 2014-2017 Raymond Hill This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,7 +19,7 @@ Home: https://github.com/gorhill/uBlock */ -/* global YaMD5, objectAssign, punycode, publicSuffixList */ +/* global objectAssign, punycode, publicSuffixList */ 'use strict'; @@ -104,6 +104,12 @@ case 'string': out[name] = value; break; + case 'number': + out[name] = parseInt(value, 10); + if ( isNaN(out[name]) ) { + out[name] = this.hiddenSettingsDefault[name]; + } + break; default: break; } @@ -151,54 +157,225 @@ this.netWhitelistModifyTime = Date.now(); }; -/******************************************************************************/ +/******************************************************************************* + + TODO(seamless migration): + The code related to 'remoteBlacklist' can be removed when I am confident + all users have moved to a version of uBO which no longer depends on + the property 'remoteBlacklists, i.e. v1.11 and beyond. -// This will remove all unused filter list entries from -// µBlock.remoteBlacklists`. This helps reduce the size of backup files. +**/ -µBlock.extractSelectedFilterLists = function(callback) { +µBlock.loadSelectedFilterLists = function(callback) { var µb = this; + vAPI.storage.get([ 'selectedFilterLists', 'remoteBlacklists' ], function(bin) { + if ( !bin || !bin.selectedFilterLists && !bin.remoteBlacklists ) { + // Select default filter lists if first-time launch. + µb.assets.metadata(function(availableLists) { + µb.saveSelectedFilterLists(µb.autoSelectRegionalFilterLists(availableLists)); + callback(); + }); + return; + } + var listKeys = []; + if ( bin.selectedFilterLists ) { + listKeys = bin.selectedFilterLists; + } + if ( bin.remoteBlacklists ) { + var oldListKeys = µb.newListKeysFromOldData(bin.remoteBlacklists); + if ( oldListKeys.sort().join() !== listKeys.sort().join() ) { + listKeys = oldListKeys; + µb.saveSelectedFilterLists(listKeys); + } + // TODO(seamless migration): + // Uncomment when all have moved to v1.11 and beyond. + //vAPI.storage.remove('remoteBlacklists'); + } + µb.selectedFilterLists = listKeys; + callback(); + }); +}; + +µBlock.saveSelectedFilterLists = function(newKeys, append, callback) { + if ( typeof append === 'function' ) { + callback = append; + append = false; + } + var oldKeys = this.selectedFilterLists.slice(); + if ( append ) { + newKeys = newKeys.concat(oldKeys); + } + var newSet = new Set(newKeys); + // Purge unused filter lists from cache. + for ( var i = 0, n = oldKeys.length; i < n; i++ ) { + if ( newSet.has(oldKeys[i]) === false ) { + this.removeFilterList(oldKeys[i]); + } + } + newKeys = this.setToArray(newSet); + var bin = { + selectedFilterLists: newKeys, + remoteBlacklists: this.oldDataFromNewListKeys(newKeys) + }; + this.selectedFilterLists = newKeys; + vAPI.storage.set(bin, callback); +}; - var onBuiltinListsLoaded = function(details) { - var builtin; - try { - builtin = JSON.parse(details.content); - } catch (e) { - builtin = {}; +// TODO(seamless migration): +// Remove when all have moved to v1.11 and beyond. +// >>>>>>>> +µBlock.newListKeysFromOldData = function(oldLists) { + var aliases = this.assets.listKeyAliases, + listKeys = [], newKey; + for ( var oldKey in oldLists ) { + if ( oldLists[oldKey].off !== true ) { + newKey = aliases[oldKey]; + listKeys.push(newKey ? newKey : oldKey); } + } + return listKeys; +}; - var result = JSON.parse(JSON.stringify(µb.remoteBlacklists)); - var entry, builtinPath, defaultState; +µBlock.oldDataFromNewListKeys = function(selectedFilterLists) { + var µb = this, + remoteBlacklists = {}; + var reverseAliases = Object.keys(this.assets.listKeyAliases).reduce( + function(a, b) { + a[µb.assets.listKeyAliases[b]] = b; return a; + }, + {} + ); + remoteBlacklists = selectedFilterLists.reduce( + function(a, b) { + a[reverseAliases[b] || b] = { off: false }; + return a; + }, + {} + ); + remoteBlacklists = Object.keys(µb.assets.listKeyAliases).reduce( + function(a, b) { + var aliases = µb.assets.listKeyAliases; + if ( + b.startsWith('assets/') && + aliases[b] !== 'public_suffix_list.dat' && + aliases[b] !== 'ublock-resources' && + !a[b] + ) { + a[b] = { off: true }; + } + return a; + }, + remoteBlacklists + ); + return remoteBlacklists; +}; +// <<<<<<<< - for ( var path in result ) { - if ( result.hasOwnProperty(path) === false ) { - continue; +/******************************************************************************/ + +µBlock.applyFilterListSelection = function(details, callback) { + var µb = this, + selectedListKeySet = new Set(this.selectedFilterLists), + externalLists = this.userSettings.externalLists, + i, n, assetKey; + + // Filter lists to select + if ( Array.isArray(details.toSelect) ) { + if ( details.merge ) { + for ( i = 0, n = details.toSelect.length; i < n; i++ ) { + selectedListKeySet.add(details.toSelect[i]); } - entry = result[path]; - // https://github.com/gorhill/uBlock/issues/277 - // uBlock's filter lists are always enabled by default, so we - // have to include in backup only those which are turned off. - if ( path.startsWith('assets/ublock/') ) { - if ( entry.off !== true ) { - delete result[path]; + } else { + selectedListKeySet = new Set(details.toSelect); + } + } + + // Imported filter lists to remove + if ( Array.isArray(details.toRemove) ) { + var removeURLFromHaystack = function(haystack, needle) { + return haystack.replace( + new RegExp( + '(^|\\n)' + + needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + + '(\\n|$)', 'g'), + '\n' + ).trim(); + }; + for ( i = 0, n = details.toRemove.length; i < n; i++ ) { + assetKey = details.toRemove[i]; + selectedListKeySet.delete(assetKey); + externalLists = removeURLFromHaystack(externalLists, assetKey); + this.removeFilterList(assetKey); + } + } + + // Filter lists to import + if ( typeof details.toImport === 'string' ) { + // https://github.com/gorhill/uBlock/issues/1181 + // Try mapping the URL of an imported filter list to the assetKey of an + // existing stock list. + var assetKeyFromURL = function(url) { + var needle = url.replace(/^https?:/, ''); + var assets = µb.availableFilterLists, asset; + for ( var assetKey in assets ) { + asset = assets[assetKey]; + if ( asset.content !== 'filters' ) { continue; } + if ( typeof asset.contentURL === 'string' ) { + if ( asset.contentURL.endsWith(needle) ) { return assetKey; } + continue; + } + if ( Array.isArray(asset.contentURL) === false ) { continue; } + for ( i = 0, n = asset.contentURL.length; i < n; i++ ) { + if ( asset.contentURL[i].endsWith(needle) ) { + return assetKey; + } } - continue; } - builtinPath = path.replace(/^assets\/thirdparties\//, ''); - defaultState = builtin.hasOwnProperty(builtinPath) === false || - builtin[builtinPath].off === true; - if ( entry.off === true && entry.off === defaultState ) { - delete result[path]; + return url; + }; + var importedSet = new Set(this.listKeysFromCustomFilterLists(externalLists)), + toImportSet = new Set(this.listKeysFromCustomFilterLists(details.toImport)), + iter = toImportSet.values(); + for (;;) { + var entry = iter.next(); + if ( entry.done ) { break; } + if ( importedSet.has(entry.value) ) { continue; } + assetKey = assetKeyFromURL(entry.value); + if ( assetKey === entry.value ) { + importedSet.add(entry.value); } + selectedListKeySet.add(assetKey); } + externalLists = this.setToArray(importedSet).sort().join('\n'); + } + var result = this.setToArray(selectedListKeySet); + if ( externalLists !== this.userSettings.externalLists ) { + this.userSettings.externalLists = externalLists; + vAPI.storage.set({ externalLists: externalLists }); + } + this.saveSelectedFilterLists(result); + if ( typeof callback === 'function' ) { callback(result); - }; + } +}; - // https://github.com/gorhill/uBlock/issues/63 - // Get built-in block lists: this will help us determine whether a - // specific list must be included in the result. - this.loadAndPatchStockFilterLists(onBuiltinListsLoaded); +/******************************************************************************/ + +µBlock.listKeysFromCustomFilterLists = function(raw) { + var out = new Set(), + reIgnore = /^[!#]/, + reValid = /^[a-z-]+:\/\/\S+/, + lineIter = new this.LineIterator(raw), + location; + while ( lineIter.eot() === false ) { + location = lineIter.next().trim(); + if ( reIgnore.test(location) || !reValid.test(location) ) { + continue; + } + out.add(location); + } + return this.setToArray(out); }; /******************************************************************************/ @@ -207,14 +384,11 @@ // https://github.com/gorhill/uBlock/issues/1022 // Be sure to end with an empty line. content = content.trim(); - if ( content !== '' ) { - content += '\n'; - } + if ( content !== '' ) { content += '\n'; } this.assets.put(this.userFiltersPath, content, callback); + this.removeCompiledFilterList(this.userFiltersPath); }; -/******************************************************************************/ - µBlock.loadUserFilters = function(callback) { return this.assets.get(this.userFiltersPath, callback); }; @@ -222,25 +396,23 @@ /******************************************************************************/ µBlock.appendUserFilters = function(filters) { - if ( filters.length === 0 ) { - return; - } + if ( filters.length === 0 ) { return; } var µb = this; var onSaved = function() { - var compiledFilters = µb.compileFilters(filters); - var snfe = µb.staticNetFilteringEngine; - var cfe = µb.cosmeticFilteringEngine; - var acceptedCount = snfe.acceptedCount + cfe.acceptedCount; - var discardedCount = snfe.discardedCount + cfe.discardedCount; + var compiledFilters = µb.compileFilters(filters), + snfe = µb.staticNetFilteringEngine, + cfe = µb.cosmeticFilteringEngine, + acceptedCount = snfe.acceptedCount + cfe.acceptedCount, + discardedCount = snfe.discardedCount + cfe.discardedCount; µb.applyCompiledFilters(compiledFilters, true); - var entry = µb.remoteBlacklists[µb.userFiltersPath]; - var deltaEntryCount = snfe.acceptedCount + cfe.acceptedCount - acceptedCount; - var deltaEntryUsedCount = deltaEntryCount - (snfe.discardedCount + cfe.discardedCount - discardedCount); + var entry = µb.availableFilterLists[µb.userFiltersPath], + deltaEntryCount = snfe.acceptedCount + cfe.acceptedCount - acceptedCount, + deltaEntryUsedCount = deltaEntryCount - (snfe.discardedCount + cfe.discardedCount - discardedCount); entry.entryCount += deltaEntryCount; entry.entryUsedCount += deltaEntryUsedCount; - vAPI.storage.set({ 'remoteBlacklists': µb.remoteBlacklists }); + vAPI.storage.set({ 'availableFilterLists': µb.availableFilterLists }); µb.staticNetFilteringEngine.freeze(); µb.redirectEngine.freeze(); µb.cosmeticFilteringEngine.freeze(); @@ -248,9 +420,7 @@ }; var onLoaded = function(details) { - if ( details.error ) { - return; - } + if ( details.error ) { return; } // https://github.com/chrisaljoudi/uBlock/issues/976 // If we reached this point, the filter quite probably needs to be // added for sure: do not try to be too smart, trying to avoid @@ -263,166 +433,132 @@ /******************************************************************************/ -µBlock.getAvailableLists = function(callback) { - var availableLists = {}; - var relocationMap = {}; - - var fixLocation = function(location) { - // https://github.com/chrisaljoudi/uBlock/issues/418 - // We now support built-in external filter lists - if ( /^https?:/.test(location) === false ) { - location = 'assets/thirdparties/' + location; +µBlock.autoSelectRegionalFilterLists = function(lists) { + var selectedListKeys = [ this.userFiltersPath ], + list; + for ( var key in lists ) { + if ( lists.hasOwnProperty(key) === false ) { continue; } + list = lists[key]; + if ( list.off !== true ) { + selectedListKeys.push(key); + continue; + } + if ( this.matchCurrentLanguage(list.lang) ) { + selectedListKeys.push(key); + list.off = false; } - return location; + } + return selectedListKeys; +}; + +/******************************************************************************/ + +µBlock.getAvailableLists = function(callback) { + var µb = this, + oldAvailableLists = {}, + newAvailableLists = {}; + + // User filter list. + newAvailableLists[this.userFiltersPath] = { + group: 'default', + title: vAPI.i18n('1pPageName') }; - // selected lists - var onSelectedListsLoaded = function(store) { - var µb = µBlock; - var lists = store.remoteBlacklists; - var locations = Object.keys(lists); - var location, availableEntry, storedEntry; - var off; - - while ( (location = locations.pop()) ) { - storedEntry = lists[location]; - off = storedEntry.off === true; - // New location? - if ( relocationMap.hasOwnProperty(location) ) { - µb.purgeFilterList(location); - location = relocationMap[location]; - if ( off && lists.hasOwnProperty(location) ) { - off = lists[location].off === true; - } - } - availableEntry = availableLists[location]; - if ( availableEntry === undefined ) { - µb.purgeFilterList(location); + // Custom filter lists. + var importedListKeys = this.listKeysFromCustomFilterLists(µb.userSettings.externalLists), + i = importedListKeys.length, listKey, entry; + while ( i-- ) { + listKey = importedListKeys[i]; + entry = { + content: 'filters', + contentURL: importedListKeys[i], + external: true, + group: 'custom', + submitter: 'user', + title: '' + }; + newAvailableLists[listKey] = entry; + this.assets.registerAssetSource(listKey, entry); + } + + // Final steps: + // - reuse existing list metadata if any; + // - unregister unreferenced imported filter lists if any. + var finalize = function() { + var assetKey, newEntry, oldEntry; + + // Reuse existing metadata. + for ( assetKey in oldAvailableLists ) { + oldEntry = oldAvailableLists[assetKey]; + newEntry = newAvailableLists[assetKey]; + if ( newEntry === undefined ) { + µb.removeFilterList(assetKey); continue; } - availableEntry.off = off; - if ( typeof availableEntry.homeURL === 'string' ) { - µb.assets.setHomeURL(location, availableEntry.homeURL); + if ( oldEntry.entryCount !== undefined ) { + newEntry.entryCount = oldEntry.entryCount; } - if ( storedEntry.entryCount !== undefined ) { - availableEntry.entryCount = storedEntry.entryCount; - } - if ( storedEntry.entryUsedCount !== undefined ) { - availableEntry.entryUsedCount = storedEntry.entryUsedCount; + if ( oldEntry.entryUsedCount !== undefined ) { + newEntry.entryUsedCount = oldEntry.entryUsedCount; } // This may happen if the list name was pulled from the list // content. // https://github.com/chrisaljoudi/uBlock/issues/982 // There is no guarantee the title was successfully extracted from // the list content. - if ( availableEntry.title === '' && - typeof storedEntry.title === 'string' && - storedEntry.title !== '' + if ( + newEntry.title === '' && + typeof oldEntry.title === 'string' && + oldEntry.title !== '' ) { - availableEntry.title = storedEntry.title; + newEntry.title = oldEntry.title; } } - // https://github.com/gorhill/uBlock/issues/747 - if ( µb.firstInstall ) { - µb.autoSelectFilterLists(availableLists); + // Remove unreferenced imported filter lists. + var dict = new Set(importedListKeys); + for ( assetKey in newAvailableLists ) { + newEntry = newAvailableLists[assetKey]; + if ( newEntry.submitter !== 'user' ) { continue; } + if ( dict.has(assetKey) ) { continue; } + delete newAvailableLists[assetKey]; + µb.assets.unregisterAssetSource(assetKey); + µb.removeFilterList(assetKey); } - - callback(availableLists); }; - // built-in lists - var onBuiltinListsLoaded = function(details) { - var location, locations; - try { - locations = JSON.parse(details.content); - } catch (e) { - locations = {}; + // Built-in filter lists loaded. + var onBuiltinListsLoaded = function(entries) { + for ( var assetKey in entries ) { + if ( entries.hasOwnProperty(assetKey) === false ) { continue; } + entry = entries[assetKey]; + if ( entry.content !== 'filters' ) { continue; } + newAvailableLists[assetKey] = objectAssign({}, entry); } - var entry; - for ( location in locations ) { - if ( locations.hasOwnProperty(location) === false ) { - continue; - } - entry = locations[location]; - location = fixLocation(location); - // Migrate obsolete location to new location, if any - if ( typeof entry.oldLocation === 'string' ) { - entry.oldLocation = fixLocation(entry.oldLocation); - relocationMap[entry.oldLocation] = location; + + // Load set of currently selected filter lists. + var listKeySet = new Set(µb.selectedFilterLists); + for ( listKey in newAvailableLists ) { + if ( newAvailableLists.hasOwnProperty(listKey) ) { + newAvailableLists[listKey].off = !listKeySet.has(listKey); } - availableLists[location] = entry; } - // Now get user's selection of lists - vAPI.storage.get( - { 'remoteBlacklists': availableLists }, - onSelectedListsLoaded - ); + finalize(); + callback(newAvailableLists); }; - // permanent lists - var location; - var lists = this.permanentLists; - for ( location in lists ) { - if ( lists.hasOwnProperty(location) === false ) { - continue; - } - availableLists[location] = lists[location]; - } - - // custom lists - var c; - var locations = this.userSettings.externalLists.split('\n'); - for ( var i = 0; i < locations.length; i++ ) { - location = locations[i].trim(); - c = location.charAt(0); - if ( location === '' || c === '!' || c === '#' ) { - continue; - } - // Coarse validation - if ( /[^0-9A-Za-z!*'();:@&=+$,\/?%#\[\]_.~-]/.test(location) ) { - continue; - } - availableLists[location] = { - title: '', - group: 'custom', - external: true - }; - } - - // get built-in block lists. - this.loadAndPatchStockFilterLists(onBuiltinListsLoaded); -}; - -/******************************************************************************/ - -µBlock.autoSelectFilterLists = function(lists) { - var lang = self.navigator.language.slice(0, 2), - list; - for ( var path in lists ) { - if ( lists.hasOwnProperty(path) === false ) { - continue; - } - list = lists[path]; - if ( list.off !== true ) { - continue; - } - if ( list.lang === lang ) { - list.off = false; - } - } -}; - -/******************************************************************************/ + // Available lists previously computed. + var onOldAvailableListsLoaded = function(bin) { + oldAvailableLists = bin && bin.availableFilterLists || {}; + µb.assets.metadata(onBuiltinListsLoaded); + }; -µBlock.createShortUniqueId = function(path) { - var md5 = YaMD5.hashStr(path); - return md5.slice(0, 4) + md5.slice(-4); + // Load previously saved available lists -- these contains data + // computed at run-time, we will reuse this data if possible. + vAPI.storage.get('availableFilterLists', onOldAvailableListsLoaded); }; -µBlock.createShortUniqueId.idLength = 8; - /******************************************************************************/ // This is used to be re-entrancy resistant. @@ -437,29 +573,28 @@ //quickProfiler.start('µBlock.loadFilterLists()'); - var µb = this; - var filterlistsCount = 0; + var µb = this, + filterlistsCount = 0, + loadedListKeys = []; if ( typeof callback !== 'function' ) { callback = this.noopFunc; } - // Never fetch from remote servers when we load filter lists: this has to - // be as fast as possible. - µb.assets.remoteFetchBarrier += 1; - var onDone = function() { - // Remove barrier to remote fetching - µb.assets.remoteFetchBarrier -= 1; - µb.staticNetFilteringEngine.freeze(); µb.cosmeticFilteringEngine.freeze(); µb.redirectEngine.freeze(); - vAPI.storage.set({ 'remoteBlacklists': µb.remoteBlacklists }); + vAPI.storage.set({ 'availableFilterLists': µb.availableFilterLists }); //quickProfiler.stop(0); - vAPI.messaging.broadcast({ what: 'staticFilteringDataChanged' }); + vAPI.messaging.broadcast({ + what: 'staticFilteringDataChanged', + parseCosmeticFilters: µb.userSettings.parseAllABPHideFilters, + ignoreGenericCosmeticFilters: µb.userSettings.ignoreGenericCosmeticFilters, + listKeys: loadedListKeys + }); callback(); @@ -467,21 +602,22 @@ µb.loadingFilterLists = false; }; - var applyCompiledFilters = function(path, compiled) { - var snfe = µb.staticNetFilteringEngine; - var cfe = µb.cosmeticFilteringEngine; - var acceptedCount = snfe.acceptedCount + cfe.acceptedCount; - var discardedCount = snfe.discardedCount + cfe.discardedCount; - µb.applyCompiledFilters(compiled, path === µb.userFiltersPath); - if ( µb.remoteBlacklists.hasOwnProperty(path) ) { - var entry = µb.remoteBlacklists[path]; + var applyCompiledFilters = function(assetKey, compiled) { + var snfe = µb.staticNetFilteringEngine, + cfe = µb.cosmeticFilteringEngine, + acceptedCount = snfe.acceptedCount + cfe.acceptedCount, + discardedCount = snfe.discardedCount + cfe.discardedCount; + µb.applyCompiledFilters(compiled, assetKey === µb.userFiltersPath); + if ( µb.availableFilterLists.hasOwnProperty(assetKey) ) { + var entry = µb.availableFilterLists[assetKey]; entry.entryCount = snfe.acceptedCount + cfe.acceptedCount - acceptedCount; entry.entryUsedCount = entry.entryCount - (snfe.discardedCount + cfe.discardedCount - discardedCount); } + loadedListKeys.push(assetKey); }; var onCompiledListLoaded = function(details) { - applyCompiledFilters(details.path, details.content); + applyCompiledFilters(details.assetKey, details.content); filterlistsCount -= 1; if ( filterlistsCount === 0 ) { onDone(); @@ -489,7 +625,7 @@ }; var onFilterListsReady = function(lists) { - µb.remoteBlacklists = lists; + µb.availableFilterLists = lists; µb.redirectEngine.reset(); µb.cosmeticFilteringEngine.reset(); @@ -502,14 +638,10 @@ // This happens for assets which do not exist, ot assets with no // content. var toLoad = []; - for ( var path in lists ) { - if ( lists.hasOwnProperty(path) === false ) { - continue; - } - if ( lists[path].off ) { - continue; - } - toLoad.push(path); + for ( var assetKey in lists ) { + if ( lists.hasOwnProperty(assetKey) === false ) { continue; } + if ( lists[assetKey].off ) { continue; } + toLoad.push(assetKey); } filterlistsCount = toLoad.length; if ( filterlistsCount === 0 ) { @@ -528,32 +660,17 @@ /******************************************************************************/ -µBlock.getCompiledFilterListPath = function(path) { - return 'cache://compiled-filter-list:' + this.createShortUniqueId(path); -}; - -/******************************************************************************/ - -µBlock.getCompiledFilterList = function(path, callback) { - var compiledPath = this.getCompiledFilterListPath(path); - var µb = this; +µBlock.getCompiledFilterList = function(assetKey, callback) { + var µb = this, + compiledPath = 'compiled/' + assetKey; var onRawListLoaded = function(details) { + details.assetKey = assetKey; if ( details.content === '' ) { callback(details); return; } - var listMeta = µb.remoteBlacklists[path]; - // https://github.com/gorhill/uBlock/issues/313 - // Always try to fetch the name if this is an external filter list. - if ( listMeta && (listMeta.title === '' || listMeta.group === 'custom') ) { - var matches = details.content.slice(0, 1024).match(/(?:^|\n)!\s*Title:([^\n]+)/i); - if ( matches !== null ) { - listMeta.title = matches[1].trim(); - } - } - - //console.debug('µBlock.getCompiledFilterList/onRawListLoaded: compiling "%s"', path); + µb.extractFilterListMetadata(assetKey, details.content); details.content = µb.compileFilters(details.content); µb.assets.put(compiledPath, details.content); callback(details); @@ -561,12 +678,10 @@ var onCompiledListLoaded = function(details) { if ( details.content === '' ) { - //console.debug('µBlock.getCompiledFilterList/onCompiledListLoaded: no compiled version for "%s"', path); - µb.assets.get(path, onRawListLoaded); + µb.assets.get(assetKey, onRawListLoaded); return; } - //console.debug('µBlock.getCompiledFilterList/onCompiledListLoaded: using compiled version for "%s"', path); - details.path = path; + details.assetKey = assetKey; callback(details); }; @@ -575,61 +690,70 @@ /******************************************************************************/ -µBlock.purgeCompiledFilterList = function(path) { - this.assets.purge(this.getCompiledFilterListPath(path)); +µBlock.extractFilterListMetadata = function(assetKey, raw) { + var listEntry = this.availableFilterLists[assetKey]; + if ( listEntry === undefined ) { return; } + // Metadata expected to be found at the top of content. + var head = raw.slice(0, 1024), + matches, v; + // https://github.com/gorhill/uBlock/issues/313 + // Always try to fetch the name if this is an external filter list. + if ( listEntry.title === '' || listEntry.group === 'custom' ) { + matches = head.match(/(?:^|\n)!\s*Title:([^\n]+)/i); + if ( matches !== null ) { + listEntry.title = matches[1].trim(); + } + } + // Extract update frequency information + matches = head.match(/(?:^|\n)![\t ]*Expires:[\t ]*([\d]+)[\t ]*days?/i); + if ( matches !== null ) { + v = Math.max(parseInt(matches[1], 10), 2); + if ( v !== listEntry.updateAfter ) { + this.assets.registerAssetSource(assetKey, { updateAfter: v }); + } + } }; /******************************************************************************/ -µBlock.purgeFilterList = function(path) { - this.purgeCompiledFilterList(path); - this.assets.purge(path); +µBlock.removeCompiledFilterList = function(assetKey) { + this.assets.remove('compiled/' + assetKey); +}; + +µBlock.removeFilterList = function(assetKey) { + this.removeCompiledFilterList(assetKey); + this.assets.remove(assetKey); }; /******************************************************************************/ µBlock.compileFilters = function(rawText) { - var rawEnd = rawText.length; var compiledFilters = []; // Useful references: // https://adblockplus.org/en/filter-cheatsheet // https://adblockplus.org/en/filters - var staticNetFilteringEngine = this.staticNetFilteringEngine; - var cosmeticFilteringEngine = this.cosmeticFilteringEngine; - var reIsWhitespaceChar = /\s/; - var reMaybeLocalIp = /^[\d:f]/; - var reIsLocalhostRedirect = /\s+(?:broadcasthost|local|localhost|localhost\.localdomain)(?=\s|$)/; - var reLocalIp = /^(?:0\.0\.0\.0|127\.0\.0\.1|::1|fe80::1%lo0)/; - - var lineBeg = 0, lineEnd, currentLineBeg; - var line, lineRaw, c, pos; - - while ( lineBeg < rawEnd ) { - lineEnd = rawText.indexOf('\n', lineBeg); - if ( lineEnd === -1 ) { - lineEnd = rawText.indexOf('\r', lineBeg); - if ( lineEnd === -1 ) { - lineEnd = rawEnd; - } - } + var staticNetFilteringEngine = this.staticNetFilteringEngine, + cosmeticFilteringEngine = this.cosmeticFilteringEngine, + reIsWhitespaceChar = /\s/, + reMaybeLocalIp = /^[\d:f]/, + reIsLocalhostRedirect = /\s+(?:broadcasthost|local|localhost|localhost\.localdomain)(?=\s|$)/, + reLocalIp = /^(?:0\.0\.0\.0|127\.0\.0\.1|::1|fe80::1%lo0)/, + line, lineRaw, c, pos, + lineIter = new this.LineIterator(rawText); + + while ( lineIter.eot() === false ) { + line = lineRaw = lineIter.next().trim(); // rhill 2014-04-18: The trim is important here, as without it there // could be a lingering `\r` which would cause problems in the // following parsing code. - line = lineRaw = rawText.slice(lineBeg, lineEnd).trim(); - currentLineBeg = lineBeg; - lineBeg = lineEnd + 1; - if ( line.length === 0 ) { - continue; - } + if ( line.length === 0 ) { continue; } // Strip comments c = line.charAt(0); - if ( c === '!' || c === '[' ) { - continue; - } + if ( c === '!' || c === '[' ) { continue; } // Parse or skip cosmetic filters // All cosmetic filters are caught here @@ -640,9 +764,7 @@ // Whatever else is next can be assumed to not be a cosmetic filter // Most comments start in first column - if ( c === '#' ) { - continue; - } + if ( c === '#' ) { continue; } // Catch comments somewhere on the line // Remove: @@ -663,15 +785,11 @@ // Ignore hosts file redirect configuration // 127.0.0.1 localhost // 255.255.255.255 broadcasthost - if ( reIsLocalhostRedirect.test(line) ) { - continue; - } + if ( reIsLocalhostRedirect.test(line) ) { continue; } line = line.replace(reLocalIp, '').trim(); } - if ( line.length === 0 ) { - continue; - } + if ( line.length === 0 ) { continue; } staticNetFilteringEngine.compile(line, compiledFilters); } @@ -699,55 +817,6 @@ /******************************************************************************/ -// `switches` contains the filter lists for which the switch must be revisited. - -µBlock.selectFilterLists = function(switches) { - switches = switches || {}; - - // Only the lists referenced by the switches are touched. - var filterLists = this.remoteBlacklists; - var entry, state, location; - var i = switches.length; - while ( i-- ) { - entry = switches[i]; - state = entry.off === true; - location = entry.location; - if ( filterLists.hasOwnProperty(location) === false ) { - if ( state !== true ) { - filterLists[location] = { off: state }; - } - continue; - } - if ( filterLists[location].off === state ) { - continue; - } - filterLists[location].off = state; - } - - vAPI.storage.set({ 'remoteBlacklists': filterLists }); -}; - -/******************************************************************************/ - -// Plain reload of all filters. - -µBlock.reloadAllFilters = function() { - var µb = this; - - // We are just reloading the filter lists: we do not want assets to update. - // TODO: probably not needed anymore, since filter lists are now always - // loaded without update => see `µb.assets.remoteFetchBarrier`. - this.assets.autoUpdate = false; - - var onFiltersReady = function() { - µb.assets.autoUpdate = µb.userSettings.autoUpdate; - }; - - this.loadFilterLists(onFiltersReady); -}; - -/******************************************************************************/ - µBlock.loadRedirectResources = function(callback) { var µb = this; @@ -762,40 +831,46 @@ callback(); }; - this.assets.get('assets/ublock/resources.txt', onResourcesLoaded); + this.assets.get('ublock-resources', onResourcesLoaded); }; /******************************************************************************/ µBlock.loadPublicSuffixList = function(callback) { - var µb = this; - var path = µb.pslPath; - var compiledPath = 'cache://compiled-publicsuffixlist'; + var µb = this, + assetKey = µb.pslAssetKey, + compiledAssetKey = 'compiled/' + assetKey; if ( typeof callback !== 'function' ) { callback = this.noopFunc; } var onRawListLoaded = function(details) { if ( details.content !== '' ) { - //console.debug('µBlock.loadPublicSuffixList/onRawListLoaded: compiling "%s"', path); - publicSuffixList.parse(details.content, punycode.toASCII); - µb.assets.put(compiledPath, JSON.stringify(publicSuffixList.toSelfie())); + µb.compilePublicSuffixList(details.content); } callback(); }; var onCompiledListLoaded = function(details) { if ( details.content === '' ) { - //console.debug('µBlock.loadPublicSuffixList/onCompiledListLoaded: no compiled version for "%s"', path); - µb.assets.get(path, onRawListLoaded); + µb.assets.get(assetKey, onRawListLoaded); return; } - //console.debug('µBlock.loadPublicSuffixList/onCompiledListLoaded: using compiled version for "%s"', path); publicSuffixList.fromSelfie(JSON.parse(details.content)); callback(); }; - this.assets.get(compiledPath, onCompiledListLoaded); + this.assets.get(compiledAssetKey, onCompiledListLoaded); +}; + +/******************************************************************************/ + +µBlock.compilePublicSuffixList = function(content) { + publicSuffixList.parse(content, punycode.toASCII); + this.assets.put( + 'compiled/' + this.pslAssetKey, + JSON.stringify(publicSuffixList.toSelfie()) + ); }; /******************************************************************************/ @@ -814,7 +889,7 @@ var selfie = { magic: µb.systemSettings.selfieMagic, publicSuffixList: publicSuffixList.toSelfie(), - filterLists: µb.remoteBlacklists, + availableFilterLists: µb.availableFilterLists, staticNetFilteringEngine: µb.staticNetFilteringEngine.toSelfie(), redirectEngine: µb.redirectEngine.toSelfie(), cosmeticFilteringEngine: µb.cosmeticFilteringEngine.toSelfie() @@ -885,6 +960,13 @@ var bin = {}; var binNotEmpty = false; + // Allows an admin to set their own 'assets.json' file, with their own + // set of stock assets. + if ( typeof data.assetsBootstrapLocation === 'string' ) { + bin.assetsBootstrapLocation = data.assetsBootstrapLocation; + binNotEmpty = true; + } + if ( typeof data.userSettings === 'object' ) { for ( var name in µb.userSettings ) { if ( µb.userSettings.hasOwnProperty(name) === false ) { @@ -898,8 +980,13 @@ } } - if ( typeof data.filterLists === 'object' ) { - bin.remoteBlacklists = data.filterLists; + // 'selectedFilterLists' is an array of filter list tokens. Each token + // is a reference to an asset in 'assets.json'. + if ( Array.isArray(data.selectedFilterLists) ) { + bin.selectedFilterLists = data.selectedFilterLists; + binNotEmpty = true; + } else if ( typeof data.filterLists === 'object' ) { + bin.selectedFilterLists = µb.newListKeysFromOldData(data.filterLists); binNotEmpty = true; } @@ -939,203 +1026,123 @@ /******************************************************************************/ -µBlock.updateStartHandler = function(callback) { - var µb = this; - var onListsReady = function(lists) { - var assets = {}; - for ( var location in lists ) { - if ( lists.hasOwnProperty(location) === false ) { - continue; - } - if ( lists[location].off ) { - continue; - } - assets[location] = true; +µBlock.scheduleAssetUpdater = (function() { + var timer, next = 0; + return function(updateDelay) { + if ( timer ) { + clearTimeout(timer); + timer = undefined; } - assets[µb.pslPath] = true; - assets['assets/ublock/resources.txt'] = true; - callback(assets); + if ( updateDelay === 0 ) { + next = 0; + return; + } + var now = Date.now(); + // Use the new schedule if and only if it is earlier than the previous + // one. + if ( next !== 0 ) { + updateDelay = Math.min(updateDelay, Math.max(next - now, 0)); + } + next = now + updateDelay; + timer = vAPI.setTimeout(function() { + timer = undefined; + next = 0; + var µb = µBlock; + µb.assets.updateStart({ + delay: µb.hiddenSettings.autoUpdateAssetFetchPeriod * 1000 || 120000 + }); + }, updateDelay); }; - - this.getAvailableLists(onListsReady); -}; - -/******************************************************************************/ - -µBlock.assetUpdatedHandler = function(details) { - var path = details.path || ''; - if ( this.remoteBlacklists.hasOwnProperty(path) === false ) { - return; - } - var entry = this.remoteBlacklists[path]; - if ( entry.off ) { - return; - } - // Compile the list while we have the raw version in memory - //console.debug('µBlock.getCompiledFilterList/onRawListLoaded: compiling "%s"', path); - this.assets.put( - this.getCompiledFilterListPath(path), - this.compileFilters(details.content) - ); -}; +})(); /******************************************************************************/ -µBlock.updateCompleteHandler = function(details) { - var µb = this; - var updatedCount = details.updatedCount; - - // Assets are supposed to have been all updated, prevent fetching from - // remote servers. - µb.assets.remoteFetchBarrier += 1; - - var onFiltersReady = function() { - µb.assets.remoteFetchBarrier -= 1; - }; - - var onPSLReady = function() { - if ( updatedCount !== 0 ) { - //console.debug('storage.js > µBlock.updateCompleteHandler: reloading filter lists'); - µb.loadFilterLists(onFiltersReady); - } else { - onFiltersReady(); +µBlock.assetObserver = function(topic, details) { + // Do not update filter list if not in use. + if ( topic === 'before-asset-updated' ) { + if ( + this.availableFilterLists.hasOwnProperty(details.assetKey) && + this.selectedFilterLists.indexOf(details.assetKey) === -1 + ) { + return false; } - }; - - if ( details.hasOwnProperty(this.pslPath) ) { - //console.debug('storage.js > µBlock.updateCompleteHandler: reloading PSL'); - this.loadPublicSuffixList(onPSLReady); - updatedCount -= 1; - } else { - onPSLReady(); + return; } -}; - -/******************************************************************************/ -µBlock.assetCacheRemovedHandler = (function() { - var barrier = false; - - var handler = function(paths) { - if ( barrier ) { - return; - } - barrier = true; - var i = paths.length; - var path; - while ( i-- ) { - path = paths[i]; - if ( this.remoteBlacklists.hasOwnProperty(path) ) { - //console.debug('µBlock.assetCacheRemovedHandler: decompiling "%s"', path); - this.purgeCompiledFilterList(path); - continue; + // Compile the list while we have the raw version in memory + if ( topic === 'after-asset-updated' ) { + var cached = typeof details.content === 'string' && details.content !== ''; + if ( this.availableFilterLists.hasOwnProperty(details.assetKey) ) { + if ( cached ) { + if ( this.selectedFilterLists.indexOf(details.assetKey) !== -1 ) { + this.extractFilterListMetadata( + details.assetKey, + details.content + ); + this.assets.put( + 'compiled/' + details.assetKey, + this.compileFilters(details.content) + ); + } + } else { + this.removeCompiledFilterList(details.assetKey); } - if ( path === this.pslPath ) { - //console.debug('µBlock.assetCacheRemovedHandler: decompiling "%s"', path); - this.assets.purge('cache://compiled-publicsuffixlist'); - continue; + } else if ( details.assetKey === this.pslAssetKey ) { + if ( cached ) { + this.compilePublicSuffixList(details.content); + } + } else if ( details.assetKey === 'ublock-resources' ) { + if ( cached ) { + this.redirectEngine.resourcesFromString(details.content); } } - this.selfieManager.destroy(); - barrier = false; - }; - - return handler; -})(); + vAPI.messaging.broadcast({ + what: 'assetUpdated', + key: details.assetKey, + cached: cached + + }); + return; + } -/******************************************************************************/ + // Update failed. + if ( topic === 'asset-update-failed' ) { + vAPI.messaging.broadcast({ + what: 'assetUpdated', + key: details.assetKey, + failed: true + }); + return; + } -// https://github.com/gorhill/uBlock/issues/602 -// - Load and patch `filter-list.json` -// - Load and patch user's `remoteBlacklists` -// - Load and patch cached filter lists -// - Load and patch compiled filter lists -// -// Once enough time has passed to safely assume all uBlock Origin -// installations have been converted to the new stock filter lists, this code -// can be removed. - -µBlock.patchFilterLists = function(filterLists) { - var modified = false; - var oldListKey, newListKey, listEntry; - for ( var listKey in filterLists ) { - if ( filterLists.hasOwnProperty(listKey) === false ) { - continue; + // Reload all filter lists if needed. + if ( topic === 'after-assets-updated' ) { + if ( details.assetKeys.length !== 0 ) { + this.loadFilterLists(); } - oldListKey = listKey; - if ( this.oldListToNewListMap.hasOwnProperty(oldListKey) === false ) { - oldListKey = 'assets/thirdparties/' + listKey; - if ( this.oldListToNewListMap.hasOwnProperty(oldListKey) === false ) { - continue; - } - } - newListKey = this.oldListToNewListMap[oldListKey]; - // https://github.com/gorhill/uBlock/issues/668 - // https://github.com/gorhill/uBlock/issues/669 - // Beware: an entry for the new list key may already exists. If it is - // the case, leave it as is. - if ( newListKey !== '' && filterLists.hasOwnProperty(newListKey) === false ) { - listEntry = filterLists[listKey]; - listEntry.homeURL = undefined; - filterLists[newListKey] = listEntry; + if ( this.userSettings.autoUpdate ) { + this.scheduleAssetUpdater(this.hiddenSettings.autoUpdatePeriod * 3600000 || 25200000); + } else { + this.scheduleAssetUpdater(0); } - delete filterLists[listKey]; - modified = true; + vAPI.messaging.broadcast({ + what: 'assetsUpdated', + assetKeys: details.assetKeys + }); + return; } - return modified; -}; -µBlock.loadAndPatchStockFilterLists = function(callback) { - var onStockListsLoaded = function(details) { - var µb = µBlock; - var stockLists; - try { - stockLists = JSON.parse(details.content); - } catch (e) { - stockLists = {}; - } - - // Migrate assets affected by the change to their new name. - var reExternalURL = /^https?:\/\//; - var newListKey; - for ( var oldListKey in stockLists ) { - if ( stockLists.hasOwnProperty(oldListKey) === false ) { - continue; - } - // https://github.com/gorhill/uBlock/issues/708 - // Support migrating external stock filter lists as well. - if ( reExternalURL.test(oldListKey) === false ) { - oldListKey = 'assets/thirdparties/' + oldListKey; - } - if ( µb.oldListToNewListMap.hasOwnProperty(oldListKey) === false ) { - continue; - } - newListKey = µb.oldListToNewListMap[oldListKey]; - if ( newListKey === '' ) { - continue; + // New asset source became available, if it's a filter list, should we + // auto-select it? + if ( topic === 'builtin-asset-source-added' ) { + if ( details.entry.content === 'filters' ) { + if ( + details.entry.off !== true || + this.matchCurrentLanguage(details.entry.lang) + ) { + this.saveSelectedFilterLists([ details.assetKey ], true); } - // Rename cached asset to preserve content -- so it does not - // need to be fetched from remote server. - µb.assets.rename(oldListKey, newListKey); - µb.assets.purge(µb.getCompiledFilterListPath(oldListKey)); } - µb.patchFilterLists(stockLists); - - // Stock lists information cascades into - // - In-memory user's selected filter lists, so we need to patch this. - µb.patchFilterLists(µb.remoteBlacklists); - - // Stock lists information cascades into - // - In-storage user's selected filter lists, so we need to patch this. - vAPI.storage.get('remoteBlacklists', function(bin) { - var userLists = bin.remoteBlacklists || {}; - if ( µb.patchFilterLists(userLists) ) { - µb.keyvalSetOne('remoteBlacklists', userLists); - } - details.content = JSON.stringify(stockLists); - callback(details); - }); - }; - - this.assets.get('assets/ublock/filter-lists.json', onStockListsLoaded); + return; + } }; diff --git a/src/js/tab.js b/src/js/tab.js index 4ddbc6dda25e4..9b9976d6263ce 100644 --- a/src/js/tab.js +++ b/src/js/tab.js @@ -561,26 +561,23 @@ vAPI.tabs.onPopupUpdated = (function() { context.requestHostname = µb.URI.hostnameFromURI(targetURL); context.requestType = 'popup'; - // https://github.com/gorhill/uBlock/commit/1d448b85b2931412508aa01bf899e0b6f0033626#commitcomment-14944764 - // Ignore bad target URL. On Firefox, an `about:blank` tab may be - // opened for a new tab before it is filled in with the real target - // URL. // https://github.com/gorhill/uBlock/issues/1735 // Do not bail out on `data:` URI, they are commonly used for popups. // https://github.com/uBlockOrigin/uAssets/issues/255 // Do not bail out on `about:blank`: an `about:blank` popup can be // opened, with the sole purpose to serve as an intermediary in // a sequence of chained popups. - if ( - context.requestHostname === '' && - targetURL.startsWith('data:') === false && - targetURL !== 'about:blank' - ) { - return ''; - } + // https://github.com/uBlockOrigin/uAssets/issues/263#issuecomment-272615772 + // Do not bail out, period: the static filtering engine must be + // able to examine all sorts of URLs for popup filtering purpose. - // Dynamic filtering makes sense only when we have a valid hostname. - if ( openerHostname !== '' ) { + // Dynamic filtering makes sense only when we have a valid opener + // hostname. + // https://github.com/gorhill/uBlock/commit/1d448b85b2931412508aa01bf899e0b6f0033626#commitcomment-14944764 + // Ignore bad target URL. On Firefox, an `about:blank` tab may be + // opened for a new tab before it is filled in with the real target + // URL. + if ( openerHostname !== '' && targetURL !== 'about:blank' ) { // Check per-site switch first if ( µb.hnSwitches.evaluateZ('no-popups', openerHostname) ) { if ( typeof clickedURL === 'string' && areDifferentURLs(targetURL, clickedURL) ) { diff --git a/src/js/traffic.js b/src/js/traffic.js index 1c67e0de52c28..29918bfbe0073 100644 --- a/src/js/traffic.js +++ b/src/js/traffic.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2014-2016 Raymond Hill + Copyright (C) 2014-2017 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 @@ -386,95 +386,69 @@ var onBeforeBehindTheSceneRequest = function(details) { // To handle: // - inline script tags +// - websockets // - media elements larger than n kB var onHeadersReceived = function(details) { // Do not interfere with behind-the-scene requests. var tabId = details.tabId; - if ( vAPI.isBehindTheSceneTabId(tabId) ) { - return; - } + if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; } - var requestType = details.type; + var µb = µBlock, + requestType = details.type; if ( requestType === 'main_frame' ) { - return onRootFrameHeadersReceived(details); - } - - if ( requestType === 'sub_frame' ) { - return onFrameHeadersReceived(details); + µb.tabContextManager.push(tabId, details.url); } - if ( requestType === 'image' || requestType === 'media' ) { - return foilLargeMediaElement(details); - } -}; - -/******************************************************************************/ - -var onRootFrameHeadersReceived = function(details) { - var µb = µBlock, - tabId = details.tabId; - - µb.tabContextManager.push(tabId, details.url); - - // Lookup the page store associated with this tab id. var pageStore = µb.pageStoreFromTabId(tabId); - if ( !pageStore ) { + if ( pageStore === null ) { + if ( requestType !== 'main_frame' ) { return; } pageStore = µb.bindTabToPageStats(tabId, 'beforeRequest'); } - // I can't think of how pageStore could be null at this point. - - return processCSP(details, pageStore, pageStore.createContextFromPage()); -}; + if ( pageStore.getNetFilteringSwitch() === false ) { return; } -/******************************************************************************/ - -var onFrameHeadersReceived = function(details) { - // Lookup the page store associated with this tab id. - var pageStore = µBlock.pageStoreFromTabId(details.tabId); - if ( !pageStore ) { - return; + if ( requestType === 'image' || requestType === 'media' ) { + return foilLargeMediaElement(pageStore, details); } - // Frame id of frame request is their own id, while the request is made - // in the context of the parent. - return processCSP( - details, - pageStore, - pageStore.createContextFromFrameId(details.frameId) - ); + // https://github.com/gorhill/uBO-Extra/issues/19 + // Turns out scripts must also be considered as potential embedded + // contexts (as workers) and as such we may need to inject content + // security policy directives. + if ( requestType === 'script' || requestType === 'main_frame' || requestType === 'sub_frame' ) { + return processCSP(pageStore, details); + } }; /******************************************************************************/ -var processCSP = function(details, pageStore, context) { +var processCSP = function(pageStore, details) { var µb = µBlock, tabId = details.tabId, requestURL = details.url, loggerEnabled = µb.logger.isEnabled(); + var context = pageStore.createContextFromPage(); context.requestURL = requestURL; context.requestHostname = µb.URI.hostnameFromURI(requestURL); + if ( details.type !== 'main_frame' ) { + context.pageHostname = context.pageDomain = context.requestHostname; + } - context.requestType = 'inline-script'; - var inlineScriptResult = pageStore.filterRequestNoCache(context), + var inlineScriptResult, blockInlineScript; + if ( details.type !== 'script' ) { + context.requestType = 'inline-script'; + inlineScriptResult = pageStore.filterRequestNoCache(context); blockInlineScript = µb.isBlockResult(inlineScriptResult); + } context.requestType = 'websocket'; µb.staticNetFilteringEngine.matchStringExactType(context, requestURL, 'websocket'); var websocketResult = µb.staticNetFilteringEngine.toResultString(loggerEnabled), blockWebsocket = µb.isBlockResult(websocketResult); - // https://github.com/gorhill/uBlock/issues/2050 - // Blanket-blocking websockets is exceptional, so we test whether the - // page is whitelisted if and only if there is a hit against a websocket - // filter. - if ( blockWebsocket && pageStore.getNetFilteringSwitch() === false ) { - websocketResult = ''; - blockWebsocket = false; - } - var headersChanged = false; + var headersChanged; if ( blockInlineScript || blockWebsocket ) { headersChanged = foilWithCSP( details.responseHeaders, @@ -483,35 +457,34 @@ var processCSP = function(details, pageStore, context) { ); } - if ( loggerEnabled ) { - µb.logger.writeOne( - tabId, - 'net', - inlineScriptResult, - 'inline-script', - requestURL, - context.rootHostname, - context.pageHostname - ); - } - - if ( loggerEnabled && blockWebsocket ) { - µb.logger.writeOne( - tabId, - 'net', - websocketResult, - 'websocket', - requestURL, - context.rootHostname, - context.pageHostname - ); + if ( loggerEnabled && details.type !== 'script' ) { + if ( blockInlineScript !== undefined ) { + µb.logger.writeOne( + tabId, + 'net', + inlineScriptResult, + 'inline-script', + requestURL, + context.rootHostname, + context.pageHostname + ); + } + if ( websocketResult !== '' ) { + µb.logger.writeOne( + tabId, + 'net', + websocketResult, + 'websocket', + requestURL, + context.rootHostname, + context.pageHostname + ); + } } context.dispose(); - if ( headersChanged !== true ) { - return; - } + if ( headersChanged !== true ) { return; } µb.updateBadgeAsync(tabId); @@ -523,19 +496,14 @@ var processCSP = function(details, pageStore, context) { // https://github.com/gorhill/uBlock/issues/1163 // "Block elements by size" -var foilLargeMediaElement = function(details) { +var foilLargeMediaElement = function(pageStore, details) { var µb = µBlock; var i = headerIndexFromName('content-length', details.responseHeaders); if ( i === -1 ) { return; } var tabId = details.tabId, - pageStore = µb.pageStoreFromTabId(tabId); - if ( pageStore === null || pageStore.getNetFilteringSwitch() === false ) { - return; - } - - var size = parseInt(details.responseHeaders[i].value, 10) || 0, + size = parseInt(details.responseHeaders[i].value, 10) || 0, result = pageStore.filterLargeMediaElement(size); if ( result === undefined ) { return; } @@ -691,7 +659,8 @@ vAPI.net.onHeadersReceived = { 'main_frame', 'sub_frame', 'image', - 'media' + 'media', + 'script' ], extra: [ 'blocking', 'responseHeaders' ], callback: onHeadersReceived diff --git a/src/js/ublock.js b/src/js/ublock.js index 528edb534897f..18c61e5cd04c2 100644 --- a/src/js/ublock.js +++ b/src/js/ublock.js @@ -1,7 +1,7 @@ /******************************************************************************* uBlock Origin - a browser extension to block requests. - Copyright (C) 2014-2016 Raymond Hill + Copyright (C) 2014-2017 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 @@ -340,6 +340,9 @@ var reInvalidHostname = /[^a-z0-9.\-\[\]:]/, us.dynamicFilteringEnabled = true; } break; + case 'autoUpdate': + this.scheduleAssetUpdater(value ? 7 * 60 * 1000 : 0); + break; case 'collapseBlocked': if ( value === false ) { this.cosmeticFilteringEngine.removeFromSelectorCache('*', 'net'); diff --git a/src/js/utils.js b/src/js/utils.js index 04c9a3e41000c..e7834c3de6a6f 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -245,3 +245,15 @@ }; /******************************************************************************/ + +// https://github.com/gorhill/uBlock/issues/2344 + +µBlock.matchCurrentLanguage = function(s) { + if ( typeof s !== 'string' ) { return false; } + if ( this.matchCurrentLanguage.reLang === undefined ) { + this.matchCurrentLanguage.reLang = new RegExp('\\b' + self.navigator.language.slice(0, 2) + '\\b'); + } + return this.matchCurrentLanguage.reLang.test(s); +}; + +/******************************************************************************/ diff --git a/src/settings.html b/src/settings.html index c4d293a65940a..39646a3a54a00 100644 --- a/src/settings.html +++ b/src/settings.html @@ -2,6 +2,7 @@ + uBlock — Settings diff --git a/src/whitelist.html b/src/whitelist.html index 67a70ab92dabd..f8a45905292c5 100644 --- a/src/whitelist.html +++ b/src/whitelist.html @@ -2,6 +2,7 @@ + uBlock — Whitelist diff --git a/tools/make-assets.sh b/tools/make-assets.sh index fade452dcbafe..ec3c5b38daca2 100755 --- a/tools/make-assets.sh +++ b/tools/make-assets.sh @@ -14,6 +14,7 @@ fi rm -rf $DES mkdir $DES +cp ./assets/assets.json $DES/ mkdir $DES/thirdparties cp -R ../uAssets/thirdparties/easylist-downloads.adblockplus.org $DES/thirdparties/ @@ -24,8 +25,5 @@ cp -R ../uAssets/thirdparties/www.malwaredomainlist.com $DES/thirdparti mkdir $DES/ublock cp -R ../uAssets/filters/* $DES/ublock/ -cp -R ./assets/ublock/filter-lists.json $DES/ublock/ - -cp ../uAssets/checksums/ublock0.txt $DES/checksums.txt echo "done." diff --git a/tools/make-chromium.sh b/tools/make-chromium.sh index 8d1f58911131a..eda30f2991edf 100755 --- a/tools/make-chromium.sh +++ b/tools/make-chromium.sh @@ -5,7 +5,11 @@ echo "*** uBlock0.chromium: Creating web store package" echo "*** uBlock0.chromium: Copying files" -DES=dist/build/uBlock0.chromium +if [ "$1" = experimental ]; then + DES=dist/build/experimental/uBlock0.chromium +else + DES=dist/build/uBlock0.chromium +fi rm -rf $DES mkdir -p $DES