diff --git a/gradle.properties b/gradle.properties index 84364242..4e1d8784 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,7 +15,7 @@ android.minSdk=21 #Versions GROUP=io.github.kevinnzou POM_ARTIFACT_ID=compose-webview-multiplatform -VERSION_NAME=2.0.0 +VERSION_NAME=2.0.1 POM_NAME=Compose WebView Multiplatform POM_INCEPTION_YEAR=2023 POM_DESCRIPTION=WebView for JetBrains Compose Multiplatform diff --git a/webview/src/androidMain/kotlin/com/multiplatform/webview/util/InternalStoragePathHandler.kt b/webview/src/androidMain/kotlin/com/multiplatform/webview/util/InternalStoragePathHandler.kt new file mode 100644 index 00000000..1be2cae7 --- /dev/null +++ b/webview/src/androidMain/kotlin/com/multiplatform/webview/util/InternalStoragePathHandler.kt @@ -0,0 +1,43 @@ +package com.multiplatform.webview.util + +import android.util.Log +import android.webkit.WebResourceResponse +import androidx.webkit.WebViewAssetLoader +import java.io.File +import java.io.FileInputStream + +class InternalStoragePathHandler : WebViewAssetLoader.PathHandler { + override fun handle(path: String): WebResourceResponse? { + Log.d("InternalStorageHandler", "Intercepted: $path") + val file = File(path.removePrefix("/")) + if (!file.exists() || !file.isFile) return null + + val mimeType = + when { + path.endsWith(".html") -> "text/html" + path.endsWith(".js") -> "application/javascript" + path.endsWith(".css") -> "text/css" + path.endsWith(".json") -> "application/json" + path.endsWith(".png") -> "image/png" + path.endsWith(".jpg") || path.endsWith(".jpeg") -> "image/jpeg" + path.endsWith(".svg") -> "image/svg+xml" + path.endsWith(".webp") -> "image/webp" + path.endsWith(".ico") -> "image/x-icon" + path.endsWith(".woff") -> "font/woff" + path.endsWith(".woff2") -> "font/woff2" + path.endsWith(".ttf") -> "font/ttf" + path.endsWith(".mp4") -> "video/mp4" + path.endsWith(".webm") -> "video/webm" + path.endsWith(".ogg") -> "video/ogg" + path.endsWith(".mp3") -> "audio/mpeg" + path.endsWith(".wav") -> "audio/wav" + path.endsWith(".wasm") -> "application/wasm" + path.endsWith(".pdf") -> "application/pdf" + path.endsWith(".zip") -> "application/zip" + path.endsWith(".csv") -> "text/csv" + else -> "application/octet-stream" + } + + return WebResourceResponse(mimeType, "utf-8", FileInputStream(file)) + } +} diff --git a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AccompanistWebView.kt b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AccompanistWebView.kt index 3397b9fe..68613571 100644 --- a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AccompanistWebView.kt +++ b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AccompanistWebView.kt @@ -10,6 +10,7 @@ import android.webkit.PermissionRequest 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 @@ -24,10 +25,12 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat import androidx.core.graphics.createBitmap import androidx.webkit.WebSettingsCompat +import androidx.webkit.WebViewAssetLoader import androidx.webkit.WebViewFeature import com.multiplatform.webview.jsbridge.WebViewJsBridge import com.multiplatform.webview.request.WebRequest import com.multiplatform.webview.request.WebRequestInterceptResult +import com.multiplatform.webview.util.InternalStoragePathHandler import com.multiplatform.webview.util.KLogger /** @@ -189,7 +192,6 @@ fun AccompanistWebView( userAgentString = it.customUserAgentString allowFileAccessFromFileURLs = it.allowFileAccessFromFileURLs allowUniversalAccessFromFileURLs = it.allowUniversalAccessFromFileURLs - setSupportZoom(it.supportZoom) } state.webSettings.androidWebSettings.let { @@ -208,6 +210,16 @@ fun AccompanistWebView( loadsImagesAutomatically = it.loadsImagesAutomatically domStorageEnabled = it.domStorageEnabled mediaPlaybackRequiresUserGesture = it.mediaPlaybackRequiresUserGesture + + if (it.enableSandbox) { + client.assetLoader = + WebViewAssetLoader + .Builder() + .addPathHandler( + it.sandboxSubdomain, + InternalStoragePathHandler(), + ).build() + } } } if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { @@ -261,6 +273,8 @@ open class AccompanistWebViewClient : WebViewClient() { internal set private var isRedirect = false + var assetLoader: WebViewAssetLoader? = null + override fun onPageStarted( view: WebView, url: String?, @@ -274,14 +288,26 @@ open class AccompanistWebViewClient : WebViewClient() { state.errorsForCurrentRequest.clear() state.pageTitle = null state.lastLoadedUrl = url + val supportZoom = if (state.webSettings.supportZoom) "yes" else "no" // set scale level @Suppress("ktlint:standard:max-line-length") val script = - "var meta = document.createElement('meta');meta.setAttribute('name', 'viewport');meta.setAttribute('content', 'width=device-width, initial-scale=${state.webSettings.zoomLevel}, maximum-scale=10.0, minimum-scale=0.1,user-scalable=yes');document.getElementsByTagName('head')[0].appendChild(meta);" + "var meta = document.createElement('meta');meta.setAttribute('name', 'viewport');meta.setAttribute('content', 'width=device-width, initial-scale=${state.webSettings.zoomLevel}, maximum-scale=10.0, minimum-scale=0.1,user-scalable=$supportZoom');document.getElementsByTagName('head')[0].appendChild(meta);" navigator.evaluateJavaScript(script) } + override fun shouldInterceptRequest( + view: WebView?, + request: WebResourceRequest?, + ): WebResourceResponse? { + val url = request?.url + KLogger.d { "Intercepting request for URL: $url" } + return url?.let { + assetLoader?.shouldInterceptRequest(it) + } ?: super.shouldInterceptRequest(view, request) + } + override fun onPageFinished( view: WebView, url: String?, diff --git a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AndroidWebView.kt b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AndroidWebView.kt index bf2a1b0e..faeae866 100644 --- a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AndroidWebView.kt +++ b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/AndroidWebView.kt @@ -37,7 +37,7 @@ class AndroidWebView( webView.loadUrl(url, additionalHttpHeaders) } - override fun loadHtml( + override suspend fun loadHtml( html: String?, baseUrl: String?, mimeType: String?, diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/setting/PlatformWebSettings.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/setting/PlatformWebSettings.kt index 053d1fb9..f2a07e22 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/setting/PlatformWebSettings.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/setting/PlatformWebSettings.kt @@ -188,6 +188,37 @@ sealed class PlatformWebSettings { * Default is [LayerType.HARDWARE] */ var layerType: Int = LayerType.HARDWARE, + /** + * Enables sandboxing of local file access via WebViewAssetLoader. + * + * When true, instead of using file:// URLs (which are insecure and restrict modern features), + * the WebView uses WebViewAssetLoader to serve local files (assets/resources/internal storage) + * over secure virtual https:// URLs. This improves compatibility with cookies, service workers, + * and CSP (Content Security Policy), and prevents file access vulnerabilities. + * + * This must be used in combination with a proper PathHandler setup in your WebView client + * (e.g., mapping /app/ to internal files or app assets). + * + * For example, if your WebViewAssetLoader maps the path "/app/" to your internal storage, + * you can load a file by navigating to a virtual URL like: + * `https://appassets.androidplatform.net/app/index.html` + * (the standard host used by WebViewAssetLoader) + * This URL will internally resolve to your app's internal file path. and enable cookies + * for them as well + */ + var enableSandbox: Boolean = false, + /** + * The virtual subdomain prefix to be used with WebViewAssetLoader for local file access. + * + * This is typically set to something like "/app/" or "/assets/" and must match the path + * used in your PathHandler configuration inside WebViewAssetLoader. + * + * When you load a URL such as `https://appassets.androidplatform.net/app/index.html` + * (the standard host used by WebViewAssetLoader) in your WebView, + * the WebViewAssetLoader will map it to the correct local file or asset if configured properly. + * This URL should be used instead of file:// URLs to ensure secure and modern WebView behavior. + */ + var sandboxSubdomain: String = "/app/", ) : PlatformWebSettings() { object LayerType { const val NONE = 0 diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/IWebView.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/IWebView.kt index a0ac8394..b4811280 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/IWebView.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/IWebView.kt @@ -55,7 +55,7 @@ interface IWebView { * @param encoding The encoding of the data in the string. * @param historyUrl The history URL for the loaded HTML. Leave null to use about:blank. */ - fun loadHtml( + suspend fun loadHtml( html: String? = null, baseUrl: String? = null, mimeType: String? = "text/html", diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebView.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebView.kt index fd9fe029..a13a3466 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebView.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebView.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import com.multiplatform.webview.jsbridge.WebViewJsBridge -import com.multiplatform.webview.util.KLogger import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.merge diff --git a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewNavigator.kt b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewNavigator.kt index 03aeac7d..ddf0b405 100644 --- a/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewNavigator.kt +++ b/webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewNavigator.kt @@ -74,6 +74,7 @@ class WebViewNavigator( data class LoadHtmlFile( val fileName: String, + val readType: WebViewFileReadType, ) : NavigationEvent /** @@ -140,7 +141,7 @@ class WebViewNavigator( ) is NavigationEvent.LoadHtmlFile -> { - loadHtmlFile(event.fileName) + loadHtmlFile(event.fileName, event.readType) } is NavigationEvent.LoadUrl -> { @@ -218,11 +219,15 @@ class WebViewNavigator( } } - fun loadHtmlFile(fileName: String) { + fun loadHtmlFile( + fileName: String, + readType: WebViewFileReadType = WebViewFileReadType.ASSET_RESOURCES, + ) { coroutineScope.launch { navigationEvents.emit( NavigationEvent.LoadHtmlFile( fileName, + readType, ), ) } diff --git a/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/DesktopWebView.kt b/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/DesktopWebView.kt index 17f0ead9..c2d54b6d 100644 --- a/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/DesktopWebView.kt +++ b/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/DesktopWebView.kt @@ -60,7 +60,7 @@ class DesktopWebView( } } - override fun loadHtml( + override suspend fun loadHtml( html: String?, baseUrl: String?, mimeType: String?, @@ -72,6 +72,7 @@ class DesktopWebView( } if (html != null) { try { + delay(500) webView.loadHtml(html, baseUrl ?: KCEFBrowser.BLANK_URI) } catch (e: Exception) { KLogger.e { "DesktopWebView loadHtml error: ${e.message}" } @@ -99,7 +100,7 @@ class DesktopWebView( throw Exception("Resource not found: $attemptedResourcePath (for readType: $readType)") } - val outFile = java.io.File(tempDirectory, path.substringAfterLast("/")) + val outFile = File(tempDirectory, path.substringAfterLast("/")) outFile.outputStream().use { output -> inputStream.copyTo(output) } diff --git a/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebEngineExt.kt b/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebEngineExt.kt index 69d6848f..55efdd4b 100644 --- a/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebEngineExt.kt +++ b/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebEngineExt.kt @@ -11,8 +11,6 @@ import org.cef.handler.CefDisplayHandler import org.cef.handler.CefLoadHandler import org.cef.handler.CefRequestHandlerAdapter import org.cef.network.CefRequest -import kotlin.math.abs -import kotlin.math.ln /** * Created By Kevin Zou On 2023/9/12 @@ -37,13 +35,12 @@ internal fun CefBrowser.addDisplayHandler(state: WebViewState) { ) { // https://magpcss.org/ceforum/viewtopic.php?t=11491 // https://github.com/KevinnZou/compose-webview-multiplatform/issues/46 + // I found this formula much near to the other platforms, so I replace it val givenZoomLevel = state.webSettings.zoomLevel - val realZoomLevel = - if (givenZoomLevel >= 0.0) { - ln(abs(givenZoomLevel)) / ln(1.2) - } else { - -ln(abs(givenZoomLevel)) / ln(1.2) - } + + val percentage = givenZoomLevel * 100.0 + val realZoomLevel = (percentage - 100.0) / 25.0 + KLogger.d { "titleProperty: $title" } zoomLevel = realZoomLevel state.pageTitle = title diff --git a/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebView.desktop.kt b/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebView.desktop.kt index 01d4d1a7..fde1872e 100644 --- a/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebView.desktop.kt +++ b/webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebView.desktop.kt @@ -72,9 +72,8 @@ actual fun defaultWebViewFactory(param: WebViewFactoryParam): NativeWebView = param.requestContext, ) is WebContent.Data -> - param.client.createBrowserWithHtml( - content.data, - content.baseUrl ?: KCEFBrowser.BLANK_URI, + param.client.createBrowser( + KCEFBrowser.BLANK_URI, param.rendering, param.transparent, ) diff --git a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/IOSWebView.kt b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/IOSWebView.kt index 786c9246..85db87ba 100644 --- a/webview/src/iosMain/kotlin/com/multiplatform/webview/web/IOSWebView.kt +++ b/webview/src/iosMain/kotlin/com/multiplatform/webview/web/IOSWebView.kt @@ -66,7 +66,7 @@ class IOSWebView( ) } - override fun loadHtml( + override suspend fun loadHtml( html: String?, baseUrl: String?, mimeType: String?, diff --git a/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/WasmJsWebView.kt b/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/WasmJsWebView.kt index da5a9968..63fd493b 100644 --- a/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/WasmJsWebView.kt +++ b/webview/src/wasmJsMain/kotlin/com/multiplatform/webview/web/WasmJsWebView.kt @@ -54,7 +54,7 @@ class WasmJsWebView( } } - override fun loadHtml( + override suspend fun loadHtml( html: String?, baseUrl: String?, mimeType: String?,