package nl.vpro.magnolia.module.vproforum.model; import info.magnolia.cms.core.Content; import info.magnolia.cms.core.SystemProperty; import info.magnolia.cms.core.search.Query; import info.magnolia.cms.core.search.QueryManager; import info.magnolia.cms.security.SecurityUtil; import info.magnolia.cms.util.NodeDataUtil; import info.magnolia.context.MgnlContext; import info.magnolia.module.commenting.CommentingModule; import info.magnolia.module.forum.*; import info.magnolia.module.forum.frontend.action.ThreadView; import info.magnolia.module.templating.RenderableDefinition; import info.magnolia.module.templating.RenderingModel; import net.sf.akismet.Akismet; import nl.vpro.magnolia.module.vproforum.setup.VproForumModule; import org.apache.commons.lang.StringUtils; import javax.jcr.RepositoryException; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; /** * VPRO's version of info.magnolia.module.commenting.frontend.action.PageComments. * Look for 'vpro' to see what we changed to store extra request info in the forum node. */ @SuppressWarnings({"JavaDoc", "UnusedDeclaration"}) public class PageComments extends ThreadView { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(PageComments.class); private static final String HEADER_X_FORWARDED_FOR = "X-FORWARDED-FOR"; private boolean enableSpamFiltering = true; public PageComments(Content content, RenderableDefinition renderable, RenderingModel parent, ForumManager forumManager, ForumConfiguration config) { super(content, renderable, parent, forumManager, config); if (SystemProperty.hasProperty("enable.spam.filtering")) { enableSpamFiltering = SystemProperty.getBooleanProperty("enable.spam.filtering"); } } public PageComments(Content content, RenderableDefinition renderable, RenderingModel parent) { super(content, renderable, parent); } public String execute() { if (!CommentingModule.isLicenseValid()) { log.warn("Detected license is not valid for commenting. Commenting disabled."); return "error"; } try { setupThreadIdFromContent(); final String messageTitle = param("messageTitle"); String messageText = param("messageText"); final String inReplyTo = param("inReplyTo"); if ("null".equals(threadId)) { threadId = null; } /* this happens when: - the form was committed on a page with no comments yet. - a page is rendered with no comments yet. the latter may occur quite often, so it's not bad to step out quickly, as both form processing and comments page generation is not necessary. but it would be nice to make a distinction between the two cases, because the latter does not need special treatment. You can distinguish between the two cases by checking for the request param 'command'. */ if (noFormToProcessAndNoCommentsToShow(messageText)) { return exit(); } if (!StringUtils.isBlank(messageText) && !isSpam(messageText)) { Content message = createNewMessageContent(messageTitle, messageText, inReplyTo); if (message != null) { setValidated(message); storeExtraFormfieldsAsProperties(message); } } } catch (RepositoryException e) { throw new RuntimeException(e); } return super.execute(); } private String exit() { setMessages(new PagedResult(new ArrayList(), 0)); threadId = "null"; return "success"; } private String param(String paramName) { final String value = MgnlContext.getWebContext().getRequest().getParameter(paramName); return StringUtils.stripToNull(value); } protected void setupThreadIdFromContent() throws RepositoryException { Content page = getRoot().getContent(); QueryManager qman = MgnlContext.getInstance().getQueryManager(ForumUtil.getInstance().getConfig().getRepository()); Query query = qman.createQuery("select * from mgnl:thread where reference = '" + page.getUUID() + "'", Query.SQL); Collection res = query.execute().getContent(DefaultForumManager.THREAD_NODETYPE); if (!res.isEmpty()) { // get the first thread that is referencing this page threadId = res.iterator().next().getUUID(); } if (res.size() > 1) { log.warn("Expected only one thread reference for {}, but found {}", page.getHandle(), "" + res.size()); } } private boolean noFormToProcessAndNoCommentsToShow(String messageText) { return threadId == null && messageText == null; } @SuppressWarnings({"unchecked"}) private void storeExtraFormfieldsAsProperties(Content message) throws RepositoryException { // store extra properties defined in page comment form. Content props = message.getContent("properties"); Enumeration names = MgnlContext.getWebContext().getRequest().getParameterNames(); while (names.hasMoreElements()) { String paramName = names.nextElement(); if ("threadId".equals(paramName) || "messageTitle".equals(paramName) || "messageText".equals(paramName) || "inReplyTo".equals(paramName) || "targetPage".equals(paramName)) { // skip params already used/saved elsewhere continue; } String val = param(paramName); if (val != null) { props.setNodeData(paramName, val); } } props.save(); } private void setValidated(Content message) throws RepositoryException { // added by vpro: was checked for spam, so set validated as true NodeDataUtil.getOrCreateAndSet(message, "validated", true); message.save(); } private Content createNewMessageContent(String messageTitle, String messageText, String inReplyTo) throws RepositoryException { Content message; final String userId = MgnlContext.getUser().getName(); if (threadId == null) { resolveForumId(); // page might not have title specified ... use page name as a title in that case String newForumThreadTitle = StringUtils.defaultIfEmpty(getRoot().getContent().getTitle(), getRoot().getContent().getName()); thread = forumManager.createThread(forumId, newForumThreadTitle, messageTitle, messageText, userId, SecurityUtil.isAnonymous()); threadId = thread.getUUID(); thread.setNodeData("reference", getRoot().getContent().getUUID()); thread.save(); message = thread.getNodeData("firstMessage").getReferencedContent(); } else { message = forumManager.replyToThread(threadId, inReplyTo, messageTitle, messageText, userId, SecurityUtil.isAnonymous()); } return message; } private void resolveForumId() throws RepositoryException, IllegalStateException { setupForumIdFromContent(); if (forumId == null) { resolveForumFromParagraphDefenition(); } if (forumId == null) { throw new IllegalStateException("No forum id given"); } } private void resolveForumFromParagraphDefenition() throws RepositoryException { String forumName = (String) getDefinition().getParameters().get("forumName"); if (forumName == null) { throw new RuntimeException("Define /modules/yourModule/paragraphs/" + getDefinition().getName() + "/parameters/forumName to specify default forum for comments"); } forumId = forumManager.getForumId(forumName); } // [added by vpro] /** * Get the clients ipaddress from the request. * * @param request * @return */ private String getClientIp(HttpServletRequest request) { String clientIp = request.getRemoteAddr(); // look for ipaddress in X-Forwarded-For header String xForwardedFor = request.getHeader(HEADER_X_FORWARDED_FOR); if (StringUtils.isNotEmpty(xForwardedFor)) { String[] split = StringUtils.split(xForwardedFor, ","); if (split.length >= 1) { clientIp = split[0]; } } return clientIp; } // [added by vpro] /** * Gets the servername from an url. E.g. "http://blabla.com/abc -> http://blabla.com" * * @param requestUrl * @return */ private String getServerName(String requestUrl) { int thirdSlashPosition = requestUrl.indexOf("/", "https://".length()); if (thirdSlashPosition == -1) { thirdSlashPosition = requestUrl.length(); } return StringUtils.substring(requestUrl, 0, thirdSlashPosition); } /** * Checks if current user is allowed to post on the current thread. */ public boolean isAllowedToPost() { return getThread() == null || super.isAllowedToPost(); } /** * Check the contents of a string for spam. Added by vpro. * * @param message * @return */ private boolean isSpam(String message) { if (enableSpamFiltering) { // get information from request, use defaults if not known String requestUrl = MgnlContext.getWebContext().getRequest().getRequestURL().toString(); String commentHostUrl = getServerName(requestUrl); if (StringUtils.isBlank(commentHostUrl)) { commentHostUrl = VproForumModule.getInstance().getAkismetDefaultServername(); } String remoteAddr = getClientIp(MgnlContext.getWebContext().getRequest()); if (StringUtils.isBlank(remoteAddr)) { remoteAddr = VproForumModule.getInstance().getAkismetDefaultIpaddress(); } // verify Akismet key Akismet akismet = new Akismet(VproForumModule.getInstance().getAkismetKey(), commentHostUrl); boolean verifyAPIKey = akismet.verifyAPIKey(); log.debug("Verify akismet key: " + verifyAPIKey); // ask Akismet if this is spam boolean isSpam = akismet.commentCheck(remoteAddr, VproForumModule.getInstance().getAkismetUserAgent(), null, null, null, null, null, null, message, null); log.debug("Is this (" + message + ") spam: " + isSpam); return isSpam; } else { log.warn("Spam filtering disabled!! Development only"); return false; } } }