io.stallion.plugins.flatBlog.comments.CommentsEndpoints.java Source code

Java tutorial

Introduction

Here is the source code for io.stallion.plugins.flatBlog.comments.CommentsEndpoints.java

Source

/*
 * Stallion Flat-file Blog: A simple blog-engine
 *
 * Copyright (C) 2015 - 2016 Stallion Software LLC
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation, either version 2 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 General Public
 * License for more details. You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
 *
 */

package io.stallion.plugins.flatBlog.comments;

import ac.simons.akismet.Akismet;
import ac.simons.akismet.AkismetComment;
import ac.simons.akismet.AkismetException;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import com.mashape.unirest.http.Unirest;
import static io.stallion.utils.Literals.*;

import com.mashape.unirest.http.exceptions.UnirestException;
import io.stallion.Context;
import io.stallion.dataAccess.filtering.FilterChain;
import io.stallion.dataAccess.filtering.Pager;
import io.stallion.exceptions.*;
import io.stallion.exceptions.NotFoundException;
import io.stallion.plugins.flatBlog.contacts.Contact;
import io.stallion.plugins.flatBlog.contacts.ContactsController;
import io.stallion.plugins.flatBlog.contacts.SubscriptionFrequency;
import io.stallion.plugins.flatBlog.FlatBlogSettings;
import io.stallion.requests.validators.SafeMerger;
import io.stallion.restfulEndpoints.MinRole;
import io.stallion.restfulEndpoints.EndpointResource;
import io.stallion.settings.Settings;
import io.stallion.templating.TemplateRenderer;
import io.stallion.testing.Stubbing;
import io.stallion.users.Role;

import io.stallion.restfulEndpoints.ObjectParam;
import io.stallion.services.Log;
import io.stallion.utils.DateUtils;
import io.stallion.utils.Literals;
import io.stallion.utils.json.JSON;
import io.stallion.utils.json.RestrictedViews;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClients;

import javax.servlet.http.Cookie;
import javax.ws.rs.*;
import java.io.IOException;
import java.net.URL;
import java.util.Map;

public class CommentsEndpoints implements EndpointResource {

    @POST
    @Path("/comments/submit")
    @JsonView(RestrictedViews.Owner.class)
    @Produces("application/json")
    public CommentWrapper submitComment(@ObjectParam Comment rawComment) throws Exception {

        Comment comment = SafeMerger.with()
                .nonEmpty("bodyMarkdown", "authorDisplayName", "threadId", "parentPermalink", "parentTitle")
                .email("authorEmail").optional("authorFirstName", "authorLastName", "authorWebSite",
                        "captchaResponse", "threadSubscribe", "mentionSubscribe")
                .merge(rawComment);

        checkRECaptcha(comment);
        checkAkismet(comment);
        processAndSaveComment(comment);
        NewCommentEmailHandler.enqueue(comment);
        if (comment.isApproved()) {
            CommentsController.instance().postCommentApproved(comment);
        }
        Log.info("Submmited comment {0} {1}", comment.getAuthorEmail(), comment.getId());
        return CommentsController.instance().forId(comment.getId()).toWrapper(true);
    }

    private void checkRECaptcha(Comment comment) {
        try {
            Stubbing.checkExecuteStub(comment);
        } catch (Stubbing.StubbedOut stubbedOut) {
            return;
        }
        if (!StringUtils.isEmpty(FlatBlogSettings.getInstance().getReCaptchaSecret())) {
            Log.finer("Check captcha");
            String secret = FlatBlogSettings.getInstance().getReCaptchaSecret();
            HttpResponse<JsonNode> jsonResponse = null;
            try {
                jsonResponse = Unirest.get("https://www.google.com/recaptcha/api/siteverify")
                        .queryString("secret", secret).queryString("response", comment.getCaptchaResponse())
                        .queryString("remoteip", getIp()).asJson();
            } catch (UnirestException e) {
                throw new RuntimeException(e);
            }
            Boolean succeeded = jsonResponse.getBody().getObject().getBoolean("success");
            if (!succeeded) {
                throw new ClientException("Invalid captcha. Please enter the code again.", 422);
            }
        }

    }

    private void checkAkismet(Comment comment) {
        try {
            Stubbing.checkExecuteStub(comment);
        } catch (Stubbing.StubbedOut stubbedOut) {
            return;
        }
        if (StringUtils.isEmpty(FlatBlogSettings.getInstance().getAkismetKey())) {
            return;
        }
        Log.finer("Check akismet");
        HttpClient client = HttpClients.createDefault();
        Akismet akismet = new Akismet(client);
        AkismetComment ac = new AkismetComment();
        Log.info("User agent: {0} IP: {1}", Context.getRequest().getHeader("User-agent"),
                Context.getRequest().getRemoteAddr());

        ac.setCommentAuthor(comment.getAuthorDisplayName());
        ac.setCommentAuthorEmail(comment.getAuthorEmail());
        ac.setCommentAuthorUrl(comment.getAuthorWebSite());
        ac.setCommentType("flatBlog");
        ac.setCommentContent(comment.getBodyHtml());
        ac.setPermalink(comment.getParentPermalink());
        ac.setUserAgent(Context.getRequest().getHeader("User-agent"));
        ac.setReferrer(Context.getRequest().getHeader("Referer"));
        ac.setUserIp(getIp());
        akismet.setApiKey(FlatBlogSettings.getInstance().getAkismetKey());
        boolean isSpam = false;
        try {
            isSpam = akismet.commentCheck(ac);
        } catch (AkismetException e) {
            throw new RuntimeException(e);
        }
        comment.setAkismetCheckedAt(mils());
        if (isSpam) {
            comment.setAkismetApproved(false);
            comment.setState(State.AKISMET_SPAM);
        } else {
            comment.setAkismetApproved(true);
        }

    }

    public Comment processAndSaveComment(Comment comment) {
        if (FlatBlogSettings.getInstance().getModerationEnabled()) {
            if (comment.getAkismetApproved()
                    || StringUtils.isEmpty(FlatBlogSettings.getInstance().getAkismetKey())) {
                comment.setState(State.PENDING_MODERATION);
            }
        } else {
            if (comment.getAkismetApproved() || empty(FlatBlogSettings.getInstance().getAkismetKey())) {
                comment.setState(State.APPROVED);
                comment.setApproved(true);
            }
        }

        if (comment.isApproved()) {
            comment.setPreviouslyApproved(true);
        }

        Cookie cookie = Context.getRequest().getCookie(Constants.AUTHOR_SECRET_COOKIE);
        if (cookie == null) {
            cookie = Context.getResponse().addCookie(Constants.AUTHOR_SECRET_COOKIE,
                    RandomStringUtils.randomAlphanumeric(30), 20 * 365 * 60 * 60 * 24);
        }
        comment.setAuthorSecret(cookie.getValue());

        Log.info("Save comment from {0}", comment.getAuthorEmail());

        comment.setCreatedTicks(DateUtils.mils());

        Contact contact = new Contact();
        contact.setEmail(comment.getAuthorEmail());
        contact.setDisplayName(comment.getAuthorDisplayName());
        contact.setWebSite(comment.getAuthorWebSite());
        contact.setEverCookie(RandomStringUtils.randomAlphanumeric(30));
        if (Literals.empty(contact.getId())) {
            Context.getResponse().addCookie("stContactToken", contact.getEverCookie(), 20 * 365 * 60 * 60 * 24);
        }
        contact = ContactsController.instance().getOrCreate(contact);

        comment.setContactId(contact.getId());
        CommentsController.instance().save(comment);

        /* Subscribe the commenter to updates */
        CommentSubscriptionInfo subscriptionInfo = new CommentSubscriptionInfo();
        if (comment.getMentionSubscribe() == true) {
            subscriptionInfo.setReplyNotifyFrequency(SubscriptionFrequency.DAILY);
        } else {
            subscriptionInfo.setReplyNotifyFrequency(SubscriptionFrequency.NEVER);
        }
        if (comment.getThreadSubscribe() == true) {
            subscriptionInfo.setThreadNotifyFrequency(SubscriptionFrequency.DAILY);
        } else {
            subscriptionInfo.setThreadNotifyFrequency(SubscriptionFrequency.NEVER);
        }
        CommentsController.instance().updateCommentSubscriptionInfo(subscriptionInfo, comment, contact);

        return comment;
    }

    private void enqueueAdminEmail(Comment comment) {

    }

    public String getIp() {
        String ip;
        if (!StringUtils.isEmpty(FlatBlogSettings.getInstance().getRealIpHeader())) {
            ip = Context.getRequest().getHeader(FlatBlogSettings.getInstance().getRealIpHeader());
        } else {
            ip = Context.getRequest().getRemoteAddr();
        }
        return ip;
    }

    @GET
    @Produces("text/xml")
    @Path("/comments/rss.xml")
    public String rss() {
        Pager<Comment> pager = CommentsController.instance().filter("isApproved", "true")
                .sort("createdTicks", "desc").pager(1);
        Map<String, Object> context = map();
        context.put("commentsPager", pager);
        URL url = getClass().getClassLoader().getResource("templates/comments-rss.jinja");
        return TemplateRenderer.instance().renderTemplate(url, context);
    }

    @GET
    @Path("/comments/dashboard")
    @Produces("text/html")
    @MinRole(Role.STAFF)
    public Object adminDashboard() throws Exception {
        URL url = getClass().getClassLoader().getResource("templates/comments-manage-dashboard.jinja");
        Context.getResponse().getMeta().setTitle("Comments Dashboard");
        return TemplateRenderer.instance().renderTemplate(url.toString());
    }

    @GET
    @Path("/comments/assets/:path")
    public String dashboardJsx(@PathParam("path") String path) throws IOException {
        if (StringUtils.countMatches(path, ".") > 1) {
            throw new UsageException("Invalid asset file path: " + path);
        }
        URL url = getClass().getResource("/assets/" + path);
        if (url == null) {
            throw new io.stallion.exceptions.NotFoundException("Missing file path: " + path);
        }
        if (Settings.instance().getDevMode() && url.toString().contains("/target/classes/")) {
            String newPath = url.toString().replace("/target/classes/", "/src/main/resources/");
            url = new URL(newPath);

        }
        if (path.endsWith(".css")) {
            Context.getResponse().setContentType("text/css");
        } else {
            Context.getResponse().setContentType("text/javascript");
        }
        return org.apache.commons.io.IOUtils.toString(url);
    }

    @GET
    @Path("/comments/comment-subscriptions/")
    @Produces("text/html")
    public String commentSubscriptions(@QueryParam("commentId") Long commentId) throws JsonProcessingException

    {
        Comment comment = CommentsController.instance().forIdOrNotFound(commentId);
        if (!comment.isEditable()) {
            throw new ClientException("You do not have permission to edit this comment", 403);
        }
        Contact contact = ContactsController.instance().forId(comment.getContactId());
        Map<String, Object> ctx = map();
        ctx.put("contact", contact);
        ctx.put("comment", comment);
        ctx.put("subscriptionInfo", CommentsController.instance().getCommentSubscriptionInfo(comment, contact));
        ctx.put("contextJson", JSON.stringify(ctx, RestrictedViews.Owner.class));

        URL url = getClass().getClassLoader().getResource("templates/comments-subscribe.jinja");
        Context.getResponse().getMeta().setTitle("Subscribe to emails?");
        return TemplateRenderer.instance().renderTemplate(url.toString(), ctx);
    }

    @POST
    @Path("/comments/update-comment-subscriptions")
    public CommentSubscriptionInfo updateCommentSubscriptions(@QueryParam("commentId") Long commentId,
            @ObjectParam(targetClass = CommentSubscriptionInfo.class) CommentSubscriptionInfo subscriptionInfo) {
        Comment comment = CommentsController.instance().forIdOrNotFound(commentId);
        if (!comment.isEditable()) {
            throw new ClientException("You do not have permission to edit this comment", 403);
        }
        return updateCommentSubscriptions(comment, subscriptionInfo);
    }

    public CommentSubscriptionInfo updateCommentSubscriptions(Comment comment,
            CommentSubscriptionInfo subscriptionInfo) {
        Contact contact = ContactsController.instance().forId(comment.getContactId());
        subscriptionInfo = CommentsController.instance().updateCommentSubscriptionInfo(subscriptionInfo, comment,
                contact);
        return subscriptionInfo;

    }

    @POST
    @Path("/comments/:id/revise")
    public Object reviseComment(@PathParam("id") Long id, @ObjectParam Comment revisedComment) throws Exception {
        // Either, Auth level is staff or the author wrote the comment
        Comment existing = CommentsController.instance().forIdOrNotFound(id);
        if (!Context.getUser().isInRole(Role.STAFF) && !existing.isEditable()) {
            throw new ClientException("You do not have permission to edit this comment", 403);
        }
        revisedComment = SafeMerger.with()
                .optional("authorDisplayName", "authorFirstName", "authorLastName", "authorWebSite", "bodyMarkdown")
                .optionalEmail("authorEmail").merge(revisedComment, existing);
        CommentsController.instance().save(revisedComment);
        return revisedComment.toWrapper();
    }

    @POST
    @MinRole(Role.STAFF)
    @Path("/comments/:id/delete")
    public Object deleteComment(@PathParam("id") Long id) {
        Comment cmt = CommentsController.instance().forIdOrNotFound(id);
        if (cmt.isApproved()) {
            cmt.setPreviouslyApproved(true);
        }
        cmt.setApproved(false);
        cmt.setModeratedAt(mils());
        cmt.setModeratorApproved(false);
        cmt.setState(State.REJECTED);
        CommentsController.instance().softDelete(cmt);
        return true;
    }

    @POST
    @MinRole(Role.STAFF)
    @Path("/comments/:id/restore-and-approve")
    public Object restoreAndApproveComment(@PathParam("id") Long id) {
        Comment cmt = CommentsController.instance().forIdWithDeleted(id);
        if (cmt == null) {
            throw new NotFoundException("Could not find comment with that id " + id);
        }
        cmt.setDeleted(false);
        cmt.setApproved(true);
        cmt.setModeratorApproved(true);
        cmt.setModeratedAt(mils());
        cmt.setState(State.APPROVED);
        CommentsController.instance().save(cmt);

        if (!cmt.getPreviouslyApproved()) {
            CommentsController.instance().postCommentApproved(cmt);

            cmt.setPreviouslyApproved(true);
            CommentsController.instance().save(cmt);
        }

        return true;
    }

    @PUT
    @Path("/comments/:id/update")
    @MinRole(Role.STAFF)
    public Object updateComment(@PathParam("id") Object id, @ObjectParam(targetClass = Comment.class) String key) {
        // Either, Auth level is staff, or the cookie/secret matches
        return "comment updated";
    }

    @GET
    @Path("/comments/:id/view")
    @MinRole(Role.STAFF)
    @Produces("application/json")
    public Object viewComment(@PathParam("id") Long id) {
        Comment cmt = CommentsController.instance().forIdWithDeleted(id);
        if (cmt == null) {
            throw new NotFoundException("Comment not found.");
        }
        return cmt.toWrapper();
    }

    @GET
    @Path("/comments/dashboard.json")
    @MinRole(Role.STAFF)
    @Produces("application/json")
    public Pager dashboardScreen(@QueryParam("deleted") Boolean deleted, @QueryParam("page") Integer page) {
        page = or(page, 1);
        DashboardScreen screen = new DashboardScreen();
        FilterChain<Comment> comments = CommentsController.instance().filterChain();
        Log.info("Deleted: {0}", deleted);
        if (deleted != null && true == deleted) {
            Log.info("Include deleted!");
            comments = comments.includeDeleted();
        }
        comments = comments.sort("createdTicks", "desc");

        return comments.pager(page, 50);
    }

    @GET
    @Path("/comments/find")
    public Object findComments() {

        return "find comments";
    }

}