From 97509f6d03d3b3443fd3cdd8934c322c04bd6b5c Mon Sep 17 00:00:00 2001 From: gorhill Date: Wed, 29 Mar 2017 13:46:19 -0400 Subject: [PATCH] hybrid webextension for seamless migration from legacy --- platform/webext/background.html | 36 +++++ platform/webext/bootstrap.js | 276 ++++++++++++++++++++++++++++++++ platform/webext/chrome.manifest | 1 + platform/webext/from-legacy.js | 68 ++++++++ platform/webext/install.rdf | 28 ++++ src/js/assets.js | 2 - src/js/background.js | 4 +- src/js/start.js | 21 ++- tools/make-firefox-meta.py | 2 +- tools/make-webext-meta.py | 59 ++++++- tools/make-webext.sh | 26 +-- 11 files changed, 504 insertions(+), 19 deletions(-) create mode 100644 platform/webext/background.html create mode 100644 platform/webext/bootstrap.js create mode 100644 platform/webext/chrome.manifest create mode 100644 platform/webext/from-legacy.js create mode 100644 platform/webext/install.rdf diff --git a/platform/webext/background.html b/platform/webext/background.html new file mode 100644 index 0000000..2b9603f --- /dev/null +++ b/platform/webext/background.html @@ -0,0 +1,36 @@ + + + + +uBlock Origin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/webext/bootstrap.js b/platform/webext/bootstrap.js new file mode 100644 index 0000000..0fe4fd8 --- /dev/null +++ b/platform/webext/bootstrap.js @@ -0,0 +1,276 @@ +/******************************************************************************* + + uMatrix - a browser extension to black/white list requests. + 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 + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uMatrix +*/ + +/* exported startup, shutdown, install, uninstall */ + +'use strict'; + +/******************************************************************************/ + +const hostName = 'umatrix'; + +/******************************************************************************/ + +function startup({ webExtension }) { + webExtension.startup().then(api => { + let { browser } = api, + storageMigrator; + let onMessage = function(message, sender, callback) { + if ( message.what === 'webext:storageMigrateNext' ) { + storageMigrator = storageMigrator || getStorageMigrator(); + storageMigrator.getNext((key, value) => { + if ( key === undefined ) { + storageMigrator.markAsDone(); + storageMigrator = undefined; + browser.runtime.onMessage.removeListener(onMessage); + } + callback({ key: key, value: JSON.stringify(value) }); + }); + return true; + } + if ( message.what === 'webext:storageMigrateDone' ) { + browser.runtime.onMessage.removeListener(onMessage); + } + if ( typeof callback === 'function' ) { + callback(); + } + }; + browser.runtime.onMessage.addListener(onMessage); + }); +} + +function shutdown() { +} + +function install() { +} + +function uninstall() { +} + +/******************************************************************************/ + +var getStorageMigrator = function() { + var db = null; + var dbOpenError = ''; + + var close = function() { + if ( db !== null ) { + db.asyncClose(); + } + db = null; + }; + + var open = function() { + if ( db !== null ) { + return db; + } + + // Create path + var { Services } = Components.utils.import('resource://gre/modules/Services.jsm', null), + path = Services.dirsvc.get('ProfD', Components.interfaces.nsIFile); + path.append('extension-data'); + path.append(hostName + '.sqlite'); + if ( !path.exists() || !path.isFile() ) { + return null; + } + + // Open database. + try { + db = Services.storage.openDatabase(path); + if ( db.connectionReady === false ) { + db.asyncClose(); + db = null; + } + } catch (ex) { + if ( dbOpenError === '' ) { + dbOpenError = ex.name; + if ( ex.name === 'NS_ERROR_FILE_CORRUPTED' ) { + close(); + } + } + } + + if ( db === null ) { + return null; + } + + // Since database could be opened successfully, reset error flag (its + // purpose is to avoid spamming console with error messages). + dbOpenError = ''; + + return db; + }; + + // Execute a query + var runStatement = function(stmt, callback) { + var result = {}; + + stmt.executeAsync({ + handleResult: function(rows) { + if ( !rows || typeof callback !== 'function' ) { + return; + } + + var row; + + while ( (row = rows.getNextRow()) ) { + // we assume that there will be two columns, since we're + // using it only for preferences + result[row.getResultByIndex(0)] = row.getResultByIndex(1); + } + }, + handleCompletion: function(reason) { + if ( typeof callback === 'function' && reason === 0 ) { + callback(result); + } + result = null; + }, + handleError: function(error) { + // Caller expects an answer regardless of failure. + if ( typeof callback === 'function' ) { + callback({}); + } + result = null; + // https://github.com/gorhill/uBlock/issues/1768 + // Error cases which warrant a removal of the SQL file, so far: + // - SQLLite error 11 database disk image is malformed + // Can't find doc on MDN about the type of error.result, so I + // force a string comparison. + if ( error.result.toString() === '11' ) { + close(); + } + } + }); + }; + + var bindNames = function(stmt, names) { + if ( Array.isArray(names) === false || names.length === 0 ) { + return; + } + var params = stmt.newBindingParamsArray(); + var i = names.length, bp; + while ( i-- ) { + bp = params.newBindingParams(); + bp.bindByName('name', names[i]); + params.addParams(bp); + } + stmt.bindParameters(params); + }; + + var read = function(details, callback) { + if ( typeof callback !== 'function' ) { + return; + } + + var prepareResult = function(result) { + var key; + for ( key in result ) { + if ( result.hasOwnProperty(key) === false ) { + continue; + } + result[key] = JSON.parse(result[key]); + } + if ( typeof details === 'object' && details !== null ) { + for ( key in details ) { + if ( result.hasOwnProperty(key) === false ) { + result[key] = details[key]; + } + } + } + callback(result); + }; + + if ( open() === null ) { + prepareResult({}); + return; + } + + var names = []; + if ( details !== null ) { + if ( Array.isArray(details) ) { + names = details; + } else if ( typeof details === 'object' ) { + names = Object.keys(details); + } else { + names = [details.toString()]; + } + } + + var stmt; + if ( names.length === 0 ) { + stmt = db.createAsyncStatement('SELECT * FROM "settings"'); + } else { + stmt = db.createAsyncStatement('SELECT * FROM "settings" WHERE "name" = :name'); + bindNames(stmt, names); + } + + runStatement(stmt, prepareResult); + }; + + let allKeys; + + let readNext = function(key, callback) { + if ( key === undefined ) { + callback(); + return; + } + read(key, bin => { + if ( bin instanceof Object && bin.hasOwnProperty(key) ) { + callback(key, bin[key]); + } else { + callback(key); + } + }); + }; + + let getNext = function(callback) { + if ( Array.isArray(allKeys) ) { + readNext(allKeys.pop(), callback); + return; + } + if ( open() === null ) { + callback(); + return; + } + let stmt = db.createAsyncStatement('SELECT "name",\'dummy\' FROM "settings"'); + runStatement(stmt, result => { + allKeys = []; + for ( let key in result ) { + if ( result.hasOwnProperty(key) ) { + allKeys.push(key); + } + } + readNext(allKeys.pop(), callback); + }); + }; + + let markAsDone = function() { + close(); + }; + + return { + getNext: getNext, + markAsDone: markAsDone, + }; +}; + +/******************************************************************************/ diff --git a/platform/webext/chrome.manifest b/platform/webext/chrome.manifest new file mode 100644 index 0000000..9516057 --- /dev/null +++ b/platform/webext/chrome.manifest @@ -0,0 +1 @@ +content umatrix ./ diff --git a/platform/webext/from-legacy.js b/platform/webext/from-legacy.js new file mode 100644 index 0000000..48aa6a5 --- /dev/null +++ b/platform/webext/from-legacy.js @@ -0,0 +1,68 @@ +/******************************************************************************* + + uMatrix - a browser extension to black/white list requests. + Copyright (C) 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 + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uMatrix +*/ + +// For background page + +'use strict'; + +/******************************************************************************/ + +(function() { + let µm = µMatrix; + + let migrateAll = function(callback) { + let mustRestart = false; + + let migrateKeyValue = function(details, callback) { + let bin = {}; + bin[details.key] = JSON.parse(details.value); + self.browser.storage.local.set(bin, callback); + mustRestart = true; + }; + + let migrateNext = function() { + self.browser.runtime.sendMessage({ what: 'webext:storageMigrateNext' }, response => { + if ( response.key === undefined ) { + if ( mustRestart ) { + self.browser.runtime.reload(); + } else { + callback(); + } + return; + } + migrateKeyValue(response, migrateNext); + }); + }; + + self.browser.storage.local.get('legacyStorageMigrated', bin => { + if ( bin && bin.legacyStorageMigrated ) { + self.browser.runtime.sendMessage({ what: 'webext:storageMigrateDone' }); + return callback(); + } + self.browser.storage.local.set({ legacyStorageMigrated: true }); + migrateNext(); + }); + }; + + µm.onBeforeStartQueue.push(migrateAll); +})(); + +/******************************************************************************/ diff --git a/platform/webext/install.rdf b/platform/webext/install.rdf new file mode 100644 index 0000000..5c9244b --- /dev/null +++ b/platform/webext/install.rdf @@ -0,0 +1,28 @@ + + + + uMatrix-webext@raymondhill.net + {version} + {name} + {description} + https://github.com/gorhill/uMatrix + {author} + Deathamns + Alex Vallat + 2 + true + true + true +{localized} + + + + + {{ec8030f7-c20a-464f-9b0e-13a3a9e97384}} + 53.0a1 + * + + + + + diff --git a/src/js/assets.js b/src/js/assets.js index a0e2411..0c33e94 100644 --- a/src/js/assets.js +++ b/src/js/assets.js @@ -28,7 +28,6 @@ /******************************************************************************/ var reIsExternalPath = /^(?:[a-z-]+):\/\//, - reIsUserAsset = /^user-/, errorCantConnectTo = vAPI.i18n('errorCantConnectTo'), noopfunc = function(){}; @@ -188,7 +187,6 @@ var migrate = function(callback) { } 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; diff --git a/src/js/background.js b/src/js/background.js index 0376346..54b2ea6 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -1,6 +1,6 @@ /******************************************************************************* - µMatrix - a Chromium browser extension to black/white list requests. + uMatrix - a browser extension to black/white list requests. Copyright (C) 2014-2017 Raymond Hill This program is free software: you can redistribute it and/or modify @@ -100,6 +100,8 @@ var requestStatsFactory = function() { /******************************************************************************/ return { + onBeforeStartQueue: [], + userSettings: { autoUpdate: false, clearBrowserCache: true, diff --git a/src/js/start.js b/src/js/start.js index 3d361fc..ffe67f2 100644 --- a/src/js/start.js +++ b/src/js/start.js @@ -1,6 +1,6 @@ /******************************************************************************* - µMatrix - a Chromium browser extension to black/white list requests. + uMatrix - a Chromium browser extension to black/white list requests. Copyright (C) 2014-2017 Raymond Hill This program is free software: you can redistribute it and/or modify @@ -89,6 +89,20 @@ var rwLocalUserSettings = { /******************************************************************************/ +var processCallbackQueue = function(queue, callback) { + var processOne = function() { + var fn = queue.pop(); + if ( fn ) { + fn(processOne); + } else if ( typeof callback === 'function' ) { + callback(); + } + }; + processOne(); +}; + +/******************************************************************************/ + var onAllDone = function() { µm.webRequest.start(); @@ -151,8 +165,9 @@ var onPSLReady = function() { vAPI.tabs.getAll(onTabsReady); }; -// Must be done ASAP -µm.loadPublicSuffixList(onPSLReady); +processCallbackQueue(µm.onBeforeStartQueue, function() { + µm.loadPublicSuffixList(onPSLReady); +}); /******************************************************************************/ diff --git a/tools/make-firefox-meta.py b/tools/make-firefox-meta.py index 8aa2f4e..49b3b61 100755 --- a/tools/make-firefox-meta.py +++ b/tools/make-firefox-meta.py @@ -116,5 +116,5 @@ install_rdf = pj(build_dir, 'install.rdf') with open(install_rdf, 'r+t', encoding='utf-8', newline='\n') as f: install_rdf = f.read() f.seek(0) - f.write(install_rdf.format(**manifest)) + f.truncate() diff --git a/tools/make-webext-meta.py b/tools/make-webext-meta.py index 15df315..41c8a85 100755 --- a/tools/make-webext-meta.py +++ b/tools/make-webext-meta.py @@ -2,7 +2,11 @@ import os import json +import re import sys +from io import open as uopen +from collections import OrderedDict +from xml.sax.saxutils import escape if len(sys.argv) == 1 or not sys.argv[1]: raise SystemExit('Build dir missing.') @@ -10,7 +14,7 @@ if len(sys.argv) == 1 or not sys.argv[1]: proj_dir = os.path.join(os.path.split(os.path.abspath(__file__))[0], '..') build_dir = os.path.abspath(sys.argv[1]) -# Import version number from chromium platform +# Import data from chromium platform chromium_manifest = {} webext_manifest = {} @@ -18,7 +22,8 @@ chromium_manifest_file = os.path.join(proj_dir, 'platform', 'chromium', 'manifes with open(chromium_manifest_file) as f1: chromium_manifest = json.load(f1) -webext_manifest_file = os.path.join(build_dir, 'manifest.json') +# WebExtension part +webext_manifest_file = os.path.join(build_dir, 'webextension', 'manifest.json') with open(webext_manifest_file) as f2: webext_manifest = json.load(f2) @@ -27,3 +32,53 @@ webext_manifest['version'] = chromium_manifest['version'] with open(webext_manifest_file, 'w') as f2: json.dump(webext_manifest, f2, indent=2, separators=(',', ': '), sort_keys=True) f2.write('\n') + +# Legacy part +descriptions = OrderedDict({}) +source_locale_dir = os.path.join(build_dir, 'webextension', '_locales') +for alpha2 in sorted(os.listdir(source_locale_dir)): + locale_path = os.path.join(source_locale_dir, alpha2, 'messages.json') + with uopen(locale_path, encoding='utf-8') as f: + strings = json.load(f, object_pairs_hook=OrderedDict) + alpha2 = alpha2.replace('_', '-') + descriptions[alpha2] = strings['extShortDesc']['message'] + +webext_manifest['author'] = chromium_manifest['author']; +webext_manifest['name'] = chromium_manifest['name'] + '/webext'; +webext_manifest['homepage'] = 'https://github.com/gorhill/uMatrix' +webext_manifest['description'] = escape(descriptions['en']) +del descriptions['en'] + +match = re.search('^(\d+\.\d+\.\d+)(\.\d+)$', chromium_manifest['version']) +if match: + buildtype = int(match.group(2)[1:]) + if buildtype < 100: + builttype = 'b' + str(buildtype) + else: + builttype = 'rc' + str(buildtype - 100) + webext_manifest['version'] = match.group(1) + builttype + +webext_manifest['localized'] = [] +t = ' ' +t3 = 3 * t +for alpha2 in descriptions: + if alpha2 == 'en': + continue + webext_manifest['localized'].append( + '\n' + t*2 + '\n' + + t3 + '' + alpha2 + '\n' + + t3 + '' + webext_manifest['name'] + '\n' + + t3 + '' + escape(descriptions[alpha2]) + '\n' + + t3 + '' + webext_manifest['author'] + '\n' + + # t3 + '' + ??? + '\n' + + t3 + '' + webext_manifest['homepage'] + '\n' + + t*2 + '' + ) +webext_manifest['localized'] = '\n'.join(webext_manifest['localized']) + +install_rdf = os.path.join(build_dir, 'install.rdf') +with uopen(install_rdf, 'r+t', encoding='utf-8', newline='\n') as f: + install_rdf = f.read() + f.seek(0) + f.write(install_rdf.format(**webext_manifest)) + f.truncate() diff --git a/tools/make-webext.sh b/tools/make-webext.sh index 326c59e..5bbf730 100755 --- a/tools/make-webext.sh +++ b/tools/make-webext.sh @@ -7,17 +7,23 @@ echo "*** uMatrix.webext: Copying files" DES=dist/build/uMatrix.webext rm -rf $DES -mkdir -p $DES +mkdir -p $DES/webextension -cp -R ./assets $DES/ -cp -R ./src/* $DES/ -cp -R $DES/_locales/nb $DES/_locales/no -cp platform/chromium/*.html $DES/ -cp platform/webext/polyfill.js $DES/js/ -cp platform/chromium/*.js $DES/js/ -cp -R platform/chromium/img/* $DES/img/ -cp platform/webext/manifest.json $DES/ -cp LICENSE.txt $DES/ +cp -R ./assets $DES/webextension/ +cp -R ./src/* $DES/webextension/ +cp platform/chromium/*.html $DES/webextension/ +cp platform/chromium/*.js $DES/webextension/js/ +cp -R platform/chromium/img/* $DES/webextension/img/ +cp LICENSE.txt $DES/webextension/ + +cp platform/webext/background.html $DES/webextension/ +cp platform/webext/polyfill.js $DES/webextension/js/ +cp platform/webext/from-legacy.js $DES/webextension/js/ +cp platform/webext/manifest.json $DES/webextension/ +cp platform/webext/bootstrap.js $DES/ +cp platform/webext/chrome.manifest $DES/ +cp platform/webext/install.rdf $DES/ +mv $DES/webextension/img/icon_128.png $DES/icon.png echo "*** uMatrix.webext: Generating meta..." python tools/make-webext-meta.py $DES/