mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-08 15:09:39 +07:00
[markdown] replacing the transient token with a randomized page name for JCEF preview (IJPL-187567)
(cherry picked from commit 1deea1b9794b926a7aaa3b1ec2d244391fd4e10c) IJ-CR-170152 GitOrigin-RevId: ea41949904247a6eb586eec1a1ecace48b48daba
This commit is contained in:
committed by
intellij-monorepo-bot
parent
842623e56e
commit
bac6daff8b
@@ -1,20 +1,18 @@
|
||||
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.intellij.plugins.markdown.ui.preview
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.ApplicationInfo
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.util.Urls.parseEncoded
|
||||
import com.intellij.util.Urls
|
||||
import io.netty.buffer.Unpooled
|
||||
import io.netty.channel.Channel
|
||||
import io.netty.channel.ChannelHandlerContext
|
||||
import io.netty.handler.codec.http.*
|
||||
import org.jetbrains.ide.BuiltInServerManager.Companion.getInstance
|
||||
import org.jetbrains.ide.BuiltInServerManager
|
||||
import org.jetbrains.ide.HttpRequestHandler
|
||||
import org.jetbrains.io.FileResponses.checkCache
|
||||
import org.jetbrains.io.FileResponses.getContentType
|
||||
import org.jetbrains.io.send
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@@ -54,17 +52,12 @@ class PreviewStaticServer : HttpRequestHandler() {
|
||||
return synchronized(resourceProviders) { resourceProviders.getOrDefault(providerHash, null) }
|
||||
}
|
||||
|
||||
override fun isSupported(request: FullHttpRequest): Boolean {
|
||||
if (!super.isSupported(request)) {
|
||||
return false
|
||||
}
|
||||
val path = request.uri()
|
||||
return path.startsWith(prefixPath)
|
||||
}
|
||||
override fun isSupported(request: FullHttpRequest): Boolean =
|
||||
super.isSupported(request) && request.uri().startsWith(ENDPOINT_PREFIX_PATH)
|
||||
|
||||
override fun process(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): Boolean {
|
||||
val path = urlDecoder.path()
|
||||
check(path.startsWith(prefixPath)) { "prefix should have been checked by #isSupported" }
|
||||
check(path.startsWith(ENDPOINT_PREFIX_PATH)) { "prefix should have been checked by #isSupported" }
|
||||
val resourceProvider = obtainResourceProvider(path) ?: return false
|
||||
val resourceName = getStaticPath(path)
|
||||
if (resourceProvider.canProvide(resourceName)) {
|
||||
@@ -80,25 +73,21 @@ class PreviewStaticServer : HttpRequestHandler() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val endpointPrefix = "markdownPreview"
|
||||
private const val prefixPath = "/$endpointPrefix"
|
||||
private const val ENDPOINT_PREFIX = "markdownPreview"
|
||||
private const val ENDPOINT_PREFIX_PATH = "/${ENDPOINT_PREFIX}"
|
||||
|
||||
@JvmStatic
|
||||
val instance: PreviewStaticServer
|
||||
get() = EP_NAME.findExtension(PreviewStaticServer::class.java) ?: error("Could not get server instance!")
|
||||
|
||||
@JvmStatic
|
||||
fun createCSP(scripts: List<String>, styles: List<String>): String {
|
||||
// We need to remove any query parameters to stop annoying errors in the browser console
|
||||
fun stripQueryParameters(url: String) = url.replace("?${URL(url).query}", "")
|
||||
return """
|
||||
default-src 'none';
|
||||
script-src ${StringUtil.join(scripts.map(::stripQueryParameters), " ")};
|
||||
style-src https: ${StringUtil.join(styles.map(::stripQueryParameters), " ")} 'unsafe-inline';
|
||||
img-src file: * data:; connect-src 'none'; font-src * data: *;
|
||||
object-src 'none'; media-src 'none'; child-src 'none';
|
||||
"""
|
||||
}
|
||||
internal fun createCSP(scripts: List<String>, styles: List<String>): String = """
|
||||
default-src 'none';
|
||||
script-src ${scripts.joinToString(" ")};
|
||||
style-src https: ${styles.joinToString(" ")} 'unsafe-inline';
|
||||
img-src file: * data:; connect-src 'none'; font-src * data: *;
|
||||
object-src 'none'; media-src 'none'; child-src 'none';
|
||||
"""
|
||||
|
||||
/**
|
||||
* Expected to return same URL on each call for same [resourceProvider] and [staticPath],
|
||||
@@ -107,11 +96,10 @@ class PreviewStaticServer : HttpRequestHandler() {
|
||||
@JvmStatic
|
||||
fun getStaticUrl(resourceProvider: ResourceProvider, staticPath: String): String {
|
||||
val providerHash = resourceProvider.hashCode()
|
||||
val port = getInstance().port
|
||||
val raw = "http://localhost:$port/$endpointPrefix/$providerHash/$staticPath"
|
||||
val url = parseEncoded(raw)
|
||||
requireNotNull(url) { "Could not parse url!" }
|
||||
return getInstance().addAuthToken(url).toExternalForm()
|
||||
val port = BuiltInServerManager.getInstance().port
|
||||
val raw = "http://localhost:${port}/${ENDPOINT_PREFIX}/${providerHash}/${staticPath}"
|
||||
requireNotNull(Urls.parseEncoded(raw)) { "Invalid URL: ${raw}" }
|
||||
return raw
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.intellij.plugins.markdown.ui.preview.jcef
|
||||
|
||||
import com.intellij.ide.ui.UISettingsListener
|
||||
@@ -22,6 +22,7 @@ import com.intellij.ui.jcef.JBCefApp
|
||||
import com.intellij.ui.jcef.JBCefClient
|
||||
import com.intellij.ui.jcef.JCEFHtmlPanel
|
||||
import com.intellij.util.application
|
||||
import com.intellij.util.io.DigestUtil
|
||||
import com.intellij.util.net.NetUtils
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
@@ -58,13 +59,10 @@ class MarkdownJCEFHtmlPanel(
|
||||
) : JCEFHtmlPanel(isOffScreenRendering(), null, null), MarkdownHtmlPanelEx, UserDataHolder by UserDataHolderBase() {
|
||||
constructor() : this(null, null)
|
||||
|
||||
private val pageBaseName = "markdown-preview-index-${hashCode()}.html"
|
||||
|
||||
private val pageBaseName = "markdown-preview-index-${DigestUtil.randomToken()}.html"
|
||||
private val resourceProvider = MyAggregatingResourceProvider()
|
||||
private val browserPipe: BrowserPipe = JcefBrowserPipeImpl(
|
||||
this,
|
||||
injectionAllowedUrls = listOf(PreviewStaticServer.getStaticUrl(resourceProvider, pageBaseName))
|
||||
)
|
||||
private val pageUrl = PreviewStaticServer.getStaticUrl(resourceProvider, pageBaseName)
|
||||
private val browserPipe: BrowserPipe = JcefBrowserPipeImpl(browser = this, injectionAllowedUrls = listOf(pageUrl))
|
||||
|
||||
private val scrollListeners = ArrayList<MarkdownHtmlPanel.ScrollListener>()
|
||||
|
||||
@@ -78,40 +76,21 @@ class MarkdownJCEFHtmlPanel(
|
||||
.sorted()
|
||||
}
|
||||
|
||||
private val scripts
|
||||
get() = baseScripts + currentExtensions.flatMap { it.scripts }
|
||||
|
||||
private val styles
|
||||
get() = currentExtensions.flatMap { it.styles }
|
||||
|
||||
private val scriptingLines
|
||||
get() = scripts.joinToString("\n") {
|
||||
"<script src=\"${PreviewStaticServer.getStaticUrl(resourceProvider, it)}\"></script>"
|
||||
}
|
||||
|
||||
private val stylesLines
|
||||
get() = styles.joinToString("\n") {
|
||||
"<link rel=\"stylesheet\" href=\"${PreviewStaticServer.getStaticUrl(resourceProvider, it)}\"/>"
|
||||
}
|
||||
|
||||
private val contentSecurityPolicy get() = PreviewStaticServer.createCSP(
|
||||
scripts.map { PreviewStaticServer.getStaticUrl(resourceProvider, it) },
|
||||
styles.map { PreviewStaticServer.getStaticUrl(resourceProvider, it) }
|
||||
)
|
||||
|
||||
private val updateHandler = MarkdownUpdateHandler.Debounced()
|
||||
|
||||
private fun buildIndexContent(): String {
|
||||
val scripts = (baseScripts + currentExtensions.flatMap { it.scripts }).map { PreviewStaticServer.getStaticUrl(resourceProvider, it) }
|
||||
val styles = currentExtensions.flatMap { it.styles }.map { PreviewStaticServer.getStaticUrl(resourceProvider, it) }
|
||||
// language=HTML
|
||||
return """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>IntelliJ Markdown Preview</title>
|
||||
<meta http-equiv="Content-Security-Policy" content="$contentSecurityPolicy"/>
|
||||
<meta http-equiv="Content-Security-Policy" content="${PreviewStaticServer.createCSP(scripts, styles)}"/>
|
||||
<meta name="markdown-position-attribute-name" content="${HtmlGenerator.SRC_ATTRIBUTE_NAME}"/>
|
||||
$scriptingLines
|
||||
$stylesLines
|
||||
${scripts.joinToString("\n") { "<script src=\"${it}\"></script>" }}
|
||||
${styles.joinToString("\n") { "<link rel=\"stylesheet\" href=\"${it}\"/>" }}
|
||||
</head>
|
||||
</html>
|
||||
"""
|
||||
@@ -119,7 +98,7 @@ class MarkdownJCEFHtmlPanel(
|
||||
|
||||
private suspend fun loadIndexContent() {
|
||||
reloadExtensions()
|
||||
waitForPageLoad(PreviewStaticServer.getStaticUrl(resourceProvider, pageBaseName))
|
||||
waitForPageLoad(pageUrl)
|
||||
}
|
||||
|
||||
private var previousRenderClosure: String = ""
|
||||
@@ -420,13 +399,11 @@ class MarkdownJCEFHtmlPanel(
|
||||
""".trimIndent())
|
||||
return true
|
||||
}
|
||||
val targetPageUrl = PreviewStaticServer.getStaticUrl(resourceProvider, pageBaseName)
|
||||
val requestedUrl = request.url
|
||||
if (requestedUrl != targetPageUrl) {
|
||||
if (request.url != pageUrl) {
|
||||
logger.warn("""
|
||||
Canceling request for an external page with url: $requestedUrl.
|
||||
Canceling request for an external page with url: ${request.url}.
|
||||
Current page url: ${browser.url}
|
||||
Target safe url: $targetPageUrl
|
||||
Target safe url: ${pageUrl}
|
||||
""".trimIndent())
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user