From 0351a8fbbac6cfa671128513605288fb940ce94b Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Thu, 26 May 2022 08:05:00 +0200 Subject: KB66 Add comment box. Change-Id: I9bf140ded85045b09997145ed2a9fb421fedc7d4 --- .../mulkcms2/benki/bookmarks/BookmarkText.java | 8 +--- .../mulkcms2/benki/lazychat/LazychatMessage.java | 6 +-- .../benki/lazychat/LazychatMessageText.java | 8 +--- .../java/eu/mulk/mulkcms2/benki/posts/Post.java | 14 ++++++ .../eu/mulk/mulkcms2/benki/posts/PostResource.java | 54 ++++++++++++++++++++++ .../eu/mulk/mulkcms2/benki/posts/PostText.java | 15 +++++- .../common/markdown/MarkdownConverter.java | 51 ++++++++++++++++---- 7 files changed, 129 insertions(+), 27 deletions(-) (limited to 'src/main/java') diff --git a/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkText.java b/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkText.java index c30f3df..06ea299 100644 --- a/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkText.java +++ b/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkText.java @@ -1,7 +1,6 @@ package eu.mulk.mulkcms2.benki.bookmarks; import eu.mulk.mulkcms2.benki.posts.PostText; -import eu.mulk.mulkcms2.common.markdown.MarkdownConverter; import javax.annotation.CheckForNull; import javax.persistence.Column; import javax.persistence.Entity; @@ -22,10 +21,7 @@ public class BookmarkText extends PostText { @Transient @CheckForNull - protected String computeDescriptionHtml() { - if (description == null) { - return null; - } - return new MarkdownConverter().htmlify(description); + protected String getDescriptionMarkup() { + return description; } } diff --git a/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatMessage.java b/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatMessage.java index 63f4791..7f6ed3b 100644 --- a/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatMessage.java +++ b/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatMessage.java @@ -21,11 +21,7 @@ public class LazychatMessage extends Post { joinColumns = {@JoinColumn(name = "referrer")}, inverseJoinColumns = {@JoinColumn(name = "referee")}) @JsonbTransient - public Collection referees; - - @ManyToMany(mappedBy = "referees") - @JsonbTransient - public Collection referrers; + public Collection> referees; @CheckForNull @Override diff --git a/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatMessageText.java b/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatMessageText.java index 1a60877..72bb983 100644 --- a/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatMessageText.java +++ b/src/main/java/eu/mulk/mulkcms2/benki/lazychat/LazychatMessageText.java @@ -1,7 +1,6 @@ package eu.mulk.mulkcms2.benki.lazychat; import eu.mulk.mulkcms2.benki.posts.PostText; -import eu.mulk.mulkcms2.common.markdown.MarkdownConverter; import javax.annotation.CheckForNull; import javax.json.bind.annotation.JsonbTransient; import javax.persistence.Column; @@ -19,10 +18,7 @@ public class LazychatMessageText extends PostText { @CheckForNull @Override @JsonbTransient - protected String computeDescriptionHtml() { - if (content == null) { - return null; - } - return new MarkdownConverter().htmlify(content); + protected String getDescriptionMarkup() { + return content; } } diff --git a/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java b/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java index 2c56285..d3e7712 100644 --- a/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java +++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java @@ -109,6 +109,10 @@ public abstract class Post> extends PanacheEntityBase { @JsonbTransient public Set targets; + @ManyToMany(mappedBy = "referees") + @JsonbTransient + public Collection referrers; + @OneToMany( mappedBy = "post", fetch = FetchType.LAZY, @@ -389,6 +393,16 @@ public abstract class Post> extends PanacheEntityBase { } } + public Collection getComments() { + return referrers.stream() + .filter(l -> l.scope == Scope.comment) + .sorted( + Comparator.comparing( + (LazychatMessage l) -> Objects.requireNonNullElse(l.date, OffsetDateTime.MIN)) + .reversed()) + .toList(); + } + public enum Visibility { PUBLIC, SEMIPRIVATE, diff --git a/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java b/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java index 24a564b..5a38262 100644 --- a/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java +++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java @@ -4,6 +4,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static javax.ws.rs.core.MediaType.APPLICATION_ATOM_XML; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static javax.ws.rs.core.MediaType.TEXT_HTML; +import static javax.ws.rs.core.MediaType.TEXT_PLAIN; import com.blazebit.persistence.CriteriaBuilderFactory; import com.rometools.rome.feed.atom.Content; @@ -15,8 +16,10 @@ import com.rometools.rome.io.FeedException; import com.rometools.rome.io.WireFeedOutput; import eu.mulk.mulkcms2.benki.accesscontrol.PageKey; import eu.mulk.mulkcms2.benki.accesscontrol.Role; +import eu.mulk.mulkcms2.benki.lazychat.LazychatMessage; import eu.mulk.mulkcms2.benki.login.LoginRoles; import eu.mulk.mulkcms2.benki.posts.Post.PostPage; +import eu.mulk.mulkcms2.benki.posts.Post.Scope; import eu.mulk.mulkcms2.benki.users.User; import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateExtension; @@ -25,6 +28,7 @@ import io.quarkus.security.identity.SecurityIdentity; import java.math.BigInteger; import java.net.URI; import java.net.URLEncoder; +import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.time.Instant; @@ -49,14 +53,20 @@ import javax.json.spi.JsonProvider; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.transaction.Transactional; +import javax.validation.constraints.NotEmpty; import javax.ws.rs.BadRequestException; import javax.ws.rs.ForbiddenException; +import javax.ws.rs.FormParam; import javax.ws.rs.GET; +import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.hibernate.Session; @@ -75,6 +85,8 @@ public abstract class PostResource { private static final DateTimeFormatter humanDateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG); + private static final String hashcashDigestAlgorithm = "SHA-256"; + private static final int pageKeyBytes = 32; private static final int AUTOTITLE_WORDS = 10; @@ -283,6 +295,48 @@ public abstract class PostResource { return makeFeed(pageKey, ownerName, owner); } + @POST + @Produces(TEXT_PLAIN) + @Path("{id}/comments") + @Transactional + public Response postComment( + @PathParam("id") int postId, + @FormParam("message") @NotEmpty String message, + @FormParam("hashcash-salt") long hashcashSalt) + throws NoSuchAlgorithmException { + var hashcashDigest = MessageDigest.getInstance(hashcashDigestAlgorithm); + hashcashDigest.update("Hashcash-Salt: ".getBytes(UTF_8)); + hashcashDigest.update(String.valueOf(hashcashSalt).getBytes(UTF_8)); + hashcashDigest.update("\n\n".getBytes(UTF_8)); + + for (byte b : message.getBytes(UTF_8)) { + if (b == '\r') { + // Skip CR characters. The JavaScript side does not include them in its computation. + continue; + } + hashcashDigest.update(b); + } + var hashcash = hashcashDigest.digest(); + + if (hashcash[0] != 0 || hashcash[1] != 0) { + throw new BadRequestException( + "invalid hashcash", + Response.status(Status.BAD_REQUEST).entity("invalid hashcash").build()); + } + + Post post = Post.findById(postId); + + var comment = new LazychatMessage(); + comment.date = OffsetDateTime.now(); + comment.scope = Scope.comment; + comment.referees = List.of(post); + comment.setContent(message); + assignPostTargets(post.getVisibility(), post.owner, comment); + comment.persist(); + + return Response.seeOther(UriBuilder.fromUri("/posts/{id}").build(postId)).build(); + } + private String makeFeed( @CheckForNull BigInteger pageKey, @CheckForNull String ownerName, @CheckForNull User owner) throws FeedException { diff --git a/src/main/java/eu/mulk/mulkcms2/benki/posts/PostText.java b/src/main/java/eu/mulk/mulkcms2/benki/posts/PostText.java index 11ac98a..80971b1 100644 --- a/src/main/java/eu/mulk/mulkcms2/benki/posts/PostText.java +++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/PostText.java @@ -1,6 +1,9 @@ package eu.mulk.mulkcms2.benki.posts; import com.vladmihalcea.hibernate.type.search.PostgreSQLTSVectorType; +import eu.mulk.mulkcms2.benki.posts.Post.Scope; +import eu.mulk.mulkcms2.common.markdown.MarkdownConverter; +import eu.mulk.mulkcms2.common.markdown.MarkdownConverter.Mode; import io.quarkus.hibernate.orm.panache.PanacheEntityBase; import javax.annotation.CheckForNull; import javax.json.bind.annotation.JsonbTransient; @@ -69,5 +72,15 @@ public abstract class PostText> extends PanacheEntity } @CheckForNull - protected abstract String computeDescriptionHtml(); + protected abstract String getDescriptionMarkup(); + + @CheckForNull + private String computeDescriptionHtml() { + var markup = getDescriptionMarkup(); + if (markup == null) { + return null; + } + return new MarkdownConverter(post.scope == Scope.top_level ? Mode.POST : Mode.COMMENT) + .htmlify(markup); + } } diff --git a/src/main/java/eu/mulk/mulkcms2/common/markdown/MarkdownConverter.java b/src/main/java/eu/mulk/mulkcms2/common/markdown/MarkdownConverter.java index 859fd71..2a144c5 100644 --- a/src/main/java/eu/mulk/mulkcms2/common/markdown/MarkdownConverter.java +++ b/src/main/java/eu/mulk/mulkcms2/common/markdown/MarkdownConverter.java @@ -11,18 +11,22 @@ import com.vladsch.flexmark.html.HtmlRenderer; import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.util.data.MutableDataSet; import java.util.Arrays; -import javax.enterprise.context.ApplicationScoped; import org.jsoup.Jsoup; import org.jsoup.safety.Cleaner; import org.jsoup.safety.Safelist; -@ApplicationScoped public class MarkdownConverter { + public enum Mode { + POST, + COMMENT, + } + private final Parser parser; private final HtmlRenderer renderer; + private final Mode mode; - public MarkdownConverter() { + public MarkdownConverter(Mode mode) { var options = new MutableDataSet(); options.set( Parser.EXTENSIONS, @@ -42,6 +46,7 @@ public class MarkdownConverter { options.set(TypographicExtension.ENABLE_QUOTES, true); options.set(FootnoteExtension.FOOTNOTE_BACK_REF_STRING, ""); + this.mode = mode; this.parser = Parser.builder(options).build(); this.renderer = HtmlRenderer.builder(options).build(); } @@ -49,14 +54,42 @@ public class MarkdownConverter { public String htmlify(String markdown) { var parsedDocument = parser.parse(markdown); var uncleanHtml = renderer.render(parsedDocument); - var cleaner = - new Cleaner( - Safelist.relaxed() - .addTags("abbr", "acronym") - .addAttributes("abbr", "title") - .addAttributes("acronym", "title")); + var cleaner = makeCleaner(); var cleanedDocument = cleaner.clean(Jsoup.parseBodyFragment(uncleanHtml)); cleanedDocument.select("table").addClass("pure-table").addClass("pure-table-horizontal"); return cleanedDocument.body().html(); } + + private Cleaner makeCleaner() { + var safelist = + switch (mode) { + case POST -> Safelist.relaxed() + .addTags("abbr", "acronym") + .addAttributes("abbr", "title") + .addAttributes("acronym", "title"); + case COMMENT -> Safelist.simpleText() + .addTags( + "p", + "blockquote", + "cite", + "code", + "pre", + "dd", + "dl", + "dt", + "s", + "sub", + "sup", + "ol", + "ul", + "li", + "abbr", + "acronym", + "ins", + "del") + .addAttributes("abbr", "title") + .addAttributes("acronym", "title"); + }; + return new Cleaner(safelist); + } } -- cgit v1.2.3