From bf1f7fbba70170dfb261c17afb6791a116a639d2 Mon Sep 17 00:00:00 2001 From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com> Date: Sat, 4 May 2024 13:11:09 -0300 Subject: [PATCH] fix: CORS issue with JS loading --- .../com/ichi2/anki/AbstractFlashcardViewer.kt | 5 +- .../com/ichi2/anki/ViewerResourceHandler.kt | 54 +++++++++++++++++++ .../anki/previewer/CardViewerFragment.kt | 10 ++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/ViewerResourceHandler.kt diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt index 44f4ad714e..6729c863e5 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt @@ -993,6 +993,7 @@ abstract class AbstractFlashcardViewer : } protected open fun createWebView(): WebView { + val resourceHandler = ViewerResourceHandler(this) val webView: WebView = MyWebView(this).apply { scrollBarStyle = View.SCROLLBARS_OUTSIDE_OVERLAY with(settings) { @@ -1010,7 +1011,7 @@ abstract class AbstractFlashcardViewer : isScrollbarFadingEnabled = true // Set transparent color to prevent flashing white when night mode enabled setBackgroundColor(Color.argb(1, 0, 0, 0)) - CardViewerWebClient(this@AbstractFlashcardViewer).apply { + CardViewerWebClient(resourceHandler, this@AbstractFlashcardViewer).apply { webViewClient = this this@AbstractFlashcardViewer.webViewClient = this } @@ -2233,6 +2234,7 @@ abstract class AbstractFlashcardViewer : } inner class CardViewerWebClient internal constructor( + private val resourceHandler: ViewerResourceHandler, private val onPageFinishedCallback: OnPageFinishedCallback? = null ) : WebViewClient(), JavascriptEvaluator { private var pageFinishedFired = true @@ -2266,6 +2268,7 @@ abstract class AbstractFlashcardViewer : if (url.toString().startsWith("file://")) { url.path?.let { path -> migrationService?.migrateFileImmediately(File(path)) } } + resourceHandler.shouldInterceptRequest(request)?.let { return it } return null } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ViewerResourceHandler.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ViewerResourceHandler.kt new file mode 100644 index 0000000000..b2c514b0f0 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ViewerResourceHandler.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024 Brayan Oliveira + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package com.ichi2.anki + +import android.content.Context +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import androidx.core.net.toFile +import com.ichi2.utils.AssetHelper.guessMimeType +import timber.log.Timber +import java.io.FileInputStream + +class ViewerResourceHandler(context: Context) { + private val mediaDir = CollectionHelper.getMediaDirectory(context).path + + /** + * Loads resources from `collection.media` when requested by JS scripts. + * + * Differently from common media requests, scripts' requests have an `Origin` header + * and are susceptible to CORS policy, so `Access-Control-Allow-Origin` is necessary. + */ + fun shouldInterceptRequest(request: WebResourceRequest): WebResourceResponse? { + val url = request.url + if (request.method != "GET" || url.scheme != "file" || "Origin" !in request.requestHeaders) { + return null + } + try { + val file = url.toFile() + if (file.parent != mediaDir || !file.exists()) { + return null + } + val inputStream = FileInputStream(file) + return WebResourceResponse(guessMimeType(file.path), null, inputStream).apply { + responseHeaders = mapOf("Access-Control-Allow-Origin" to "*") + } + } catch (e: Exception) { + Timber.d("File couldn't be loaded") + return null + } + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/CardViewerFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/CardViewerFragment.kt index d19647de37..606108e5b8 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/CardViewerFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/CardViewerFragment.kt @@ -23,6 +23,7 @@ import android.webkit.CookieManager import android.webkit.WebChromeClient import android.webkit.WebResourceError import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient import android.widget.FrameLayout @@ -37,6 +38,7 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import com.ichi2.anki.CollectionHelper import com.ichi2.anki.R +import com.ichi2.anki.ViewerResourceHandler import com.ichi2.anki.dialogs.TtsVoicesDialogFragment import com.ichi2.anki.localizedErrorMessage import com.ichi2.anki.snackbar.showSnackbar @@ -121,7 +123,15 @@ abstract class CardViewerFragment(@LayoutRes layout: Int) : Fragment(layout) { } private fun onCreateWebViewClient(savedInstanceState: Bundle?): WebViewClient { + val resourceHandler = ViewerResourceHandler(requireContext()) return object : WebViewClient() { + override fun shouldInterceptRequest( + view: WebView?, + request: WebResourceRequest + ): WebResourceResponse? { + return resourceHandler.shouldInterceptRequest(request) + } + override fun onPageFinished(view: WebView?, url: String?) { viewModel.onPageFinished(isAfterRecreation = savedInstanceState != null) }