Commit 976349ba authored by Arturo Mejia's avatar Arturo Mejia
Browse files

For issue #3264 Add api for interacting with the tracking protection

exceptions.
parent 44eac4b1
......@@ -19,6 +19,7 @@ import mozilla.components.concept.engine.EngineSessionState
import mozilla.components.concept.engine.EngineView
import mozilla.components.concept.engine.Settings
import mozilla.components.concept.engine.content.blocking.TrackerLog
import mozilla.components.concept.engine.content.blocking.TrackingProtectionExceptionStorage
import mozilla.components.concept.engine.history.HistoryTrackingDelegate
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
import mozilla.components.concept.engine.utils.EngineVersion
......@@ -40,7 +41,9 @@ class GeckoEngine(
context: Context,
private val defaultSettings: Settings? = null,
private val runtime: GeckoRuntime = GeckoRuntime.getDefault(context),
executorProvider: () -> GeckoWebExecutor = { GeckoWebExecutor(runtime) }
executorProvider: () -> GeckoWebExecutor = { GeckoWebExecutor(runtime) },
override val trackingProtectionExceptionStore: TrackingProtectionExceptionStorage =
TrackingProtectionExceptionFileStorage(context, runtime)
) : Engine {
private val executor by lazy { executorProvider.invoke() }
......@@ -56,6 +59,7 @@ class GeckoEngine(
@Suppress("TooGenericExceptionThrown")
throw RuntimeException("GeckoRuntime is shutting down")
}
trackingProtectionExceptionStore.restore()
}
/**
......
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.browser.engine.gecko
import android.content.Context
import android.util.AtomicFile
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.content.blocking.TrackingProtectionExceptionStorage
import mozilla.components.support.ktx.util.readAndDeserialize
import mozilla.components.support.ktx.util.writeString
import org.mozilla.geckoview.GeckoRuntime
import java.io.File
private const val STORE_FILE_NAME_FORMAT =
"mozilla_components_tracking_protection_storage_gecko.json"
/**
* A [TrackingProtectionExceptionStorage] implementation to store tracking protection exceptions.
*/
internal class TrackingProtectionExceptionFileStorage(
private val context: Context,
private val runtime: GeckoRuntime
) : TrackingProtectionExceptionStorage {
private val fileLock = Any()
internal var scope = CoroutineScope(Dispatchers.IO)
/**
* Restore all exceptions from the [STORE_FILE_NAME_FORMAT] file,
* and provides them to the gecko [runtime].
*/
override fun restore() {
scope.launch {
synchronized(fileLock) {
getFile(context).readAndDeserialize { json ->
if (json.isNotEmpty()) {
val exceptionList = runtime.contentBlockingController.ExceptionList(json)
runtime.contentBlockingController.restoreExceptionList(exceptionList)
}
}
}
}
}
override fun contains(session: EngineSession, onResult: (Boolean) -> Unit) {
val geckoSession = (session as GeckoEngineSession).geckoSession
runtime.contentBlockingController.checkException(geckoSession).accept {
if (it != null) {
onResult(it)
} else {
onResult(false)
}
}
}
override fun fetchAll(onResult: (List<String>) -> Unit) {
runtime.contentBlockingController.saveExceptionList().accept { exceptionList ->
val exceptions = if (exceptionList != null) {
val uris = exceptionList.uris.map { uri ->
uri
}
uris
} else {
emptyList()
}
onResult(exceptions)
}
}
override fun add(session: EngineSession) {
val geckoSession = (session as GeckoEngineSession).geckoSession
runtime.contentBlockingController.addException(geckoSession)
persist()
}
override fun remove(session: EngineSession) {
val geckoSession = (session as GeckoEngineSession).geckoSession
runtime.contentBlockingController.removeException(geckoSession)
persist()
}
override fun removeAll() {
runtime.contentBlockingController.clearExceptionList()
removeFileFromDisk(context)
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun getFile(context: Context): AtomicFile {
return AtomicFile(
File(
context.filesDir,
STORE_FILE_NAME_FORMAT
)
)
}
/**
* Take all the exception from the gecko [runtime] and saves them into the
* [STORE_FILE_NAME_FORMAT] file.
*/
private fun persist() {
runtime.contentBlockingController.saveExceptionList().accept { exceptionList ->
if (exceptionList != null) {
scope.launch {
synchronized(fileLock) {
getFile(context).writeString {
exceptionList.toJson().toString()
}
}
}
} else {
removeFileFromDisk(context)
}
}
}
private fun removeFileFromDisk(context: Context) {
scope.launch {
synchronized(fileLock) {
getFile(context)
.delete()
}
}
}
}
......@@ -15,6 +15,7 @@ import mozilla.components.concept.engine.EngineSession.SafeBrowsingPolicy
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.CookiePolicy
import mozilla.components.concept.engine.UnsupportedSettingException
import mozilla.components.concept.engine.content.blocking.TrackerLog
import mozilla.components.concept.engine.content.blocking.TrackingProtectionExceptionStorage
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
import mozilla.components.support.test.any
import mozilla.components.support.test.argumentCaptor
......@@ -585,6 +586,15 @@ class GeckoEngineTest {
assertTrue(version.isAtLeast(69, 0, 0))
}
@Test
fun `after init is called the trackingProtectionExceptionStore must be restored`() {
val mockStore: TrackingProtectionExceptionStorage = mock()
val runtime: GeckoRuntime = mock()
GeckoEngine(context, runtime = runtime, trackingProtectionExceptionStore = mockStore)
verify(mockStore).restore()
}
private fun createDummyLogEntryList(): List<ContentBlockingController.LogEntry> {
val addLogEntry = object : ContentBlockingController.LogEntry() {}
......
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.browser.engine.gecko
import android.app.Activity
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import mozilla.components.support.ktx.util.readAndDeserialize
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.whenever
import org.json.JSONObject
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.verify
import org.mozilla.geckoview.ContentBlockingController
import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoRuntime
import org.mozilla.geckoview.GeckoSession
import org.robolectric.Robolectric.buildActivity
@RunWith(AndroidJUnit4::class)
class TrackingProtectionExceptionFileStorageTest {
private val context: Context
get() = buildActivity(Activity::class.java).get()
@Test
fun `restoreAsync exception`() {
val mockContentBlocking = mock<ContentBlockingController>()
val runtime: GeckoRuntime = mock()
val session = mock<GeckoEngineSession>()
val geckoResult = GeckoResult<ContentBlockingController.ExceptionList>()
val mockGeckoSession = mock<GeckoSession>()
val mockExceptionList = mock<ContentBlockingController.ExceptionList>()
whenever(session.geckoSession).thenReturn(mockGeckoSession)
whenever(runtime.contentBlockingController).thenReturn(mockContentBlocking)
whenever(runtime.contentBlockingController.saveExceptionList()).thenReturn(geckoResult)
whenever(mockExceptionList.toJson()).thenReturn(JSONObject("{\"principals\":[\"eyIxIjp7IjAiOiJodHRwczovL3d3dy5jbm4uY29tLyJ9fQ==\"],\"uris\":[\"https:\\/\\/www.cnn.com\\/\"]}"))
val storage = TrackingProtectionExceptionFileStorage(testContext, runtime)
storage.scope = CoroutineScope(Dispatchers.Main)
assertNull(storage.getFile(context).readAndDeserialize { })
storage.add(session)
geckoResult.complete(mockExceptionList)
storage.restore()
verify(mockContentBlocking).restoreExceptionList(any())
assertNotNull(storage.getFile(context).readAndDeserialize { })
}
@Test
fun `add exception`() {
val mockContentBlocking = mock<ContentBlockingController>()
val runtime: GeckoRuntime = mock()
val session = mock<GeckoEngineSession>()
val geckoResult = GeckoResult<ContentBlockingController.ExceptionList>()
val mockGeckoSession = mock<GeckoSession>()
val mockExceptionList = mock<ContentBlockingController.ExceptionList>()
whenever(session.geckoSession).thenReturn(mockGeckoSession)
whenever(runtime.contentBlockingController).thenReturn(mockContentBlocking)
whenever(runtime.contentBlockingController.saveExceptionList()).thenReturn(geckoResult)
whenever(mockExceptionList.toJson()).thenReturn(JSONObject("{\"principals\":[\"eyIxIjp7IjAiOiJodHRwczovL3d3dy5jbm4uY29tLyJ9fQ==\"],\"uris\":[\"https:\\/\\/www.cnn.com\\/\"]}"))
val storage = TrackingProtectionExceptionFileStorage(testContext, runtime)
storage.scope = CoroutineScope(Dispatchers.Main)
assertNull(storage.getFile(context).readAndDeserialize { })
storage.add(session)
geckoResult.complete(mockExceptionList)
verify(mockContentBlocking).addException(mockGeckoSession)
verify(mockContentBlocking).saveExceptionList()
assertNotNull(storage.getFile(context).readAndDeserialize { })
}
@Test
fun `remove all exceptions`() {
val mockContentBlocking = mock<ContentBlockingController>()
val runtime: GeckoRuntime = mock()
val session = mock<GeckoEngineSession>()
val geckoResult = GeckoResult<ContentBlockingController.ExceptionList>()
val mockGeckoSession = mock<GeckoSession>()
val mockExceptionList = mock<ContentBlockingController.ExceptionList>()
whenever(session.geckoSession).thenReturn(mockGeckoSession)
whenever(runtime.contentBlockingController).thenReturn(mockContentBlocking)
whenever(runtime.contentBlockingController.saveExceptionList()).thenReturn(geckoResult)
whenever(mockExceptionList.toJson()).thenReturn(JSONObject("{\"principals\":[\"eyIxIjp7IjAiOiJodHRwczovL3d3dy5jbm4uY29tLyJ9fQ==\"],\"uris\":[\"https:\\/\\/www.cnn.com\\/\"]}"))
// Adding exception
val storage = TrackingProtectionExceptionFileStorage(testContext, runtime)
storage.scope = CoroutineScope(Dispatchers.Main)
assertNull(storage.getFile(context).readAndDeserialize { })
storage.add(session)
geckoResult.complete(mockExceptionList)
verify(mockContentBlocking).addException(mockGeckoSession)
verify(mockContentBlocking).saveExceptionList()
assertNotNull(storage.getFile(context).readAndDeserialize { })
// Removing exceptions
storage.removeAll()
verify(mockContentBlocking).clearExceptionList()
assertNull(storage.getFile(context).readAndDeserialize { })
}
@Test
fun `remove exception`() {
val mockContentBlocking = mock<ContentBlockingController>()
val runtime: GeckoRuntime = mock()
val session = mock<GeckoEngineSession>()
var geckoResult = GeckoResult<ContentBlockingController.ExceptionList>()
val mockGeckoSession = mock<GeckoSession>()
val mockExceptionList = mock<ContentBlockingController.ExceptionList>()
whenever(session.geckoSession).thenReturn(mockGeckoSession)
whenever(runtime.contentBlockingController).thenReturn(mockContentBlocking)
whenever(runtime.contentBlockingController.saveExceptionList()).thenReturn(geckoResult)
whenever(mockExceptionList.toJson()).thenReturn(JSONObject("{\"principals\":[\"eyIxIjp7IjAiOiJodHRwczovL3d3dy5jbm4uY29tLyJ9fQ==\"],\"uris\":[\"https:\\/\\/www.cnn.com\\/\"]}"))
// Adding exception
val storage = TrackingProtectionExceptionFileStorage(testContext, runtime)
storage.scope = CoroutineScope(Dispatchers.Main)
assertNull(storage.getFile(context).readAndDeserialize { })
storage.add(session)
geckoResult.complete(mockExceptionList)
verify(mockContentBlocking).addException(mockGeckoSession)
verify(mockContentBlocking).saveExceptionList()
assertNotNull(storage.getFile(context).readAndDeserialize { })
// Removing exception
geckoResult = GeckoResult()
whenever(runtime.contentBlockingController.saveExceptionList()).thenReturn(geckoResult)
storage.remove(session)
verify(mockContentBlocking).removeException(mockGeckoSession)
geckoResult.complete(null)
assertNull(storage.getFile(context).readAndDeserialize { })
}
@Test
fun `contains exception`() {
val mockContentBlocking = mock<ContentBlockingController>()
val runtime: GeckoRuntime = mock()
val session = mock<GeckoEngineSession>()
var geckoResult = GeckoResult<Boolean>()
val mockGeckoSession = mock<GeckoSession>()
var containsException = false
whenever(session.geckoSession).thenReturn(mockGeckoSession)
whenever(runtime.contentBlockingController).thenReturn(mockContentBlocking)
whenever(runtime.contentBlockingController.checkException(mockGeckoSession)).thenReturn(
geckoResult
)
val storage = TrackingProtectionExceptionFileStorage(testContext, runtime)
storage.contains(session) { contains ->
containsException = contains
}
geckoResult.complete(true)
verify(mockContentBlocking).checkException(mockGeckoSession)
assertTrue(containsException)
geckoResult = GeckoResult()
whenever(runtime.contentBlockingController.checkException(mockGeckoSession)).thenReturn(
geckoResult
)
storage.contains(session) { contains ->
containsException = contains
}
geckoResult.complete(null)
assertFalse(containsException)
}
@Test
fun `getAll exceptions`() {
val mockContentBlocking = mock<ContentBlockingController>()
val runtime: GeckoRuntime = mock()
val session = mock<GeckoEngineSession>()
var geckoResult = GeckoResult<ContentBlockingController.ExceptionList>()
val mockGeckoSession = mock<GeckoSession>()
val mockExceptionList = mock<ContentBlockingController.ExceptionList>()
var exceptionList: List<String>? = null
whenever(session.geckoSession).thenReturn(mockGeckoSession)
whenever(runtime.contentBlockingController).thenReturn(mockContentBlocking)
whenever(runtime.contentBlockingController.saveExceptionList()).thenReturn(geckoResult)
whenever(mockExceptionList.uris).thenReturn(arrayOf("mozilla.com"))
val storage = TrackingProtectionExceptionFileStorage(testContext, runtime)
storage.fetchAll { exceptions ->
exceptionList = exceptions
}
geckoResult.complete(mockExceptionList)
verify(mockContentBlocking).saveExceptionList()
assertTrue(exceptionList!!.isNotEmpty())
geckoResult = GeckoResult()
whenever(runtime.contentBlockingController.saveExceptionList()).thenReturn(geckoResult)
storage.fetchAll { exceptions ->
exceptionList = exceptions
}
geckoResult.complete(null)
assertTrue(exceptionList!!.isEmpty())
}
}
......@@ -19,6 +19,7 @@ import mozilla.components.concept.engine.EngineSessionState
import mozilla.components.concept.engine.EngineView
import mozilla.components.concept.engine.Settings
import mozilla.components.concept.engine.content.blocking.TrackerLog
import mozilla.components.concept.engine.content.blocking.TrackingProtectionExceptionStorage
import mozilla.components.concept.engine.history.HistoryTrackingDelegate
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
import mozilla.components.concept.engine.utils.EngineVersion
......@@ -40,10 +41,11 @@ class GeckoEngine(
context: Context,
private val defaultSettings: Settings? = null,
private val runtime: GeckoRuntime = GeckoRuntime.getDefault(context),
executorProvider: () -> GeckoWebExecutor = { GeckoWebExecutor(runtime) }
executorProvider: () -> GeckoWebExecutor = { GeckoWebExecutor(runtime) },
override val trackingProtectionExceptionStore: TrackingProtectionExceptionStorage =
TrackingProtectionExceptionFileStorage(context, runtime)
) : Engine {
private val executor by lazy { executorProvider.invoke() }
private val localeUpdater = LocaleSettingUpdater(context, runtime)
init {
......@@ -56,6 +58,7 @@ class GeckoEngine(
@Suppress("TooGenericExceptionThrown")
throw RuntimeException("GeckoRuntime is shutting down")
}
trackingProtectionExceptionStore.restore()
}
/**
......
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.browser.engine.gecko
import android.content.Context
import android.util.AtomicFile
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.content.blocking.TrackingProtectionExceptionStorage
import mozilla.components.support.ktx.util.readAndDeserialize
import mozilla.components.support.ktx.util.writeString
import org.mozilla.geckoview.GeckoRuntime
import java.io.File
private const val STORE_FILE_NAME_FORMAT =
"mozilla_components_tracking_protection_storage_gecko.json"
/**
* A [TrackingProtectionExceptionStorage] implementation to store tracking protection exceptions.
*/
internal class TrackingProtectionExceptionFileStorage(
private val context: Context,
private val runtime: GeckoRuntime
) : TrackingProtectionExceptionStorage {
private val fileLock = Any()
internal var scope = CoroutineScope(Dispatchers.IO)
/**
* Restore all exceptions from the [STORE_FILE_NAME_FORMAT] file,
* and provides them to the gecko [runtime].
*/
override fun restore() {
scope.launch {
synchronized(fileLock) {
getFile(context).readAndDeserialize { json ->
if (json.isNotEmpty()) {
val exceptionList = runtime.contentBlockingController.ExceptionList(json)
runtime.contentBlockingController.restoreExceptionList(exceptionList)
}
}
}
}
}
override fun contains(session: EngineSession, onResult: (Boolean) -> Unit) {
val geckoSession = (session as GeckoEngineSession).geckoSession
runtime.contentBlockingController.checkException(geckoSession).accept {
if (it != null) {
onResult(it)
} else {
onResult(false)
}
}
}
override fun fetchAll(onResult: (List<String>) -> Unit) {
runtime.contentBlockingController.saveExceptionList().accept { exceptionList ->
val exceptions = if (exceptionList != null) {
val uris = exceptionList.uris.map { uri ->
uri
}
uris
} else {
emptyList()
}
onResult(exceptions)
}
}
override fun add(session: EngineSession) {
val geckoSession = (session as GeckoEngineSession).geckoSession
runtime.contentBlockingController.addException(geckoSession)
persist()
}
override fun remove(session: EngineSession) {
val geckoSession = (session as GeckoEngineSession).geckoSession
runtime.contentBlockingController.removeException(geckoSession)
persist()
}
override fun removeAll() {
runtime.contentBlockingController.clearExceptionList()
removeFileFromDisk(context)
}