/** * Yobi, Project Hosting SW * * Copyright 2013 NAVER Corp. * http://yobi.io * * @author Keesun Baik * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package utils; import models.Project; import org.apache.commons.lang.StringEscapeUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import javax.annotation.Nonnull; import javax.script.Invocable; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.net.URI; import java.net.URISyntaxException; public class Markdown { private static final String XSS_JS_FILE = "public/javascripts/lib/xss.js"; private static final String MARKED_JS_FILE = "public/javascripts/lib/marked.js"; private static final String HIGHLIGHT_JS_FILE = "public/javascripts/lib/highlight/highlight.pack.js"; private static ScriptEngine engine = buildEngine(); private static ScriptEngine buildEngine() { ScriptEngineManager manager = new ScriptEngineManager(null); InputStream is = null; Reader reader = null; ScriptEngine _engine = manager.getEngineByName("JavaScript"); try { is = Thread.currentThread().getContextClassLoader().getResourceAsStream(XSS_JS_FILE); reader = new InputStreamReader(is, Config.getCharset()); _engine.eval(reader); is = Thread.currentThread().getContextClassLoader().getResourceAsStream(MARKED_JS_FILE); reader = new InputStreamReader(is, Config.getCharset()); _engine.eval(reader); is = Thread.currentThread().getContextClassLoader().getResourceAsStream(HIGHLIGHT_JS_FILE); reader = new InputStreamReader(is, Config.getCharset()); _engine.eval(reader); } catch (Exception ex) { throw new RuntimeException(ex); } finally { if(reader != null) { try{ reader.close(); } catch (Exception e) { throw new RuntimeException(e); } } if(is != null) { try{ is.close(); } catch (Exception e) { throw new RuntimeException(e); } } } return _engine; } private static String removeJavascriptInHref(String source) { Document doc = Jsoup.parse(source); Elements elements = doc.getElementsByAttribute("href"); for (Element el : elements) { String href = el.attr("href").replaceAll("[^\\w:]", "").toLowerCase(); if (href.startsWith("javascript:")) { el.attr("href", "#"); } } return doc.body().html(); } private static String checkReferrer(String source) { Boolean noReferrer = play.Configuration.root().getBoolean("application.noreferrer", false); if (noReferrer) { String hostname = Config.getHostname(); Document doc = Jsoup.parse(source); Elements elements = doc.getElementsByAttribute("href"); for (Element el : elements) { String href = el.attr("href"); try { URI uri = new URI(href); if (uri.getHost() != null && !uri.getHost().startsWith(hostname)) { el.attr("rel", el.attr("rel") + " noreferrer"); } } catch (URISyntaxException e) { // Just skip the wrong link. } } return doc.body().html(); } return source; } private static String sanitize(String source) { try { Object filter = engine.eval("new Filter();"); return (String) ((Invocable) engine).invokeMethod(filter, "defence", source); } catch (Exception ex) { throw new RuntimeException(ex); } } private static String renderWithHighlight(String source, boolean breaks) { try { Object options = engine.eval("new Object({gfm: true, tables: true, breaks: " + breaks + ", " + "pedantic: false, sanitize: false, smartLists: true," + "highlight : function(sCode, sLang) { " + "if(sLang) { try { return hljs.highlight(sLang.toLowerCase(), sCode).value;" + " } catch(oException) { return sCode; } } }});"); String rendered = renderByMarked(source, options); rendered = removeJavascriptInHref(rendered); rendered = checkReferrer(rendered); return sanitize(rendered); } catch (Exception ex) { throw new RuntimeException(ex); } } /** * Renders the source with Marked. * * @param source * @param options * @return the rendered result or the source if timeout occurs */ private static String renderByMarked(@Nonnull final String source, final Object options) throws InterruptedException { if (source.isEmpty()) { return source; } // Try to render and wait at most 5 seconds. final String[] rendered = new String[1]; @SuppressWarnings("deprecation") Thread marked = new Thread() { @Override public void run() { try { rendered[0] = (String) ((Invocable) engine).invokeFunction( "marked", source, options); } catch (Exception e) { play.Logger.error("[Markdown] Failed to render: " + source, e); } } }; marked.start(); marked.join(5000); if (rendered[0] == null) { // This is the only way to stop the script engine. Thread.interrupt does not work. marked.stop(); return "<pre>" + StringEscapeUtils.escapeHtml(source) + "</pre>"; } else { return rendered[0]; } } public static String render(@Nonnull String source) { try { Object options = engine.eval("new Object({gfm: true, tables: true, breaks: true, " + "pedantic: false, sanitize: false, smartLists: true});"); return sanitize(renderByMarked(source, options)); } catch (Exception ex) { throw new RuntimeException(ex); } } public static String render(@Nonnull String source, Project project, boolean breaks) { AutoLinkRenderer autoLinkRenderer = new AutoLinkRenderer(renderWithHighlight(source, breaks), project); return autoLinkRenderer.render(); } public static String render(@Nonnull String source, Project project) { return render(source, project, true); } }