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
-
ea1dd032
by Michel Le Bihan at 2025-07-24T08:08:56+02:00
-
39979386
by Michel Le Bihan at 2025-07-24T08:08:58+02:00
-
c1e7fc82
by Michel Le Bihan at 2025-07-24T08:09:01+02:00
-
9e5383b4
by mimi89999 at 2025-07-24T08:09:03+02:00
-
7ab9ea81
by Michel Le Bihan at 2025-07-24T08:09:05+02:00
-
04c4f093
by Cathy Lu at 2025-07-24T08:09:08+02:00
-
85f273f4
by Cathy Lu at 2025-07-24T08:09:11+02:00
-
2d41c7c1
by Andreas Pehrson at 2025-07-24T08:09:13+02:00
-
817aea0e
by Tom Schuster at 2025-07-24T08:09:16+02:00
-
6cc06b71
by Pier Angelo Vendrame at 2025-07-24T08:09:18+02:00
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:
... | ... | @@ -3568,7 +3568,9 @@ void MediaManager::OnCameraMute(bool aMute) { |
3568 | 3568 | mCamerasMuted = aMute;
|
3569 | 3569 | // This is safe since we're on main-thread, and the windowlist can only
|
3570 | 3570 | // be added to from the main-thread
|
3571 | - for (const auto& window : mActiveWindows.Values()) {
|
|
3571 | + for (const auto& window :
|
|
3572 | + ToTArray<AutoTArray<RefPtr<GetUserMediaWindowListener>, 2>>(
|
|
3573 | + mActiveWindows.Values())) {
|
|
3572 | 3574 | window->MuteOrUnmuteCameras(aMute);
|
3573 | 3575 | }
|
3574 | 3576 | }
|
... | ... | @@ -3579,7 +3581,9 @@ void MediaManager::OnMicrophoneMute(bool aMute) { |
3579 | 3581 | mMicrophonesMuted = aMute;
|
3580 | 3582 | // This is safe since we're on main-thread, and the windowlist can only
|
3581 | 3583 | // be added to from the main-thread
|
3582 | - for (const auto& window : mActiveWindows.Values()) {
|
|
3584 | + for (const auto& window :
|
|
3585 | + ToTArray<AutoTArray<RefPtr<GetUserMediaWindowListener>, 2>>(
|
|
3586 | + mActiveWindows.Values())) {
|
|
3583 | 3587 | window->MuteOrUnmuteMicrophones(aMute);
|
3584 | 3588 | }
|
3585 | 3589 | }
|
... | ... | @@ -4767,7 +4771,7 @@ void GetUserMediaWindowListener::MuteOrUnmuteCameras(bool aMute) { |
4767 | 4771 | }
|
4768 | 4772 | mCamerasAreMuted = aMute;
|
4769 | 4773 | |
4770 | - for (auto& l : mActiveListeners) {
|
|
4774 | + for (auto& l : mActiveListeners.Clone()) {
|
|
4771 | 4775 | if (l->GetDevice()->Kind() == MediaDeviceKind::Videoinput) {
|
4772 | 4776 | l->MuteOrUnmuteCamera(aMute);
|
4773 | 4777 | }
|
... | ... | @@ -4782,7 +4786,7 @@ void GetUserMediaWindowListener::MuteOrUnmuteMicrophones(bool aMute) { |
4782 | 4786 | }
|
4783 | 4787 | mMicrophonesAreMuted = aMute;
|
4784 | 4788 | |
4785 | - for (auto& l : mActiveListeners) {
|
|
4789 | + for (auto& l : mActiveListeners.Clone()) {
|
|
4786 | 4790 | if (l->GetDevice()->Kind() == MediaDeviceKind::Audioinput) {
|
4787 | 4791 | l->MuteOrUnmuteMicrophone(aMute);
|
4788 | 4792 | }
|
... | ... | @@ -2206,11 +2206,17 @@ void nsContentSecurityUtils::LogMessageToConsole(nsIHttpChannel* aChannel, |
2206 | 2206 | }
|
2207 | 2207 | |
2208 | 2208 | /* static */
|
2209 | -long nsContentSecurityUtils::ClassifyDownload(
|
|
2210 | - nsIChannel* aChannel, const nsAutoCString& aMimeTypeGuess) {
|
|
2209 | +long nsContentSecurityUtils::ClassifyDownload(nsIChannel* aChannel) {
|
|
2211 | 2210 | MOZ_ASSERT(aChannel, "IsDownloadAllowed without channel?");
|
2212 | 2211 | |
2213 | 2212 | nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
|
2213 | + if ((loadInfo->GetTriggeringSandboxFlags() & SANDBOXED_ALLOW_DOWNLOADS) ||
|
|
2214 | + (loadInfo->GetSandboxFlags() & SANDBOXED_ALLOW_DOWNLOADS)) {
|
|
2215 | + if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel)) {
|
|
2216 | + LogMessageToConsole(httpChannel, "IframeSandboxBlockedDownload");
|
|
2217 | + }
|
|
2218 | + return nsITransfer::DOWNLOAD_FORBIDDEN;
|
|
2219 | + }
|
|
2214 | 2220 | |
2215 | 2221 | nsCOMPtr<nsIURI> contentLocation;
|
2216 | 2222 | aChannel->GetURI(getter_AddRefs(contentLocation));
|
... | ... | @@ -2243,27 +2249,11 @@ long nsContentSecurityUtils::ClassifyDownload( |
2243 | 2249 | |
2244 | 2250 | if (StaticPrefs::dom_block_download_insecure() &&
|
2245 | 2251 | decission != nsIContentPolicy::ACCEPT) {
|
2246 | - nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
|
|
2247 | - if (httpChannel) {
|
|
2252 | + if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel)) {
|
|
2248 | 2253 | LogMessageToConsole(httpChannel, "MixedContentBlockedDownload");
|
2249 | 2254 | }
|
2250 | 2255 | return nsITransfer::DOWNLOAD_POTENTIALLY_UNSAFE;
|
2251 | 2256 | }
|
2252 | 2257 | |
2253 | - if (loadInfo->TriggeringPrincipal()->IsSystemPrincipal()) {
|
|
2254 | - return nsITransfer::DOWNLOAD_ACCEPTABLE;
|
|
2255 | - }
|
|
2256 | - |
|
2257 | - uint32_t triggeringFlags = loadInfo->GetTriggeringSandboxFlags();
|
|
2258 | - uint32_t currentflags = loadInfo->GetSandboxFlags();
|
|
2259 | - |
|
2260 | - if ((triggeringFlags & SANDBOXED_ALLOW_DOWNLOADS) ||
|
|
2261 | - (currentflags & SANDBOXED_ALLOW_DOWNLOADS)) {
|
|
2262 | - nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
|
|
2263 | - if (httpChannel) {
|
|
2264 | - LogMessageToConsole(httpChannel, "IframeSandboxBlockedDownload");
|
|
2265 | - }
|
|
2266 | - return nsITransfer::DOWNLOAD_FORBIDDEN;
|
|
2267 | - }
|
|
2268 | 2258 | return nsITransfer::DOWNLOAD_ACCEPTABLE;
|
2269 | 2259 | } |
... | ... | @@ -74,8 +74,7 @@ class nsContentSecurityUtils { |
74 | 74 | const mozilla::dom::Element& aElement);
|
75 | 75 | |
76 | 76 | // Helper function to Check if a Download is allowed;
|
77 | - static long ClassifyDownload(nsIChannel* aChannel,
|
|
78 | - const nsAutoCString& aMimeTypeGuess);
|
|
77 | + static long ClassifyDownload(nsIChannel* aChannel);
|
|
79 | 78 | |
80 | 79 | // Public only for testing
|
81 | 80 | static FilenameTypeAndDetails FilenameToFilenameType(
|
... | ... | @@ -66,6 +66,8 @@ class txXPathNode { |
66 | 66 | bool operator!=(const txXPathNode& aNode) const { return !(*this == aNode); }
|
67 | 67 | ~txXPathNode() { MOZ_COUNT_DTOR(txXPathNode); }
|
68 | 68 | |
69 | + mozilla::dom::Document* OwnerDoc() const { return mNode->OwnerDoc(); }
|
|
70 | + |
|
69 | 71 | private:
|
70 | 72 | friend class txXPathNativeNode;
|
71 | 73 | friend class txXPathNodeUtils;
|
... | ... | @@ -13,10 +13,13 @@ |
13 | 13 | |
14 | 14 | #include "mozilla/CheckedInt.h"
|
15 | 15 | #include "mozilla/UniquePtrExtensions.h"
|
16 | +#include "nsRFPService.h"
|
|
16 | 17 | |
17 | 18 | using mozilla::CheckedUint32;
|
18 | 19 | using mozilla::MakeUnique;
|
19 | 20 | using mozilla::MakeUniqueFallible;
|
21 | +using mozilla::nsRFPService;
|
|
22 | +using mozilla::RFPTarget;
|
|
20 | 23 | using mozilla::UniquePtr;
|
21 | 24 | |
22 | 25 | /*
|
... | ... | @@ -74,6 +77,10 @@ nsresult txNodeSorter::addSortElement(Expr* aSelectExpr, Expr* aLangExpr, |
74 | 77 | if (aLangExpr) {
|
75 | 78 | rv = aLangExpr->evaluateToString(aContext, lang);
|
76 | 79 | NS_ENSURE_SUCCESS(rv, rv);
|
80 | + } else if (aContext->getContextNode()
|
|
81 | + .OwnerDoc()
|
|
82 | + ->ShouldResistFingerprinting(RFPTarget::JSLocale)) {
|
|
83 | + CopyUTF8toUTF16(nsRFPService::GetSpoofedJSLocale(), lang);
|
|
77 | 84 | }
|
78 | 85 | |
79 | 86 | // Case-order
|
... | ... | @@ -7,6 +7,7 @@ package mozilla.components.browser.toolbar.display |
7 | 7 | import android.animation.LayoutTransition
|
8 | 8 | import android.content.Context
|
9 | 9 | import android.graphics.Typeface
|
10 | +import android.text.Spanned
|
|
10 | 11 | import android.util.AttributeSet
|
11 | 12 | import android.util.TypedValue
|
12 | 13 | import android.view.Gravity
|
... | ... | @@ -17,6 +18,7 @@ import androidx.annotation.VisibleForTesting |
17 | 18 | import androidx.core.view.isVisible
|
18 | 19 | import mozilla.components.browser.toolbar.BrowserToolbar
|
19 | 20 | import mozilla.components.browser.toolbar.R
|
21 | +import mozilla.components.concept.toolbar.Toolbar
|
|
20 | 22 | |
21 | 23 | /**
|
22 | 24 | * View displaying the URL and optionally the title of a website.
|
... | ... | @@ -48,6 +50,9 @@ internal class OriginView @JvmOverloads constructor( |
48 | 50 | isClickable = true
|
49 | 51 | isFocusable = true
|
50 | 52 | |
53 | + textDirection = View.TEXT_DIRECTION_LTR
|
|
54 | + layoutDirection = View.LAYOUT_DIRECTION_LTR
|
|
55 | + |
|
51 | 56 | setOnClickListener {
|
52 | 57 | if (onUrlClicked()) {
|
53 | 58 | toolbar.editMode()
|
... | ... | @@ -134,9 +139,50 @@ internal class OriginView @JvmOverloads constructor( |
134 | 139 | titleView.setOnLongClickListener(handler)
|
135 | 140 | }
|
136 | 141 | |
142 | + /**
|
|
143 | + * Scrolls the URL view to ensure the registrable domain is visible.
|
|
144 | + */
|
|
145 | + @VisibleForTesting
|
|
146 | + internal fun scrollToShowRegistrableDomain() {
|
|
147 | + val text = urlView.text
|
|
148 | + |
|
149 | + val spans = (text as? Spanned)?.getSpans(
|
|
150 | + 0,
|
|
151 | + text.length,
|
|
152 | + Toolbar.RegistrableDomainColorSpan::class.java,
|
|
153 | + )
|
|
154 | + |
|
155 | + if (spans?.size == 1) {
|
|
156 | + val registrableDomainSpan = (urlView.text as? Spanned)?.getSpans(
|
|
157 | + 0,
|
|
158 | + text.length,
|
|
159 | + Toolbar.RegistrableDomainColorSpan::class.java,
|
|
160 | + )?.getOrNull(0)
|
|
161 | + |
|
162 | + val valueUntilRegistrableDomainEnd = text.subSequence(0, text.getSpanEnd(registrableDomainSpan))
|
|
163 | + |
|
164 | + val urlViewWidth = urlView.width
|
|
165 | + val valueWidth = measureUrlTextWidh(valueUntilRegistrableDomainEnd.toString())
|
|
166 | + |
|
167 | + if (valueWidth > urlViewWidth) {
|
|
168 | + urlView.scrollTo((valueWidth - urlViewWidth).toInt(), 0)
|
|
169 | + return
|
|
170 | + }
|
|
171 | + }
|
|
172 | + |
|
173 | + urlView.scrollTo(0, 0)
|
|
174 | + }
|
|
175 | + |
|
176 | + @VisibleForTesting
|
|
177 | + internal fun measureUrlTextWidh(text: String) = urlView.paint.measureText(text)
|
|
178 | + |
|
137 | 179 | internal var url: CharSequence
|
138 | 180 | get() = urlView.text
|
139 | - set(value) { urlView.text = value }
|
|
181 | + set(value) {
|
|
182 | + urlView.text = value
|
|
183 | + |
|
184 | + scrollToShowRegistrableDomain()
|
|
185 | + }
|
|
140 | 186 | |
141 | 187 | /**
|
142 | 188 | * Sets the colour of the text to be displayed when the URL of the toolbar is empty.
|
1 | +/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2 | + * License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3 | + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
4 | + |
|
5 | +package mozilla.components.browser.toolbar.display
|
|
6 | + |
|
7 | +import android.graphics.Color
|
|
8 | +import android.text.SpannableStringBuilder
|
|
9 | +import android.text.SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE
|
|
10 | +import android.text.style.ForegroundColorSpan
|
|
11 | +import android.view.View
|
|
12 | +import androidx.annotation.ColorInt
|
|
13 | +import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
14 | +import mozilla.components.concept.toolbar.Toolbar
|
|
15 | +import mozilla.components.support.test.any
|
|
16 | +import mozilla.components.support.test.robolectric.testContext
|
|
17 | +import org.junit.Assert.assertEquals
|
|
18 | +import org.junit.Test
|
|
19 | +import org.junit.runner.RunWith
|
|
20 | +import org.mockito.Mockito.doReturn
|
|
21 | +import org.mockito.Mockito.spy
|
|
22 | + |
|
23 | +@RunWith(AndroidJUnit4::class)
|
|
24 | +class OriginViewTest {
|
|
25 | + |
|
26 | + private fun SpannableStringBuilder.applyUrlColors(
|
|
27 | + @ColorInt urlColor: Int,
|
|
28 | + @ColorInt registrableDomainColor: Int,
|
|
29 | + registrableDomainOrHostSpan: Pair<Int, Int>,
|
|
30 | + ): SpannableStringBuilder = apply {
|
|
31 | + setSpan(
|
|
32 | + ForegroundColorSpan(urlColor),
|
|
33 | + 0,
|
|
34 | + length,
|
|
35 | + SPAN_INCLUSIVE_INCLUSIVE,
|
|
36 | + )
|
|
37 | + |
|
38 | + val (start, end) = registrableDomainOrHostSpan
|
|
39 | + setSpan(
|
|
40 | + Toolbar.RegistrableDomainColorSpan(registrableDomainColor),
|
|
41 | + start,
|
|
42 | + end,
|
|
43 | + SPAN_INCLUSIVE_INCLUSIVE,
|
|
44 | + )
|
|
45 | + }
|
|
46 | + |
|
47 | + @Test
|
|
48 | + fun `scrollToShowRegistrableDomain scrolls when domain exceeds width`() {
|
|
49 | + val view = spy(OriginView(testContext))
|
|
50 | + val url = "https://www.really-long-example-domain.com/"
|
|
51 | + val spannedUrl = SpannableStringBuilder(url).apply {
|
|
52 | + applyUrlColors(
|
|
53 | + urlColor = Color.GREEN,
|
|
54 | + registrableDomainColor = Color.RED,
|
|
55 | + registrableDomainOrHostSpan = 8 to 42,
|
|
56 | + )
|
|
57 | + }
|
|
58 | + |
|
59 | + // Long domain wouldn't fit in the view
|
|
60 | + doReturn(500f).`when`(view).measureUrlTextWidh(any())
|
|
61 | + view.urlView.layout(0, 0, 200, 100)
|
|
62 | + |
|
63 | + view.url = spannedUrl
|
|
64 | + |
|
65 | + assertEquals(300, view.urlView.scrollX)
|
|
66 | + }
|
|
67 | + |
|
68 | + @Test
|
|
69 | + fun `scrollToShowRegistrableDomain does not scroll when domain fits in view`() {
|
|
70 | + val view = spy(OriginView(testContext))
|
|
71 | + val url = "https://mozilla.org/"
|
|
72 | + val spannedUrl = SpannableStringBuilder(url).apply {
|
|
73 | + applyUrlColors(
|
|
74 | + urlColor = Color.GREEN,
|
|
75 | + registrableDomainColor = Color.RED,
|
|
76 | + registrableDomainOrHostSpan = 8 to 19,
|
|
77 | + )
|
|
78 | + }
|
|
79 | + |
|
80 | + doReturn(50f).`when`(view).measureUrlTextWidh(any())
|
|
81 | + view.urlView.layout(0, 0, 200, 100)
|
|
82 | + |
|
83 | + view.url = spannedUrl
|
|
84 | + |
|
85 | + assertEquals(0, view.urlView.scrollX)
|
|
86 | + }
|
|
87 | + |
|
88 | + @Test
|
|
89 | + fun `scrollToShowRegistrableDomain does not scroll when no span exists`() {
|
|
90 | + val view = OriginView(testContext)
|
|
91 | + |
|
92 | + val spanned = SpannableStringBuilder("nospan.com") // no span set
|
|
93 | + |
|
94 | + view.measure(0, 0)
|
|
95 | + view.layout(0, 0, 500, 100)
|
|
96 | + |
|
97 | + view.url = spanned
|
|
98 | + |
|
99 | + assertEquals(0, view.urlView.scrollX)
|
|
100 | + }
|
|
101 | + |
|
102 | + @Test
|
|
103 | + fun `URL text direction is always LTR`() {
|
|
104 | + val originView = OriginView(testContext)
|
|
105 | + originView.url = "ختار.ار/www.mozilla.org/1"
|
|
106 | + assertEquals(View.TEXT_DIRECTION_LTR, originView.urlView.textDirection)
|
|
107 | + assertEquals(View.LAYOUT_DIRECTION_LTR, originView.urlView.layoutDirection)
|
|
108 | + }
|
|
109 | +} |
... | ... | @@ -5,11 +5,13 @@ |
5 | 5 | package mozilla.components.concept.toolbar
|
6 | 6 | |
7 | 7 | import android.graphics.drawable.Drawable
|
8 | +import android.text.style.ForegroundColorSpan
|
|
8 | 9 | import android.view.View
|
9 | 10 | import android.view.View.NO_ID
|
10 | 11 | import android.view.ViewGroup
|
11 | 12 | import android.widget.ImageButton
|
12 | 13 | import android.widget.ImageView
|
14 | +import androidx.annotation.ColorInt
|
|
13 | 15 | import androidx.annotation.ColorRes
|
14 | 16 | import androidx.annotation.Dimension
|
15 | 17 | import androidx.annotation.Dimension.Companion.DP
|
... | ... | @@ -548,6 +550,13 @@ interface Toolbar : ScrollableToolbar { |
548 | 550 | */
|
549 | 551 | END,
|
550 | 552 | }
|
553 | + |
|
554 | + /**
|
|
555 | + * Registrable domain foreground color span.
|
|
556 | + *
|
|
557 | + * This simple class extension is used so that we can filter for it elsewhere.
|
|
558 | + */
|
|
559 | + class RegistrableDomainColorSpan(@ColorInt color: Int) : ForegroundColorSpan(color)
|
|
551 | 560 | }
|
552 | 561 | |
553 | 562 | private fun AppCompatImageButton.setTintResource(@ColorRes tintColorResource: Int) {
|
... | ... | @@ -88,11 +88,13 @@ class ToolbarFeature( |
88 | 88 | )
|
89 | 89 | |
90 | 90 | /**
|
91 | - * Controls how the url should be styled
|
|
91 | + * Controls how the URL should be styled
|
|
92 | 92 | *
|
93 | 93 | * RegistrableDomain: displays only the eTLD+1 (direct subdomain of the public suffix), uncolored
|
94 | - * ColoredUrl: displays the registrableDomain with color and url with another color
|
|
95 | - * UncoloredUrl: displays the full url, uncolored
|
|
94 | + * ColoredUrl: displays the full URL with distinct colors for the registrable domain and the rest of the URL.
|
|
95 | + * Colors the entire hostname if the registrable domain cannot be determined or is an IP address.
|
|
96 | + * Leaves non http(s) URLs uncolored.
|
|
97 | + * UncoloredUrl: displays the full URL, uncolored
|
|
96 | 98 | */
|
97 | 99 | sealed class RenderStyle {
|
98 | 100 | object RegistrableDomain : RenderStyle()
|
... | ... | @@ -18,6 +18,11 @@ import kotlinx.coroutines.channels.trySendBlocking |
18 | 18 | import kotlinx.coroutines.launch
|
19 | 19 | import mozilla.components.concept.toolbar.Toolbar
|
20 | 20 | import mozilla.components.feature.toolbar.ToolbarFeature
|
21 | +import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
|
22 | +import mozilla.components.support.ktx.android.net.isHttpOrHttps
|
|
23 | +import mozilla.components.support.ktx.kotlin.isIpv4OrIpv6
|
|
24 | + |
|
25 | +private const val BLOB_URL_PREFIX = "blob:"
|
|
21 | 26 | |
22 | 27 | /**
|
23 | 28 | * Asynchronous URL renderer.
|
... | ... | @@ -73,13 +78,21 @@ internal class URLRenderer( |
73 | 78 | toolbar.url = when (configuration.renderStyle) {
|
74 | 79 | // Display only the eTLD+1 (direct subdomain of the public suffix), uncolored
|
75 | 80 | ToolbarFeature.RenderStyle.RegistrableDomain -> {
|
76 | - val host = url.toUri().host?.ifEmpty { null }
|
|
77 | - host?.let { getRegistrableDomain(host, configuration) } ?: url
|
|
81 | + getRegistrableDomainOrHostSpan(url, configuration.publicSuffixList)?.let { (start, end) ->
|
|
82 | + url.substring(start, end)
|
|
83 | + } ?: url
|
|
78 | 84 | }
|
79 | 85 | // Display the registrableDomain with color and URL with another color
|
80 | 86 | ToolbarFeature.RenderStyle.ColoredUrl -> SpannableStringBuilder(url).apply {
|
81 | - color(configuration.urlColor)
|
|
82 | - colorRegistrableDomain(configuration)
|
|
87 | + val span = getRegistrableDomainOrHostSpan(url, configuration.publicSuffixList)
|
|
88 | + |
|
89 | + if (configuration.urlColor != null && span != null) {
|
|
90 | + applyUrlColors(
|
|
91 | + configuration.urlColor,
|
|
92 | + configuration.registrableDomainColor,
|
|
93 | + span,
|
|
94 | + )
|
|
95 | + }
|
|
83 | 96 | }
|
84 | 97 | // Display the full URL, uncolored
|
85 | 98 | ToolbarFeature.RenderStyle.UncoloredUrl -> url
|
... | ... | @@ -87,43 +100,98 @@ internal class URLRenderer( |
87 | 100 | }
|
88 | 101 | }
|
89 | 102 | |
90 | -private suspend fun getRegistrableDomain(host: String, configuration: ToolbarFeature.UrlRenderConfiguration) =
|
|
91 | - configuration.publicSuffixList.getPublicSuffixPlusOne(host).await()
|
|
103 | +/**
|
|
104 | + * Determines the position span of the registrable domain within a host string.
|
|
105 | + *
|
|
106 | + * @param host The host string to analyze
|
|
107 | + * @param publicSuffixList The [PublicSuffixList] used to get the eTLD+1 for the host
|
|
108 | + * @return A Pair of (startIndex, endIndex) for the registrable domain within the host,
|
|
109 | + * or null if the host is an IP address or no registrable domain could be found
|
|
110 | + */
|
|
111 | +@VisibleForTesting
|
|
112 | +internal suspend fun getRegistrableDomainSpanInHost(
|
|
113 | + host: String,
|
|
114 | + publicSuffixList: PublicSuffixList,
|
|
115 | +): Pair<Int, Int>? {
|
|
116 | + if (host.isIpv4OrIpv6()) return null
|
|
117 | + |
|
118 | + val normalizedHost = host.removeSuffix(".")
|
|
119 | + |
|
120 | + val registrableDomain = publicSuffixList
|
|
121 | + .getPublicSuffixPlusOne(normalizedHost)
|
|
122 | + .await() ?: return null
|
|
123 | + |
|
124 | + val start = normalizedHost.lastIndexOf(registrableDomain)
|
|
125 | + return if (start == -1) {
|
|
126 | + null
|
|
127 | + } else {
|
|
128 | + start to start + registrableDomain.length
|
|
129 | + }
|
|
130 | +}
|
|
92 | 131 | |
93 | -private suspend fun SpannableStringBuilder.colorRegistrableDomain(
|
|
94 | - configuration: ToolbarFeature.UrlRenderConfiguration,
|
|
95 | -) {
|
|
96 | - val url = toString()
|
|
97 | - val host = url.toUri().host?.removeSuffix(".") ?: return
|
|
98 | - |
|
99 | - val registrableDomain = configuration
|
|
100 | - .publicSuffixList
|
|
101 | - .getPublicSuffixPlusOne(host)
|
|
102 | - .await() ?: return
|
|
103 | - |
|
104 | - val indexOfHost = url.indexOf(host)
|
|
105 | - val indexOfRegistrableDomain = host.lastIndexOf(registrableDomain)
|
|
106 | - if (indexOfHost == -1 || indexOfRegistrableDomain == -1) {
|
|
107 | - return
|
|
132 | +/**
|
|
133 | + * Determines the position span of either the registrable domain or the full host
|
|
134 | + * within a URL string.
|
|
135 | + *
|
|
136 | + * @param url The complete URL to analyze
|
|
137 | + * @param publicSuffixList The [PublicSuffixList] used to get the eTLD+1 for the host
|
|
138 | + * @param allowBlobUnwrapping Whether to allow unwrapping blob URLs
|
|
139 | + * @return A Pair of (startIndex, endIndex) for either:
|
|
140 | + * - The registrable domain's position within the URL, or
|
|
141 | + * - The host's position within the URL if no registrable domain was found, or
|
|
142 | + * - null if the URL has no host or the host couldn't be located in the URL
|
|
143 | + */
|
|
144 | +@Suppress("ReturnCount")
|
|
145 | +@VisibleForTesting
|
|
146 | +internal suspend fun getRegistrableDomainOrHostSpan(
|
|
147 | + url: String,
|
|
148 | + publicSuffixList: PublicSuffixList,
|
|
149 | + allowBlobUnwrapping: Boolean = true,
|
|
150 | +): Pair<Int, Int>? {
|
|
151 | + if (url.startsWith(BLOB_URL_PREFIX)) {
|
|
152 | + if (!allowBlobUnwrapping) return null
|
|
153 | + |
|
154 | + val innerUrl = url.substring(BLOB_URL_PREFIX.length)
|
|
155 | + return getRegistrableDomainOrHostSpan(
|
|
156 | + innerUrl,
|
|
157 | + publicSuffixList,
|
|
158 | + allowBlobUnwrapping = false,
|
|
159 | + )?.let { (start, end) ->
|
|
160 | + BLOB_URL_PREFIX.length + start to BLOB_URL_PREFIX.length + end
|
|
161 | + }
|
|
108 | 162 | }
|
109 | 163 | |
110 | - val index = indexOfHost + indexOfRegistrableDomain
|
|
164 | + val uri = url.toUri()
|
|
165 | + if (!uri.isHttpOrHttps) return null
|
|
111 | 166 | |
112 | - setSpan(
|
|
113 | - ForegroundColorSpan(configuration.registrableDomainColor),
|
|
114 | - index,
|
|
115 | - index + registrableDomain.length,
|
|
116 | - SPAN_INCLUSIVE_INCLUSIVE,
|
|
117 | - )
|
|
118 | -}
|
|
167 | + val host = uri.host ?: return null
|
|
168 | + |
|
169 | + val hostStart = url.indexOf(host)
|
|
170 | + if (hostStart == -1) return null
|
|
119 | 171 | |
120 | -private fun SpannableStringBuilder.color(@ColorInt urlColor: Int?) {
|
|
121 | - urlColor ?: return
|
|
172 | + val domainSpan = getRegistrableDomainSpanInHost(host, publicSuffixList)
|
|
173 | + return domainSpan?.let { (start, end) ->
|
|
174 | + hostStart + start to hostStart + end
|
|
175 | + } ?: (hostStart to hostStart + host.length)
|
|
176 | +}
|
|
122 | 177 | |
178 | +private fun SpannableStringBuilder.applyUrlColors(
|
|
179 | + @ColorInt urlColor: Int,
|
|
180 | + @ColorInt registrableDomainColor: Int,
|
|
181 | + registrableDomainOrHostSpan: Pair<Int, Int>,
|
|
182 | +): SpannableStringBuilder = apply {
|
|
123 | 183 | setSpan(
|
124 | 184 | ForegroundColorSpan(urlColor),
|
125 | 185 | 0,
|
126 | 186 | length,
|
127 | 187 | SPAN_INCLUSIVE_INCLUSIVE,
|
128 | 188 | )
|
189 | + |
|
190 | + val (start, end) = registrableDomainOrHostSpan
|
|
191 | + setSpan(
|
|
192 | + Toolbar.RegistrableDomainColorSpan(registrableDomainColor),
|
|
193 | + start,
|
|
194 | + end,
|
|
195 | + SPAN_INCLUSIVE_INCLUSIVE,
|
|
196 | + )
|
|
129 | 197 | } |
... | ... | @@ -5,8 +5,10 @@ |
5 | 5 | package mozilla.components.feature.toolbar.internal
|
6 | 6 | |
7 | 7 | import android.graphics.Color
|
8 | +import android.net.InetAddresses
|
|
8 | 9 | import android.text.SpannableStringBuilder
|
9 | 10 | import android.text.style.ForegroundColorSpan
|
11 | +import android.util.Patterns
|
|
10 | 12 | import androidx.test.ext.junit.runners.AndroidJUnit4
|
11 | 13 | import kotlinx.coroutines.Dispatchers
|
12 | 14 | import mozilla.components.concept.toolbar.Toolbar
|
... | ... | @@ -26,8 +28,12 @@ import org.junit.Rule |
26 | 28 | import org.junit.Test
|
27 | 29 | import org.junit.runner.RunWith
|
28 | 30 | import org.mockito.Mockito.verify
|
31 | +import org.robolectric.annotation.Config
|
|
32 | +import org.robolectric.annotation.Implementation
|
|
33 | +import org.robolectric.annotation.Implements
|
|
29 | 34 | |
30 | 35 | @RunWith(AndroidJUnit4::class)
|
36 | +@Config(shadows = [ShadowInetAddresses::class])
|
|
31 | 37 | class URLRendererTest {
|
32 | 38 | |
33 | 39 | @get:Rule
|
... | ... | @@ -104,10 +110,7 @@ class URLRendererTest { |
104 | 110 | }
|
105 | 111 | }
|
106 | 112 | |
107 | - private suspend fun testRenderWithColoredUrl(
|
|
108 | - testUrl: String,
|
|
109 | - expectedRegistrableDomainSpan: Pair<Int, Int>,
|
|
110 | - ) {
|
|
113 | + private suspend fun getSpannedUrl(testUrl: String): SpannableStringBuilder {
|
|
111 | 114 | val configuration = ToolbarFeature.UrlRenderConfiguration(
|
112 | 115 | publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
|
113 | 116 | registrableDomainColor = Color.RED,
|
... | ... | @@ -124,9 +127,14 @@ class URLRendererTest { |
124 | 127 | val captor = argumentCaptor<CharSequence>()
|
125 | 128 | verify(toolbar).url = captor.capture()
|
126 | 129 | |
127 | - assertNotNull(captor.value)
|
|
128 | - assertTrue(captor.value is SpannableStringBuilder)
|
|
129 | - val url = captor.value as SpannableStringBuilder
|
|
130 | + return requireNotNull(captor.value as? SpannableStringBuilder) { "Toolbar URL should not be null" }
|
|
131 | + }
|
|
132 | + |
|
133 | + private suspend fun testRenderWithColoredUrl(
|
|
134 | + testUrl: String,
|
|
135 | + expectedRegistrableDomainSpan: Pair<Int, Int>,
|
|
136 | + ) {
|
|
137 | + val url = getSpannedUrl(testUrl)
|
|
130 | 138 | |
131 | 139 | assertEquals(testUrl, url.toString())
|
132 | 140 | |
... | ... | @@ -143,8 +151,237 @@ class URLRendererTest { |
143 | 151 | assertEquals(expectedRegistrableDomainSpan.second, url.getSpanEnd(spans[1]))
|
144 | 152 | }
|
145 | 153 | |
154 | + private suspend fun testRenderWithUncoloredUrl(testUrl: String) {
|
|
155 | + val url = getSpannedUrl(testUrl)
|
|
156 | + |
|
157 | + assertEquals(testUrl, url.toString())
|
|
158 | + |
|
159 | + val spans = url.getSpans(0, url.length, ForegroundColorSpan::class.java)
|
|
160 | + |
|
161 | + assertEquals(0, spans.size)
|
|
162 | + }
|
|
163 | + |
|
164 | + private suspend fun testRenderWithRegistrableDomain(
|
|
165 | + testUrl: String,
|
|
166 | + expectedUrl: String,
|
|
167 | + ) {
|
|
168 | + val configuration = ToolbarFeature.UrlRenderConfiguration(
|
|
169 | + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
|
|
170 | + registrableDomainColor = Color.RED,
|
|
171 | + urlColor = Color.GREEN,
|
|
172 | + renderStyle = ToolbarFeature.RenderStyle.RegistrableDomain,
|
|
173 | + )
|
|
174 | + |
|
175 | + val toolbar: Toolbar = mock()
|
|
176 | + |
|
177 | + val renderer = URLRenderer(toolbar, configuration)
|
|
178 | + |
|
179 | + renderer.updateUrl(testUrl)
|
|
180 | + |
|
181 | + val captor = argumentCaptor<CharSequence>()
|
|
182 | + verify(toolbar).url = captor.capture()
|
|
183 | + |
|
184 | + assertNotNull(captor.value)
|
|
185 | + assertTrue(captor.value is String)
|
|
186 | + val url = captor.value as String
|
|
187 | + |
|
188 | + assertEquals(expectedUrl, url)
|
|
189 | + }
|
|
190 | + |
|
191 | + @Test
|
|
192 | + fun `GIVEN a simple domain WHEN getting registrable domain span in host THEN span is returned`() {
|
|
193 | + runTestOnMain {
|
|
194 | + val domainSpan = getRegistrableDomainSpanInHost(
|
|
195 | + host = "www.mozilla.org",
|
|
196 | + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
|
|
197 | + )
|
|
198 | + |
|
199 | + assertEquals(4 to 15, domainSpan)
|
|
200 | + }
|
|
201 | + }
|
|
202 | + |
|
203 | + @Test
|
|
204 | + fun `GIVEN a host with a trailing period in the domain WHEN getting registrable domain span in host THEN span is returned`() {
|
|
205 | + runTestOnMain {
|
|
206 | + val domainSpan = getRegistrableDomainSpanInHost(
|
|
207 | + host = "www.mozilla.org.",
|
|
208 | + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
|
|
209 | + )
|
|
210 | + |
|
211 | + assertEquals(4 to 15, domainSpan)
|
|
212 | + }
|
|
213 | + }
|
|
214 | + |
|
215 | + @Test
|
|
216 | + 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`() {
|
|
217 | + runTestOnMain {
|
|
218 | + val domainSpan = getRegistrableDomainSpanInHost(
|
|
219 | + host = "mozilla.org.mozilla.org",
|
|
220 | + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
|
|
221 | + )
|
|
222 | + |
|
223 | + assertEquals(12 to 23, domainSpan)
|
|
224 | + }
|
|
225 | + }
|
|
226 | + |
|
227 | + @Test
|
|
228 | + fun `GIVEN an IPv4 address as host WHEN getting registrable domain span in host THEN null is returned`() {
|
|
229 | + runTestOnMain {
|
|
230 | + val domainSpan = getRegistrableDomainSpanInHost(
|
|
231 | + host = "127.0.0.1",
|
|
232 | + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
|
|
233 | + )
|
|
234 | + |
|
235 | + assertNull(domainSpan)
|
|
236 | + }
|
|
237 | + }
|
|
238 | + |
|
239 | + @Test
|
|
240 | + fun `GIVEN an IPv6 address as host WHEN getting registrable domain span in host THEN null is returned`() {
|
|
241 | + runTestOnMain {
|
|
242 | + val domainSpan = getRegistrableDomainSpanInHost(
|
|
243 | + host = "[::1]",
|
|
244 | + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
|
|
245 | + )
|
|
246 | + |
|
247 | + assertNull(domainSpan)
|
|
248 | + }
|
|
249 | + }
|
|
250 | + |
|
251 | + @Test
|
|
252 | + fun `GIVEN a non PSL domain as host WHEN getting registrable domain span in host THEN null is returned`() {
|
|
253 | + runTestOnMain {
|
|
254 | + val domainSpan = getRegistrableDomainSpanInHost(
|
|
255 | + host = "localhost",
|
|
256 | + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
|
|
257 | + )
|
|
258 | + |
|
259 | + assertNull(domainSpan)
|
|
260 | + }
|
|
261 | + }
|
|
262 | + |
|
263 | + @Test
|
|
264 | + fun `GIVEN a simple URL WHEN getting registrable domain or host span THEN span is returned`() {
|
|
265 | + runTestOnMain {
|
|
266 | + val span = getRegistrableDomainOrHostSpan(
|
|
267 | + url = "https://www.mozilla.org/",
|
|
268 | + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
|
|
269 | + )
|
|
270 | + |
|
271 | + assertEquals(12 to 23, span)
|
|
272 | + }
|
|
273 | + }
|
|
274 | + |
|
275 | + @Test
|
|
276 | + fun `GIVEN a URL with a trailing period in the domain WHEN getting registrable domain or host span THEN span is returned`() {
|
|
277 | + runTestOnMain {
|
|
278 | + val span = getRegistrableDomainOrHostSpan(
|
|
279 | + url = "https://www.mozilla.org./",
|
|
280 | + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
|
|
281 | + )
|
|
282 | + |
|
283 | + assertEquals(12 to 23, span)
|
|
284 | + }
|
|
285 | + }
|
|
286 | + |
|
287 | + @Test
|
|
288 | + 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`() {
|
|
289 | + runTestOnMain {
|
|
290 | + val span = getRegistrableDomainOrHostSpan(
|
|
291 | + url = "https://mozilla.org.mozilla.org/",
|
|
292 | + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
|
|
293 | + )
|
|
294 | + |
|
295 | + assertEquals(20 to 31, span)
|
|
296 | + }
|
|
297 | + }
|
|
298 | + |
|
299 | + @Test
|
|
300 | + fun `GIVEN a URL with an IPv4 address WHEN getting registrable domain or host span THEN the span of the IP part is returned`() {
|
|
301 | + runTestOnMain {
|
|
302 | + val span = getRegistrableDomainOrHostSpan(
|
|
303 | + url = "http://127.0.0.1/",
|
|
304 | + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
|
|
305 | + )
|
|
306 | + |
|
307 | + assertEquals(7 to 16, span)
|
|
308 | + }
|
|
309 | + }
|
|
310 | + |
|
311 | + @Test
|
|
312 | + fun `GIVEN a URL with an IPv6 address WHEN getting registrable domain or host span THEN the span of the IP part is returned`() {
|
|
313 | + runTestOnMain {
|
|
314 | + val span = getRegistrableDomainOrHostSpan(
|
|
315 | + url = "http://[::1]/",
|
|
316 | + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
|
|
317 | + )
|
|
318 | + |
|
319 | + assertEquals(7 to 12, span)
|
|
320 | + }
|
|
321 | + }
|
|
322 | + |
|
146 | 323 | @Test
|
147 | - fun `Render with simple URL`() {
|
|
324 | + 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`() {
|
|
325 | + runTestOnMain {
|
|
326 | + val span = getRegistrableDomainOrHostSpan(
|
|
327 | + url = "http://localhost/",
|
|
328 | + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
|
|
329 | + )
|
|
330 | + |
|
331 | + assertEquals(7 to 16, span)
|
|
332 | + }
|
|
333 | + }
|
|
334 | + |
|
335 | + @Test
|
|
336 | + fun `GIVEN an internal page name WHEN getting registrable domain or host span THEN null is returned`() {
|
|
337 | + runTestOnMain {
|
|
338 | + val span = getRegistrableDomainOrHostSpan(
|
|
339 | + url = "about:mozilla",
|
|
340 | + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
|
|
341 | + )
|
|
342 | + |
|
343 | + assertNull(span)
|
|
344 | + }
|
|
345 | + }
|
|
346 | + |
|
347 | + @Test
|
|
348 | + fun `GIVEN a content URI WHEN getting registrable domain or host span THEN null is returned`() {
|
|
349 | + runTestOnMain {
|
|
350 | + val span = getRegistrableDomainOrHostSpan(
|
|
351 | + url = "content://media/external/file/1000000000",
|
|
352 | + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
|
|
353 | + )
|
|
354 | + |
|
355 | + assertNull(span)
|
|
356 | + }
|
|
357 | + }
|
|
358 | + |
|
359 | + @Test
|
|
360 | + fun `GIVEN a blob URI WHEN getting registrable domain or host span THEN domain span is returned`() {
|
|
361 | + runTestOnMain {
|
|
362 | + val span = getRegistrableDomainOrHostSpan(
|
|
363 | + url = "blob:https://www.mozilla.org/69a29afb-938c-4b9e-9fca-b2f79755047a",
|
|
364 | + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
|
|
365 | + )
|
|
366 | + |
|
367 | + assertEquals(17 to 28, span)
|
|
368 | + }
|
|
369 | + }
|
|
370 | + |
|
371 | + @Test
|
|
372 | + fun `GIVEN a blob URI with duplicated blob prefix WHEN getting registrable domain or host span THEN null is returned`() {
|
|
373 | + runTestOnMain {
|
|
374 | + val span = getRegistrableDomainOrHostSpan(
|
|
375 | + url = "blob:blob:https://www.mozilla.org/69a29afb-938c-4b9e-9fca-b2f79755047a",
|
|
376 | + publicSuffixList = PublicSuffixList(testContext, Dispatchers.Unconfined),
|
|
377 | + )
|
|
378 | + |
|
379 | + assertNull(span)
|
|
380 | + }
|
|
381 | + }
|
|
382 | + |
|
383 | + @Test
|
|
384 | + fun `GIVEN a simple URL WHEN rendering it THEN registrable domain is colored`() {
|
|
148 | 385 | runTestOnMain {
|
149 | 386 | testRenderWithColoredUrl(
|
150 | 387 | testUrl = "https://www.mozilla.org/",
|
... | ... | @@ -154,7 +391,7 @@ class URLRendererTest { |
154 | 391 | }
|
155 | 392 | |
156 | 393 | @Test
|
157 | - fun `Render with URL containing domain with trailing period`() {
|
|
394 | + fun `GIVEN a URL with a trailing period in the domain WHEN rendering it THEN registrable domain is colored`() {
|
|
158 | 395 | runTestOnMain {
|
159 | 396 | testRenderWithColoredUrl(
|
160 | 397 | testUrl = "https://www.mozilla.org./",
|
... | ... | @@ -164,7 +401,7 @@ class URLRendererTest { |
164 | 401 | }
|
165 | 402 | |
166 | 403 | @Test
|
167 | - fun `Render with URL containing repeated domain`() {
|
|
404 | + fun `GIVEN a URL with a repeated domain WHEN rendering it THEN the last occurrence of domain is colored`() {
|
|
168 | 405 | runTestOnMain {
|
169 | 406 | testRenderWithColoredUrl(
|
170 | 407 | testUrl = "https://mozilla.org.mozilla.org/",
|
... | ... | @@ -172,4 +409,144 @@ class URLRendererTest { |
172 | 409 | )
|
173 | 410 | }
|
174 | 411 | }
|
412 | + |
|
413 | + @Test
|
|
414 | + fun `GIVEN a URL with an IPv4 address WHEN rendering it THEN the IP part is colored`() {
|
|
415 | + runTestOnMain {
|
|
416 | + testRenderWithColoredUrl(
|
|
417 | + testUrl = "http://127.0.0.1/",
|
|
418 | + expectedRegistrableDomainSpan = 7 to 16,
|
|
419 | + )
|
|
420 | + }
|
|
421 | + }
|
|
422 | + |
|
423 | + @Test
|
|
424 | + fun `GIVEN a URL with an IPv6 address WHEN rendering it THEN the IP part is colored`() {
|
|
425 | + runTestOnMain {
|
|
426 | + testRenderWithColoredUrl(
|
|
427 | + testUrl = "http://[::1]/",
|
|
428 | + expectedRegistrableDomainSpan = 7 to 12,
|
|
429 | + )
|
|
430 | + }
|
|
431 | + }
|
|
432 | + |
|
433 | + @Test
|
|
434 | + fun `GIVEN a URL with a non PSL domain WHEN rendering it THEN host colored`() {
|
|
435 | + runTestOnMain {
|
|
436 | + testRenderWithColoredUrl(
|
|
437 | + testUrl = "http://localhost/",
|
|
438 | + expectedRegistrableDomainSpan = 7 to 16,
|
|
439 | + )
|
|
440 | + }
|
|
441 | + }
|
|
442 | + |
|
443 | + @Test
|
|
444 | + fun `GIVEN an internal page name WHEN rendering it THEN nothing is colored`() {
|
|
445 | + runTestOnMain {
|
|
446 | + testRenderWithUncoloredUrl("about:mozilla")
|
|
447 | + }
|
|
448 | + }
|
|
449 | + |
|
450 | + @Test
|
|
451 | + fun `GIVEN a content URI WHEN rendering it THEN nothing is colored`() {
|
|
452 | + runTestOnMain {
|
|
453 | + testRenderWithUncoloredUrl("content://media/external/file/1000000000")
|
|
454 | + }
|
|
455 | + }
|
|
456 | + |
|
457 | + @Test
|
|
458 | + fun `GIVEN a simple URL WHEN rendering it THEN registrable domain is set`() {
|
|
459 | + runTestOnMain {
|
|
460 | + testRenderWithRegistrableDomain(
|
|
461 | + testUrl = "https://www.mozilla.org/",
|
|
462 | + expectedUrl = "mozilla.org",
|
|
463 | + )
|
|
464 | + }
|
|
465 | + }
|
|
466 | + |
|
467 | + @Test
|
|
468 | + fun `GIVEN a URL with a trailing period in the domain WHEN rendering it THEN registrable domain is set`() {
|
|
469 | + runTestOnMain {
|
|
470 | + testRenderWithRegistrableDomain(
|
|
471 | + testUrl = "https://www.mozilla.org./",
|
|
472 | + expectedUrl = "mozilla.org",
|
|
473 | + )
|
|
474 | + }
|
|
475 | + }
|
|
476 | + |
|
477 | + @Test
|
|
478 | + fun `GIVEN a URL with a repeated domain WHEN rendering it THEN the last occurrence of domain is set`() {
|
|
479 | + runTestOnMain {
|
|
480 | + testRenderWithRegistrableDomain(
|
|
481 | + testUrl = "https://mozilla.org.mozilla.org/",
|
|
482 | + expectedUrl = "mozilla.org",
|
|
483 | + )
|
|
484 | + }
|
|
485 | + }
|
|
486 | + |
|
487 | + @Test
|
|
488 | + fun `GIVEN a URL with an IPv4 address WHEN rendering it THEN the IP part is set`() {
|
|
489 | + runTestOnMain {
|
|
490 | + testRenderWithRegistrableDomain(
|
|
491 | + testUrl = "http://127.0.0.1/",
|
|
492 | + expectedUrl = "127.0.0.1",
|
|
493 | + )
|
|
494 | + }
|
|
495 | + }
|
|
496 | + |
|
497 | + @Test
|
|
498 | + fun `GIVEN a URL with an IPv6 address WHEN rendering it THEN the IP part is set`() {
|
|
499 | + runTestOnMain {
|
|
500 | + testRenderWithRegistrableDomain(
|
|
501 | + testUrl = "http://[::1]/",
|
|
502 | + expectedUrl = "[::1]",
|
|
503 | + )
|
|
504 | + }
|
|
505 | + }
|
|
506 | + |
|
507 | + @Test
|
|
508 | + fun `GIVEN a URL with a non PSL domain WHEN rendering it THEN host set`() {
|
|
509 | + runTestOnMain {
|
|
510 | + testRenderWithRegistrableDomain(
|
|
511 | + testUrl = "http://localhost/",
|
|
512 | + expectedUrl = "localhost",
|
|
513 | + )
|
|
514 | + }
|
|
515 | + }
|
|
516 | + |
|
517 | + @Test
|
|
518 | + fun `GIVEN an internal page name WHEN rendering it THEN it is set`() {
|
|
519 | + runTestOnMain {
|
|
520 | + testRenderWithRegistrableDomain(
|
|
521 | + testUrl = "about:mozilla",
|
|
522 | + expectedUrl = "about:mozilla",
|
|
523 | + )
|
|
524 | + }
|
|
525 | + }
|
|
526 | + |
|
527 | + @Test
|
|
528 | + fun `GIVEN a content URI WHEN rendering it THEN it is set`() {
|
|
529 | + runTestOnMain {
|
|
530 | + testRenderWithRegistrableDomain(
|
|
531 | + testUrl = "content://media/external/file/1000000000",
|
|
532 | + expectedUrl = "content://media/external/file/1000000000",
|
|
533 | + )
|
|
534 | + }
|
|
535 | + }
|
|
536 | +}
|
|
537 | + |
|
538 | +/**
|
|
539 | + * Robolectric default implementation of [InetAddresses] returns false for any address.
|
|
540 | + * This shadow is used to override that behavior and return true for any IP address.
|
|
541 | + */
|
|
542 | +@Implements(InetAddresses::class)
|
|
543 | +class ShadowInetAddresses {
|
|
544 | + companion object {
|
|
545 | + @Implementation
|
|
546 | + @JvmStatic
|
|
547 | + @Suppress("DEPRECATION")
|
|
548 | + fun isNumericAddress(address: String): Boolean {
|
|
549 | + return Patterns.IP_ADDRESS.matcher(address).matches() || address.contains(":")
|
|
550 | + }
|
|
551 | + }
|
|
175 | 552 | } |
... | ... | @@ -7,8 +7,6 @@ package mozilla.components.support.ktx.util |
7 | 7 | import android.text.TextUtils
|
8 | 8 | import androidx.annotation.VisibleForTesting
|
9 | 9 | import androidx.core.net.toUri
|
10 | -import androidx.core.text.TextDirectionHeuristicCompat
|
|
11 | -import androidx.core.text.TextDirectionHeuristicsCompat
|
|
12 | 10 | import java.util.regex.Pattern
|
13 | 11 | |
14 | 12 | object URLStringUtils {
|
... | ... | @@ -102,25 +100,9 @@ object URLStringUtils { |
102 | 100 | /**
|
103 | 101 | * Generates a shorter version of the provided URL for display purposes by stripping it of
|
104 | 102 | * https/http and/or WWW prefixes and/or trailing slash when applicable.
|
105 | - *
|
|
106 | - * The returned text will always be displayed from left to right.
|
|
107 | - * If the directionality would otherwise be RTL "\u200E" will be prepended to the result to force LTR.
|
|
108 | 103 | */
|
109 | - fun toDisplayUrl(
|
|
110 | - originalUrl: CharSequence,
|
|
111 | - textDirectionHeuristic: TextDirectionHeuristicCompat = TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR,
|
|
112 | - ): CharSequence {
|
|
113 | - val strippedText = maybeStripTrailingSlash(maybeStripUrlProtocol(originalUrl))
|
|
114 | - |
|
115 | - return if (
|
|
116 | - strippedText.isNotBlank() &&
|
|
117 | - textDirectionHeuristic.isRtl(strippedText, 0, 1)
|
|
118 | - ) {
|
|
119 | - "\u200E" + strippedText
|
|
120 | - } else {
|
|
121 | - strippedText
|
|
122 | - }
|
|
123 | - }
|
|
104 | + fun toDisplayUrl(originalUrl: CharSequence): CharSequence =
|
|
105 | + maybeStripTrailingSlash(maybeStripUrlProtocol(originalUrl))
|
|
124 | 106 | |
125 | 107 | private fun maybeStripUrlProtocol(url: CharSequence): CharSequence {
|
126 | 108 | if (url.startsWith(HTTPS)) {
|
... | ... | @@ -16,8 +16,6 @@ import org.junit.Assert.assertTrue |
16 | 16 | import org.junit.Before
|
17 | 17 | import org.junit.Test
|
18 | 18 | import org.junit.runner.RunWith
|
19 | -import org.mockito.Mockito.spy
|
|
20 | -import org.mockito.Mockito.verify
|
|
21 | 19 | import kotlin.random.Random
|
22 | 20 | |
23 | 21 | @RunWith(AndroidJUnit4::class)
|
... | ... | @@ -246,20 +244,9 @@ class URLStringUtilsTest { |
246 | 244 | }
|
247 | 245 | |
248 | 246 | @Test
|
249 | - fun showDisplayUrlAsLTREvenIfTextStartsWithArabicCharacters() {
|
|
247 | + fun toDisplayUrlDoesNotAddImplicitDirectionalMarks() {
|
|
250 | 248 | val testDisplayUrl = URLStringUtils.toDisplayUrl("http://ختار.ار/www.mozilla.org/1")
|
251 | - assertEquals("\u200Eختار.ار/www.mozilla.org/1", testDisplayUrl)
|
|
252 | - }
|
|
253 | - |
|
254 | - @Test
|
|
255 | - fun toDisplayUrlAlwaysUseATextDirectionHeuristicToDetermineDirectionality() {
|
|
256 | - val textHeuristic = spy(TestTextDirectionHeuristicCompat())
|
|
257 | - |
|
258 | - URLStringUtils.toDisplayUrl("http://ختار.ار/www.mozilla.org/1", textHeuristic)
|
|
259 | - verify(textHeuristic).isRtl("ختار.ار/www.mozilla.org/1", 0, 1)
|
|
260 | - |
|
261 | - URLStringUtils.toDisplayUrl("http://www.mozilla.org/1", textHeuristic)
|
|
262 | - verify(textHeuristic).isRtl("mozilla.org/1", 0, 1)
|
|
249 | + assertEquals("ختار.ار/www.mozilla.org/1", testDisplayUrl)
|
|
263 | 250 | }
|
264 | 251 | |
265 | 252 | @Test
|
... | ... | @@ -16,6 +16,8 @@ |
16 | 16 | #include "nsIWebProgressListener.h"
|
17 | 17 | #include "nsIX509Cert.h"
|
18 | 18 | #include "nsPrintfCString.h"
|
19 | +#include "nsContentSecurityUtils.h"
|
|
20 | +#include "nsITransfer.h"
|
|
19 | 21 | |
20 | 22 | #include "nsNetUtil.h"
|
21 | 23 | |
... | ... | @@ -85,6 +87,16 @@ GeckoViewStreamListener::OnStartRequest(nsIRequest* aRequest) { |
85 | 87 | return NS_OK;
|
86 | 88 | }
|
87 | 89 | |
90 | + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
|
|
91 | + if (channel) {
|
|
92 | + int32_t classification = nsContentSecurityUtils::ClassifyDownload(channel);
|
|
93 | + if (classification == nsITransfer::DOWNLOAD_FORBIDDEN) {
|
|
94 | + channel->Cancel(NS_ERROR_ABORT);
|
|
95 | + CompleteWithError(NS_ERROR_ABORT, channel);
|
|
96 | + return NS_OK;
|
|
97 | + }
|
|
98 | + }
|
|
99 | + |
|
88 | 100 | // We're expecting data later via OnDataAvailable, so create the stream now.
|
89 | 101 | InitializeStreamSupport(aRequest);
|
90 | 102 |
... | ... | @@ -58,6 +58,7 @@ abstract class ToolbarIntegration( |
58 | 58 | urlRenderConfiguration = ToolbarFeature.UrlRenderConfiguration(
|
59 | 59 | context.components.publicSuffixList,
|
60 | 60 | context.getColorFromAttr(R.attr.textPrimary),
|
61 | + context.getColorFromAttr(R.attr.textSecondary),
|
|
61 | 62 | renderStyle = renderStyle,
|
62 | 63 | ),
|
63 | 64 | )
|
... | ... | @@ -140,7 +141,7 @@ class DefaultToolbarIntegration( |
140 | 141 | interactor = interactor,
|
141 | 142 | customTabId = customTabId,
|
142 | 143 | isPrivate = isPrivate,
|
143 | - renderStyle = ToolbarFeature.RenderStyle.UncoloredUrl,
|
|
144 | + renderStyle = ToolbarFeature.RenderStyle.ColoredUrl,
|
|
144 | 145 | ) {
|
145 | 146 | |
146 | 147 | @VisibleForTesting
|
... | ... | @@ -6,7 +6,6 @@ |
6 | 6 | if (os == "linux") and not fission and not debug: [PASS, FAIL]
|
7 | 7 | if (os == "mac") and debug: [PASS, FAIL]
|
8 | 8 | if (os == "mac") and not debug: [PASS, FAIL]
|
9 | - if os == "android": FAIL
|
|
10 | 9 | |
11 | 10 | [<a download> triggered download in sandbox is blocked before a request is made.]
|
12 | 11 | expected: FAIL
|
... | ... | @@ -15,15 +14,12 @@ |
15 | 14 | expected:
|
16 | 15 | if (os == "mac") and debug: [PASS, FAIL]
|
17 | 16 | if (os == "mac") and not debug: [PASS, FAIL]
|
18 | - if os == "android": FAIL
|
|
19 | 17 | |
20 | 18 | [<a target="_blank" > triggered download in sandbox is blocked.]
|
21 | 19 | expected:
|
22 | 20 | if (os == "mac") and debug: [PASS, FAIL]
|
23 | 21 | if (os == "mac") and not debug: [PASS, FAIL]
|
24 | - if os == "android": FAIL
|
|
25 | 22 | |
26 | 23 | [<a target="_blank" rel="noopener" > triggered download in sandbox is blocked.]
|
27 | 24 | expected:
|
28 | 25 | if (os == "mac") and debug: [PASS, FAIL] |
29 | - if os == "android": FAIL |
... | ... | @@ -2,10 +2,8 @@ |
2 | 2 | [Navigation resulted download in sandbox is blocked.]
|
3 | 3 | expected:
|
4 | 4 | if (os == "mac") and not debug: [PASS, FAIL]
|
5 | - if os == "android": FAIL
|
|
6 | 5 | |
7 | 6 | [Navigation resulted download in sandbox from <object> is blocked.]
|
8 | 7 | expected:
|
9 | 8 | if (os == "mac") and debug: [PASS, FAIL]
|
10 | 9 | if (os == "mac") and not debug: [PASS, FAIL] |
11 | - if os == "android": FAIL |
... | ... | @@ -3,17 +3,15 @@ |
3 | 3 | expected:
|
4 | 4 | if (os == "linux") and debug and not fission: [PASS, FAIL]
|
5 | 5 | if (os == "linux") and not debug: [PASS, FAIL]
|
6 | - if os == "android": FAIL
|
|
7 | 6 | |
8 | 7 | [window.open(download, "_blank") triggering download in sandbox is blocked.]
|
9 | 8 | expected:
|
10 | 9 | if (os == "mac") and debug: [PASS, FAIL]
|
11 | 10 | if (os == "linux") and not debug: [PASS, FAIL]
|
12 | - if os == "android": FAIL
|
|
13 | 11 | |
14 | 12 | [window.open(download, "_blank", "noopener") triggering download in sandbox is blocked.]
|
15 | 13 | expected:
|
16 | 14 | if (os == "linux") and debug: PASS
|
17 | 15 | if os == "win": PASS
|
18 | - if os == "android": FAIL
|
|
16 | + if os == "android": PASS
|
|
19 | 17 | [PASS, FAIL] |
1 | 1 | function StreamDownloadFinishDelay() {
|
2 | - return 1000;
|
|
2 | + return 2000;
|
|
3 | 3 | }
|
4 | 4 | |
5 | 5 | function DownloadVerifyDelay() {
|
... | ... | @@ -1626,8 +1626,7 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { |
1626 | 1626 | return NS_OK;
|
1627 | 1627 | }
|
1628 | 1628 | |
1629 | - mDownloadClassification =
|
|
1630 | - nsContentSecurityUtils::ClassifyDownload(aChannel, MIMEType);
|
|
1629 | + mDownloadClassification = nsContentSecurityUtils::ClassifyDownload(aChannel);
|
|
1631 | 1630 | |
1632 | 1631 | if (mDownloadClassification == nsITransfer::DOWNLOAD_FORBIDDEN) {
|
1633 | 1632 | // If the download is rated as forbidden,
|