From ba96d73fc1f8337d4b2aa68a3387f8e5810254ff Mon Sep 17 00:00:00 2001 From: uBlock <35694050+uBlockAdmin@users.noreply.github.com> Date: Thu, 31 May 2018 13:35:09 +0530 Subject: [PATCH] Added Firefox Embedded WebExtension code. --- .travis.yml | 1 + platform/chromium/vapi-background.js | 19 ++- platform/webext/background.html | 35 +++++ platform/webext/bootstrap.js | 196 +++++++++++++++++++++++++++ platform/webext/chrome.manifest | 1 + platform/webext/from-legacy.js | 62 +++++++++ platform/webext/install.rdf | 26 ++++ platform/webext/manifest.json | 70 ++++++++++ src/js/messaging.js | 14 +- src/js/settings.js | 22 ++- src/js/start.js | 58 +++++--- src/js/storage.js | 6 +- tools/make-embed-webext-meta.py | 69 ++++++++++ tools/make-embed-webext.sh | 45 ++++++ 14 files changed, 590 insertions(+), 34 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 create mode 100644 platform/webext/manifest.json create mode 100644 tools/make-embed-webext-meta.py create mode 100755 tools/make-embed-webext.sh diff --git a/.travis.yml b/.travis.yml index d9a2d15ee..f2b1830cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ env: matrix: - BROWSER=chromium EXT=zip - BROWSER=firefox EXT=xpi + - BROWSER=embed-webext EXT=xpi script: ./tools/make-${BROWSER}.sh all deploy: provider: releases diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index 88678c113..9199a9090 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -38,6 +38,16 @@ var manifest = chrome.runtime.getManifest(); vAPI.chrome = true; +vAPI.webextFlavor = ''; +if ( + self.browser instanceof Object && + typeof self.browser.runtime.getBrowserInfo === 'function' +) { + self.browser.runtime.getBrowserInfo().then(function(info) { + vAPI.webextFlavor = info.vendor + '-' + info.name + '-' + info.version; + }); +} + var noopFunc = function(){}; /******************************************************************************/ @@ -219,7 +229,9 @@ vAPI.tabs.registerListeners = function() { onClosedClient(tabId); }; - chrome.webNavigation.onCreatedNavigationTarget.addListener(onCreatedNavigationTarget); + if ( chrome.webNavigation.onCreatedNavigationTarget instanceof Object ) { + chrome.webNavigation.onCreatedNavigationTarget.addListener(onCreatedNavigationTarget); + } chrome.webNavigation.onBeforeNavigate.addListener(onBeforeNavigate); chrome.webNavigation.onCommitted.addListener(onCommitted); chrome.tabs.onUpdated.addListener(onUpdated); @@ -337,6 +349,11 @@ vAPI.tabs.open = function(details) { wrapper(); return; } + + if ( /^Mozilla-Firefox-5[2-5]\./.test(vAPI.webextFlavor) ) { + wrapper(); + return; + } chrome.tabs.query({ url: targetURL }, function(tabs) { var tab = tabs[0]; diff --git a/platform/webext/background.html b/platform/webext/background.html new file mode 100644 index 000000000..61005fc90 --- /dev/null +++ b/platform/webext/background.html @@ -0,0 +1,35 @@ + + + + +uBlock + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/webext/bootstrap.js b/platform/webext/bootstrap.js new file mode 100644 index 000000000..4128c8f97 --- /dev/null +++ b/platform/webext/bootstrap.js @@ -0,0 +1,196 @@ +/******************************************************************************* + + µBlock - a browser extension to block requests. + Copyright (C) 2014 The µBlock authors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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/uBlockAdmin/uBlock +*/ + +'use strict'; + +const hostName = 'ublock'; +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; +const {Services} = Cu.import('resource://gre/modules/Services.jsm', null); + +function startup({ webExtension }) { + webExtension.startup().then(api => { + let { browser } = api, + dataMigrator; + let onMessage = function(message, sender, callback) { + if ( message.what === 'getNextMigrateItem' ) { + dataMigrator = dataMigrator || getDataMigrate(); + dataMigrator.sendNextItemData((key, value) => { + if ( key === undefined ) { + dataMigrator.closeDbConn(); + dataMigrator = undefined; + browser.runtime.onMessage.removeListener(onMessage); + } + callback({ key: key, value: JSON.stringify(value) }); + }); + return true; + } + if ( message.what === 'dataMigrateDone' ) { + browser.runtime.onMessage.removeListener(onMessage); + } + if ( typeof callback === 'function' ) { + callback(); + } + }; + browser.runtime.onMessage.addListener(onMessage); + }); +} + +function shutdown() { +} + +function install() { +} + +function uninstall() { +} + +var SQLite = { + + open: function() { + + var path = Services.dirsvc.get('ProfD', Ci.nsIFile); + path.append('extension-data'); + path.append(hostName + '.sqlite'); + if ( !path.exists() || !path.isFile() ) { + return null; + } + this.db = Services.storage.openDatabase(path); + return this.db; + }, + + close: function() { + SQLite.db.asyncClose(); + }, + + run: function(query, values, callback) { + + if ( !this.db ) { + if ( this.open() === null ) { + callback({}); + return; + } + } + + var result = {}; + query = this.db.createAsyncStatement(query); + + if ( Array.isArray(values) && values.length ) { + var i = values.length; + + while ( i-- ) { + query.bindByIndex(i, values[i]); + } + } + + query.executeAsync({ + handleResult: function(rows) { + if ( !rows || typeof callback !== 'function' ) { + return; + } + + var row; + + while ( row = rows.getNextRow() ) { + result[row.getResultByIndex(0)] = row.getResultByIndex(1); + } + }, + handleCompletion: function(reason) { + if ( typeof callback === 'function' && reason === 0 ) { + callback(result); + } + }, + handleError: function(error) { + if ( typeof callback === 'function' && reason === 0 ) { + callback(); + } + result = null; + if ( error.result.toString() === '11' ) { + close(); + } + } + }); + } +}; + +var getDataMigrate = function() { + + var legacyData = null; + var legacyDataKeys = null; + + var fetchLegacyData = function(cb) { + var values = []; + + var prepareResult = function(result) { + + if ( result === undefined ) { + cb(); + return; + } + + var key; + for ( key in result ) { + result[key] = JSON.parse(result[key]); + } + + legacyData = result; + legacyDataKeys = Object.keys(result); + + var key = legacyDataKeys.pop(); + var value = legacyData[key]; + cb({ key: key, value: value }); + }; + + SQLite.run( + 'SELECT * FROM settings', + values, + prepareResult + ); + }; + + var sendNextItemData = function(callback) { + + if(!legacyData) { + fetchLegacyData( bin => { + callback(bin.key, bin.value); + }); + return; + } + else { + var key = legacyDataKeys.pop(); + var value = legacyData[key]; + callback(key,value); + return; + } + }; + + var closeDbConn = function() { + SQLite.close(); + }; + + return { + sendNextItemData: sendNextItemData, + closeDbConn: closeDbConn + }; +} + + + +/******************************************************************************/ diff --git a/platform/webext/chrome.manifest b/platform/webext/chrome.manifest new file mode 100644 index 000000000..5433b6dca --- /dev/null +++ b/platform/webext/chrome.manifest @@ -0,0 +1 @@ +content ublock ./ diff --git a/platform/webext/from-legacy.js b/platform/webext/from-legacy.js new file mode 100644 index 000000000..b5584fa21 --- /dev/null +++ b/platform/webext/from-legacy.js @@ -0,0 +1,62 @@ +/******************************************************************************* + + µBlock - a browser extension to block requests. + Copyright (C) 2014 The µBlock authors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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/uBlockAdmin/uBlock +*/ + +'use strict'; + +/******************************************************************************/ + +µBlock.migrateLegacyData = (function() { + + let µb = µBlock; + + let migrateLegacyData = function(callback) { + + let storeKeyValue = function(details, callback) { + let bin = {}; + bin[details.key] = JSON.parse(details.value); + vAPI.storage.set(bin, callback); + }; + + let migrateNextDataItem = function() { + self.browser.runtime.sendMessage({ what: 'getNextMigrateItem' }, response => { + if ( response.key === undefined ) { + return callback(); + } + storeKeyValue(response, migrateNextDataItem); + }); + }; + + self.browser.storage.local.get('dataMigrateDone', bin => { + if ( bin && bin.dataMigrateDone ) { + self.browser.runtime.sendMessage({ what: 'dataMigrateDone' }); + return callback(); + } + self.browser.storage.local.set({ dataMigrateDone: true }); + migrateNextDataItem(); + }); + }; + + return migrateLegacyData; + +})(); + + +/******************************************************************************/ diff --git a/platform/webext/install.rdf b/platform/webext/install.rdf new file mode 100644 index 000000000..676ba1692 --- /dev/null +++ b/platform/webext/install.rdf @@ -0,0 +1,26 @@ + + + + {{2b10c1c8-a11f-4bad-fe9c-1c11e82cac42}} + {version} + {name} + {description} + {homepage} + {author} + 2 + true + true + true +{localized} + + + + + {{ec8030f7-c20a-464f-9b0e-13a3a9e97384}} + 52.0a1 + 56.* + + + + + diff --git a/platform/webext/manifest.json b/platform/webext/manifest.json new file mode 100644 index 000000000..4cffd9cdc --- /dev/null +++ b/platform/webext/manifest.json @@ -0,0 +1,70 @@ +{ + "manifest_version": 2, + + "name": "uBlock", + "version": "0.9.5.5", + + "applications": { + "gecko": { + "id": "{2b10c1c8-a11f-4bad-fe9c-1c11e82cac42}", + "strict_min_version": "52.0a1" + } + }, + + "default_locale": "en", + "description": "__MSG_extShortDesc__", + "icons": { + "16": "img/icon_16.png", + "128": "img/icon_128.png" + }, + + "browser_action": { + "default_icon": "img/browsericons/icon19-off.png", + "default_title": "uBlock", + "default_popup": "popup.html" + }, + + "author": "The uBlock Development Team", + "background": { + "page": "background.html" + }, + "content_scripts": [ + { + "matches": ["http://*/*", "https://*/*"], + "js": ["js/vapi-client.js", "js/contentscript-start.js"], + "run_at": "document_start", + "all_frames": true + }, + { + "matches": ["http://*/*", "https://*/*"], + "js": ["js/contentscript-end.js"], + "run_at": "document_end", + "all_frames": true + }, + { + "matches": [ + "https://*.adblockplus.org/*", + "https://*.adblockplus.me/*", + "https://www.fanboy.co.nz/*" + ], + "js": ["js/subscriber.js"], + "run_at": "document_idle" + } + ], + "minimum_chrome_version": "22.0", + "options_ui": { + "page": "options_ui.html" + }, + "permissions": [ + "contextMenus", + "storage", + "tabs", + "unlimitedStorage", + "webNavigation", + "webRequest", + "webRequestBlocking", + "http://*/*", + "https://*/*" + ], + "short_name": "uBlock" +} diff --git a/src/js/messaging.js b/src/js/messaging.js index cc4b26840..4ad932041 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -1075,17 +1075,13 @@ var backupUserData = function(callback) { var filename = vAPI.i18n('aboutBackupFilename') .replace('{{datetime}}', now.toLocaleString()) .replace(/ +/g, '_'); - - vAPI.download({ - '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.preferences.set(µb.restoreBackupSettings); - - getLocalData(callback); + + getLocalData(function(localData) { + callback({ localData: localData, userData: userData }); + }); }; var onUserFiltersReady = function(details) { @@ -1100,7 +1096,7 @@ var backupUserData = function(callback) { var restoreUserData = function(request) { var userData = request.userData; - var countdown = 7; + var countdown = 6; var onCountdown = function() { countdown -= 1; if ( countdown === 0 ) { diff --git a/src/js/settings.js b/src/js/settings.js index 972c4f00e..48372fa84 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -99,14 +99,32 @@ var startImportFilePicker = function() { /******************************************************************************/ var exportToFile = function() { - messager.send({ what: 'backupUserData' }, onLocalDataReceived); + //messager.send({ what: 'backupUserData' }, onLocalDataReceived); + messager.send({ what: 'backupUserData' }, function(response) { + if ( + response instanceof Object === false || + response.userData instanceof Object === false + ) { + return; + } + vAPI.download({ + 'url': 'data:text/plain;charset=utf-8,' + + encodeURIComponent(JSON.stringify(response.userData, null, ' ')), + 'filename': response.localData.lastBackupFile + }); + onLocalDataReceived(response.localData); + }); }; /******************************************************************************/ var onLocalDataReceived = function(details) { uDom('#localData > ul > li:nth-of-type(1)').text( - vAPI.i18n('settingsStorageUsed').replace('{{value}}', details.storageUsed.toLocaleString()) + vAPI.i18n('settingsStorageUsed') + .replace( + '{{value}}', + typeof details.storageUsed === 'number' ? details.storageUsed.toLocaleString() : '?' + ) ); var elem, dt; diff --git a/src/js/start.js b/src/js/start.js index 1eeea6931..50e0262a4 100644 --- a/src/js/start.js +++ b/src/js/start.js @@ -182,7 +182,7 @@ var onUserFiltersReady = function(userFilters) { var onFirstFetchReady = function(fetched) { // Order is important -- do not change: - onInstalled(fetched.version); + onInstalled(); onSystemSettingsReady(fetched); fromFetch(µb.localSettings, fetched); onUserSettingsReady(fetched); @@ -199,21 +199,30 @@ var onFirstFetchReady = function(fetched) { µb.loadPublicSuffixList(onPSLReady); }; -var onInstalled = function(version) { - var firstInstall = version === '0.0.0.0'; - if(!firstInstall) { - return; - } - var onDataReceived = function(data) { - entries = data.stats || {userId: µBlock.stats.generateUserId(),totalPings: 0 }; - vAPI.storage.set({ 'stats': entries }); - vAPI.tabs.open({ - url: µBlock.donationUrl+"?u=" + entries.userId + "&lg=" + navigator.language, - select: true, - index: -1 - }); - } - vAPI.storage.get('stats',onDataReceived); + +var onInstalled = function() { + + var onVersionRead = function(store) { + + var lastVersion = store.extensionLastVersion || '0.0.0.0'; + + var firstInstall = lastVersion === '0.0.0.0'; + + if(!firstInstall) { + return; + } + var onDataReceived = function(data) { + entries = data.stats || {userId: µBlock.stats.generateUserId(),totalPings: 0 }; + vAPI.storage.set({ 'stats': entries }); + vAPI.tabs.open({ + url: µBlock.donationUrl+"?u=" + entries.userId + "&lg=" + navigator.language, + select: true, + index: -1 + }); + } + vAPI.storage.get('stats',onDataReceived); + }; + vAPI.storage.get('extensionLastVersion', onVersionRead); } /******************************************************************************/ @@ -249,10 +258,7 @@ var fromFetch = function(to, fetched) { } }; -/******************************************************************************/ - -return function() { - // Forbid remote fetching of assets +var onSelectedFilterListsLoaded = function() { µb.assets.remoteFetchBarrier += 1; var fetchableProps = { @@ -265,6 +271,7 @@ return function() { 'netWhitelist': '', 'userFilters': '', 'cached_asset_content://assets/user/filters.txt': '', + 'selfie': null, 'selfieMagic': '', 'version': '0.0.0.0' }; @@ -274,7 +281,16 @@ return function() { vAPI.storage.preferences.get(fetchableProps, onPrefFetchReady); }; -/******************************************************************************/ +return function() { + if ( typeof µb.migrateLegacyData === 'function' ) { + µb.migrateLegacyData(function() { + onSelectedFilterListsLoaded(); + }); + } + else { + onSelectedFilterListsLoaded(); + } +}; })(); diff --git a/src/js/storage.js b/src/js/storage.js index 8271608e5..92ecb9db5 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -33,7 +33,11 @@ µBlock.storageUsed = bytesInUse; callback(bytesInUse); }; - vAPI.storage.getBytesInUse(null, getBytesInUseHandler); + if ( vAPI.storage.getBytesInUse instanceof Function ) { + vAPI.storage.getBytesInUse(null, getBytesInUseHandler); + } else { + callback(); + } }; /******************************************************************************/ diff --git a/tools/make-embed-webext-meta.py b/tools/make-embed-webext-meta.py new file mode 100644 index 000000000..bac916f38 --- /dev/null +++ b/tools/make-embed-webext-meta.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +import os +import json +import re +import sys +from io import open as uopen +from collections import OrderedDict + +if len(sys.argv) == 1 or not sys.argv[1]: + raise SystemExit('Build dir missing.') + +proj_dir = os.path.join(os.path.split(os.path.abspath(__file__))[0], '..') +build_dir = os.path.abspath(sys.argv[1]) + +chromium_manifest = {} +webext_manifest = {} + +chromium_manifest_file = os.path.join(proj_dir, 'platform', 'chromium', 'manifest.json') +with open(chromium_manifest_file) as f1: + chromium_manifest = json.load(f1) + +webext_manifest_file = os.path.join(build_dir, 'webextension', 'manifest.json') +with open(webext_manifest_file) as f2: + webext_manifest = json.load(f2) + +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') + +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'] + '/embed-webext'; +webext_manifest['homepage'] = 'https://github.com/uBlockAdmin/uBlock' +webext_manifest['description'] = descriptions['en'] +del descriptions['en'] +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 + '' + descriptions[alpha2] + '\n' + + t3 + '' + webext_manifest['author'] + '\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-embed-webext.sh b/tools/make-embed-webext.sh new file mode 100755 index 000000000..ad2939103 --- /dev/null +++ b/tools/make-embed-webext.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# +# This script assumes a linux environment + +echo "*** uBlock.embed-webext: Creating web store package" +echo "*** uBlock.embed-webext: Copying files" + +DES=dist/build/uBlock.embed-webext +rm -rf $DES +mkdir -p $DES/webextension + +cp -R assets $DES/webextension/ +rm $DES/webextension/assets/*.sh +cp -R src/css $DES/webextension/ +cp -R src/img $DES/webextension/ +cp -R src/js $DES/webextension/ +cp -R src/lib $DES/webextension/ +cp -R src/_locales $DES/webextension/ +cp -R $DES/webextension/_locales/nb $DES/webextension/_locales/no +cp src/*.html $DES/webextension/ +cp platform/chromium/*.js $DES/webextension/js/ +cp -R platform/chromium/img $DES/webextension/ +cp platform/chromium/*.html $DES/webextension/ +cp platform/chromium/*.json $DES/webextension/ +cp LICENSE.txt $DES/webextension/ + +cp platform/webext/background.html $DES/webextension/ +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 "*** uBlock.embed-webext: Generating meta..." +python tools/make-embed-webext-meta.py $DES/ + +if [ "$1" = all ]; then + echo "*** uBlock.embed-webext: Creating package..." + pushd $DES > /dev/null + zip ../$(basename $DES).xpi -qr * + popd > /dev/null +fi + +echo "*** uBlock.embed-webext: Package done."