diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt index aacc1bbf97..d75b8754ee 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt @@ -10,6 +10,7 @@ package org.readium.r2.navigator.epub import android.app.Application import android.os.PatternMatcher +import android.webkit.MimeTypeMap import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import androidx.webkit.WebViewAssetLoader @@ -28,9 +29,11 @@ import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.asInputStream import org.readium.r2.shared.util.http.HttpHeaders import org.readium.r2.shared.util.http.HttpRange +import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.StringResource import org.readium.r2.shared.util.resource.fallback +import org.readium.r2.shared.util.toUrl /** * Serves the publication resources and application assets in the EPUB navigator web views. @@ -44,8 +47,11 @@ internal class WebViewServer( private val onResourceLoadFailed: (Url, ReadError) -> Unit, ) { companion object { - val publicationBaseHref = AbsoluteUrl("https://readium/publication/")!! - val assetsBaseHref = AbsoluteUrl("https://readium/assets/")!! + const val READIUM_PACKAGE_HOSTNAME = "readium_package" + const val ASSETS_HOSTNAME = "readium_assets" + + val publicationBaseHref = AbsoluteUrl("https://$READIUM_PACKAGE_HOSTNAME/")!! + val assetsBaseHref = AbsoluteUrl("https://$ASSETS_HOSTNAME/")!! fun assetUrl(path: String): Url? = Url.fromDecodedPath(path)?.let { assetsBaseHref.resolve(it) } @@ -54,28 +60,64 @@ internal class WebViewServer( /** * Serves the requests of the navigator web views. * - * https://readium/publication/ serves the publication resources through its fetcher. - * https://readium/assets/ serves the application assets. + * https://readium_package/ serves the publication resources through its fetcher. + * https://readium_assets/ serves the application assets. */ fun shouldInterceptRequest(request: WebResourceRequest, css: ReadiumCss): WebResourceResponse? { - if (request.url.host != "readium") return null val path = request.url.path ?: return null + val hostname = request.url.host ?: return null + val requestUrl = request.url.toUrl() ?: return null + val range = HttpHeaders(request.requestHeaders).range - return when { - path.startsWith("/publication/") -> { - val href = Url.fromDecodedPath(path.removePrefix("/publication/")) - ?: return null + return when (hostname) { + READIUM_PACKAGE_HOSTNAME -> { + // Request is for a packaged resource. + val href = Url.fromDecodedPath(path.removePrefix("/")) + ?: return serveErrorResponse() servePublicationResource( href = href, - range = HttpHeaders(request.requestHeaders).range, + range = range, css = css ) } - path.startsWith("/assets/") && isServedAsset(path.removePrefix("/assets/")) -> { + + ASSETS_HOSTNAME if isServedAsset(path.removePrefix("/")) -> { + // Request is for a known asset. assetsLoader.shouldInterceptRequest(request.url) } - else -> null + + ASSETS_HOSTNAME -> { + val error = ReadError.Decoding( + "Attempted to load an unknown asset from $requestUrl" + ) + onResourceLoadFailed(requestUrl, error) + serveErrorResponse() // Request is for an unknown asset. + } + + else -> { + // Request is for streaming a resource, if the baseUrl an AbsoluteUrl and hostname + // is not ASSETS_HOSTNAME or READIUM_PACKAGE_HOSTNAME + val baseUrl = publication.baseUrl as? AbsoluteUrl ?: run { + val error = ReadError.Decoding( + "baseUrl is not an AbsoluteUrl, cannot load remote resource from $requestUrl" + ) + onResourceLoadFailed(requestUrl, error) + return serveErrorResponse() + } + + val href = + // Look up the link in the publication to make sure we have the right resource. + publicationLinkFromHref(baseUrl.resolve(requestUrl))?.href?.resolve() + ?: publicationLinkFromHref(baseUrl.relativize(requestUrl))?.href?.resolve() + ?: requestUrl + + servePublicationResource( + href = href, + range = range, + css = css + ) + } } } @@ -84,15 +126,17 @@ internal class WebViewServer( * * If the [Resource] is an HTML document, injects the required JavaScript and CSS files. */ - private fun servePublicationResource(href: Url, range: HttpRange?, css: ReadiumCss): WebResourceResponse { - val link = publication.linkWithHref(href) - // Query parameters must be kept as they might be relevant for the fetcher. - ?.copy(href = Href(href)) - ?: Link(href = href) + private fun servePublicationResource( + href: Url, + range: HttpRange?, + css: ReadiumCss + ): WebResourceResponse { + val link = publicationLinkFromHref(href) + // Link not found, create a Link from the href and guess the MediaType + ?: Link(href = href, mediaType = mediaTypeFromUrl(href)) // Drop anchor because it is meant to be interpreted by the client. val urlWithoutAnchor = href.removeFragment() - var resource = publication .get(urlWithoutAnchor) ?.fallback { @@ -106,33 +150,44 @@ internal class WebViewServer( errorResource() } - link.mediaType - ?.takeIf { it.isHtml } - ?.let { - resource = resource.injectHtml( - publication, - mediaType = it, - css, - baseHref = assetsBaseHref, - disableSelectionWhenProtected = disableSelectionWhenProtected - ) - } + // Only inject html when the profile is EPUB + if (publication.conformsTo(Publication.Profile.EPUB)) { + link.mediaType + ?.takeIf { it.isHtml } + ?.let { + resource = resource.injectHtml( + publication, + mediaType = it, + css, + baseHref = assetsBaseHref, + disableSelectionWhenProtected = disableSelectionWhenProtected + ) + } + } + return serveResource(resource, range, link.mediaType) + } + + private fun serveResource( + resource: Resource, + range: HttpRange?, + mediaType: MediaType?, + ): WebResourceResponse { val headers = mutableMapOf( "Accept-Ranges" to "bytes" ) + val stream = resource.asInputStream() if (range == null) { return WebResourceResponse( - link.mediaType?.toString(), + mediaType?.toString(), null, 200, "OK", headers, - resource.asInputStream() + stream ) } else { // Byte range request - val stream = resource.asInputStream() val length = stream.available() val longRange = range.toLongRange(length.toLong()) headers["Content-Range"] = "bytes ${longRange.first}-${longRange.last}/$length" @@ -140,7 +195,7 @@ internal class WebViewServer( // headers["Content-Length"] = (longRange.last - longRange.first + 1).toString() // Weirdly, the WebView will call itself stream.skip to skip to the requested range. return WebResourceResponse( - link.mediaType?.toString(), + mediaType?.toString(), null, 206, "Partial Content", @@ -149,6 +204,26 @@ internal class WebViewServer( ) } } + + /** + * Resolve the [MediaType] from an [Url]. + */ + private fun mediaTypeFromUrl(href: Url): MediaType? { + val ext = MimeTypeMap.getFileExtensionFromUrl(href.normalize().toString()) ?: return null + val mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return null + + return MediaType.invoke(mimetype) + } + + /** + * Get link in publication from a [Url] and replace href to preserve request query parameters. + */ + private fun publicationLinkFromHref(href: Url): Link? { + return publication.linkWithHref(href) + // Query parameters must be kept as they might be relevant for the fetcher. + ?.copy(href = Href(href)) + } + private fun errorResource(): Resource = StringResource { withContext(Dispatchers.IO) { @@ -161,6 +236,10 @@ internal class WebViewServer( } } + private fun serveErrorResponse(): WebResourceResponse { + return serveResource(errorResource(), null, MediaType.XHTML) + } + private fun isServedAsset(path: String): Boolean = servedAssetPatterns.any { it.match(path) } @@ -169,7 +248,7 @@ internal class WebViewServer( private val assetsLoader = WebViewAssetLoader.Builder() - .setDomain("readium") - .addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(application)) + .setDomain(ASSETS_HOSTNAME) + .addPathHandler("/", WebViewAssetLoader.AssetsPathHandler(application)) .build() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Url.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Url.kt index 6a42d1c60d..9627f21887 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Url.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Url.kt @@ -310,6 +310,11 @@ public class AbsoluteUrl private constructor(override val uri: Uri) : Url() { public val isContent: Boolean get() = scheme.isContent + /** + * Hostname of the URL. + */ + public val host: String? get() = uri.host + /** * Converts the URL to a [File], if it's a file URL. */