tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 6d0ce54a76f1f400087f3cb81802373eed7f0373
parent 73c5ed37142330dca83ddb21659cab1eb6199e5f
Author: hackademix <giorgio@maone.net>
Date:   Thu,  1 Sep 2022 16:30:50 +0200

[android] Modify add-on support

Bug 41160: One-time ultimate switch Tor Browser Android to HTTPS-Only.
Bug 41159: Remove HTTPS-Everywhere extension from Tor Browser Android.

Bug 41094: Enable HTTPS-Only Mode by default in Tor Browser Android.

Turn shouldUseHttpsOnly's default to true.

Bug 40225: Bundled extensions don't get updated with Android Tor
           Browser updates.

Bug 40030: Install NoScript addon on startup.

Also 40070: Consider storing the list of recommended addons

This implements our own AddonsProvider, which loads the list of
available addons from assets instead of fetching it from an
endpoint.

Also, we hide the uninstall button for builtin addons.

Bug 40058: Hide option for disallowing addon in private mode

Diffstat:
Mmobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtension.kt | 2++
Mmobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtension.kt | 13+++++++++++++
Mmobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/Addon.kt | 3+++
Mmobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/PermissionsDialogFragment.kt | 3+++
Mmobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionSupport.kt | 1+
Mmobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt | 3+--
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt | 10++++++++--
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragment.kt | 11+++++++++--
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Core.kt | 1+
Amobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/TorBrowserFeatures.kt | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt | 17++++++++++++++++-
Mmobile/android/fenix/app/src/main/res/values/preference_keys.xml | 4++++
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/menu/MenuNavigationMiddlewareTest.kt | 2+-
Mmobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java | 29+++++++++++++++++++++++++++++
Mmobile/shared/.eslintrc.mjs | 1+
Mmobile/shared/modules/geckoview/GeckoViewWebExtension.sys.mjs | 7++++++-
Mtoolkit/mozapps/extensions/AddonManager.sys.mjs | 26++++++++++++++++++++++++++
Mtoolkit/mozapps/extensions/components.conf | 19+++++++++----------
Mtoolkit/mozapps/extensions/internal/XPIInstall.sys.mjs | 8++++++++
19 files changed, 276 insertions(+), 19 deletions(-)

diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtension.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtension.kt @@ -382,6 +382,7 @@ class GeckoWebExtension( temporary = it.temporary, detailUrl = it.amoListingUrl, incognito = Incognito.fromString(it.incognito), + defaultPrivateBrowsingAllowed = it.allowedInPrivateBrowsing, ) } } @@ -396,6 +397,7 @@ class GeckoWebExtension( override fun isAllowedInPrivateBrowsing(): Boolean { return isBuiltIn() || nativeExtension.metaData.allowedInPrivateBrowsing + || isBundled() } override suspend fun loadIcon(size: Int): Bitmap? { diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtension.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtension.kt @@ -165,6 +165,14 @@ abstract class WebExtension( open fun isBuiltIn(): Boolean = url.toUri().scheme == "resource" /** + * Checks whether or not this extension is bundled with this browser, + * but otherwise behaves as an unprivileged (non built-in) extension, + * except it cannot be disabled or uninstalled from the UI (e.g. + * NoScript in the Tor Browser). + */ + open fun isBundled(): Boolean = id == "{73a6fe31-595d-460b-a920-fcc0f8843232}" + + /** * Checks whether or not this extension is enabled. */ abstract fun isEnabled(): Boolean @@ -486,6 +494,11 @@ data class Metadata( * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/incognito */ val incognito: Incognito, + + /** + * Wether this extension should default to pbm-allowed because being installed in global PBM + */ + val defaultPrivateBrowsingAllowed : Boolean = false, ) /** diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/Addon.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/Addon.kt @@ -54,6 +54,7 @@ val logger = Logger("Addon") * @property ratingUrl The link to the ratings page (user reviews) for this [Addon]. * @property detailUrl The link to the detail page for this [Addon]. * @property incognito Indicates how the extension works with private browsing windows. + * @property defaultPrivateBrowsingAllowed whether the extension should default to pbm-enabled. */ @SuppressLint("ParcelCreator") @Parcelize @@ -81,6 +82,7 @@ data class Addon( val ratingUrl: String = "", val detailUrl: String = "", val incognito: Incognito = Incognito.SPANNING, + val defaultPrivateBrowsingAllowed: Boolean = false, ) : Parcelable { /** @@ -660,6 +662,7 @@ data class Addon( detailUrl = detailUrl, incognito = incognito, installedState = installedState, + defaultPrivateBrowsingAllowed = metadata?.defaultPrivateBrowsingAllowed == true, ) } diff --git a/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/PermissionsDialogFragment.kt b/mobile/android/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/ui/PermissionsDialogFragment.kt @@ -287,6 +287,9 @@ class PermissionsDialogFragment : AddonDialogFragment() { ) { optionalsSettingsTitle.isVisible = false allowedInPrivateBrowsing.isVisible = false + } else { + allowedInPrivateBrowsing.isChecked = addon.defaultPrivateBrowsingAllowed + allowedInPrivateBrowsing.isVisible = !addon.defaultPrivateBrowsingAllowed } if (dataCollectionPermissions.contains(TECHNICAL_AND_INTERACTION_PERM) && !forOptionalPermissions) { diff --git a/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionSupport.kt b/mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionSupport.kt @@ -249,6 +249,7 @@ object WebExtensionSupport { // when the add-on has already been installed, we don't need to show anything // either. val shouldDispatchAction = !installedExtensions.containsKey(extension.id) && !extension.isBuiltIn() + && !extension.isBundled() registerInstalledExtension(store, extension) if (shouldDispatchAction) { store.dispatch( diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt @@ -156,8 +156,7 @@ class BrowserRobot(private val composeTestRule: ComposeTestRule) { verifyUrl("firefox.com/en-US/firefox/android/") } catch (e: AssertionError) { Log.i(TAG, "verifyWhatsNewURL: AssertionError caught, checking redirect URL") - val redirectURL = SupportUtils.WHATS_NEW_URL.toUri() - verifyUrl(redirectURL.authority?.removePrefix("www.") + redirectURL.encodedPath) + verifyUrl(SupportUtils.getTorWhatsNewUrl()) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -112,6 +112,7 @@ import org.mozilla.fenix.session.VisibilityLifecycleCallback import org.mozilla.fenix.settings.doh.DefaultDohSettingsProvider import org.mozilla.fenix.settings.doh.DohSettingsProvider import org.mozilla.fenix.startupCrash.StartupCrashActivity +import org.mozilla.fenix.tor.RunOnceBootstrapped import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.isLargeScreenSize import org.mozilla.fenix.wallpapers.Wallpaper @@ -736,8 +737,13 @@ open class FenixApplication : LocaleAwareApplication(), Provider { components.useCases.tabsUseCases.selectTab(sessionId) }, onExtensionsLoaded = { extensions -> - components.addonUpdater.registerForFutureUpdates(extensions) - subscribeForNewAddonsIfNeeded(components.supportedAddonsChecker, extensions) + // Delay until bootstrap is finished so that it will actually update tor-browser#44303 + components.torController.registerRunOnceBootstrapped(object : RunOnceBootstrapped { + override fun onBootstrapped() { + components.addonUpdater.registerForFutureUpdates(extensions) + subscribeForNewAddonsIfNeeded(components.supportedAddonsChecker, extensions) + } + }) // Bug 1948634 - Make sure the webcompat-reporter extension is fully uninstalled. // This is added here because we need gecko to load the extension first. diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragment.kt @@ -29,6 +29,7 @@ import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.ktx.android.content.appName import mozilla.components.support.ktx.android.content.appVersionName import org.mozilla.fenix.BuildConfig +import mozilla.components.support.webextensions.WebExtensionSupport.installedExtensions import org.mozilla.fenix.databinding.FragmentInstalledAddOnDetailsBinding import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.runIfFragmentIsAttached @@ -51,6 +52,8 @@ class InstalledAddonDetailsFragment : Fragment() { @Suppress("VariableNaming") internal var _binding: FragmentInstalledAddOnDetailsBinding? = null + private var isBundledAddon = false + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -58,6 +61,7 @@ class InstalledAddonDetailsFragment : Fragment() { ): View { if (!::addon.isInitialized) { addon = AddonDetailsFragmentArgs.fromBundle(requireNotNull(arguments)).addon + isBundledAddon = installedExtensions[addon.id]?.isBundled() ?: false } setBindingAndBindUI( @@ -194,6 +198,7 @@ class InstalledAddonDetailsFragment : Fragment() { // When the ad-on is blocklisted or not correctly signed, we do not want to enable the toggle switch // because users shouldn't be able to re-enable an add-on in this state. if ( + isBundledAddon || addon.isDisabledAsBlocklisted() || addon.isDisabledAsNotCorrectlySigned() || addon.isDisabledAsIncompatible() @@ -212,7 +217,7 @@ class InstalledAddonDetailsFragment : Fragment() { runIfFragmentIsAttached { this.addon = it switch.isClickable = true - privateBrowsingSwitch.isVisible = it.isEnabled() + privateBrowsingSwitch.isVisible = false privateBrowsingSwitch.isChecked = it.incognito != Addon.Incognito.NOT_ALLOWED && it.isAllowedInPrivateBrowsing() binding.settings.isVisible = shouldSettingsBeVisible() @@ -294,7 +299,7 @@ class InstalledAddonDetailsFragment : Fragment() { @VisibleForTesting internal fun bindAllowInPrivateBrowsingSwitch() { val switch = providePrivateBrowsingSwitch() - switch.isVisible = addon.isEnabled() + switch.isVisible = false if (addon.incognito == Addon.Incognito.NOT_ALLOWED) { switch.isChecked = false @@ -333,6 +338,7 @@ class InstalledAddonDetailsFragment : Fragment() { } private fun bindReportButton() { + binding.reportAddOn.isVisible = !isBundledAddon binding.reportAddOn.setOnClickListener { v -> val shouldCreatePrivateSession = v.context.components.appStore.state.mode.isPrivate @@ -396,6 +402,7 @@ class InstalledAddonDetailsFragment : Fragment() { } private fun bindRemoveButton() { + binding.removeAddOn.isVisible = !isBundledAddon binding.removeAddOn.setOnClickListener { setAllInteractiveViewsClickable(binding, false) requireContext().components.addonManager.uninstallAddon( diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Core.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -238,6 +238,7 @@ class Core( geckoRuntime, ).also { WebCompatFeature.install(it) + TorBrowserFeatures.install(context, it) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/TorBrowserFeatures.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/TorBrowserFeatures.kt @@ -0,0 +1,135 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Copyright (c) 2020, The Tor Project, Inc. + +package org.mozilla.fenix.components + +import android.os.StrictMode +import android.content.Context +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.IOException +import mozilla.components.concept.engine.webextension.WebExtension +import mozilla.components.concept.engine.webextension.WebExtensionRuntime +import mozilla.components.support.webextensions.WebExtensionSupport +import mozilla.components.support.base.log.logger.Logger +import org.mozilla.fenix.ext.settings + +object TorBrowserFeatures { + private val logger = Logger("torbrowser-features") + private const val NOSCRIPT_ID = "{73a6fe31-595d-460b-a920-fcc0f8843232}" + + private fun installNoScript( + context: Context, + runtime: WebExtensionRuntime, + onSuccess: ((WebExtension) -> Unit), + onError: ((Throwable) -> Unit) + ) { + /** + * Copy the xpi from assets to cacheDir, we do not care if the file is later deleted. + */ + val xpiName = "$NOSCRIPT_ID.xpi" + val addonPath = context.cacheDir.resolve(xpiName) + val policy = StrictMode.getThreadPolicy() + try { + context.assets.open("extensions/$xpiName") + .use { inStream -> + // we don't want penaltyDeath() on disk write + StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.LAX) + + addonPath.outputStream().use { outStream -> + inStream.copyTo(outStream) + } + } + } catch (throwable: IOException) { + onError(throwable) + return + } finally { + StrictMode.setThreadPolicy(policy) + } + + /** + * Install with a file:// URI pointing to the temp location where the addon was copied to. + */ + runtime.installWebExtension( + url = addonPath.toURI().toString(), + onSuccess = { extension -> + runtime.setAllowedInPrivateBrowsing( + extension, + true, + onSuccess, + onError + ) + }, + onError = { throwable -> onError(throwable) }) + } + + @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage + private fun uninstallHTTPSEverywhere( + runtime: WebExtensionRuntime, + onSuccess: (() -> Unit), + onError: ((Throwable) -> Unit) + ) { + // Wait for WebExtensionSupport on the I/O thread to avoid deadlocks. + GlobalScope.launch(Dispatchers.IO) { + WebExtensionSupport.awaitInitialization() + // Back to the main thread. + withContext(Dispatchers.Main) { + val extension = + WebExtensionSupport.installedExtensions["https-everywhere-eff@eff.org"] + ?: return@withContext onSuccess() // Fine, nothing to uninstall. + runtime.uninstallWebExtension( + extension, + onSuccess = onSuccess, + onError = { _, throwable -> onError(throwable) } + ) + } + } + } + + fun install(context: Context, runtime: WebExtensionRuntime) { + val settings = context.settings() + /** + * Remove HTTPS Everywhere if we didn't yet. + */ + if (!settings.httpsEverywhereRemoved) { + /** + * Ensure HTTPS-Only is enabled. + */ + settings.shouldUseHttpsOnly = true + settings.shouldUseHttpsOnlyInAllTabs = true + uninstallHTTPSEverywhere( + runtime, + onSuccess = { + settings.httpsEverywhereRemoved = true + logger.debug("HTTPS Everywhere extension was uninstalled successfully") + }, + onError = { throwable -> + logger.error("Could not uninstall HTTPS Everywhere extension", throwable) + } + ) + } + /** + * Install NoScript as a user WebExtension if we have not already done so. + * AMO signature is checked, but automatic updates still need to be enabled. + */ + if (!settings.noscriptInstalled) { + installNoScript( + context, + runtime, + onSuccess = { + settings.noscriptInstalled = true + logger.debug("NoScript extension was installed successfully") + }, + onError = { throwable -> + logger.error("Could not install NoScript extension", throwable) + } + ) + } + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -1043,7 +1043,7 @@ class Settings( var shouldUseHttpsOnly by booleanPreference( appContext.getPreferenceKey(R.string.pref_key_https_only), - default = false, + default = true ) var shouldUseHttpsOnlyInAllTabs by booleanPreference( @@ -2427,6 +2427,21 @@ class Settings( default = false, ) + var noscriptInstalled by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_noscript_installed), + default = false + ) + + var noscriptUpdated by intPreference( + appContext.getPreferenceKey(R.string.pref_key_noscript_updated), + default = 0 + ) + + var httpsEverywhereRemoved by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_https_everywhere_removed), + default = false + ) + /** * Indicates if hidden engines were restored due to migration to unified search settings UI. * Should be removed once we expect the majority of the users to migrate. diff --git a/mobile/android/fenix/app/src/main/res/values/preference_keys.xml b/mobile/android/fenix/app/src/main/res/values/preference_keys.xml @@ -525,4 +525,8 @@ <!-- Tab Search --> <string name="pref_key_tab_search" translatable="false">pref_key_tab_search_feature</string> + + <string name="pref_key_noscript_installed" translatable="false">pref_key_noscript_installed</string> + <string name="pref_key_noscript_updated" translatable="false">pref_key_noscript_updated</string> + <string name="pref_key_https_everywhere_removed" translatable="false">pref_key_https_everywhere_removed</string> </resources> diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/menu/MenuNavigationMiddlewareTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/menu/MenuNavigationMiddlewareTest.kt @@ -250,7 +250,7 @@ class MenuNavigationMiddlewareTest { store.dispatch(MenuAction.Navigate.ReleaseNotes) - assertEquals(SupportUtils.WHATS_NEW_URL, params?.url) + assertEquals(SupportUtils.getTorWhatsNewUrl(), params?.url) } @Test diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java @@ -1186,6 +1186,27 @@ public class WebExtensionController { }); } + private boolean isBundledExtension(final String extensionId) { + return "{73a6fe31-595d-460b-a920-fcc0f8843232}".equals(extensionId); + } + + private boolean promptBypass(final WebExtension extension, final EventCallback callback) { + // allow bundled extensions, e.g. NoScript, to be installed with no prompt + if (isBundledExtension(extension.id)) { + callback.resolveTo( + GeckoResult.allow().map( + allowOrDeny -> { + final GeckoBundle response = new GeckoBundle(1); + response.putBoolean("allow", true); + return response; + } + ) + ); + return true; + } + return false; + } + private void installPromptRequest(final GeckoBundle message, final EventCallback callback) { final GeckoBundle extensionBundle = message.getBundle("extension"); if (extensionBundle == null @@ -1201,6 +1222,10 @@ public class WebExtensionController { final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle); + if (promptBypass(extension, callback)) { + return; + } + if (mPromptDelegate == null) { Log.e( LOGTAG, "Tried to install extension " + extension.id + " but no delegate is registered"); @@ -1239,6 +1264,10 @@ public class WebExtensionController { final String[] newDataCollectionPermissions = message.getStringArray("newDataCollectionPermissions"); + if (promptBypass(extension, callback)) { + return; + } + if (mPromptDelegate == null) { Log.e(LOGTAG, "Tried to update extension " + extension.id + " but no delegate is registered"); return; diff --git a/mobile/shared/.eslintrc.mjs b/mobile/shared/.eslintrc.mjs @@ -11,6 +11,7 @@ export default [ "actors/**", ], rules: { + complexity: ["error", 35], "no-unused-vars": [ "error", { diff --git a/mobile/shared/modules/geckoview/GeckoViewWebExtension.sys.mjs b/mobile/shared/modules/geckoview/GeckoViewWebExtension.sys.mjs @@ -370,7 +370,9 @@ async function exportExtension(aAddon, aSourceURI) { privateBrowsingAllowed = policy.privateBrowsingAllowed; } else { const { permissions } = await lazy.ExtensionPermissions.get(aAddon.id); - privateBrowsingAllowed = permissions.includes(PRIVATE_BROWSING_PERM_NAME); + privateBrowsingAllowed = + permissions.includes(PRIVATE_BROWSING_PERM_NAME) || + lazy.PrivateBrowsingUtils.permanentPrivateBrowsing; } let updateDate; @@ -535,6 +537,9 @@ class ExtensionInstallListener { async onInstallEnded(aInstall, aAddon) { debug`onInstallEnded addonId=${aAddon.id}`; + if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) { + await GeckoViewWebExtension.setPrivateBrowsingAllowed(aAddon.id, true); + } const extension = await exportExtension(aAddon, aInstall.sourceURI); this.resolve({ extension }); } diff --git a/toolkit/mozapps/extensions/AddonManager.sys.mjs b/toolkit/mozapps/extensions/AddonManager.sys.mjs @@ -87,8 +87,11 @@ ChromeUtils.defineESModuleGetters(lazy, { AbuseReporter: "resource://gre/modules/AbuseReporter.sys.mjs", AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs", DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", + GeckoViewWebExtension: "resource://gre/modules/GeckoViewWebExtension.sys.mjs", + EventDispatcher: "resource://gre/modules/Messaging.sys.mjs", Extension: "resource://gre/modules/Extension.sys.mjs", ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", TelemetryTimestamps: "resource://gre/modules/TelemetryTimestamps.sys.mjs", TelemetryUtils: "resource://gre/modules/TelemetryUtils.sys.mjs", @@ -2430,6 +2433,24 @@ var AddonManagerInternal = { return promiseInstall; }, + async installGeckoViewWebExtension(extensionUri) { + const installId = Services.uuid.generateUUID().toString(); + let { extension } = await lazy.GeckoViewWebExtension.installWebExtension( + installId, + extensionUri + ); + if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) { + extension = await lazy.GeckoViewWebExtension.setPrivateBrowsingAllowed( + extension.webExtensionId, + true + ); + } + await lazy.EventDispatcher.instance.sendRequest({ + type: "GeckoView:WebExtension:OnInstalled", + extension, + }); + }, + /** * Starts installation of an AddonInstall notifying the registered * web install listener of a blocked or started install. @@ -2602,6 +2623,11 @@ var AddonManagerInternal = { ); if (installAllowed) { + if (AppConstants.platform == "android") { + aInstall.cancel(); + this.installGeckoViewWebExtension(aInstall.sourceURI); + return; + } startInstall("AMO"); } else if (installPerm === Ci.nsIPermissionManager.DENY_ACTION) { // Block without prompt diff --git a/toolkit/mozapps/extensions/components.conf b/toolkit/mozapps/extensions/components.conf @@ -33,14 +33,13 @@ Classes = [ 'esModule': 'resource://gre/modules/amWebAPI.sys.mjs', 'constructor': 'WebAPI', }, + # tor-browser#43132: re-enable XPI handler on Android to allow scriptless extensions installation. + # This reverts https://bugzilla.mozilla.org/show_bug.cgi?id=1610571, which made sense for generic + # GeckoView extensionless embedders and for Firefox, relying on navigator.mozAddonManager. + { + 'cid': '{7beb3ba8-6ec3-41b4-b67c-da89b8518922}', + 'contract_ids': ['@mozilla.org/uriloader/content-handler;1?type=application/x-xpinstall'], + 'esModule': 'resource://gre/modules/amContentHandler.sys.mjs', + 'constructor': 'amContentHandler', + }, ] - -if buildconfig.substs['MOZ_WIDGET_TOOLKIT'] != 'android': - Classes += [ - { - 'cid': '{7beb3ba8-6ec3-41b4-b67c-da89b8518922}', - 'contract_ids': ['@mozilla.org/uriloader/content-handler;1?type=application/x-xpinstall'], - 'esModule': 'resource://gre/modules/amContentHandler.sys.mjs', - 'constructor': 'amContentHandler', - }, - ] diff --git a/toolkit/mozapps/extensions/internal/XPIInstall.sys.mjs b/toolkit/mozapps/extensions/internal/XPIInstall.sys.mjs @@ -4684,6 +4684,14 @@ export var XPIInstall = { return false; } + // tor-browser#43132: short-circuit permission check on Android scriptless install from AMO + if ( + AppConstants.platform == "android" && + uri.prePath == "https://addons.mozilla.org" + ) { + return true; + } + let requireWhitelist = Services.prefs.getBoolPref( PREF_XPI_WHITELIST_REQUIRED, true