Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
pvorb committed Jul 10, 2018
1 parent cbe1ed3 commit f2411d6
Show file tree
Hide file tree
Showing 13 changed files with 132 additions and 20 deletions.
6 changes: 6 additions & 0 deletions src/main/java/de/vorb/platon/PlatonApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import java.security.SecureRandom;
import java.time.Clock;

@SpringBootApplication
Expand All @@ -34,4 +35,9 @@ public Clock systemClock() {
return Clock.systemUTC();
}

@Bean
public SecureRandom secureRandom() {
return new SecureRandom();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@
import de.vorb.platon.model.CommentStatus;
import de.vorb.platon.persistence.jooq.tables.pojos.Comment;

import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

@Transactional(propagation = Propagation.MANDATORY)
public interface CommentRepository {

List<Comment> findByThreadId(long threadId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

package de.vorb.platon.persistence;

import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Transactional(propagation = Propagation.MANDATORY)
public interface PropertyRepository {

String findValueByKey(String key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@

import de.vorb.platon.persistence.jooq.tables.pojos.CommentThread;

import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

@Transactional(propagation = Propagation.MANDATORY)
public interface ThreadRepository {

Optional<CommentThread> findById(long id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
Expand All @@ -42,6 +43,7 @@ public class DatabaseSecretKeyProvider implements SecretKeyProvider {
private SecretKey secretKey;

@Override
@Transactional
public SecretKey getSecretKey() {

if (secretKey == null) {
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/de/vorb/platon/view/ByteArrayEqualsMethod.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package de.vorb.platon.view;

import freemarker.template.DefaultArrayAdapter;
import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModelException;

import java.util.Arrays;
import java.util.List;

public enum ByteArrayEqualsMethod implements TemplateMethodModelEx {

INSTANCE;

@Override
public Object exec(List arguments) throws TemplateModelException {
try {
return Arrays.equals(
(byte[]) ((DefaultArrayAdapter) arguments.get(0)).getWrappedObject(),
(byte[]) ((DefaultArrayAdapter) arguments.get(1)).getWrappedObject()
);
} catch (Exception e) {
throw new TemplateModelException(e);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class FreemarkerConfiguration {
@PostConstruct
private void addSharedVariables() {
configuration.setSharedVariable("base64Url", Base64UrlMethod.INSTANCE);
configuration.setSharedVariable("byteArrayEquals", ByteArrayEqualsMethod.INSTANCE);
}

}
2 changes: 2 additions & 0 deletions src/main/java/de/vorb/platon/web/mvc/AtomFeedController.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -40,6 +41,7 @@ public class AtomFeedController {
private URI publicSelfUrl;

@GetMapping(value = "/threads/{threadId}/feed", produces = MediaType.APPLICATION_XML_VALUE)
@Transactional(readOnly = true)
public AtomFeed getAtomFeedForThread(@PathVariable("threadId") long threadId) {

final CommentThread thread = threadRepository.findById(threadId).orElseThrow(RuntimeException::new);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,16 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -38,8 +43,10 @@ public class CommentController {
private final CommentRepository commentRepository;

@GetMapping(value = PATH_SINGLE_THREAD, produces = TEXT_HTML_VALUE)
public ModelAndView findCommentsByThreadUrl(@PathVariable(PATH_VAR_THREAD_ID) long threadId,
HttpServletRequest request) {
@Transactional(readOnly = true)
public ModelAndView findCommentsByThreadUrl(HttpServletRequest request,
@PathVariable(PATH_VAR_THREAD_ID) long threadId,
@CookieValue(value = CommentFormController.AUTHOR_ID_COOKIE, required = false) String authorId) {

final CommentThread thread = threadRepository.findById(threadId)
.orElseThrow(() -> RequestException.withStatus(HttpStatus.NOT_FOUND)
Expand All @@ -50,8 +57,21 @@ public ModelAndView findCommentsByThreadUrl(@PathVariable(PATH_VAR_THREAD_ID) lo
.collect(Collectors.toMap(Comment::getId, Function.identity(), throwingMerger(),
LinkedHashMap::new));

byte[] authorHash = null;
final String authorOrSessionId = authorId != null ? authorId : request.getSession(true).getId();
try {
final MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
authorHash = sha1.digest(authorOrSessionId.getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException e) {
log.warn("SHA-1 not supported");
}
if (authorHash == null) {
authorHash = CommentFormController.EMPTY_STRING_HASH;
}

return new ModelAndView("comments-flat",
ImmutableMap.of("thread", thread, "commentCount", comments.size(), "comments", commentsById));
ImmutableMap.of("thread", thread, "commentCount", comments.size(), "comments", commentsById,
"authorHash", authorHash));
}

private static <T> BinaryOperator<T> throwingMerger() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,41 @@
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.Clock;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.regex.Pattern;

@Slf4j
@Controller
@RequiredArgsConstructor
public class ReplyFormController {
public class CommentFormController {

private static final byte[] EMPTY_STRING_HASH = new byte[20];
static final String AUTHOR_ID_COOKIE = "author_id";
private static final int MAX_COOKIE_AGE = (int) Duration.ofDays(10 * 365).getSeconds();

static final byte[] EMPTY_STRING_HASH = new byte[20];

static {
try {
Expand All @@ -59,10 +69,12 @@ public class ReplyFormController {
private final CommentRepository commentRepository;
private final MarkdownRenderer markdownRenderer;
private final Clock clock;
private final SecureRandom secureRandom;

@GetMapping(value = {"/threads/{threadId}/reply", "/threads/{threadId}/comments/{parentCommentId}/reply"},
produces = MediaType.TEXT_HTML_VALUE)
public String showThreadReplyForm(
@Transactional(readOnly = true)
public String showReplyForm(
@PathVariable(CommentController.PATH_VAR_THREAD_ID) long threadId,
@PathVariable(value = "parentCommentId", required = false) Long parentCommentId,
@ModelAttribute("comment") CommentFormData comment, Model model) {
Expand All @@ -74,39 +86,66 @@ public String showThreadReplyForm(

@PostMapping(value = {"/threads/{threadId}/reply", "/threads/{threadId}/comments/{parentCommentId}/reply"},
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.TEXT_HTML_VALUE)
public String postComment(HttpServletRequest request,
@Transactional
public String postNewComment(HttpServletRequest request, HttpServletResponse response,
@PathVariable(CommentController.PATH_VAR_THREAD_ID) long threadId,
@PathVariable(value = "parentCommentId", required = false) Long parentCommentId,
@CookieValue(value = AUTHOR_ID_COOKIE, required = false) String authorId,
@Valid @ModelAttribute("comment") CommentFormData formData, BindingResult bindingResult, Model model) {

applyCommentFormModel(model, threadId, parentCommentId);

if (formData.isAcceptCookie() && authorId == null) {
final Cookie commentAuthorCookie = new Cookie(AUTHOR_ID_COOKIE, UUID.randomUUID().toString());
commentAuthorCookie.setMaxAge(MAX_COOKIE_AGE);
response.addCookie(commentAuthorCookie);
}

if (bindingResult.hasErrors()) {
return VIEW_NAME;
} else {
final Comment comment = createComment(request, threadId, parentCommentId, formData);
final Comment comment = createComment(request, threadId, parentCommentId, authorId, formData);

if (formData.getAction() == CommentAction.CREATE) {
final Comment storedComment = commentRepository.insert(comment);
return "redirect:" + CommentController.pathSingleThread(threadId) + "#comment-" + storedComment.getId();
} else {
} else if (formData.getAction() == CommentAction.PREVIEW) {
model.addAttribute("previewComment", comment);
return VIEW_NAME;
} else {
throw new RuntimeException();
}
}
}

@GetMapping(value = {"/threads/{threadId}/comments/{commentId}/edit"}, produces = MediaType.TEXT_HTML_VALUE)
@Transactional(readOnly = true)
public String showEditForm(
@PathVariable(CommentController.PATH_VAR_THREAD_ID) long threadId,
@PathVariable(value = "commentId") Long commentId,
@ModelAttribute("comment") CommentFormData comment, Model model) {

final CommentThread thread = threadRepository.findById(threadId).orElseThrow(RuntimeException::new);
final Comment edit = commentRepository.findById(commentId).orElseThrow(RuntimeException::new);

model.addAttribute("thread", thread);
comment.setText(edit.getTextSource());
comment.setAuthor(edit.getAuthor());
comment.setUrl(edit.getUrl());

return VIEW_NAME;
}

private Comment createComment(HttpServletRequest request, long threadId, Long parentCommentId,
CommentFormData formData) {
String authorId, CommentFormData formData) {
byte[] authorHash = null;
if (formData.isAcceptCookie()) {
final String sessionId = request.getSession(true).getId();
try {
final MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
authorHash = sha1.digest(sessionId.getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException e) {
log.warn("SHA-1 not supported");
}

final String authorOrSessionId = authorId != null ? authorId : request.getSession(true).getId();
try {
final MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
authorHash = sha1.digest(authorOrSessionId.getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException e) {
log.warn("SHA-1 not supported");
}
if (authorHash == null) {
authorHash = EMPTY_STRING_HASH;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public String redirectToComments(

@GetMapping(value = CommentController.PATH_LIST_THREADS + '/' + THREAD_ID_REPLACEMENT_TARGET)
@ResponseStatus(HttpStatus.TEMPORARY_REDIRECT)
@Transactional(readOnly = true)
public String redirectToComments(@PathVariable("threadId") long threadId) {

CommentThread thread = threadRepository.findById(threadId).orElseThrow(IndexOutOfBoundsException::new);
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/templates/comment-form.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<#include "snippets/page-comment.ftl"/>

<#macro page_title>
<title>Comments for${thread.title}”</title>
<title>Leave a comment on${thread.title}”</title>
</#macro>

<#macro page_header>
Expand Down
5 changes: 4 additions & 1 deletion src/main/resources/templates/snippets/page-comment.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@
<#if !in_form>
<footer class="mt-3">
<a href="/threads/${thread.id}/comments/${comment.id}/reply" class="mr-3">Reply</a>
<a href="/threads/${thread.id}/comments/${comment.id}/edit">Edit</a>
<#if byteArrayEquals(authorHash, comment.authorHash)>
<a href="/threads/${thread.id}/comments/${comment.id}/edit" class="mr-3">Edit</a>
<a href="/threads/${thread.id}/comments/${comment.id}/delete" class="mr-3">Delete</a>
</#if>
</footer>
</#if>
</div>
Expand Down

0 comments on commit f2411d6

Please sign in to comment.