Skip to content
This repository has been archived by the owner on Jun 17, 2024. It is now read-only.

Commit

Permalink
[components] Closes mozilla-mobile/android-components#1504: Add suppo…
Browse files Browse the repository at this point in the history
…rt for alert popup in WebView.
  • Loading branch information
Amejia481 committed Jan 8, 2019
1 parent 4adacd3 commit b1ab769
Show file tree
Hide file tree
Showing 4 changed files with 264 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import android.util.AttributeSet
import android.view.View
import android.webkit.CookieManager
import android.webkit.DownloadListener
import android.webkit.JsResult
import android.webkit.PermissionRequest
import android.webkit.SslErrorHandler
import android.webkit.ValueCallback
Expand Down Expand Up @@ -47,6 +48,7 @@ import mozilla.components.concept.engine.prompt.PromptRequest
import mozilla.components.concept.engine.request.RequestInterceptor.InterceptionResponse
import mozilla.components.support.ktx.android.content.isOSOnLowMemory
import mozilla.components.support.utils.DownloadUtils
import java.util.Date

/**
* WebView-based implementation of EngineView.
Expand All @@ -59,6 +61,9 @@ class SystemEngineView @JvmOverloads constructor(
) : FrameLayout(context, attrs, defStyleAttr), EngineView, View.OnLongClickListener {

private var session: SystemEngineSession? = null
internal var jsAlertCount = 0
internal var shouldShowMoreDialogs = true
internal var lastDialogShownAt = Date()

/**
* Render the content of the given session.
Expand Down Expand Up @@ -123,6 +128,7 @@ class SystemEngineView @JvmOverloads constructor(
onNavigationStateChange(view.canGoBack(), view.canGoForward())
}
}
resetJSAlertAbuseState()
}

override fun onPageFinished(view: WebView?, url: String?) {
Expand Down Expand Up @@ -300,6 +306,44 @@ class SystemEngineView @JvmOverloads constructor(
session?.internalNotifyObservers { onContentPermissionRequest(SystemPermissionRequest(request)) }
}

override fun onJsAlert(view: WebView, url: String?, message: String?, result: JsResult): Boolean {
val session = session ?: return super.onJsAlert(view, url, message, result)

// When an alert is triggered from a iframe, url is equals to about:blank, using currentUrl as a fallback.
val safeUrl = if (url.isNullOrBlank()) {
session.currentUrl
} else {
if (url.contains("about")) session.currentUrl else url
}

val title = context.getString(R.string.mozac_browser_engine_system_alert_title, safeUrl)

val onDismiss: () -> Unit = {
result.cancel()
}

if (shouldShowMoreDialogs) {

session.notifyObservers {
onPromptRequest(
PromptRequest.Alert(
title,
message ?: "",
areDialogsBeingAbused(),
onDismiss
) { shouldNotShowMoreDialogs ->
shouldShowMoreDialogs = !shouldNotShowMoreDialogs
result.confirm()
})
}
} else {
result.cancel()
}

updateJSDialogAbusedState()
return true
}

override fun onShowFileChooser(
webView: WebView?,
filePathCallback: ValueCallback<Array<Uri>>?,
Expand Down Expand Up @@ -449,7 +493,46 @@ class SystemEngineView @JvmOverloads constructor(

override fun canScrollVerticallyDown() = session?.webView?.canScrollVertically(1) ?: false

private fun resetJSAlertAbuseState() {
jsAlertCount = 0
shouldShowMoreDialogs = true
}

internal fun updateJSDialogAbusedState() {
if (!areDialogsAbusedByTime()) {
jsAlertCount = 0
}
++jsAlertCount
lastDialogShownAt = Date()
}

internal fun areDialogsBeingAbused(): Boolean {
return areDialogsAbusedByTime() || areDialogsAbusedByCount()
}

@Suppress("MagicNumber")
internal fun areDialogsAbusedByTime(): Boolean {
return if (jsAlertCount == 0) {
false
} else {
val now = Date()
val diffInSeconds = (now.time - lastDialogShownAt.time) / 1000 // 1 second equal to 1000 milliseconds
diffInSeconds < MAX_SUCCESSIVE_DIALOG_SECONDS_LIMIT
}
}

internal fun areDialogsAbusedByCount(): Boolean {
return jsAlertCount > MAX_SUCCESSIVE_DIALOG_COUNT
}

companion object {

// Maximum number of successive dialogs before we prompt users to disable dialogs.
internal const val MAX_SUCCESSIVE_DIALOG_COUNT: Int = 2

// Minimum time required between dialogs in seconds before enabling the stop dialog.
internal const val MAX_SUCCESSIVE_DIALOG_SECONDS_LIMIT: Int = 3

@Volatile
internal var URL_MATCHER: UrlMatcher? = null

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<resources>
<!-- Text for a title dialog on web page. -->
<string name="mozac_browser_engine_system_alert_title">The page at %1$s says:</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import android.net.http.SslError
import android.os.Bundle
import android.os.Message
import android.view.View
import android.webkit.JsResult
import android.webkit.SslErrorHandler
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
Expand All @@ -24,6 +25,8 @@ import android.webkit.WebView.HitTestResult
import androidx.test.core.app.ApplicationProvider
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.engine.system.SystemEngineView.Companion.MAX_SUCCESSIVE_DIALOG_COUNT
import mozilla.components.browser.engine.system.SystemEngineView.Companion.MAX_SUCCESSIVE_DIALOG_SECONDS_LIMIT
import mozilla.components.browser.engine.system.matcher.UrlMatcher
import mozilla.components.browser.errorpages.ErrorType
import mozilla.components.concept.engine.EngineSession
Expand Down Expand Up @@ -55,6 +58,10 @@ import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyZeroInteractions
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import java.util.Calendar
import java.util.Calendar.SECOND
import java.util.Calendar.YEAR
import java.util.Date

@RunWith(RobolectricTestRunner::class)
class SystemEngineViewTest {
Expand Down Expand Up @@ -1026,4 +1033,167 @@ class SystemEngineViewTest {
engineView.render(SystemEngineSession(getApplicationContext()))
assertFalse(engineView.onLongClick(null))
}
@Test
fun `Calling onJsAlert must provide an Alert PromptRequest`() {
val context = getApplicationContext<Context>()
val engineSession = SystemEngineSession(context)
val engineView = SystemEngineView(context)
var request: PromptRequest? = null

engineSession.register(object : EngineSession.Observer {
override fun onPromptRequest(promptRequest: PromptRequest) {
request = promptRequest
}
})

engineView.render(engineSession)

val mockJSResult = mock<JsResult>()

engineSession.webView.webChromeClient!!.onJsAlert(mock(), "http://www.mozilla.org", "message", mockJSResult)

val alertRequest = request as PromptRequest.Alert
assertTrue(request is PromptRequest.Alert)

assertTrue(alertRequest.title.contains("mozilla.org"))
assertEquals(alertRequest.hasShownManyDialogs, false)
assertEquals(alertRequest.message, "message")

alertRequest.onShouldShowNoMoreDialogs(true)
verify(mockJSResult).confirm()
assertEquals(engineView.jsAlertCount, 1)

alertRequest.onDismiss()
verify(mockJSResult).cancel()

alertRequest.onShouldShowNoMoreDialogs(true)
assertEquals(engineView.shouldShowMoreDialogs, false)

engineView.lastDialogShownAt = engineView.lastDialogShownAt.add(YEAR, -1)
engineSession.webView.webChromeClient!!.onJsAlert(mock(), "http://www.mozilla.org", "message", mockJSResult)

assertEquals(engineView.jsAlertCount, 1)
verify(mockJSResult, times(2)).cancel()

engineView.lastDialogShownAt = engineView.lastDialogShownAt.add(YEAR, -1)
engineView.jsAlertCount = 100
engineView.shouldShowMoreDialogs = true

engineSession.webView.webChromeClient!!.onJsAlert(mock(), "http://www.mozilla.org", null, mockJSResult)

assertTrue((request as PromptRequest.Alert).hasShownManyDialogs)

engineSession.currentUrl = "http://www.mozilla.org"
engineSession.webView.webChromeClient!!.onJsAlert(mock(), null, "message", mockJSResult)
assertTrue((request as PromptRequest.Alert).title.contains("mozilla.org"))
}

@Test
fun `are dialogs by count`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val engineView = SystemEngineView(context)

with(engineView) {

assertFalse(areDialogsAbusedByCount())

jsAlertCount = MAX_SUCCESSIVE_DIALOG_COUNT + 1

assertTrue(areDialogsAbusedByCount())

jsAlertCount = MAX_SUCCESSIVE_DIALOG_COUNT - 1

assertFalse(areDialogsAbusedByCount())
}
}

@Test
fun `are dialogs by time`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val engineView = SystemEngineView(context)

with(engineView) {

assertFalse(areDialogsAbusedByTime())

lastDialogShownAt = Date()

jsAlertCount = 1

assertTrue(areDialogsAbusedByTime())
}
}

@Test
fun `are dialogs being abused`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val engineView = SystemEngineView(context)

with(engineView) {

assertFalse(areDialogsBeingAbused())

jsAlertCount = MAX_SUCCESSIVE_DIALOG_COUNT + 1

assertTrue(areDialogsBeingAbused())

jsAlertCount = 0
lastDialogShownAt = Date()

assertFalse(areDialogsBeingAbused())

jsAlertCount = 1
lastDialogShownAt = Date()

assertTrue(areDialogsBeingAbused())
}
}

@Test
fun `update JSDialog abused state`() {

val context = ApplicationProvider.getApplicationContext<Context>()
val engineView = SystemEngineView(context)

with(engineView) {
val thresholdInSeconds = MAX_SUCCESSIVE_DIALOG_SECONDS_LIMIT + 1
lastDialogShownAt = lastDialogShownAt.add(SECOND, -thresholdInSeconds)

val initialDate = lastDialogShownAt
updateJSDialogAbusedState()

assertEquals(jsAlertCount, 1)
assertTrue(lastDialogShownAt.after(initialDate))

lastDialogShownAt = lastDialogShownAt.add(SECOND, -thresholdInSeconds)
updateJSDialogAbusedState()
assertEquals(jsAlertCount, 1)
}
}

@Test
fun `js alert abuse state must be reset every time a page is started`() {

val context = ApplicationProvider.getApplicationContext<Context>()
val engineSession = SystemEngineSession(context)
val engineView = SystemEngineView(context)

with(engineView) {
jsAlertCount = 20
shouldShowMoreDialogs = false

render(engineSession)
engineSession.webView.webViewClient!!.onPageStarted(mock(), "www.mozilla.org", null)

assertEquals(jsAlertCount, 0)
assertTrue(shouldShowMoreDialogs)
}
}

private fun Date.add(timeUnit: Int, amountOfTime: Int): Date {
val calendar = Calendar.getInstance()
calendar.time = this
calendar.add(timeUnit, amountOfTime)
return calendar.time
}
}
3 changes: 3 additions & 0 deletions android-components/docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ permalink: /changelog/
[Milestone](https://github.com/mozilla-mobile/android-components/milestone/40?closed=1),
[API reference](https://mozilla-mobile.github.io/android-components/api/0.38.0/index)

* **browser-engine-system**
* Added support for js alerts on SystemEngineView.

* Compiled against:
* Android (SDK: 28, Support Libraries: 28.0.0)
* Kotlin (Stdlib: 1.3.10, Coroutines: 1.0.1)
Expand Down

0 comments on commit b1ab769

Please sign in to comment.