ma1 pushed to branch tor-browser-128.2.0esr-14.0-1 at The Tor Project / Applications / Tor Browser
Commits: 4eb9b64f by hackademix at 2024-09-11T21:59:25+02:00 fixup! [android] Modify add-on support
Bug 43097: Use default (non-builtin) extension installation method which works with xpi files.
- - - - -
6 changed files:
- mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtension.kt - mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtension.kt - mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionSupport.kt - mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragment.kt - mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/TorBrowserFeatures.kt - mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java
Changes:
===================================== mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtension.kt ===================================== @@ -393,6 +393,7 @@ class GeckoWebExtension(
override fun isAllowedInPrivateBrowsing(): Boolean { return isBuiltIn() || nativeExtension.metaData.allowedInPrivateBrowsing + || isBundled() }
override suspend fun loadIcon(size: Int): Bitmap? {
===================================== mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/webextension/WebExtension.kt ===================================== @@ -164,6 +164,14 @@ abstract class WebExtension( */ open fun isBuiltIn(): Boolean = Uri.parse(url).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. */
===================================== mobile/android/android-components/components/support/webextensions/src/main/java/mozilla/components/support/webextensions/WebExtensionSupport.kt ===================================== @@ -234,6 +234,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(
===================================== mobile/android/fenix/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragment.kt ===================================== @@ -44,6 +44,8 @@ class InstalledAddonDetailsFragment : Fragment() {
private var _binding: FragmentInstalledAddOnDetailsBinding? = null
+ private var isBundledAddon = false; + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -51,6 +53,7 @@ class InstalledAddonDetailsFragment : Fragment() { ): View { if (!::addon.isInitialized) { addon = AddonDetailsFragmentArgs.fromBundle(requireNotNull(arguments)).addon + isBundledAddon = installedExtensions[addon.id]?.isBundled() ?: false }
setBindingAndBindUI( @@ -148,6 +151,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() @@ -303,6 +307,7 @@ class InstalledAddonDetailsFragment : Fragment() { }
private fun bindReportButton() { + binding.reportAddOn.isVisible = !isBundledAddon binding.reportAddOn.setOnClickListener { val shouldCreatePrivateSession = (activity as HomeActivity).browsingModeManager.mode.isPrivate
@@ -367,8 +372,7 @@ class InstalledAddonDetailsFragment : Fragment() { }
private fun bindRemoveButton() { - val isBuiltin = installedExtensions[addon.id]?.isBuiltIn() ?: false - binding.removeAddOn.isVisible = !isBuiltin + binding.removeAddOn.isVisible = !isBundledAddon binding.removeAddOn.setOnClickListener { setAllInteractiveViewsClickable(binding, false) requireContext().components.addonManager.uninstallAddon(
===================================== mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/TorBrowserFeatures.kt ===================================== @@ -6,12 +6,14 @@
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 @@ -25,14 +27,39 @@ object TorBrowserFeatures { 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)
- runtime.installBuiltInWebExtension( - id = NOSCRIPT_ID, - url = "resource://android/assets/extensions/" + NOSCRIPT_ID + ".xpi", + 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, @@ -95,6 +122,7 @@ object TorBrowserFeatures { */ if (!settings.noscriptInstalled) { installNoScript( + context, runtime, onSuccess = { settings.noscriptInstalled = true
===================================== mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java ===================================== @@ -1166,6 +1166,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 installPrompt(final GeckoBundle message, final EventCallback callback) { final GeckoBundle extensionBundle = message.getBundle("extension"); if (extensionBundle == null @@ -1181,6 +1202,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"); @@ -1220,6 +1245,10 @@ public class WebExtensionController { final WebExtension currentExtension = new WebExtension(mDelegateControllerProvider, currentBundle);
+ if (promptBypass(currentExtension, callback)) { + return; + } + final WebExtension updatedExtension = new WebExtension(mDelegateControllerProvider, updatedBundle);
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/4eb9b64f...