Skip to content

Commit

Permalink
fix: reviewer fetch and XMLHttpRequest issues (#16457)
Browse files Browse the repository at this point in the history
* feat: handle audio and videos seeking in ViewerResourceHandler

basically restore what e55cae2 had deleted

* fix: use http base url in card viewers
  • Loading branch information
BrayanDSO authored May 23, 2024
1 parent 302ffdc commit 1246253
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 33 deletions.
2 changes: 1 addition & 1 deletion AnkiDroid/src/main/assets/scripts/js-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class AnkiDroidJS {
}

handleRequest = async (endpoint, data) => {
const url = `${ankidroid.postBaseUrl}jsapi/${endpoint}`;
const url = `/jsapi/${endpoint}`;
try {
const response = await fetch(url, {
method: "POST",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,6 @@ abstract class AbstractFlashcardViewer :
@get:VisibleForTesting
var cardContent: String? = null
private set
private val baseUrl
get() = getMediaBaseUrl(CollectionHelper.getMediaDirectory(this).path)

private var viewerUrl: String? = null
private val fadeDuration = 300
Expand Down Expand Up @@ -1502,7 +1500,7 @@ abstract class AbstractFlashcardViewer :
if (card != null) {
card.settings.mediaPlaybackRequiresUserGesture = !cardMediaPlayer.config.autoplay
card.loadDataWithBaseURL(
baseUrl,
server.baseUrl(),
content,
"text/html",
null,
Expand Down Expand Up @@ -2265,9 +2263,6 @@ abstract class AbstractFlashcardViewer :
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
pageRenderStopwatch.reset()
pageFinishedFired = false
val script = "globalThis.ankidroid = globalThis.ankidroid || {};" +
"ankidroid.postBaseUrl = `${server.baseUrl()}`"
view?.evaluateJavascript(script, null)
}

override fun shouldInterceptRequest(
Expand Down
87 changes: 70 additions & 17 deletions AnkiDroid/src/main/java/com/ichi2/anki/ViewerResourceHandler.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
* Copyright (c) 2024 Brayan Oliveira <[email protected]>
* Copyright (c) 2023 Brayan Oliveira <[email protected]>
* Copyright (c) 2023 David Allison <[email protected]>
*
* 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
Expand All @@ -18,37 +19,89 @@ 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.ByteArrayInputStream
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.io.RandomAccessFile
import java.nio.channels.Channels

private const val RANGE_HEADER = "Range"

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.
*/
private val mediaDir = CollectionHelper.getMediaDirectory(context)

fun shouldInterceptRequest(request: WebResourceRequest): WebResourceResponse? {
val url = request.url
if (request.method != "GET" || url.scheme != "file" || "Origin" !in request.requestHeaders) {
val path = url.path

if (request.method != "GET" || path == null) {
return null
}
if (path == "/favicon.ico") {
return WebResourceResponse(null, null, ByteArrayInputStream(ByteArray(0)))
}

try {
val file = url.toFile()
if (file.parent != mediaDir || !file.exists()) {
val file = File(mediaDir, path)
if (!file.exists()) {
return null
}
val inputStream = FileInputStream(file)
return WebResourceResponse(guessMimeType(file.path), null, inputStream).apply {
responseHeaders = mapOf("Access-Control-Allow-Origin" to "*")
request.requestHeaders[RANGE_HEADER]?.let { range ->
return handlePartialContent(file, range)
}
val inputStream = FileInputStream(file)
val mimeType = guessMimeType(path)
return WebResourceResponse(mimeType, null, inputStream)
} catch (e: Exception) {
Timber.d("File couldn't be loaded")
Timber.d("File not found")
return null
}
}

private fun handlePartialContent(file: File, range: String): WebResourceResponse {
val rangeHeader = RangeHeader.from(range, defaultEnd = file.length() - 1)

val mimeType = guessMimeType(file.path)
val inputStream = file.toInputStream(rangeHeader)
val (start, end) = rangeHeader
val responseHeaders = mapOf(
"Content-Range" to "bytes $start-$end/${file.length()}",
"Accept-Range" to "bytes"
)
return WebResourceResponse(
mimeType,
null,
206,
"Partial Content",
responseHeaders,
inputStream
)
}
}

/**
* Handles the "range" header in a HTTP Request
*/
data class RangeHeader(val start: Long, val end: Long) {
companion object {
fun from(range: String, defaultEnd: Long): RangeHeader {
val numbers = range.substring("bytes=".length).split('-')
val unspecifiedEnd = numbers.getOrNull(1).isNullOrEmpty()
return RangeHeader(
start = numbers[0].toLong(),
end = if (unspecifiedEnd) defaultEnd else numbers[1].toLong()
)
}
}
}

fun File.toInputStream(header: RangeHeader): InputStream {
// PERF: Test to see if a custom FileInputStream + available() would be faster
val randomAccessFile = RandomAccessFile(this, "r")
return Channels.newInputStream(randomAccessFile.channel).also {
it.skip(header.start)
}
}
6 changes: 0 additions & 6 deletions AnkiDroid/src/main/java/com/ichi2/anki/pages/AnkiServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ open class AnkiServer(
Timber.d("Rejecting GET request to server %s", session.uri)
newFixedLengthResponse(Response.Status.NOT_FOUND, null, null)
}
Method.OPTIONS -> buildResponse(null)
else -> {
Timber.d("Ignored request of unhandled method %s, uri %s", session.method, session.uri)
newFixedLengthResponse(null)
Expand All @@ -70,11 +69,6 @@ open class AnkiServer(
newFixedLengthResponse(null)
} else {
newChunkedResponse(status, mimeType, ByteArrayInputStream(data))
}.apply {
addHeader("Access-Control-Allow-Origin", "*")
addHeader("Access-Control-Allow-Headers", "Content-Type")
addHeader("Access-Control-Allow-Methods", "POST")
addHeader("Access-Control-Max-Age", "7200")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.Fragment
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.pages.AnkiServer
import com.ichi2.anki.snackbar.showSnackbar
import com.ichi2.anki.utils.ext.packageManager
import com.ichi2.compat.CompatHelper.Companion.resolveActivityCompat
Expand Down Expand Up @@ -87,9 +87,9 @@ abstract class CardViewerFragment(@LayoutRes layout: Int) : Fragment(layout) {
// allow videos to autoplay via our JavaScript eval
mediaPlaybackRequiresUserGesture = false
}
val baseUrl = CollectionHelper.getMediaDirectory(requireContext()).toURI().toString()

loadDataWithBaseURL(
baseUrl,
"http://${AnkiServer.LOCALHOST}/",
stdHtml(requireContext(), Themes.currentTheme.isNightMode),
"text/html",
null,
Expand Down

0 comments on commit 1246253

Please sign in to comment.