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,
 |