/* * Copyright (C) 2008-2019, Juick * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package com.juick.www.controllers; import com.juick.model.Message; import com.juick.model.Tag; import com.juick.model.User; import com.juick.util.formatters.PlainTextFormatter; import com.juick.util.HttpForbiddenException; import com.juick.util.HttpNotFoundException; import com.juick.util.WebUtils; import com.juick.www.WebApp; import com.juick.service.*; import com.juick.service.security.annotation.Visitor; import com.juick.util.MessageUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.text.StringEscapeUtils; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.*; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; /** * * @author Ugnich Anton */ @Controller public class Site { @Inject private UserService userService; @Inject private TagService tagService; @Inject private MessagesService messagesService; @Inject private PMQueriesService pmQueriesService; @Inject private CrosspostService crosspostService; @Inject private WebApp webApp; private void fillUserModel(ModelMap model, User user, User visitor) { user.setAvatar(webApp.getAvatarWebPath(user)); model.addAttribute("user", user); model.addAttribute("isSubscribed", userService.isSubscribed(visitor.getUid(), user.getUid())); model.addAttribute("isInBL", userService.isInBL(visitor.getUid(), user.getUid())); model.addAttribute("isInBLAny", userService.isInBLAny(user.getUid(), visitor.getUid())); model.addAttribute("statsIRead", userService.getUserFriends(user.getUid()).size()); model.addAttribute("statsMyReaders", userService.getStatsMyReaders(user.getUid())); model.addAttribute("statsMyBL", userService.getUserBLUsers(user.getUid()).size()); model.addAttribute("statsMessages", userService.getStatsMessages(user.getUid())); model.addAttribute("statsReplies", userService.getStatsReplies(user.getUid())); model.addAttribute("iread", userService.getUserReadLeastPopular(user.getUid(), 8)); model.addAttribute("tagStats", tagService.getUserTagStats(user.getUid()) .stream().sorted((e1, e2) -> Integer.compare(e2.getUsageCount(), e1.getUsageCount())).limit(20).map(t -> t.getTag().getName()).collect(Collectors.toList())); } @GetMapping("/") protected String doGet( @Visitor User visitor, @RequestParam(required = false) String tag, @RequestParam(name = "show", required = false) String paramShow, @RequestParam(name = "search", required = false) String paramSearch, @RequestParam(name = "before", required = false, defaultValue = "0") Integer paramBefore, @RequestParam(name = "to", required = false, defaultValue = "0") Long paramTo, @RequestParam(name = "page", required = false, defaultValue = "0") Integer page, ModelMap model) { if (tag != null) { return "redirect:/tag/" + URLEncoder.encode(tag, StandardCharsets.UTF_8); } visitor.setAvatar(webApp.getAvatarWebPath(visitor)); if (paramSearch != null && paramSearch.length() > 64) { paramSearch = null; } model.addAttribute("discover", false); String title; List mids; if (paramSearch != null) { title = "Поиск: " + StringEscapeUtils.escapeHtml4(paramSearch); mids = messagesService.getSearch(visitor, WebUtils.encodeSphinx(paramSearch), page); } else if (paramShow == null) { title = "Обсуждения"; mids = messagesService.getDiscussions(visitor.getUid(), paramTo); } else if (paramShow.equals("top")) { title = "Популярные"; mids = messagesService.getPopular(visitor.getUid(), paramBefore); model.addAttribute("discover", true); } else if (paramShow.equals("my") && !visitor.isAnonymous()) { title = "Моя лента"; mids = messagesService.getMyFeed(visitor.getUid(), paramBefore, true); } else if (paramShow.equals("private") && !visitor.isAnonymous()) { title = "Приватные"; mids = messagesService.getPrivate(visitor.getUid(), paramBefore); } else if (paramShow.equals("discuss")) { return "redirect:/"; } else if (paramShow.equals("recommended") && !visitor.isAnonymous()) { title = "Рекомендации"; mids = messagesService.getRecommended(visitor.getUid(), paramBefore); } else if (paramShow.equals("photos")) { title = "Фотографии"; mids = messagesService.getPhotos(visitor.getUid(), paramBefore); model.addAttribute("discover", true); } else if (paramShow.equals("all")) { title = "Все сообщения"; mids = messagesService.getAll(visitor.getUid(), paramBefore); model.addAttribute("discover", true); } else { throw new HttpNotFoundException(); } String head = "\n"; if (paramBefore > 0 || paramShow != null) { head = ""; } model.addAttribute("title", title); model.addAttribute("headers", head); model.addAttribute("visitor", visitor); model.addAttribute("noindex", !(paramShow == null && paramBefore == 0)); List msgs = messagesService.getMessages(visitor, mids); msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser()))); if (!visitor.isAnonymous()) { fillUserModel(model, visitor, visitor); List unread = messagesService.getUnread(visitor); visitor.setUnreadCount(unread.size()); List blUIDs = userService.checkBL(visitor.getUid(), msgs.stream().map(m -> m.getUser().getUid()).collect(Collectors.toList())); msgs.forEach(m -> m.ReadOnly |= blUIDs.contains(m.getUser().getUid())); } model.addAttribute("msgs", msgs); model.addAttribute("tags", tagService.getPopularTags()); model.addAttribute("headers", head); if (mids.size() >= 20) { String nextpage = paramSearch != null ? String.format("?page=%d", page + 1) : (paramShow == null) ? "?to=" + msgs.get(msgs.size() - 1).getUpdated().toEpochMilli() : "?before=" + mids.get(mids.size() - 1); if (paramShow != null) { nextpage += "&show=" + paramShow; } if (paramSearch != null) { nextpage += "&search=" + URLEncoder.encode(paramSearch, StandardCharsets.UTF_8); } model.addAttribute("nextpage", nextpage); } return "views/index"; } @GetMapping(path = "/{uname}/", headers = "Connection!=Upgrade") protected String doGetBlog( @Visitor User visitor, @RequestParam(required = false, name = "show") String paramShow, @RequestParam(required = false, name = "tag") String paramTagStr, @RequestParam(required = false, name = "search") String paramSearch, @RequestParam(required = false, name = "page", defaultValue = "0") Integer page, @PathVariable String uname, @RequestParam(required = false, defaultValue = "0") Integer before, @CookieValue(name = "sape_cookie", required = false, defaultValue = StringUtils.EMPTY) String sapeCookie, ModelMap model) { User user = userService.getUserByName(uname); if (user.isBanned() || user.isAnonymous()) { throw new HttpNotFoundException(); } visitor.setAvatar(webApp.getAvatarWebPath(visitor)); List mids; Tag paramTag = null; if (paramTagStr != null) { if (paramTagStr.length() < 64) { paramTag = tagService.getTag(paramTagStr, false); } if (paramTag == null) { throw new HttpNotFoundException(); } else if (!paramTag.getName().equals(paramTagStr)) { String url = user.getName() + "/?tag=" + URLEncoder.encode(paramTag.getName(), StandardCharsets.UTF_8); return "redirect:/" + url; } } if (paramSearch != null && paramSearch.length() > 64) { paramSearch = null; } int privacy = 0; if (!visitor.isAnonymous()) { if (user.getUid() == visitor.getUid() || visitor.getUid() == 1) { privacy = -3; } else if (userService.isInWL(user.getUid(), visitor.getUid())) { privacy = -2; } } String title; if (paramShow == null) { if (paramTag != null) { title = "Блог " + user.getName() + ": *" + StringEscapeUtils.escapeHtml4(paramTag.getName()); mids = messagesService.getUserTag(user.getUid(), paramTag.TID, privacy, before); } else if (paramSearch != null) { title = "Блог " + user.getName() + ": " + StringEscapeUtils.escapeHtml4(paramSearch); mids = messagesService.getUserSearch(visitor, user.getUid(), WebUtils.encodeSphinx(paramSearch), privacy, page); } else { title = "Блог " + user.getName(); mids = messagesService.getUserBlog(user.getUid(), privacy, before); } } else if (paramShow.equals("recomm")) { title = "Рекомендации " + user.getName(); mids = messagesService.getUserRecommendations(user.getUid(), before); } else if (paramShow.equals("photos")) { title = "Фотографии " + user.getName(); mids = messagesService.getUserPhotos(user.getUid(), privacy, before); } else { throw new HttpNotFoundException(); } String head = ""; head += "\n"; if (paramTag != null && tagService.getTagNoIndex(paramTag.TID)) { head += ""; } else if (before > 0 || paramShow != null) { head += ""; } model.addAttribute("pageUrl", "http://juick.com/" + user.getName()); model.addAttribute("title", title); model.addAttribute("headers", head); model.addAttribute("visitor", visitor); model.addAttribute("noindex", paramShow == null && before == 0); fillUserModel(model, user, visitor); model.addAttribute("paramTag", paramTag); List msgs = messagesService.getMessages(visitor, mids); msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser()))); if (!visitor.isAnonymous()) { List unread = messagesService.getUnread(visitor); visitor.setUnreadCount(unread.size()); List blUIDs = userService.checkBL(visitor.getUid(), msgs.stream().map(m -> m.getUser().getUid()).collect(Collectors.toList())); msgs.forEach(m -> m.ReadOnly |= blUIDs.contains(m.getUser().getUid())); } model.addAttribute("msgs", msgs); model.addAttribute("headers", head); if (mids.size() >= 20) { String nextpage = paramSearch != null ? String.format("?page=%d", page + 1) : "?before=" + mids.get(mids.size() - 1); if (paramShow != null) { nextpage += "&show=" + paramShow; } if (paramSearch != null) { nextpage += "&search=" + URLEncoder.encode(paramSearch, StandardCharsets.UTF_8); } if (paramTag != null) { nextpage += "&tag=" + URLEncoder.encode(paramTag.getName(), StandardCharsets.UTF_8); } model.addAttribute("nextpage", nextpage); } return "views/blog"; } @GetMapping("/{uname}/tags") protected String doGetTags( @Visitor User visitor, @PathVariable String uname, ModelMap model) { User user = userService.getUserByName(uname); if (visitor.isBanned()) { throw new HttpNotFoundException(); } visitor.setAvatar(webApp.getAvatarWebPath(visitor)); model.addAttribute("title", "Теги " + user.getName()); model.addAttribute("headers", ""); model.addAttribute("visitor", visitor); fillUserModel(model, user, visitor); model.addAttribute("tags", tagService.getUserTagStats(user.getUid()).stream() .sorted((e1, e2) -> Integer.compare(e2.getUsageCount(), e1.getUsageCount())).map(t -> t.getTag().getName()).collect(Collectors.toList())); return "views/blog_tags"; } @GetMapping("/{uname}/friends") protected String doGetFriends( @Visitor User visitor, @PathVariable String uname, ModelMap model) { User user = userService.getUserByName(uname); if (visitor.isBanned()) { throw new HttpNotFoundException(); } visitor.setAvatar(webApp.getAvatarWebPath(visitor)); model.addAttribute("title", "Подписки " + user.getName()); model.addAttribute("headers", ""); model.addAttribute("visitor", visitor); fillUserModel(model, user, visitor); model.addAttribute("users", userService.getUserFriends(user.getUid())); return "views/users"; } @GetMapping("/{uname}/readers") protected String doGetReaders( @Visitor User visitor, @PathVariable String uname, ModelMap model) { User user = userService.getUserByName(uname); visitor.setAvatar(webApp.getAvatarWebPath(visitor)); model.addAttribute("title", "Читатели " + user.getName()); model.addAttribute("headers", ""); model.addAttribute("visitor", visitor); fillUserModel(model, user, visitor); model.addAttribute("users", userService.getUserReaders(user.getUid())); return "views/users"; } @GetMapping("/{uname}/bl") protected String doGetBL( @Visitor User visitor, @PathVariable String uname, ModelMap model) { User user = userService.getUserByName(uname); if (visitor.getUid() != user.getUid()) { throw new HttpForbiddenException(); } visitor.setAvatar(webApp.getAvatarWebPath(visitor)); model.addAttribute("title", "Черный список " + user.getName()); model.addAttribute("headers", ""); model.addAttribute("visitor", visitor); fillUserModel(model, user, visitor); model.addAttribute("users", userService.getUserBLUsers(user.getUid())); return "views/users"; } @GetMapping("/tag/{tagName}") protected String tagAction( @Visitor User visitor, HttpServletRequest request, @PathVariable String tagName, @RequestParam(required = false, defaultValue = "0") int before, ModelMap model) { visitor.setAvatar(webApp.getAvatarWebPath(visitor)); String paramTagStr = StringEscapeUtils.unescapeHtml4(tagName); Tag paramTag = tagService.getTag(paramTagStr, false); if (paramTag == null) { throw new HttpNotFoundException(); } else if (paramTag.SynonymID > 0 && paramTag.TID != paramTag.SynonymID) { Tag synTag = tagService.getTag(paramTag.SynonymID); String url = "/tag/" + URLEncoder.encode(StringEscapeUtils.escapeHtml4(synTag.getName()), StandardCharsets.UTF_8); if (request.getQueryString() != null) { url += "?" + request.getQueryString(); } return "redirect:" + url; } else if (!paramTag.getName().equals(paramTagStr)) { String url = "/tag/" + URLEncoder.encode(StringEscapeUtils.escapeHtml4(paramTag.getName()), StandardCharsets.UTF_8); if (request.getQueryString() != null) { url += "?" + request.getQueryString(); } return "redirect:" + url; } String title = "*" + StringEscapeUtils.escapeHtml4(paramTag.getName()); model.addAttribute("title", title); List mids = messagesService.getTag(paramTag.TID, visitor.getUid(), before, (visitor.isAnonymous()) ? 40 : 20); List msgs = messagesService.getMessages(visitor, mids); msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser()))); if (!visitor.isAnonymous()) { List unread = messagesService.getUnread(visitor); visitor.setUnreadCount(unread.size()); List blUIDs = userService.checkBL( visitor.getUid(), msgs.stream().map(m -> m.getUser().getUid()).collect(Collectors.toList()) ); msgs.forEach(m -> m.ReadOnly |= blUIDs.contains(m.getUser().getUid())); fillUserModel(model, visitor, visitor); } String head = StringUtils.EMPTY; if (tagService.getTagNoIndex(paramTag.TID)) { head = ""; } else if (before > 0 || mids.size() < 5) { head = ""; } model.addAttribute("headers", head); model.addAttribute("visitor", visitor); model.addAttribute("tag", paramTag); model.addAttribute("title", title); model.addAttribute("msgs", msgs); model.addAttribute("tags", tagService.getPopularTags()); model.addAttribute("noindex", before > 0); model.addAttribute("isSubscribed", tagService.isSubscribed(visitor, paramTag)); model.addAttribute("isInBL", tagService.isInBL(visitor, paramTag)); if (mids.size() >= 20) { String nextpage = "/tag/" + URLEncoder.encode(paramTag.getName(), StandardCharsets.UTF_8) + "?before=" + mids.get(mids.size() - 1); model.addAttribute("nextpage", nextpage); } return "views/index"; } @GetMapping("/pm/inbox") protected String doGetInbox(@Visitor User visitor, ModelMap model) { if (visitor.isAnonymous()) { return "redirect:/login"; } visitor.setAvatar(webApp.getAvatarWebPath(visitor)); String title = "PM: Inbox"; List msgs = pmQueriesService.getLastPMInbox(visitor.getUid()); msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser()))); fillUserModel(model, visitor, visitor); model.addAttribute("title", title); model.addAttribute("visitor", visitor); model.addAttribute("msgs", msgs); model.addAttribute("tags", tagService.getPopularTags()); return "views/pm_inbox"; } @GetMapping("/pm/sent") protected String doGetSent( @Visitor User visitor, @RequestParam(required = false) String uname, ModelMap model) { if (visitor.isAnonymous()) { return "redirect:/login"; } visitor.setAvatar(webApp.getAvatarWebPath(visitor)); String title = "PM: Sent"; List msgs = pmQueriesService.getLastPMSent(visitor.getUid()); msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarWebPath(m.getUser()))); if (WebUtils.isNotUserName(uname)) { uname = StringUtils.EMPTY; } fillUserModel(model, visitor, visitor); model.addAttribute("title", title); model.addAttribute("visitor", visitor); model.addAttribute("msgs", msgs); model.addAttribute("tags", tagService.getPopularTags()); model.addAttribute("uname", uname); return "views/pm_sent"; } @GetMapping(value = "/{uname}/{mid}", produces = MediaType.TEXT_HTML_VALUE) protected String threadAction( @Visitor User visitor, ModelMap model, @PathVariable String uname, @PathVariable int mid, @CookieValue(name = "sape_cookie", required = false, defaultValue = StringUtils.EMPTY) String sapeCookie) { if (!messagesService.canViewThread(mid, visitor.getUid())) { throw new HttpForbiddenException(); } visitor.setAvatar(webApp.getAvatarWebPath(visitor)); Optional message = messagesService.getMessage(mid); if (message.isEmpty()) { throw new HttpNotFoundException(); } Message msg = message.get(); User user = userService.getUserByName(uname); if (user.isAnonymous() || !msg.getUser().equals(user)) { return String.format("redirect:/%s/%d", msg.getUser().getName(), mid); } msg.VisitorCanComment = !visitor.isAnonymous(); msg.getUser().setAvatar(webApp.getAvatarWebPath(msg.getUser())); List replies = messagesService.getReplies(visitor, msg.getMid()); // this should be after getReplies to mark thread as read fillUserModel(model, user, visitor); if (!visitor.isAnonymous()) { List unread = messagesService.getUnread(visitor); visitor.setUnreadCount(unread.size()); boolean isMsgAuthor = visitor.getUid() == msg.getUser().getUid(); boolean isInBL = userService.isInBLAny(msg.getUser().getUid(), visitor.getUid()); msg.VisitorCanComment = isMsgAuthor || !(msg.ReadOnly || isInBL); } model.addAttribute("msg", msg); String title = msg.getUser().getName() + ": " + MessageUtils.getTagsString(msg); model.addAttribute("title", title); model.addAttribute("visitor", visitor); String headers = ""; String pageUrl = "https://juick.com/" + msg.getUser().getName() + "/" + msg.getMid(); if (msg.Hidden) { headers += ""; } String cardType = StringUtils.isNotEmpty(msg.getAttachmentType()) ? "summary_large_image" : "summary"; if (StringUtils.isNotEmpty(msg.getAttachmentType())) { // additional check in case of broken images if (msg.getAttachment() != null) { String msgImage = msg.getAttachment().getMedium().getUrl(); headers += ""; } } else { String msgImage = webApp.getAvatarWebPath(msg.getUser()); headers += ""; } model.addAttribute("ogtype", "article"); String cardDescription = StringEscapeUtils.escapeHtml4(PlainTextFormatter.formatTwitterCard(msg)); headers += "\n" + "\n" + "\n" + "\n" + "\n" + "\n"; String twitterName = crosspostService.getTwitterName(msg.getUser().getUid()); if (StringUtils.isNotEmpty(twitterName)) { headers += "\n"; } if (msg.getTags().size() > 0) { headers += "\n"; } model.addAttribute("headers", headers); model.addAttribute("visitorSubscribed", messagesService.isSubscribed(visitor.getUid(), msg.getMid())); model.addAttribute("visitorInBL", userService.isInBL(msg.getUser().getUid(), visitor.getUid())); model.addAttribute("recomm", messagesService.getMessagesRecommendations( Collections.singletonList(msg.getMid())).stream() .map(Pair::getRight).collect(Collectors.toList())); List blUIDs = new ArrayList<>(); for (Message reply : replies) { if (reply.getUser().getUid() != msg.getUser().getUid() && !blUIDs.contains(reply.getUser().getUid())) { blUIDs.add(reply.getUser().getUid()); } reply.VisitorCanComment = !visitor.isAnonymous(); reply.getUser().setAvatar(webApp.getAvatarWebPath(reply.getUser())); if (!visitor.isAnonymous()) { boolean isMsgAuthor = visitor.getUid() == msg.getUser().getUid(); boolean isReplyAuthor = visitor.getUid() == reply.getUser().getUid(); reply.VisitorCanComment = isMsgAuthor || (!msg.ReadOnly && msg.VisitorCanComment && (isReplyAuthor || !userService.isInBLAny(visitor.getUid(), reply.getUser().getUid()))); } } model.addAttribute("replies", replies); return "views/thread"; } @GetMapping("/post") protected String postAction( @Visitor User visitor, @RequestParam(required = false) String body, ModelMap model) { fillUserModel(model, visitor, visitor); visitor.setAvatar(webApp.getAvatarWebPath(visitor)); model.addAttribute("title", "Написать"); model.addAttribute("headers", ""); model.addAttribute("visitor", visitor); if (body == null) { body = StringUtils.EMPTY; } else { if (body.length() > 4096) { body = body.substring(0, 4096); } body = StringEscapeUtils.escapeHtml4(body); } model.addAttribute("body", body); model.addAttribute("visitor", visitor); model.addAttribute("user", visitor); model.addAttribute("tags", tagService.getUserTagStats(visitor.getUid()).stream() .sorted((e1, e2) -> Integer.compare(e2.getUsageCount(), e1.getUsageCount())).map(t -> t.getTag().getName()).collect(Collectors.toList())); return "views/post"; } // when message id is not fit to int @ExceptionHandler(NumberFormatException.class) public ResponseEntity notFoundAction() { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } }