package org.intellij.plugins.markdown.ui.preview;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.CharsetToolkit;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.*;
import io.netty.handler.stream.ChunkedStream;
import org.intellij.plugins.markdown.settings.MarkdownCssSettings;
import org.intellij.plugins.markdown.ui.preview.javafx.JavaFxHtmlPanel;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.ide.BuiltInServerManager;
import org.jetbrains.ide.HttpRequestHandler;
import org.jetbrains.io.FileResponses;
import org.jetbrains.io.Responses;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
public class PreviewStaticServer extends HttpRequestHandler {
public static final String INLINE_CSS_FILENAME = "inline.css";
private static final String PREFIX = "/api/markdown-preview/";
@Nullable
private String myInlineStyle = null;
private long myInlineStyleTimestamp = 0;
public static PreviewStaticServer getInstance() {
return HttpRequestHandler.Companion.getEP_NAME().findExtension(PreviewStaticServer.class);
}
@NotNull
public static String createCSP(@NotNull List<String> scripts, @NotNull List<String> styles) {
return "default-src 'none'; script-src " + StringUtil.join(scripts, " ") + "; "
+ "style-src https: " + StringUtil.join(styles, " ") + "; "
+ "img-src *; connect-src 'none'; font-src *; " +
"object-src 'none'; media-src 'none'; child-src 'none';";
}
@NotNull
private static String getStaticUrl(@NotNull String staticPath) {
return "http://localhost:" + BuiltInServerManager.getInstance().getPort() + PREFIX + staticPath;
}
@NotNull
public static String getScriptUrl(@NotNull String scriptFileName) {
return getStaticUrl("scripts/" + scriptFileName);
}
@NotNull
public static String getStyleUrl(@NotNull String scriptFileName) {
return getStaticUrl("styles/" + scriptFileName);
}
public void setInlineStyle(@Nullable String inlineStyle) {
myInlineStyle = inlineStyle;
myInlineStyleTimestamp = System.currentTimeMillis();
}
@Override
public boolean isSupported(@NotNull FullHttpRequest request) {
return super.isSupported(request) && request.uri().startsWith(PREFIX);
}
@Override
public boolean process(@NotNull QueryStringDecoder urlDecoder,
@NotNull FullHttpRequest request,
@NotNull ChannelHandlerContext context) throws IOException {
final String path = urlDecoder.path();
if (!path.startsWith(PREFIX)) {
throw new IllegalStateException("prefix should have been checked by #isSupported");
}
final String payLoad = path.substring(PREFIX.length());
final List<String> typeAndName = StringUtil.split(payLoad, "/");
if (typeAndName.size() != 2) {
return false;
}
final String contentType = typeAndName.get(0);
final String fileName = typeAndName.get(1);
if ("scripts".equals(contentType) && MarkdownHtmlPanel.SCRIPTS.contains(fileName)) {
sendResource(request,
context.channel(),
JavaFxHtmlPanel.class,
fileName);
}
else if ("styles".equals(contentType) && MarkdownHtmlPanel.STYLES.contains(fileName)) {
if (INLINE_CSS_FILENAME.equals(fileName)) {
sendInlineStyle(request, context.channel());
}
else {
sendResource(request,
context.channel(),
MarkdownCssSettings.class,
fileName);
}
}
else {
return false;
}
return true;
}
private void sendInlineStyle(@NotNull HttpRequest request,
@NotNull Channel channel) {
final HttpResponse response =
FileResponses.INSTANCE.prepareSend(request, channel, myInlineStyleTimestamp, INLINE_CSS_FILENAME, EmptyHttpHeaders.INSTANCE);
if (response == null) {
return;
}
Responses.addKeepAliveIfNeed(response, request);
if (myInlineStyle == null) {
Responses.send(HttpResponseStatus.NOT_FOUND, channel, request);
return;
}
channel.write(response);
if (request.method() != HttpMethod.HEAD) {
channel.write(new ChunkedStream(new ByteArrayInputStream(myInlineStyle.getBytes(CharsetToolkit.UTF8_CHARSET))));
}
final ChannelFuture future = channel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
future.addListener(ChannelFutureListener.CLOSE);
}
private static void sendResource(@NotNull HttpRequest request,
@NotNull Channel channel,
@NotNull Class<?> clazz,
@NotNull String resourceName) {
final String fileName = resourceName.substring(resourceName.lastIndexOf('/') + 1);
final HttpResponse response =
FileResponses.INSTANCE.prepareSend(request, channel, 0, fileName, EmptyHttpHeaders.INSTANCE);
if (response == null) {
return;
}
Responses.addKeepAliveIfNeed(response, request);
try (final InputStream resource = clazz.getResourceAsStream(resourceName)) {
if (resource == null) {
Responses.send(HttpResponseStatus.NOT_FOUND, channel, request);
return;
}
channel.write(response);
if (request.method() != HttpMethod.HEAD) {
channel.write(new ChunkedStream(resource));
}
}
catch (IOException ignored) {
}
final ChannelFuture future = channel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
future.addListener(ChannelFutureListener.CLOSE);
}
}