
ma1 pushed to branch base-browser-140.1.0esr-15.0-1 at The Tor Project / Applications / Tor Browser Commits: 076f29f7 by Michel Le Bihan at 2025-07-24T08:08:48+02:00 Bug 1961829 - Only color HTTP(S) URIs and fallback to coloring the host in Android toolbar URLRenderer. r=tthibaud,android-reviewers Differential Revision: https://phabricator.services.mozilla.com/D248132 - - - - - ea1dd032 by Michel Le Bihan at 2025-07-24T08:08:56+02:00 Bug 1961757 - Set text direction in Android toolbar instead of adding directional marks. r=tthibaud,android-reviewers,petru Differential Revision: https://phabricator.services.mozilla.com/D246181 - - - - - 39979386 by Michel Le Bihan at 2025-07-24T08:08:58+02:00 Bug 1812898 - Part 1: Add domain alignment in Android toolbar component. r=tthibaud,android-reviewers Differential Revision: https://phabricator.services.mozilla.com/D244508 - - - - - c1e7fc82 by Michel Le Bihan at 2025-07-24T08:09:01+02:00 Bug 1812898 - Part 2: Enable domain highlighting in Fenix toolbar. r=tthibaud,android-reviewers Differential Revision: https://phabricator.services.mozilla.com/D244509 - - - - - 9e5383b4 by mimi89999 at 2025-07-24T08:09:03+02:00 Bug 1964251 - Replace logic of RegistrableDomain renderStyle in Android toolbar component. r=android-reviewers,petru Differential Revision: https://phabricator.services.mozilla.com/D251501 - - - - - 7ab9ea81 by Michel Le Bihan at 2025-07-24T08:09:05+02:00 Bug 1969937 - Add handling of blob URIs in Android toolbar URLRenderer. r=petru,android-reviewers Differential Revision: https://phabricator.services.mozilla.com/D252879 - - - - - 04c4f093 by Cathy Lu at 2025-07-24T08:09:08+02:00 Bug 1791322 - iframe sandbox wpt tests modified with delay r=nika Differential Revision: https://phabricator.services.mozilla.com/D253052 - - - - - 85f273f4 by Cathy Lu at 2025-07-24T08:09:11+02:00 Bug 1791322 - GeckoView should call classifyDownloads to sandbox downloads r=geckoview-reviewers,nika Differential Revision: https://phabricator.services.mozilla.com/D249683 - - - - - 2d41c7c1 by Andreas Pehrson at 2025-07-24T08:09:13+02:00 Bug 1971116 - For global mute events, iterate on copies of containers. r=dbaker Mute/unmute events are fired synchronously to content, which if it stops an (event target) track in the event handler, may call back into and mutate the containers we're iterating over. Differential Revision: https://phabricator.services.mozilla.com/D254352 - - - - - 817aea0e by Tom Schuster at 2025-07-24T08:09:16+02:00 Bug 1971704 - Cleanup nsContentSecurityUtils::ClassifyDownload. r=smaug Differential Revision: https://phabricator.services.mozilla.com/D253491 - - - - - 6cc06b71 by Pier Angelo Vendrame at 2025-07-24T08:09:18+02:00 Bug 1972282 - Check for spoof English in xsl:sort. r=smaug Differential Revision: https://phabricator.services.mozilla.com/D254784 - - - - - 20 changed files: - dom/media/MediaManager.cpp - dom/security/nsContentSecurityUtils.cpp - dom/security/nsContentSecurityUtils.h - dom/xslt/xpath/txXPathNode.h - dom/xslt/xslt/txNodeSorter.cpp - mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/OriginView.kt - + mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/OriginViewTest.kt - mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/Toolbar.kt - mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarFeature.kt - mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/internal/URLRenderer.kt - mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/internal/URLRendererTest.kt - mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/ktx/util/URLStringUtils.kt - mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/URLStringUtilsTest.kt - mobile/android/components/geckoview/GeckoViewStreamListener.cpp - mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt - testing/web-platform/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_anchor_download_block_downloads.tentative.html.ini - testing/web-platform/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_navigation_download_block_downloads.sub.tentative.html.ini - testing/web-platform/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_window_open_download_block_downloads.tentative.html.ini - testing/web-platform/tests/html/semantics/embedded-content/the-iframe-element/support/iframe_sandbox_download_helper.js - uriloader/exthandler/nsExternalHelperAppService.cpp Changes: ===================================== dom/media/MediaManager.cpp ===================================== @@ -3568,7 +3568,9 @@ void MediaManager::OnCameraMute(bool aMute) { mCamerasMuted = aMute; // This is safe since we're on main-thread, and the windowlist can only // be added to from the main-thread - for (const auto& window : mActiveWindows.Values()) { + for (const auto& window : + ToTArray<AutoTArray<RefPtr<GetUserMediaWindowListener>, 2>>( + mActiveWindows.Values())) { window->MuteOrUnmuteCameras(aMute); } } @@ -3579,7 +3581,9 @@ void MediaManager::OnMicrophoneMute(bool aMute) { mMicrophonesMuted = aMute; // This is safe since we're on main-thread, and the windowlist can only // be added to from the main-thread - for (const auto& window : mActiveWindows.Values()) { + for (const auto& window : + ToTArray<AutoTArray<RefPtr<GetUserMediaWindowListener>, 2>>( + mActiveWindows.Values())) { window->MuteOrUnmuteMicrophones(aMute); } } @@ -4767,7 +4771,7 @@ void GetUserMediaWindowListener::MuteOrUnmuteCameras(bool aMute) { } mCamerasAreMuted = aMute; - for (auto& l : mActiveListeners) { + for (auto& l : mActiveListeners.Clone()) { if (l->GetDevice()->Kind() == MediaDeviceKind::Videoinput) { l->MuteOrUnmuteCamera(aMute); } @@ -4782,7 +4786,7 @@ void GetUserMediaWindowListener::MuteOrUnmuteMicrophones(bool aMute) { } mMicrophonesAreMuted = aMute; - for (auto& l : mActiveListeners) { + for (auto& l : mActiveListeners.Clone()) { if (l->GetDevice()->Kind() == MediaDeviceKind::Audioinput) { l->MuteOrUnmuteMicrophone(aMute); } ===================================== dom/security/nsContentSecurityUtils.cpp ===================================== @@ -2206,11 +2206,17 @@ void nsContentSecurityUtils::LogMessageToConsole(nsIHttpChannel* aChannel, } /* static */ -long nsContentSecurityUtils::ClassifyDownload( - nsIChannel* aChannel, const nsAutoCString& aMimeTypeGuess) { +long nsContentSecurityUtils::ClassifyDownload(nsIChannel* aChannel) { MOZ_ASSERT(aChannel, "IsDownloadAllowed without channel?"); nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + if ((loadInfo->GetTriggeringSandboxFlags() & SANDBOXED_ALLOW_DOWNLOADS) || + (loadInfo->GetSandboxFlags() & SANDBOXED_ALLOW_DOWNLOADS)) { + if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel)) { + LogMessageToConsole(httpChannel, "IframeSandboxBlockedDownload"); + } + return nsITransfer::DOWNLOAD_FORBIDDEN; + } nsCOMPtr<nsIURI> contentLocation; aChannel->GetURI(getter_AddRefs(contentLocation)); @@ -2243,27 +2249,11 @@ long nsContentSecurityUtils::ClassifyDownload( if (StaticPrefs::dom_block_download_insecure() && decission != nsIContentPolicy::ACCEPT) { - nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel); - if (httpChannel) { + if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel)) { LogMessageToConsole(httpChannel, "MixedContentBlockedDownload"); } return nsITransfer::DOWNLOAD_POTENTIALLY_UNSAFE; } - if (loadInfo->TriggeringPrincipal()->IsSystemPrincipal()) { - return nsITransfer::DOWNLOAD_ACCEPTABLE; - } - - uint32_t triggeringFlags = loadInfo->GetTriggeringSandboxFlags(); - uint32_t currentflags = loadInfo->GetSandboxFlags(); - - if ((triggeringFlags & SANDBOXED_ALLOW_DOWNLOADS) || - (currentflags & SANDBOXED_ALLOW_DOWNLOADS)) { - nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel); - if (httpChannel) { - LogMessageToConsole(httpChannel, "IframeSandboxBlockedDownload"); - } - return nsITransfer::DOWNLOAD_FORBIDDEN; - } return nsITransfer::DOWNLOAD_ACCEPTABLE; } ===================================== dom/security/nsContentSecurityUtils.h ===================================== @@ -74,8 +74,7 @@ class nsContentSecurityUtils { const mozilla::dom::Element& aElement); // Helper function to Check if a Download is allowed; - static long ClassifyDownload(nsIChannel* aChannel, - const nsAutoCString& aMimeTypeGuess); + static long ClassifyDownload(nsIChannel* aChannel); // Public only for testing static FilenameTypeAndDetails FilenameToFilenameType( ===================================== dom/xslt/xpath/txXPathNode.h ===================================== @@ -66,6 +66,8 @@ class txXPathNode { bool operator!=(const txXPathNode& aNode) const { return !(*this == aNode); } ~txXPathNode() { MOZ_COUNT_DTOR(txXPathNode); } + mozilla::dom::Document* OwnerDoc() const { return mNode->OwnerDoc(); } + private: friend class txXPathNativeNode; friend class txXPathNodeUtils; ===================================== dom/xslt/xslt/txNodeSorter.cpp ===================================== @@ -13,10 +13,13 @@ #include "mozilla/CheckedInt.h" #include "mozilla/UniquePtrExtensions.h" +#include "nsRFPService.h" using mozilla::CheckedUint32; using mozilla::MakeUnique; using mozilla::MakeUniqueFallible; +using mozilla::nsRFPService; +using mozilla::RFPTarget; using mozilla::UniquePtr; /* @@ -74,6 +77,10 @@ nsresult txNodeSorter::addSortElement(Expr* aSelectExpr, Expr* aLangExpr, if (aLangExpr) { rv = aLangExpr->evaluateToString(aContext, lang); NS_ENSURE_SUCCESS(rv, rv); + } else if (aContext->getContextNode() + .OwnerDoc() + ->ShouldResistFingerprinting(RFPTarget::JSLocale)) { + CopyUTF8toUTF16(nsRFPService::GetSpoofedJSLocale(), lang); } // Case-order ===================================== mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/OriginView.kt ===================================== @@ -7,6 +7,7 @@ package mozilla.components.browser.toolbar.display import android.animation.LayoutTransition import android.content.Context import android.graphics.Typeface +import android.text.Spanned import android.util.AttributeSet import android.util.TypedValue import android.view.Gravity @@ -17,6 +18,7 @@ import androidx.annotation.VisibleForTesting import androidx.core.view.isVisible import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.R +import mozilla.components.concept.toolbar.Toolbar /** * View displaying the URL and optionally the title of a website. @@ -48,6 +50,9 @@ internal class OriginView @JvmOverloads constructor( isClickable = true isFocusable = true + textDirection = View.TEXT_DIRECTION_LTR + layoutDirection = View.LAYOUT_DIRECTION_LTR + setOnClickListener { if (onUrlClicked()) { toolbar.editMode() @@ -134,9 +139,50 @@ internal class OriginView @JvmOverloads constructor( titleView.setOnLongClickListener(handler) } + /** + * Scrolls the URL view to ensure the registrable domain is visible. + */ + @VisibleForTesting + internal fun scrollToShowRegistrableDomain() { + val text = urlView.text + + val spans = (text as? Spanned)?.getSpans( + 0, + text.length, + Toolbar.RegistrableDomainColorSpan::class.java, + ) + + if (spans?.size == 1) { + val registrableDomainSpan = (urlView.text as? Spanned)?.getSpans( + 0, + text.length, + Toolbar.RegistrableDomainColorSpan::class.java, + )?.getOrNull(0) + + val valueUntilRegistrableDomainEnd = text.subSequence(0, text.getSpanEnd(registrableDomainSpan)) + + val urlViewWidth = urlView.width + val valueWidth = measureUrlTextWidh(valueUntilRegistrableDomainEnd.toString()) + + if (valueWidth > urlViewWidth) { + urlView.scrollTo((valueWidth - urlViewWidth).toInt(), 0) + return + } + } + + urlView.scrollTo(0, 0) + } + + @VisibleForTesting + internal fun measureUrlTextWidh(text: String) = urlView.paint.measureText(text) + internal var url: CharSequence get() = urlView.text - set(value) { urlView.text = value } + set(value) { + urlView.text = value + + scrollToShowRegistrableDomain() + } /** * Sets the colour of the text to be displayed when the URL of the toolbar is empty. ===================================== mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/OriginViewTest.kt ===================================== @@ -0,0 +1,109 @@ +/* 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/. */ + +package mozilla.components.browser.toolbar.display + +import android.graphics.Color +import android.text.SpannableStringBuilder +import android.text.SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE +import android.text.style.ForegroundColorSpan +import android.view.View +import androidx.annotation.ColorInt +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.concept.toolbar.Toolbar +import mozilla.components.support.test.any +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.spy + +@RunWith(AndroidJUnit4::class) +class OriginViewTest { + + private fun SpannableStringBuilder.applyUrlColors( + @ColorInt urlColor: Int, + @ColorInt registrableDomainColor: Int, + registrableDomainOrHostSpan: Pair<Int, Int>, + ): SpannableStringBuilder = apply { + setSpan( + ForegroundColorSpan(urlColor), + 0, + length, + SPAN_INCLUSIVE_INCLUSIVE, + ) + + val (start, end) = registrableDomainOrHostSpan + setSpan( + Toolbar.RegistrableDomainColorSpan(registrableDomainColor), + start, + end, + SPAN_INCLUSIVE_INCLUSIVE, + ) + } + + @Test + fun `scrollToShowRegistrableDomain scrolls when domain exceeds width`() { + val view = spy(OriginView(testContext)) + val url = "https://www.really-long-example-domain.com/" + val spannedUrl = SpannableStringBuilder(url).apply { + applyUrlColors( + urlColor = Color.GREEN, + registrableDomainColor = Color.RED, + registrableDomainOrHostSpan = 8 to 42, + ) + } + + // Long domain wouldn't fit in the view + doReturn(500f).`when`(view).measureUrlTextWidh(any()) + view.urlView.layout(0, 0, 200, 100) + + view.url = spannedUrl + + assertEquals(300, view.urlView.scrollX) + } + + @Test + fun `scrollToShowRegistrableDomain does not scroll when domain fits in view`() { + val view = spy(OriginView(testContext)) + val url = "https://mozilla.org/" + val spannedUrl = SpannableStringBuilder(url).apply { + applyUrlColors( + urlColor = Color.GREEN, + registrableDomainColor = Color.RED, + registrableDomainOrHostSpan = 8 to 19, + ) + } + + doReturn(50f).`when`(view).measureUrlTextWidh(any()) + view.urlView.layout(0, 0, 200, 100) + + view.url = spannedUrl + + assertEquals(0, view.urlView.scrollX) + } + + @Test + fun `scrollToShowRegistrableDomain does not scroll when no span exists`() { + val view = OriginView(testContext) + + val spanned = SpannableStringBuilder("nospan.com") // no span set + + view.measure(0, 0) + view.layout(0, 0, 500, 100) + + view.url = spanned + + assertEquals(0, view.urlView.scrollX) + } + + @Test + fun `URL text direction is always LTR`() { + val originView = OriginView(testContext) + originView.url = "ختار.ار/www.mozilla.org/1" + assertEquals(View.TEXT_DIRECTION_LTR, originView.urlView.textDirection) + assertEquals(View.LAYOUT_DIRECTION_LTR, originView.urlView.layoutDirection) + } +} ===================================== mobile/android/android-components/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/Toolbar.kt ===================================== @@ -5,11 +5,13 @@ package mozilla.components.concept.toolbar import android.graphics.drawable.Drawable +import android.text.style.ForegroundColorSpan import android.view.View import android.view.View.NO_ID import android.view.ViewGroup import android.widget.ImageButton import android.widget.ImageView +import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.annotation.Dimension import androidx.annotation.Dimension.Companion.DP @@ -548,6 +550,13 @@ interface Toolbar : ScrollableToolbar { */ END, } + + /** + * Registrable domain foreground color span. + * + * This simple class extension is used so that we can filter for it elsewhere. + */ + class RegistrableDomainColorSpan(@ColorInt color: Int) : ForegroundColorSpan(color) } private fun AppCompatImageButton.setTintResource(@ColorRes tintColorResource: Int) { ===================================== mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarFeature.kt ===================================== @@ -88,11 +88,13 @@ class ToolbarFeature( ) /** - * Controls how the url should be styled + * Controls how the URL should be styled * * RegistrableDomain: displays only the eTLD+1 (direct subdomain of the public suffix), uncolored - * ColoredUrl: displays the registrableDomain with color and url with another color - * UncoloredUrl: displays the full url, uncolored + * ColoredUrl: displays the full URL with distinct colors for the registrable domain and the rest of the URL. + * Colors the entire hostname if the registrable domain cannot be determined or is an IP address. + * Leaves non http(s) URLs uncolored. + * UncoloredUrl: displays the full URL, uncolored */ sealed class RenderStyle { object RegistrableDomain : RenderStyle() ===================================== mobile/android/android-components/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/internal/URLRenderer.kt ===================================== @@ -18,6 +18,11 @@ import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.launch import mozilla.components.concept.toolbar.Toolbar import mozilla.components.feature.toolbar.ToolbarFeature +import mozilla.components.lib.publicsuffixlist.PublicSuffixList +import mozilla.components.support.ktx.android.net.isHttpOrHttps +import mozilla.components.support.ktx.kotlin.isIpv4OrIpv6 + +private const val BLOB_URL_PREFIX = "blob:" /** * Asynchronous URL renderer. @@ -73,13 +78,21 @@ internal class URLRenderer( toolbar.url = when (configuration.renderStyle) { // Display only the eTLD+1 (direct subdomain of the public suffix), uncolored ToolbarFeature.RenderStyle.RegistrableDomain -> { - val host = url.toUri().host?.ifEmpty { null } - host?.let { getRegistrableDomain(host, configuration) } ?: url + getRegistrableDomainOrHostSpan(url, configuration.publicSuffixList)?.let { (start, end) -> + url.substring(start, end) + } ?: url } // Display the registrableDomain with color and URL with another color ToolbarFeature.RenderStyle.ColoredUrl -> SpannableStringBuilder(url).apply { - color(configuration.urlColor) - colorRegistrableDomain(configuration) + val span = getRegistrableDomainOrHostSpan(url, configuration.publicSuffixList) + + if (configuration.urlColor != null && span != null) { + applyUrlColors( + configuration.urlColor, + configuration.registrableDomainColor, + span, + ) + } } // Display the full URL, uncolored ToolbarFeature.RenderStyle.UncoloredUrl -> url @@ -87,43 +100,98 @@ internal class URLRenderer( } } -private suspend fun getRegistrableDomain(host: String, configuration: ToolbarFeature.UrlRenderConfiguration) = - configuration.publicSuffixList.getPublicSuffixPlusOne(host).await() +/** + * Determines the position span of the registrable domain within a host string. + * + * @param host The host string to analyze + * @param publicSuffixList The [PublicSuffixList] used to get the eTLD+1 for the host + * @return A Pair of (startIndex, endIndex) for the registrable domain within the host, + * or null if the host is an IP address or no registrable domain could be found + */ +@VisibleForTesting +internal suspend fun getRegistrableDomainSpanInHost( + host: String, + publicSuffixList: PublicSuffixList, +): Pair<Int, Int>? { + if (host.isIpv4OrIpv6()) return null + + val normalizedHost = host.removeSuffix(".") + + val registrableDomain = publicSuffixList + .getPublicSuffixPlusOne(normalizedHost) + .await() ?: return null + + val start = normalizedHost.lastIndexOf(registrableDomain) + return if (start == -1) { + null + } else { + start to start + registrableDomain.length + } +} -private suspend fun SpannableStringBuilder.colorRegistrableDomain( - configuration: ToolbarFeature.UrlRenderConfiguration, -) { - val url = toString() - val host = url.toUri().host?.removeSuffix(".") ?: return - - val registrableDomain = configuration - .publicSuffixList - .getPublicSuffixPlusOne(host) - .await() ?: return - - val indexOfHost = url.indexOf(host) - val indexOfRegistrableDomain = host.lastIndexOf(registrableDomain) - if (indexOfHost == -1 || indexOfRegistrableDomain == -1) { - return +/** + * Determines the position span of either the registrable domain or the full host + * within a URL string. + * + * @param url The complete URL to analyze + * @param publicSuffixList The [PublicSuffixList] used to get the eTLD+1 for the host + * @param allowBlobUnwrapping Whether to allow unwrapping blob URLs + * @return A Pair of (startIndex, endIndex) for either: + * - The registrable domain's position within the URL, or + * - The host's position within the URL if no registrable domain was found, or + * - null if the URL has no host or the host couldn't be located in the URL + */ +@Suppress("ReturnCount") +@VisibleForTesting +internal suspend fun getRegistrableDomainOrHostSpan( + url: String, + publicSuffixList: PublicSuffixList, + allowBlobUnwrapping: Boolean = true, +): Pair<Int, Int>? { + if (url.startsWith(BLOB_URL_PREFIX)) { + if (!allowBlobUnwrapping) return null + + val innerUrl = url.substring(BLOB_URL_PREFIX.length) + return getRegistrableDomainOrHostSpan( + innerUrl, + publicSuffixList, + allowBlobUnwrapping = false, + )?.let { (start, end) -> + BLOB_URL_PREFIX.length + start to BLOB_URL_PREFIX.length + end + } } - val index = indexOfHost + indexOfRegistrableDomain + val uri = url.toUri() + if (!uri.isHttpOrHttps) return null - setSpan( - ForegroundColorSpan(configuration.registrableDomainColor), - index, - index + registrableDomain.length, - SPAN_INCLUSIVE_INCLUSIVE, - ) -} + val host = uri.host ?: return null + + val hostStart = url.indexOf(host) + if (hostStart == -1) return null -private fun SpannableStringBuilder.color(@ColorInt urlColor: Int?) { - urlColor ?: return + val domainSpan = getRegistrableDomainSpanInHost(host, publicSuffixList) + return domainSpan?.let { (start, end) -> + hostStart + start to hostStart + end + } ?: (hostStart to hostStart + host.length) +} +private fun SpannableStringBuilder.applyUrlColors( + @ColorInt urlColor: Int, + @ColorInt registrableDomainColor: Int, + registrableDomainOrHostSpan: Pair<Int, Int>, +): SpannableStringBuilder = apply { setSpan( ForegroundColorSpan(urlColor), 0, length, SPAN_INCLUSIVE_INCLUSIVE, ) + + val (start, end) = registrableDomainOrHostSpan + setSpan( + Toolbar.RegistrableDomainColorSpan(registrableDomainColor), + start, + end, + SPAN_INCLUSIVE_INCLUSIVE, + ) } ===================================== mobile/android/android-components/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/internal/URLRendererTest.kt ===================================== @@ -5,8 +5,10 @@ package mozilla.components.feature.toolbar.internal import android.graphics.Color +import android.net.InetAddresses import android.text.SpannableStringBuilder import android.text.style.ForegroundColorSpan +import android.util.Patterns import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.Dispatchers import mozilla.components.concept.toolbar.Toolbar @@ -26,8 +28,12 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.verify +import org.robolectric.annotation.Config +import org.robolectric.annotation.Implementation +import org.robolectric.annotation.Implements @RunWith(AndroidJUnit4::class) +@Config(shadows = [ShadowInetAddresses::class]) class URLRendererTest { @get:Rule @@ -104,10 +110,7 @@ class URLRendererTest { } } - private suspend fun testRenderWithColoredUrl( - testUrl: String, - expectedRegistrableDomainSpan: Pair<Int, Int>, - ) { + private suspend fun getSpannedUrl(testUrl: String): SpannableStringBuilder { val configuration = ToolbarFeature.UrlRenderConfiguration( publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined), registrableDomainColor = Color.RED, @@ -124,9 +127,14 @@ class URLRendererTest { val captor = argumentCaptor<CharSequence>() verify(toolbar).url = captor.capture() - assertNotNull(captor.value) - assertTrue(captor.value is SpannableStringBuilder) - val url = captor.value as SpannableStringBuilder + return requireNotNull(captor.value as? SpannableStringBuilder) { "Toolbar URL should not be null" } + } + + private suspend fun testRenderWithColoredUrl( + testUrl: String, + expectedRegistrableDomainSpan: Pair<Int, Int>, + ) { + val url = getSpannedUrl(testUrl) assertEquals(testUrl, url.toString()) @@ -143,8 +151,237 @@ class URLRendererTest { assertEquals(expectedRegistrableDomainSpan.second, url.getSpanEnd(spans[1])) } + private suspend fun testRenderWithUncoloredUrl(testUrl: String) { + val url = getSpannedUrl(testUrl) + + assertEquals(testUrl, url.toString()) + + val spans = url.getSpans(0, url.length, ForegroundColorSpan::class.java) + + assertEquals(0, spans.size) + } + + private suspend fun testRenderWithRegistrableDomain( + testUrl: String, + expectedUrl: String, + ) { + val configuration = ToolbarFeature.UrlRenderConfiguration( + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined), + registrableDomainColor = Color.RED, + urlColor = Color.GREEN, + renderStyle = ToolbarFeature.RenderStyle.RegistrableDomain, + ) + + val toolbar: Toolbar = mock() + + val renderer = URLRenderer(toolbar, configuration) + + renderer.updateUrl(testUrl) + + val captor = argumentCaptor<CharSequence>() + verify(toolbar).url = captor.capture() + + assertNotNull(captor.value) + assertTrue(captor.value is String) + val url = captor.value as String + + assertEquals(expectedUrl, url) + } + + @Test + fun `GIVEN a simple domain WHEN getting registrable domain span in host THEN span is returned`() { + runTestOnMain { + val domainSpan = getRegistrableDomainSpanInHost( + host = "www.mozilla.org", + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined), + ) + + assertEquals(4 to 15, domainSpan) + } + } + + @Test + fun `GIVEN a host with a trailing period in the domain WHEN getting registrable domain span in host THEN span is returned`() { + runTestOnMain { + val domainSpan = getRegistrableDomainSpanInHost( + host = "www.mozilla.org.", + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined), + ) + + assertEquals(4 to 15, domainSpan) + } + } + + @Test + fun `GIVEN a host with a repeated domain WHEN getting registrable domain span in host THEN the span of the last occurrence of domain is returned`() { + runTestOnMain { + val domainSpan = getRegistrableDomainSpanInHost( + host = "mozilla.org.mozilla.org", + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined), + ) + + assertEquals(12 to 23, domainSpan) + } + } + + @Test + fun `GIVEN an IPv4 address as host WHEN getting registrable domain span in host THEN null is returned`() { + runTestOnMain { + val domainSpan = getRegistrableDomainSpanInHost( + host = "127.0.0.1", + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined), + ) + + assertNull(domainSpan) + } + } + + @Test + fun `GIVEN an IPv6 address as host WHEN getting registrable domain span in host THEN null is returned`() { + runTestOnMain { + val domainSpan = getRegistrableDomainSpanInHost( + host = "[::1]", + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined), + ) + + assertNull(domainSpan) + } + } + + @Test + fun `GIVEN a non PSL domain as host WHEN getting registrable domain span in host THEN null is returned`() { + runTestOnMain { + val domainSpan = getRegistrableDomainSpanInHost( + host = "localhost", + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined), + ) + + assertNull(domainSpan) + } + } + + @Test + fun `GIVEN a simple URL WHEN getting registrable domain or host span THEN span is returned`() { + runTestOnMain { + val span = getRegistrableDomainOrHostSpan( + url = "https://www.mozilla.org/", + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined), + ) + + assertEquals(12 to 23, span) + } + } + + @Test + fun `GIVEN a URL with a trailing period in the domain WHEN getting registrable domain or host span THEN span is returned`() { + runTestOnMain { + val span = getRegistrableDomainOrHostSpan( + url = "https://www.mozilla.org./", + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined), + ) + + assertEquals(12 to 23, span) + } + } + + @Test + fun `GIVEN a URL with a repeated domain WHEN getting registrable domain or host span THEN the span of the last occurrence of domain is returned`() { + runTestOnMain { + val span = getRegistrableDomainOrHostSpan( + url = "https://mozilla.org.mozilla.org/", + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined), + ) + + assertEquals(20 to 31, span) + } + } + + @Test + fun `GIVEN a URL with an IPv4 address WHEN getting registrable domain or host span THEN the span of the IP part is returned`() { + runTestOnMain { + val span = getRegistrableDomainOrHostSpan( + url = "http://127.0.0.1/", + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined), + ) + + assertEquals(7 to 16, span) + } + } + + @Test + fun `GIVEN a URL with an IPv6 address WHEN getting registrable domain or host span THEN the span of the IP part is returned`() { + runTestOnMain { + val span = getRegistrableDomainOrHostSpan( + url = "http://[::1]/", + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined), + ) + + assertEquals(7 to 12, span) + } + } + @Test - fun `Render with simple URL`() { + fun `GIVEN a URL with a non PSL domain WHEN getting registrable domain or host span THEN the span of the host part is returned`() { + runTestOnMain { + val span = getRegistrableDomainOrHostSpan( + url = "http://localhost/", + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined), + ) + + assertEquals(7 to 16, span) + } + } + + @Test + fun `GIVEN an internal page name WHEN getting registrable domain or host span THEN null is returned`() { + runTestOnMain { + val span = getRegistrableDomainOrHostSpan( + url = "about:mozilla", + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined), + ) + + assertNull(span) + } + } + + @Test + fun `GIVEN a content URI WHEN getting registrable domain or host span THEN null is returned`() { + runTestOnMain { + val span = getRegistrableDomainOrHostSpan( + url = "content://media/external/file/1000000000", + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined), + ) + + assertNull(span) + } + } + + @Test + fun `GIVEN a blob URI WHEN getting registrable domain or host span THEN domain span is returned`() { + runTestOnMain { + val span = getRegistrableDomainOrHostSpan( + url = "blob:https://www.mozilla.org/69a29afb-938c-4b9e-9fca-b2f79755047a", + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined), + ) + + assertEquals(17 to 28, span) + } + } + + @Test + fun `GIVEN a blob URI with duplicated blob prefix WHEN getting registrable domain or host span THEN null is returned`() { + runTestOnMain { + val span = getRegistrableDomainOrHostSpan( + url = "blob:blob:https://www.mozilla.org/69a29afb-938c-4b9e-9fca-b2f79755047a", + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined), + ) + + assertNull(span) + } + } + + @Test + fun `GIVEN a simple URL WHEN rendering it THEN registrable domain is colored`() { runTestOnMain { testRenderWithColoredUrl( testUrl = "https://www.mozilla.org/", @@ -154,7 +391,7 @@ class URLRendererTest { } @Test - fun `Render with URL containing domain with trailing period`() { + fun `GIVEN a URL with a trailing period in the domain WHEN rendering it THEN registrable domain is colored`() { runTestOnMain { testRenderWithColoredUrl( testUrl = "https://www.mozilla.org./", @@ -164,7 +401,7 @@ class URLRendererTest { } @Test - fun `Render with URL containing repeated domain`() { + fun `GIVEN a URL with a repeated domain WHEN rendering it THEN the last occurrence of domain is colored`() { runTestOnMain { testRenderWithColoredUrl( testUrl = "https://mozilla.org.mozilla.org/", @@ -172,4 +409,144 @@ class URLRendererTest { ) } } + + @Test + fun `GIVEN a URL with an IPv4 address WHEN rendering it THEN the IP part is colored`() { + runTestOnMain { + testRenderWithColoredUrl( + testUrl = "http://127.0.0.1/", + expectedRegistrableDomainSpan = 7 to 16, + ) + } + } + + @Test + fun `GIVEN a URL with an IPv6 address WHEN rendering it THEN the IP part is colored`() { + runTestOnMain { + testRenderWithColoredUrl( + testUrl = "http://[::1]/", + expectedRegistrableDomainSpan = 7 to 12, + ) + } + } + + @Test + fun `GIVEN a URL with a non PSL domain WHEN rendering it THEN host colored`() { + runTestOnMain { + testRenderWithColoredUrl( + testUrl = "http://localhost/", + expectedRegistrableDomainSpan = 7 to 16, + ) + } + } + + @Test + fun `GIVEN an internal page name WHEN rendering it THEN nothing is colored`() { + runTestOnMain { + testRenderWithUncoloredUrl("about:mozilla") + } + } + + @Test + fun `GIVEN a content URI WHEN rendering it THEN nothing is colored`() { + runTestOnMain { + testRenderWithUncoloredUrl("content://media/external/file/1000000000") + } + } + + @Test + fun `GIVEN a simple URL WHEN rendering it THEN registrable domain is set`() { + runTestOnMain { + testRenderWithRegistrableDomain( + testUrl = "https://www.mozilla.org/", + expectedUrl = "mozilla.org", + ) + } + } + + @Test + fun `GIVEN a URL with a trailing period in the domain WHEN rendering it THEN registrable domain is set`() { + runTestOnMain { + testRenderWithRegistrableDomain( + testUrl = "https://www.mozilla.org./", + expectedUrl = "mozilla.org", + ) + } + } + + @Test + fun `GIVEN a URL with a repeated domain WHEN rendering it THEN the last occurrence of domain is set`() { + runTestOnMain { + testRenderWithRegistrableDomain( + testUrl = "https://mozilla.org.mozilla.org/", + expectedUrl = "mozilla.org", + ) + } + } + + @Test + fun `GIVEN a URL with an IPv4 address WHEN rendering it THEN the IP part is set`() { + runTestOnMain { + testRenderWithRegistrableDomain( + testUrl = "http://127.0.0.1/", + expectedUrl = "127.0.0.1", + ) + } + } + + @Test + fun `GIVEN a URL with an IPv6 address WHEN rendering it THEN the IP part is set`() { + runTestOnMain { + testRenderWithRegistrableDomain( + testUrl = "http://[::1]/", + expectedUrl = "[::1]", + ) + } + } + + @Test + fun `GIVEN a URL with a non PSL domain WHEN rendering it THEN host set`() { + runTestOnMain { + testRenderWithRegistrableDomain( + testUrl = "http://localhost/", + expectedUrl = "localhost", + ) + } + } + + @Test + fun `GIVEN an internal page name WHEN rendering it THEN it is set`() { + runTestOnMain { + testRenderWithRegistrableDomain( + testUrl = "about:mozilla", + expectedUrl = "about:mozilla", + ) + } + } + + @Test + fun `GIVEN a content URI WHEN rendering it THEN it is set`() { + runTestOnMain { + testRenderWithRegistrableDomain( + testUrl = "content://media/external/file/1000000000", + expectedUrl = "content://media/external/file/1000000000", + ) + } + } +} + +/** + * Robolectric default implementation of [InetAddresses] returns false for any address. + * This shadow is used to override that behavior and return true for any IP address. + */ +@Implements(InetAddresses::class) +class ShadowInetAddresses { + companion object { + @Implementation + @JvmStatic + @Suppress("DEPRECATION") + fun isNumericAddress(address: String): Boolean { + return Patterns.IP_ADDRESS.matcher(address).matches() || address.contains(":") + } + } } ===================================== mobile/android/android-components/components/support/utils/src/main/java/mozilla/components/support/ktx/util/URLStringUtils.kt ===================================== @@ -7,8 +7,6 @@ package mozilla.components.support.ktx.util import android.text.TextUtils import androidx.annotation.VisibleForTesting import androidx.core.net.toUri -import androidx.core.text.TextDirectionHeuristicCompat -import androidx.core.text.TextDirectionHeuristicsCompat import java.util.regex.Pattern object URLStringUtils { @@ -102,25 +100,9 @@ object URLStringUtils { /** * Generates a shorter version of the provided URL for display purposes by stripping it of * https/http and/or WWW prefixes and/or trailing slash when applicable. - * - * The returned text will always be displayed from left to right. - * If the directionality would otherwise be RTL "\u200E" will be prepended to the result to force LTR. */ - fun toDisplayUrl( - originalUrl: CharSequence, - textDirectionHeuristic: TextDirectionHeuristicCompat = TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR, - ): CharSequence { - val strippedText = maybeStripTrailingSlash(maybeStripUrlProtocol(originalUrl)) - - return if ( - strippedText.isNotBlank() && - textDirectionHeuristic.isRtl(strippedText, 0, 1) - ) { - "\u200E" + strippedText - } else { - strippedText - } - } + fun toDisplayUrl(originalUrl: CharSequence): CharSequence = + maybeStripTrailingSlash(maybeStripUrlProtocol(originalUrl)) private fun maybeStripUrlProtocol(url: CharSequence): CharSequence { if (url.startsWith(HTTPS)) { ===================================== mobile/android/android-components/components/support/utils/src/test/java/mozilla/components/support/utils/URLStringUtilsTest.kt ===================================== @@ -16,8 +16,6 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.spy -import org.mockito.Mockito.verify import kotlin.random.Random @RunWith(AndroidJUnit4::class) @@ -246,20 +244,9 @@ class URLStringUtilsTest { } @Test - fun showDisplayUrlAsLTREvenIfTextStartsWithArabicCharacters() { + fun toDisplayUrlDoesNotAddImplicitDirectionalMarks() { val testDisplayUrl = URLStringUtils.toDisplayUrl("http://ختار.ار/www.mozilla.org/1") - assertEquals("\u200Eختار.ار/www.mozilla.org/1", testDisplayUrl) - } - - @Test - fun toDisplayUrlAlwaysUseATextDirectionHeuristicToDetermineDirectionality() { - val textHeuristic = spy(TestTextDirectionHeuristicCompat()) - - URLStringUtils.toDisplayUrl("http://ختار.ار/www.mozilla.org/1", textHeuristic) - verify(textHeuristic).isRtl("ختار.ار/www.mozilla.org/1", 0, 1) - - URLStringUtils.toDisplayUrl("http://www.mozilla.org/1", textHeuristic) - verify(textHeuristic).isRtl("mozilla.org/1", 0, 1) + assertEquals("ختار.ار/www.mozilla.org/1", testDisplayUrl) } @Test ===================================== mobile/android/components/geckoview/GeckoViewStreamListener.cpp ===================================== @@ -16,6 +16,8 @@ #include "nsIWebProgressListener.h" #include "nsIX509Cert.h" #include "nsPrintfCString.h" +#include "nsContentSecurityUtils.h" +#include "nsITransfer.h" #include "nsNetUtil.h" @@ -85,6 +87,16 @@ GeckoViewStreamListener::OnStartRequest(nsIRequest* aRequest) { return NS_OK; } + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + if (channel) { + int32_t classification = nsContentSecurityUtils::ClassifyDownload(channel); + if (classification == nsITransfer::DOWNLOAD_FORBIDDEN) { + channel->Cancel(NS_ERROR_ABORT); + CompleteWithError(NS_ERROR_ABORT, channel); + return NS_OK; + } + } + // We're expecting data later via OnDataAvailable, so create the stream now. InitializeStreamSupport(aRequest); ===================================== mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt ===================================== @@ -58,6 +58,7 @@ abstract class ToolbarIntegration( urlRenderConfiguration = ToolbarFeature.UrlRenderConfiguration( context.components.publicSuffixList, context.getColorFromAttr(R.attr.textPrimary), + context.getColorFromAttr(R.attr.textSecondary), renderStyle = renderStyle, ), ) @@ -140,7 +141,7 @@ class DefaultToolbarIntegration( interactor = interactor, customTabId = customTabId, isPrivate = isPrivate, - renderStyle = ToolbarFeature.RenderStyle.UncoloredUrl, + renderStyle = ToolbarFeature.RenderStyle.ColoredUrl, ) { @VisibleForTesting ===================================== testing/web-platform/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_anchor_download_block_downloads.tentative.html.ini ===================================== @@ -6,7 +6,6 @@ if (os == "linux") and not fission and not debug: [PASS, FAIL] if (os == "mac") and debug: [PASS, FAIL] if (os == "mac") and not debug: [PASS, FAIL] - if os == "android": FAIL [<a download> triggered download in sandbox is blocked before a request is made.] expected: FAIL @@ -15,15 +14,12 @@ expected: if (os == "mac") and debug: [PASS, FAIL] if (os == "mac") and not debug: [PASS, FAIL] - if os == "android": FAIL [<a target="_blank" > triggered download in sandbox is blocked.] expected: if (os == "mac") and debug: [PASS, FAIL] if (os == "mac") and not debug: [PASS, FAIL] - if os == "android": FAIL [<a target="_blank" rel="noopener" > triggered download in sandbox is blocked.] expected: if (os == "mac") and debug: [PASS, FAIL] - if os == "android": FAIL ===================================== testing/web-platform/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_navigation_download_block_downloads.sub.tentative.html.ini ===================================== @@ -2,10 +2,8 @@ [Navigation resulted download in sandbox is blocked.] expected: if (os == "mac") and not debug: [PASS, FAIL] - if os == "android": FAIL [Navigation resulted download in sandbox from <object> is blocked.] expected: if (os == "mac") and debug: [PASS, FAIL] if (os == "mac") and not debug: [PASS, FAIL] - if os == "android": FAIL ===================================== testing/web-platform/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_window_open_download_block_downloads.tentative.html.ini ===================================== @@ -3,17 +3,15 @@ expected: if (os == "linux") and debug and not fission: [PASS, FAIL] if (os == "linux") and not debug: [PASS, FAIL] - if os == "android": FAIL [window.open(download, "_blank") triggering download in sandbox is blocked.] expected: if (os == "mac") and debug: [PASS, FAIL] if (os == "linux") and not debug: [PASS, FAIL] - if os == "android": FAIL [window.open(download, "_blank", "noopener") triggering download in sandbox is blocked.] expected: if (os == "linux") and debug: PASS if os == "win": PASS - if os == "android": FAIL + if os == "android": PASS [PASS, FAIL] ===================================== testing/web-platform/tests/html/semantics/embedded-content/the-iframe-element/support/iframe_sandbox_download_helper.js ===================================== @@ -1,5 +1,5 @@ function StreamDownloadFinishDelay() { - return 1000; + return 2000; } function DownloadVerifyDelay() { ===================================== uriloader/exthandler/nsExternalHelperAppService.cpp ===================================== @@ -1626,8 +1626,7 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { return NS_OK; } - mDownloadClassification = - nsContentSecurityUtils::ClassifyDownload(aChannel, MIMEType); + mDownloadClassification = nsContentSecurityUtils::ClassifyDownload(aChannel); if (mDownloadClassification == nsITransfer::DOWNLOAD_FORBIDDEN) { // If the download is rated as forbidden, View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/12c1cb9... -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/12c1cb9... You're receiving this email because of your account on gitlab.torproject.org.