tbb-commits
Threads by month
- ----- 2025 -----
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- 1 participants
- 19075 discussions

[Git][tpo/applications/tor-browser-build][main] Bug 41515: Bump version of Conjure to 14e660f0a64b
by morgan (@morgan) 24 Jul '25
by morgan (@morgan) 24 Jul '25
24 Jul '25
morgan pushed to branch main at The Tor Project / Applications / tor-browser-build
Commits:
617a1477 by Cecylia Bocovich at 2025-07-22T16:45:58+00:00
Bug 41515: Bump version of Conjure to 14e660f0a64b
- - - - -
1 changed file:
- projects/conjure/config
Changes:
=====================================
projects/conjure/config
=====================================
@@ -1,7 +1,7 @@
# vim: filetype=yaml sw=2
version: '[% c("abbrev") %]'
git_url: https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/conj…
-git_hash: 505cb48152e89e12a608ec6a0101af8a2d266fb1
+git_hash: 14e660f0a64bcbb2b55f01f69c1aafdfd5a6dcfa
container:
use_container: 1
@@ -19,7 +19,7 @@ steps:
pkg_type: go_vendor
project: conjure
norec:
- sha256sum: 8f33624ebabec17e09d9aeae8eb0bfc08b78b6089986f3fe9b7fde03a692a22b
+ sha256sum: d43631d17d8b6f78152bb4341f867bb22ad4444fec5573be1d34dac968202cda
target_replace:
'^torbrowser-(?!testbuild).*': 'torbrowser-linux-x86_64'
# https://github.com/refraction-networking/conjure/pull/267
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser-build/-/commit/6…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser-build/-/commit/6…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/mullvad-browser] Pushed new tag base-browser-140.1.0esr-15.0-1-build2
by ma1 (@ma1) 24 Jul '25
by ma1 (@ma1) 24 Jul '25
24 Jul '25
ma1 pushed new tag base-browser-140.1.0esr-15.0-1-build2 at The Tor Project / Applications / Mullvad Browser
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/mullvad-browser/-/tree/base-…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/mullvad-browser] Pushed new tag base-browser-140.1.0esr-15.0-1-build1
by ma1 (@ma1) 24 Jul '25
by ma1 (@ma1) 24 Jul '25
24 Jul '25
ma1 pushed new tag base-browser-140.1.0esr-15.0-1-build1 at The Tor Project / Applications / Mullvad Browser
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/mullvad-browser/-/tree/base-…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/mullvad-browser] Pushed new branch mullvad-browser-140.1.0esr-15.0-1
by ma1 (@ma1) 24 Jul '25
by ma1 (@ma1) 24 Jul '25
24 Jul '25
ma1 pushed new branch mullvad-browser-140.1.0esr-15.0-1 at The Tor Project / Applications / Mullvad Browser
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/mullvad-browser/-/tree/mullv…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/tor-browser] Pushed new tag tor-browser-140.1.0esr-15.0-1-build2
by ma1 (@ma1) 24 Jul '25
by ma1 (@ma1) 24 Jul '25
24 Jul '25
ma1 pushed new tag tor-browser-140.1.0esr-15.0-1-build2 at The Tor Project / Applications / Tor Browser
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/tree/tor-brows…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/tor-browser][tor-browser-140.1.0esr-15.0-1] 11 commits: Bug 1961829 - Only color HTTP(S) URIs and fallback to coloring the host in...
by ma1 (@ma1) 24 Jul '25
by ma1 (@ma1) 24 Jul '25
24 Jul '25
ma1 pushed to branch tor-browser-140.1.0esr-15.0-1 at The Tor Project / Applications / Tor Browser
Commits:
421e0a1f by Michel Le Bihan at 2025-07-24T08:15:34+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
- - - - -
116d90bb by Michel Le Bihan at 2025-07-24T08:15:36+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
- - - - -
52e6b98a by Michel Le Bihan at 2025-07-24T08:15:37+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
- - - - -
84b3a932 by Michel Le Bihan at 2025-07-24T08:15:39+02:00
Bug 1812898 - Part 2: Enable domain highlighting in Fenix toolbar. r=tthibaud,android-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D244509
- - - - -
f0723224 by mimi89999 at 2025-07-24T08:15:41+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
- - - - -
52cd8fb2 by Michel Le Bihan at 2025-07-24T08:15:42+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
- - - - -
aeb2a384 by Cathy Lu at 2025-07-24T08:15:44+02:00
Bug 1791322 - iframe sandbox wpt tests modified with delay r=nika
Differential Revision: https://phabricator.services.mozilla.com/D253052
- - - - -
b2e55448 by Cathy Lu at 2025-07-24T08:15:45+02:00
Bug 1791322 - GeckoView should call classifyDownloads to sandbox downloads r=geckoview-reviewers,nika
Differential Revision: https://phabricator.services.mozilla.com/D249683
- - - - -
a6c81f5b by Andreas Pehrson at 2025-07-24T08:15:47+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
- - - - -
1684fbd9 by Tom Schuster at 2025-07-24T08:15:48+02:00
Bug 1971704 - Cleanup nsContentSecurityUtils::ClassifyDownload. r=smaug
Differential Revision: https://phabricator.services.mozilla.com/D253491
- - - - -
22375264 by Pier Angelo Vendrame at 2025-07-24T08:15:50+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
=====================================
@@ -2209,11 +2209,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));
@@ -2246,27 +2252,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
@@ -549,6 +551,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/a672bc…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/a672bc…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/tor-browser] Pushed new tag base-browser-140.1.0esr-15.0-1-build2
by ma1 (@ma1) 24 Jul '25
by ma1 (@ma1) 24 Jul '25
24 Jul '25
ma1 pushed new tag base-browser-140.1.0esr-15.0-1-build2 at The Tor Project / Applications / Tor Browser
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/tree/base-brow…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/tor-browser][base-browser-140.1.0esr-15.0-1] 11 commits: Bug 1961829 - Only color HTTP(S) URIs and fallback to coloring the host in...
by ma1 (@ma1) 24 Jul '25
by ma1 (@ma1) 24 Jul '25
24 Jul '25
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/12c1cb…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/12c1cb…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/tor-browser][tor-browser-140.1.0esr-15.0-1] fixup! TB 41089: Add tor-browser build scripts + Makefile to tor-browser
by clairehurst (@clairehurst) 23 Jul '25
by clairehurst (@clairehurst) 23 Jul '25
23 Jul '25
clairehurst pushed to branch tor-browser-140.1.0esr-15.0-1 at The Tor Project / Applications / Tor Browser
Commits:
a672bc1c by clairehurst at 2025-07-23T13:51:52-06:00
fixup! TB 41089: Add tor-browser build scripts + Makefile to tor-browser
Bug 43984: Update android build scripts and docs for ESR 140
- - - - -
4 changed files:
- tools/geckoview/Makefile
- tools/geckoview/android-env-linux-template.sh
- tools/geckoview/android-env-macos-template.sh
- tools/geckoview/install-fenix.sh
Changes:
=====================================
tools/geckoview/Makefile
=====================================
@@ -41,11 +41,13 @@ env:
test -e android-env.sh || { echo "copy android-env-...-template.sh to android-env.sh and edit appropriatly"; exit 1; }
install-release:
- ./install-fenix.sh $(DEV_ROOT) $(ANDROID_ARCH) release
+ ./install-fenix.sh $(DEV_ROOT) $(ARCH) $(ANDROID_ARCH) release
install-beta:
- ./install-fenix.sh $(DEV_ROOT) $(ANDROID_ARCH) beta
+ ./install-fenix.sh $(DEV_ROOT) $(ARCH) $(ANDROID_ARCH) beta
install-nightly:
- ./install-fenix.sh $(DEV_ROOT) $(ANDROID_ARCH) nightly
+ ./install-fenix.sh $(DEV_ROOT) $(ARCH) $(ANDROID_ARCH) nightly
+install-debug:
+ ./install-fenix.sh $(DEV_ROOT) $(ARCH) $(ANDROID_ARCH) debug
all: env geckoview fenix-nightly install-nightly
all-release: env geckoview fenix-release install-release
@@ -56,4 +58,3 @@ jslint:
clean:
rm -rf $(BUILD_OUTPUT)
-
=====================================
tools/geckoview/android-env-linux-template.sh
=====================================
@@ -1,6 +1,6 @@
-export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
+export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
export ANDROID_HOME=$HOME/.mozbuild/android-sdk-linux/ # or $HOME/Android/Sdk/ # Or .../android-toolchain/android-sdk-linux if you extract android-toolchain from tor-browser-build
-export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/r26c/ # for 128esr
-export GRADLE_HOME=/FULL/PATH/TO/tor-browser-build/out/gradle/gradle-8.8 # Or the version that we currently use
+export ANDROID_NDK_HOME=$HOME/.mozbuild/android-ndk-r28b/ # for 140esr
+export GRADLE_HOME=$HOME/.mozbuild/gradle-8.14.3 # not included by default, need to download from https://gradle.org/releases/ and put the extracted directory "gradle-8.14.3" into ~/.mozbuild/
export LOCAL_DEV_BUILD=1
export PATH=/FULL/PATH/TO/tor-browser-build/out/clang/clang-16.x.y-arm/bin/:$PATH # prepend our newly built and assembled clang to the path so it gets used to build geckoview
=====================================
tools/geckoview/android-env-macos-template.sh
=====================================
@@ -1,8 +1,6 @@
-export JAVA_HOME=/opt/homebrew/opt/openjdk(a)17/libexec/openjdk.jdk/Contents/Home/ # for arm64. Or JAVA_HOME=/usr/local/opt/openjdk(a)17/libexec/openjdk.jdk/Contents/Home/ for x86_64.
-export ANDROID_HOME=$HOME/Library/Android/sdk # or $HOME/.mozbuild/android-sdk-macosx/
-export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/26.2.11394342 # will need to download the relevant NDK via android studio (e.g. 26.2.11394342)
-GRADLE_DIR=/opt/homebrew/Cellar/gradle # for arm64, or /usr/local/Cellar/gradle for x86_64. Download via homebrew.
-GRADLE_VERSION=`ls -1 "$GRADLE_DIR" | sort -hr | head -n 1` # Finds the latest gradle version in the specified GRADLE_DIR
-export GRADLE_HOME=$GRADLE_DIR/$GRADLE_VERSION
+export JAVA_HOME=/opt/homebrew/opt/openjdk(a)17/libexec/openjdk.jdk/Contents/Home # for arm64. Or JAVA_HOME=/usr/local/opt/openjdk(a)17/libexec/openjdk.jdk/Contents/Home for x86_64.
+export ANDROID_HOME=$HOME/.mozbuild/android-sdk-macosx
+export ANDROID_NDK_HOME=$HOME/.mozbuild/android-ndk-r28b # for ESR140
+export GRADLE_HOME=$HOME/.mozbuild/gradle-8.14.3 # not included by default, need to download from https://gradle.org/releases/ and put the extracted directory "gradle-8.14.3" into ~/.mozbuild/
export LOCAL_DEV_BUILD=1
-export PATH=$ANDROID_HOME/ndk/26.2.11394342/toolchains/llvm/prebuilt/darwin-x86_64/bin/:$PATH # prepend android studios latest ndk to the path so it's clang gets used to build geckoview. Note that it doesn't need to be the same version as above
+export PATH=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin:$PATH # prepend mozbuilds NDK to the PATH so it's clang gets used to build geckoview
=====================================
tools/geckoview/install-fenix.sh
=====================================
@@ -1,9 +1,16 @@
#!/bin/bash
set -e
DEV_ROOT=$1
-ANDROID_ARCH=$2
-VARIANT=$3
+ARCH=$2
+ANDROID_ARCH=$3
+VARIANT=$4
cd $DEV_ROOT
OBJ_DIR=$(MOZCONFIG=mozconfig-android-$ARCH ./mach environment --format json --verbose | jq -r .topobjdir)
-adb install "$OBJ_DIR/gradle/build/mobile/android/fenix/app/outputs/apk/fenix/$VARIANT/app-fenix-$ANDROID_ARCH-nightly-signed.apk"
+
+if [ $VARIANT == "debug" ]
+then
+ adb install "$OBJ_DIR/gradle/build/mobile/android/fenix/app/outputs/apk/fenix/$VARIANT/app-fenix-$ANDROID_ARCH-$VARIANT.apk"
+else
+ adb install "$OBJ_DIR/gradle/build/mobile/android/fenix/app/outputs/apk/fenix/$VARIANT/app-fenix-$ANDROID_ARCH-$VARIANT-signed.apk"
+fi
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/a672bc1…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/a672bc1…
You're receiving this email because of your account on gitlab.torproject.org.
1
0

[Git][tpo/applications/tor-browser] Pushed new tag tor-browser-140.1.0esr-15.0-1-build1
by morgan (@morgan) 23 Jul '25
by morgan (@morgan) 23 Jul '25
23 Jul '25
morgan pushed new tag tor-browser-140.1.0esr-15.0-1-build1 at The Tor Project / Applications / Tor Browser
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/tree/tor-brows…
You're receiving this email because of your account on gitlab.torproject.org.
1
0