From f0152093e7732b4ccbb56bcbe399ff0371e8305a Mon Sep 17 00:00:00 2001 From: "moandji.ezana" Date: Thu, 29 Nov 2012 00:01:45 +0200 Subject: [PATCH] Mentions are expanded in rendered Essay. Page title reflects Essay title. --- pom.xml | 7 ++- .../tent/essayist/EssayServlet.java | 2 +- .../tent/essayist/EssaysServlet.java | 9 ++-- .../tent/essayist/PreviewServlet.java | 15 +++--- .../tent/essayist/WriteServlet.java | 27 +++------- .../tent/essayist/config/JamonContext.java | 5 +- .../tent/essayist/security/Csrf.java | 16 ++++-- .../essayist/text/TextTransformation.java | 53 +++++++++++++++++++ .../tent/essayist/EssayPage.jamon | 2 +- .../moandjiezana/tent/essayist/Layout.jamon | 3 +- .../essayist/partials/ReactionTemplate.jamon | 2 +- src/main/webapp/assets/essayist.js | 15 +++--- .../tent/essayist/security/CsrfTest.java | 21 +++++--- .../essayist/text/TextTransformationTest.java | 30 +++++++++++ 14 files changed, 151 insertions(+), 56 deletions(-) create mode 100644 src/main/java/com/moandjiezana/tent/essayist/text/TextTransformation.java create mode 100644 src/test/java/com/moandjiezana/tent/essayist/text/TextTransformationTest.java diff --git a/pom.xml b/pom.xml index 7976918..66138fc 100644 --- a/pom.xml +++ b/pom.xml @@ -82,7 +82,7 @@ owasp-java-html-sanitizer owasp-java-html-sanitizer - r117 + r129 junit @@ -143,5 +143,10 @@ guava 13.0.1 + + com.moandjiezana.tent + tent-text + 1.0.0-SNAPSHOT + diff --git a/src/main/java/com/moandjiezana/tent/essayist/EssayServlet.java b/src/main/java/com/moandjiezana/tent/essayist/EssayServlet.java index f329374..1e794a6 100644 --- a/src/main/java/com/moandjiezana/tent/essayist/EssayServlet.java +++ b/src/main/java/com/moandjiezana/tent/essayist/EssayServlet.java @@ -64,7 +64,7 @@ public class EssayServlet extends HttpServlet { Post post = tentClient.getPost(essayId); EssayistPostContent essayContent = post.getContentAs(EssayistPostContent.class); - essayContent.setBody(csrf.stripScripts(essayContent.getBody())); + essayContent.setBody(csrf.permissive(essayContent.getBody())); EssayPage essayPage = templates.essay(); if (user.owns(post)) { diff --git a/src/main/java/com/moandjiezana/tent/essayist/EssaysServlet.java b/src/main/java/com/moandjiezana/tent/essayist/EssaysServlet.java index 5b26bd7..108ebdc 100644 --- a/src/main/java/com/moandjiezana/tent/essayist/EssaysServlet.java +++ b/src/main/java/com/moandjiezana/tent/essayist/EssaysServlet.java @@ -6,6 +6,7 @@ import com.moandjiezana.tent.client.posts.PostQuery; import com.moandjiezana.tent.client.posts.content.EssayContent; import com.moandjiezana.tent.client.users.Permissions; import com.moandjiezana.tent.essayist.tent.Entities; +import com.moandjiezana.tent.essayist.text.TextTransformation; import java.io.IOException; import java.util.List; @@ -18,18 +19,18 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.pegdown.PegDownProcessor; - @Singleton public class EssaysServlet extends HttpServlet { private Templates templates; private Users users; private Provider sessions; + private TextTransformation textTransformation; @Inject - public EssaysServlet(Users users, Templates templates, Provider sessions) { + public EssaysServlet(Users users, TextTransformation textTransformation, Templates templates, Provider sessions) { this.users = users; + this.textTransformation = textTransformation; this.templates = templates; this.sessions = sessions; } @@ -62,7 +63,7 @@ public class EssaysServlet extends HttpServlet { post.setLicenses(new String[] { "http://creativecommons.org/licenses/by/3.0/" }); EssayContent essay = new EssayContent(); essay.setTitle(req.getParameter("title")); - essay.setBody(new PegDownProcessor().markdownToHtml(req.getParameter("body"))); + essay.setBody(textTransformation.transformEssay(req.getParameter("body"))); essay.setExcerpt(req.getParameter("excerpt")); post.setContent(essay); diff --git a/src/main/java/com/moandjiezana/tent/essayist/PreviewServlet.java b/src/main/java/com/moandjiezana/tent/essayist/PreviewServlet.java index e375552..09af82b 100644 --- a/src/main/java/com/moandjiezana/tent/essayist/PreviewServlet.java +++ b/src/main/java/com/moandjiezana/tent/essayist/PreviewServlet.java @@ -2,7 +2,7 @@ package com.moandjiezana.tent.essayist; import com.google.common.io.CharStreams; import com.moandjiezana.tent.essayist.auth.Authenticated; -import com.moandjiezana.tent.essayist.security.Csrf; +import com.moandjiezana.tent.essayist.text.TextTransformation; import java.io.IOException; @@ -13,25 +13,22 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.pegdown.PegDownProcessor; - @Singleton @Authenticated public class PreviewServlet extends HttpServlet { - private final Csrf csrf; + private final TextTransformation textTransformation; @Inject - public PreviewServlet(Csrf csrf) { - this.csrf = csrf; + public PreviewServlet(TextTransformation textTransformation) { + this.textTransformation = textTransformation; } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String body = CharStreams.toString(req.getReader()); - String html = new PegDownProcessor().markdownToHtml(body); - String sanitized = csrf.stripScripts(html); + String essay = textTransformation.transformEssay(body); - resp.getWriter().write(sanitized); + resp.getWriter().write(essay); } } diff --git a/src/main/java/com/moandjiezana/tent/essayist/WriteServlet.java b/src/main/java/com/moandjiezana/tent/essayist/WriteServlet.java index 4482d5c..18f16b3 100644 --- a/src/main/java/com/moandjiezana/tent/essayist/WriteServlet.java +++ b/src/main/java/com/moandjiezana/tent/essayist/WriteServlet.java @@ -11,6 +11,7 @@ import com.moandjiezana.tent.client.users.Permissions; import com.moandjiezana.tent.essayist.auth.Authenticated; import com.moandjiezana.tent.essayist.config.Routes; import com.moandjiezana.tent.essayist.tent.Entities; +import com.moandjiezana.tent.essayist.text.TextTransformation; import java.io.IOException; import java.util.List; @@ -23,8 +24,6 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.pegdown.PegDownProcessor; - @Singleton @Authenticated public class WriteServlet extends HttpServlet { @@ -33,9 +32,11 @@ public class WriteServlet extends HttpServlet { private Provider sessions; private Tasks tasks; private Provider routes; + private TextTransformation textTransformation; @Inject - public WriteServlet(Provider sessions, Templates templates, Provider routes, Tasks tasks) { + public WriteServlet(TextTransformation textTransformation, Provider sessions, Templates templates, Provider routes, Tasks tasks) { + this.textTransformation = textTransformation; this.sessions = sessions; this.templates = templates; this.routes = routes; @@ -71,7 +72,7 @@ public class WriteServlet extends HttpServlet { EssayContent essay = new EssayContent(); essay.setTitle(req.getParameter("title")); final String body = req.getParameter("body"); - essay.setBody(new PegDownProcessor().markdownToHtml(body)); + essay.setBody(textTransformation.transformEssay(body)); essay.setExcerpt(req.getParameter("excerpt")); post.setContent(essay); @@ -89,27 +90,11 @@ public class WriteServlet extends HttpServlet { tentClient.write(metadataPost); resp.sendRedirect(req.getContextPath() + "/" + Entities.getForUrl(tentClient.getProfile().getCore().getEntity()) + "/essay/" + newPost.getId()); - -// tasks.run(new Runnable() { -// @Override -// public void run() { -// } else { -// Post originalPost = posts.get(0); -// -// Post newPost = newPost(); -// -// EssayistMetadataContent content = originalPost.getContentAs(EssayistMetadataContent.class); -// content.setRaw(body); -// } -// -// } -// }); } @Override protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { TentClient tentClient = newTentClient(); -// List posts = tentClient.getPosts(new PostQuery().mentionedPost(essayId).postTypes(EssayistMetadataContent.URI)); String essayId = req.getPathInfo().substring(1); Post post = newPost(); @@ -118,7 +103,7 @@ public class WriteServlet extends HttpServlet { EssayContent essay = new EssayContent(); essay.setTitle(req.getParameter("title")); final String body = req.getParameter("body"); - essay.setBody(new PegDownProcessor().markdownToHtml(body)); + essay.setBody(textTransformation.transformEssay(body)); essay.setExcerpt(req.getParameter("excerpt")); post.setContent(essay); diff --git a/src/main/java/com/moandjiezana/tent/essayist/config/JamonContext.java b/src/main/java/com/moandjiezana/tent/essayist/config/JamonContext.java index 16dcbe2..a7cc925 100644 --- a/src/main/java/com/moandjiezana/tent/essayist/config/JamonContext.java +++ b/src/main/java/com/moandjiezana/tent/essayist/config/JamonContext.java @@ -5,6 +5,7 @@ import com.moandjiezana.tent.client.users.Profile; import com.moandjiezana.tent.essayist.EssayistSession; import com.moandjiezana.tent.essayist.User; import com.moandjiezana.tent.essayist.security.Csrf; +import com.moandjiezana.tent.essayist.text.TextTransformation; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; @@ -16,14 +17,16 @@ public class JamonContext { public final String contextPath; public final Routes routes; public final Csrf csrf = new Csrf(); + public final TextTransformation textTransformation; private final HttpServletRequest req; public final String currentUrl; private final EssayistSession session; @Inject - public JamonContext(EssayistSession session, Routes routes, HttpServletRequest req) { + public JamonContext(EssayistSession session, TextTransformation textTransformation, Routes routes, HttpServletRequest req) { this.session = session; + this.textTransformation = textTransformation; this.routes = routes; this.req = req; this.contextPath = req.getContextPath(); diff --git a/src/main/java/com/moandjiezana/tent/essayist/security/Csrf.java b/src/main/java/com/moandjiezana/tent/essayist/security/Csrf.java index 1d56dcb..96ba3c5 100644 --- a/src/main/java/com/moandjiezana/tent/essayist/security/Csrf.java +++ b/src/main/java/com/moandjiezana/tent/essayist/security/Csrf.java @@ -3,6 +3,7 @@ package com.moandjiezana.tent.essayist.security; import javax.inject.Singleton; import org.owasp.html.HtmlPolicyBuilder; +import org.owasp.html.PolicyFactory; @Singleton public class Csrf { @@ -13,12 +14,21 @@ public class Csrf { .allowStandardUrlProtocols() .allowStyling() .allowElements("iframe", "img", "a", "table", "thead", "tbody", "tr", "th", "td", "em") - .allowAttributes("width", "height", "title").globally() + .allowAttributes("width", "height", "title", "class").globally() .allowAttributes("src", "frameborder", "webkitAllowFullScreen", "mozallowfullscreen", "allowFullScreen").onElements("iframe") .allowAttributes("src", "alt").onElements("img") - .allowAttributes("href").onElements("a"); + .allowAttributes("href", "rel").onElements("a"); + + private final PolicyFactory restrictive = new HtmlPolicyBuilder() + .allowStandardUrlProtocols() + .allowElements("a") + .allowAttributes("href", "class", "rel").onElements("a").toFactory(); - public String stripScripts(String html) { + public String permissive(String html) { return allowScripts.toFactory().sanitize(html); } + + public String restrictive(String html) { + return restrictive.sanitize(html); + } } diff --git a/src/main/java/com/moandjiezana/tent/essayist/text/TextTransformation.java b/src/main/java/com/moandjiezana/tent/essayist/text/TextTransformation.java new file mode 100644 index 0000000..ecb3975 --- /dev/null +++ b/src/main/java/com/moandjiezana/tent/essayist/text/TextTransformation.java @@ -0,0 +1,53 @@ +package com.moandjiezana.tent.essayist.text; + +import com.moandjiezana.tent.essayist.security.Csrf; +import com.moandjiezana.tent.text.Autolink; +import com.moandjiezana.tent.text.Extractor.Entity; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.pegdown.PegDownProcessor; + +@Singleton +public class TextTransformation { + + private final Csrf csrf; + private final Autolink essayAutolink = new Autolink(); + private final Autolink commentAutolink = new Autolink(); + + @Inject + public TextTransformation(Csrf csrf) { + this.csrf = csrf; + essayAutolink.setMentionIncludeSymbol(true); + Autolink.LinkTextModifier linkTextModifier = new Autolink.LinkTextModifier() { + @Override + public CharSequence modify(Entity entity, CharSequence text) { + if (text.charAt(0) == '^') { + return text.subSequence(1, text.length()); + } + return text; + } + }; + essayAutolink.setLinkTextModifier(linkTextModifier); + essayAutolink.setMentionClass("label label-inverse"); + + commentAutolink.setMentionIncludeSymbol(true); + commentAutolink.setLinkTextModifier(linkTextModifier); + commentAutolink.setMentionClass("label label-inverse"); + } + + public String transformEssay(String text) { + essayAutolink.setNoFollow(false); + String autoLinked = essayAutolink.autoLinkHashtags(essayAutolink.autoLinkMentionsAndLists(text)); + + String html = new PegDownProcessor().markdownToHtml(autoLinked); + String sanitized = csrf.permissive(html); + + return sanitized; + } + + public Object transformComment(String comment) { + return csrf.restrictive(commentAutolink.autoLink(comment)); + } +} diff --git a/src/main/templates/com/moandjiezana/tent/essayist/EssayPage.jamon b/src/main/templates/com/moandjiezana/tent/essayist/EssayPage.jamon index e79c022..4ee5bbf 100644 --- a/src/main/templates/com/moandjiezana/tent/essayist/EssayPage.jamon +++ b/src/main/templates/com/moandjiezana/tent/essayist/EssayPage.jamon @@ -19,7 +19,7 @@ final EssayContent content = essay.getContentAs(EssayContent.class); final SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy"); final SimpleDateFormat dateTimeFormat = new SimpleDateFormat("dd MMMM yyyy HH:mm ZZZZ"); -<&| Layout; active = active &> +<&| Layout; active = active; title = Entities.essayTitle(content) &> <& partials/Essay; essay=essay; entityForUrl=Entities.getForUrl(essay.getEntity()); entityName=Entities.getName(profile); essayId=essay.getId(); formattedPublicationDate=dateFormat.format(new Date(essay.getPublishedAt() * 1000)); display=true; &> <& partials/Reactions; reactions=reactions; essay=essay; autoLoad=true; &> diff --git a/src/main/templates/com/moandjiezana/tent/essayist/Layout.jamon b/src/main/templates/com/moandjiezana/tent/essayist/Layout.jamon index 5ff4996..b5c7264 100644 --- a/src/main/templates/com/moandjiezana/tent/essayist/Layout.jamon +++ b/src/main/templates/com/moandjiezana/tent/essayist/Layout.jamon @@ -8,6 +8,7 @@ com.moandjiezana.tent.essayist.config.*; <%args> boolean showNav = true; String active = null; +String title = "Essayist"; <%def navItem> @@ -21,7 +22,7 @@ String url; - Essayist + <% title %> diff --git a/src/main/templates/com/moandjiezana/tent/essayist/partials/ReactionTemplate.jamon b/src/main/templates/com/moandjiezana/tent/essayist/partials/ReactionTemplate.jamon index 084c3e5..533c2fb 100644 --- a/src/main/templates/com/moandjiezana/tent/essayist/partials/ReactionTemplate.jamon +++ b/src/main/templates/com/moandjiezana/tent/essayist/partials/ReactionTemplate.jamon @@ -18,7 +18,7 @@ String formattedDate = dateTimeFormat.format(new Date(reaction.getPublishedAt() <%if Post.Types.equalsIgnoreVersion(Post.Types.status("v0.1.0"), reaction.getType()) %> <% displayEntity %> commented <% formattedDate %>

- <% reaction.getContentAs(StatusContent.class).getText() %> + <% jamonContext.textTransformation.transformComment(reaction.getContentAs(StatusContent.class).getText()) #n %>

<%elseif Post.Types.equalsIgnoreVersion(Favorite.URI, reaction.getType()) %> Favourited by <% displayEntity %> <% formattedDate %> diff --git a/src/main/webapp/assets/essayist.js b/src/main/webapp/assets/essayist.js index 0d56a4d..c156c5d 100644 --- a/src/main/webapp/assets/essayist.js +++ b/src/main/webapp/assets/essayist.js @@ -2,11 +2,15 @@ function initNavigation() { var essayContainers = document.querySelectorAll("[data-essay=container]"); var i; - var displaySection = function (section, target) { + var displaySection = function (section, target, title) { var j; var detailDisplay = section === "essay" ? "block" : "none"; var listDisplay = section === "list" ? "block" : "none"; + if (title !== undefined) { + document.title = title; + } + if (target !== document) { target.querySelector("[data-essay=summary]").style.display = listDisplay; target.querySelector("[data-essay=full]").style.display = detailDisplay; @@ -46,12 +50,12 @@ function initNavigation() { } var scrollPosition = document.body.scrollTop; - displaySection("essay", event.currentTarget); + displaySection("essay", event.currentTarget, event.target.text); var reactionsContainer = container.querySelector('[data-essay="reactions"]'); fetchReactions(reactionsContainer); - history.pushState({ essayId: event.currentTarget.dataset.essayId, essayAuthor: event.currentTarget.dataset.essayAuthor, scrollPosition: scrollPosition }, "essay title", event.target.href); + history.pushState({ essayId: event.currentTarget.dataset.essayId, essayAuthor: event.currentTarget.dataset.essayAuthor, essayTitle: event.target.text, scrollPosition: scrollPosition }, "essay title", event.target.href); return false; }; @@ -59,11 +63,10 @@ function initNavigation() { var essayPopStateHandler = function (event) { var essayContainer; if (window.location.href.indexOf("/essays") > -1 || window.location.href.indexOf("/global") > -1 || window.location.href.indexOf("/read") > -1) { - displaySection("list", document); - window.scrollTo(0); + displaySection("list", document, "Essayist"); } else if (event.state !== null) { essayContainer = document.querySelector("[data-essay-author=\"" + event.state.essayAuthor + "\"][data-essay-id=\"" + event.state.essayId + "\"]"); - displaySection("essay", essayContainer); + displaySection("essay", essayContainer, event.state.essayTitle); } }; diff --git a/src/test/java/com/moandjiezana/tent/essayist/security/CsrfTest.java b/src/test/java/com/moandjiezana/tent/essayist/security/CsrfTest.java index aa81000..5fe096a 100644 --- a/src/test/java/com/moandjiezana/tent/essayist/security/CsrfTest.java +++ b/src/test/java/com/moandjiezana/tent/essayist/security/CsrfTest.java @@ -8,14 +8,14 @@ public class CsrfTest { @Test public void should_remove_external_scripts() { - String sanitized = new Csrf().stripScripts("

title

sub-title

Some text

Some more text

"); + String sanitized = new Csrf().permissive("

title

sub-title

Some text

Some more text

"); assertEquals("

title

sub-title

Some text

Some more text

", sanitized); } @Test public void should_remove_internal_scripts() { - String sanitized = new Csrf().stripScripts("

title

sub-title

Some text

Some more text

"); + String sanitized = new Csrf().permissive("

title

sub-title

Some text

Some more text

"); assertEquals("

title

sub-title

Some text

Some more text

", sanitized); } @@ -24,14 +24,14 @@ public class CsrfTest { public void should_allow_images() { String img = "\"The"; - assertEquals(img, new Csrf().stripScripts(img)); + assertEquals(img, new Csrf().permissive(img)); } @Test public void should_allow_links() { String link = "Oskar van Rijswijk"; - assertEquals(link, new Csrf().stripScripts(link)); + assertEquals(link, new Csrf().permissive(link)); } @Test @@ -39,20 +39,27 @@ public class CsrfTest { String vimeoEmbed = "

An excerpt from a three screen triptych which is a part of a bigger installation.
Sound, photography & video by Einat Schlagmann.

"; String expected = "

An excerpt from a three screen triptych which is a part of a bigger installation.
Sound, photography & video by Einat Schlagmann.

"; - assertEquals(expected, new Csrf().stripScripts(vimeoEmbed)); + assertEquals(expected, new Csrf().permissive(vimeoEmbed)); } @Test public void should_allow_tables() { String table = "
header
cell
"; - assertEquals(table, new Csrf().stripScripts(table)); + assertEquals(table, new Csrf().permissive(table)); } @Test public void should_allow_emphasis() { String emphasis = "emphasised"; - assertEquals(emphasis, new Csrf().stripScripts(emphasis)); + assertEquals(emphasis, new Csrf().permissive(emphasis)); + } + + @Test + public void restrictive_should_only_allow_links() { + String html = "link emphasis bold"; + + assertEquals("link emphasis bold", new Csrf().restrictive(html)); } } diff --git a/src/test/java/com/moandjiezana/tent/essayist/text/TextTransformationTest.java b/src/test/java/com/moandjiezana/tent/essayist/text/TextTransformationTest.java new file mode 100644 index 0000000..80aef45 --- /dev/null +++ b/src/test/java/com/moandjiezana/tent/essayist/text/TextTransformationTest.java @@ -0,0 +1,30 @@ +package com.moandjiezana.tent.essayist.text; + +import static org.junit.Assert.assertEquals; + +import com.moandjiezana.tent.essayist.security.Csrf; + +import org.junit.Test; + +public class TextTransformationTest { + + private final TextTransformation textTransformation = new TextTransformation(new Csrf()); + + @Test + public void should_expand_markdown_and_all_entities_in_essay() { + String text = "[Link](http://www.example.com) by ^mention is #good!"; + + String expected = "

Link by mention is #good!

"; + + assertEquals(expected, textTransformation.transformEssay(text)); + } + + @Test + public void should_expand_all_entities_in_comment() { + String comment = "Hey ^somerandombloke, have you seen https://github.com/tent/tent.io/wiki/Explaining-Tent ?"; + + String expected = "Hey somerandombloke, have you seen https://github.com/tent/tent.io/wiki/Explaining-Tent ?"; + + assertEquals(expected, textTransformation.transformComment(comment)); + } +}