package org.kalipo.service; import org.apache.commons.lang3.StringUtils; import org.kalipo.aop.KalipoExceptionHandler; import org.kalipo.aop.RateLimit; import org.kalipo.config.Constants; import org.kalipo.config.ErrorCode; import org.kalipo.domain.Comment; import org.kalipo.domain.Markup; import org.kalipo.domain.Site; import org.kalipo.domain.Thread; import org.kalipo.repository.*; import org.kalipo.security.Privileges; import org.kalipo.security.SecurityUtils; import org.kalipo.service.util.Asserts; import org.kalipo.service.util.URLNormalizer; import org.kalipo.web.rest.KalipoException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.AsyncResult; import org.springframework.security.web.util.UrlUtils; import org.springframework.stereotype.Service; import javax.annotation.security.RolesAllowed; import javax.inject.Inject; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.util.*; import java.util.concurrent.Future; @SuppressWarnings("unused") @Service @KalipoExceptionHandler public class ThreadService { private final Logger log = LoggerFactory.getLogger(ThreadService.class); @Inject private Environment env; @Inject private ThreadRepository threadRepository; @Inject private SiteRepository siteRepository; @Inject private BanRepository banRepository; @Inject private VoteRepository voteRepository; @Inject private CommentRepository commentRepository; @Inject private UserRepository userRepository; @Inject private PrivilegeRepository privilegeRepository; @Inject private UserService userService; @Inject private NotificationService notificationService; @Inject private MarkupService markupService; @RolesAllowed(Privileges.CREATE_THREAD) @RateLimit public Thread create(Thread thread) throws KalipoException { Asserts.isNotNull(thread, "thread"); Asserts.isNull(thread.getId(), "id"); thread.setStatus(Thread.Status.OPEN); thread.setCommentCount(0); thread.setLikes(0); thread.setDislikes(0); thread.setDisplayName(SecurityUtils.getCurrentLogin()); // siteId if(StringUtils.isBlank(thread.getSiteId())) { Site site = siteRepository.findByName(env.getProperty("siteName")); Asserts.isNotNull(site, "site"); thread.setSiteId(site.getId()); } else { Asserts.isTrue(siteRepository.exists(thread.getSiteId()), "siteId"); } final String currentLogin = SecurityUtils.getCurrentLogin(); thread.setInitiatorId(currentLogin); thread = save(thread); log.info(String.format("User '%s' created thread %s on site %s", currentLogin, thread.getId(), thread.getSiteId())); return thread; } @RolesAllowed(Privileges.CREATE_THREAD) @RateLimit public Thread update(Thread dirty) throws KalipoException { Asserts.isNotNull(dirty, "thread"); Asserts.isNotNull(dirty.getId(), "id"); Thread original = threadRepository.findOne(dirty.getId()); Asserts.isNotNull(original, "thread"); Site site = siteRepository.findOne(original.getSiteId()); Set<String> moderators = site.getModeratorIds(); // Permissions final String currentLogin = SecurityUtils.getCurrentLogin(); boolean isMod = moderators.contains(currentLogin); if (!isMod && !userService.isSuperMod(currentLogin)) { throw new KalipoException(ErrorCode.PERMISSION_DENIED, "You must be mod or supermod"); } // read only values Asserts.nullOrEqual(dirty.getSiteId(), original.getSiteId(), "siteId"); Asserts.nullOrEqual(dirty.getCommentCount(), original.getCommentCount(), "commentCount"); Asserts.nullOrEqual(dirty.getLikes(), original.getLikes(), "likes"); Asserts.nullOrEqual(dirty.getDislikes(), original.getDislikes(), "dislikes"); Asserts.nullOrEqual(dirty.getInitiatorId(), original.getInitiatorId(), "initiatorId"); Asserts.nullOrEqual(dirty.getTags(), original.getTags(), "tags"); // update fields original.setUriHooks(dirty.getUriHooks()); if(dirty.getStatus() != null && dirty.getStatus() != original.getStatus()) { log.info(String.format("User '%s' changes status of thread %s to %s (before %s)", currentLogin, original.getId(), dirty.getStatus(), original.getStatus())); original.setStatus(dirty.getStatus()); } original.setTitle(dirty.getTitle()); original.setBody(dirty.getBody()); return save(original); } @Async public Future<Page<Thread>> getAllWithPages(Integer page) { Sort sort = new Sort( new Sort.Order(Sort.Direction.ASC, Constants.PARAM_CREATED_DATE) ); PageRequest pageable = new PageRequest(page, 20, sort); return new AsyncResult<>(threadRepository.findAll(pageable)); } @Async public Future<Thread> get(String id) throws KalipoException { return new AsyncResult<>(threadRepository.findOne(id)); } @Async public Future<Page<Comment>> getCommentsWithPages(String threadId, Integer page) throws KalipoException { Sort sort = new Sort( new Sort.Order(Sort.Direction.ASC, "fingerprint") ); PageRequest pageable = new PageRequest(page, 60, sort); List<Comment.Status> statuses = Arrays.asList(Comment.Status.APPROVED, Comment.Status.PENDING, Comment.Status.DELETED); return new AsyncResult<>(commentRepository.findByThreadIdAndStatusIn(threadId, statuses, pageable)); } @Async public Future<Page<Comment>> getLatestCommentsWithPages(String id, Integer page) throws KalipoException { Sort sort = new Sort( new Sort.Order(Sort.Direction.DESC, "lastModifiedDate") ); PageRequest pageable = new PageRequest(page, 60, sort); return new AsyncResult<>(commentRepository.findByThreadIdAndStatusIn(id, Collections.singletonList(Comment.Status.APPROVED), pageable)); } public void delete(String id) throws KalipoException { // todo check permissons // threadRepository.delete(id); } // -- // todo move to site // private void validateKLine(Thread dirty, Thread original) throws KalipoException { // final String currentLogin = SecurityUtils.getCurrentLogin(); // final Set<String> orgKLine = original.getBans(); // final Set<String> dirtyKLine = dirty.getBans(); // // if (dirtyKLine == null || (orgKLine.size() == dirtyKLine.size() && orgKLine.containsAll(dirtyKLine))) { // dirty.setBans(orgKLine); // } else { // // validate kLine changes from update // // // original.getBans() - thread.getBans() // Set<String> removed = orgKLine.stream().filter(uid -> dirtyKLine.contains(uid)).collect(Collectors.toSet()); // // // thread.getBans() - original.getBans(); // Set<String> added = dirtyKLine.stream().filter(uid -> original.getBans().contains(uid)).collect(Collectors.toSet()); // // for (String userId : added) { // if (!userRepository.exists(userId)) { // throw new KalipoException(ErrorCode.INVALID_PARAMETER, String.format("User %s does not exist", userId)); // } // // log.info(String.format("%s puts %s on k-Line of thread %s", currentLogin, userId, dirty.getId())); // } // // if (removed != null) { // for (String userId : removed) { // log.info(String.format("%s removes %s from k-Line of thread %s", currentLogin, userId, dirty.getId())); // } // } // } // } private Thread save(Thread thread) throws KalipoException { renderBody(thread); if (StringUtils.isNotBlank(thread.getLink())) { try { URI uri = markupService.resolveRedirects(thread.getLink()); thread.setLink(uri.toASCIIString()); thread.setDomain(uri.getHost()); } catch (URISyntaxException e) { throw new KalipoException(ErrorCode.INVALID_PARAMETER, "link is invalid"); } } // url hooks validation if (thread.getUriHooks() != null && !thread.getUriHooks().isEmpty()) { Asserts.hasPrivilege(Privileges.HOOK_THREAD_TO_URL); Set<String> normedUriHooks = new HashSet<>(); for (String uri : thread.getUriHooks()) { if (!UrlUtils.isAbsoluteUrl(uri)) { throw new KalipoException(ErrorCode.INVALID_PARAMETER, String.format("urlHook %s must be an absolute url", uri)); } // todo set property via env.getProperty("domain") Asserts.isTrue(!StringUtils.containsIgnoreCase(uri, "kalipo.org"), "You cannot hook kalipo"); // -- Assure that hooked urls has not been used anywhere else final String normed; try { normed = URLNormalizer.normalize(uri); normedUriHooks.add(normed); } catch (MalformedURLException e) { throw new KalipoException(ErrorCode.INVALID_PARAMETER, String.format("urlHook %s looks nasty", uri)); } Thread conflictingThread = threadRepository.findByUriHook(normed); if (conflictingThread != null && (thread.getId() == null || !StringUtils.equals(conflictingThread.getId(), thread.getId()))) { throw new KalipoException(ErrorCode.INVALID_PARAMETER, String.format("urlHook %s is already in use in thread %s", normed, thread.getId())); } } thread.setUriHooks(normedUriHooks); } return threadRepository.save(thread); } private void renderBody(Thread thread) { Markup markup = markupService.toHtml(thread.getBody()); thread.setBodyHtml(markup.buffer().toString()); thread.setTags(markup.hashtags()); } }