001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. The ASF licenses this file to You
004: * under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License. For additional information regarding
015: * copyright in this work, please see the NOTICE file in the top level
016: * directory of this distribution.
017: */
018:
019: package org.apache.roller.ui.rendering.servlets;
020:
021: import java.io.IOException;
022: import java.sql.Timestamp;
023: import java.util.ArrayList;
024: import java.util.Iterator;
025: import java.util.List;
026: import java.util.ResourceBundle;
027: import java.util.Set;
028: import java.util.TreeSet;
029: import javax.mail.MessagingException;
030: import javax.mail.Session;
031: import javax.naming.Context;
032: import javax.naming.InitialContext;
033: import javax.naming.NamingException;
034: import javax.servlet.RequestDispatcher;
035: import javax.servlet.ServletConfig;
036: import javax.servlet.ServletException;
037: import javax.servlet.http.HttpServlet;
038: import javax.servlet.http.HttpServletRequest;
039: import javax.servlet.http.HttpServletResponse;
040: import org.apache.commons.lang.StringUtils;
041: import org.apache.commons.logging.Log;
042: import org.apache.commons.logging.LogFactory;
043: import org.apache.roller.RollerException;
044: import org.apache.roller.config.RollerConfig;
045: import org.apache.roller.config.RollerRuntimeConfig;
046: import org.apache.roller.business.search.IndexManager;
047: import org.apache.roller.business.RollerFactory;
048: import org.apache.roller.business.UserManager;
049: import org.apache.roller.business.WeblogManager;
050: import org.apache.roller.pojos.CommentData;
051: import org.apache.roller.pojos.UserData;
052: import org.apache.roller.pojos.WeblogEntryData;
053: import org.apache.roller.pojos.WebsiteData;
054: import org.apache.roller.ui.rendering.model.UtilitiesModel;
055: import org.apache.roller.ui.rendering.util.CommentAuthenticator;
056: import org.apache.roller.ui.rendering.util.DefaultCommentAuthenticator;
057: import org.apache.roller.ui.rendering.util.WeblogCommentRequest;
058: import org.apache.roller.ui.rendering.util.WeblogEntryCommentForm;
059: import org.apache.roller.util.GenericThrottle;
060: import org.apache.roller.util.IPBanList;
061: import org.apache.roller.util.MailUtil;
062: import org.apache.roller.util.SpamChecker;
063: import org.apache.roller.util.URLUtilities;
064: import org.apache.roller.util.Utilities;
065: import org.apache.roller.util.cache.CacheManager;
066: import org.apache.struts.util.RequestUtils;
067:
068: /**
069: * The CommentServlet handles all incoming weblog entry comment posts.
070: *
071: * We validate each incoming comment based on various comment settings and
072: * if all checks are passed then the comment is saved.
073: *
074: * Incoming comments are tested against the MT Blacklist. If they are found
075: * to be spam, then they are marked as spam and hidden from view.
076: *
077: * If email notification is turned on, each new comment will result in an
078: * email sent to the blog owner and all who have commented on the same post.
079: *
080: * @web.servlet name="CommentServlet" load-on-startup="7"
081: * @web.servlet-mapping url-pattern="/roller-ui/rendering/comment/*"
082: */
083: public class CommentServlet extends HttpServlet {
084:
085: private static Log log = LogFactory.getLog(CommentServlet.class);
086:
087: private static final String EMAIL_ADDR_REGEXP = "^.*@.*[.].{2,}$";
088:
089: private ResourceBundle bundle = ResourceBundle
090: .getBundle("ApplicationResources");
091:
092: private CommentAuthenticator authenticator = null;
093: private GenericThrottle commentThrottle = null;
094:
095: /**
096: * Initialization.
097: */
098: public void init(ServletConfig servletConfig)
099: throws ServletException {
100:
101: super .init(servletConfig);
102:
103: log.info("Initializing CommentServlet");
104:
105: // lookup the authenticator we are going to use and instantiate it
106: try {
107: String name = RollerConfig
108: .getProperty("comment.authenticator.classname");
109:
110: Class clazz = Class.forName(name);
111: this .authenticator = (CommentAuthenticator) clazz
112: .newInstance();
113:
114: } catch (Exception e) {
115: log.error(e);
116: this .authenticator = new DefaultCommentAuthenticator();
117: }
118:
119: // are we doing throttling?
120: if (RollerConfig.getBooleanProperty("comment.throttle.enabled")) {
121:
122: int threshold = 25;
123: try {
124: threshold = Integer.parseInt(RollerConfig
125: .getProperty("comment.throttle.threshold"));
126: } catch (Exception e) {
127: log
128: .warn(
129: "bad input for config property comment.throttle.threshold",
130: e);
131: }
132:
133: int interval = 60000;
134: try {
135: interval = Integer.parseInt(RollerConfig
136: .getProperty("comment.throttle.interval"));
137: // convert from seconds to milliseconds
138: interval = interval * 1000;
139: } catch (Exception e) {
140: log
141: .warn(
142: "bad input for config property comment.throttle.interval",
143: e);
144: }
145:
146: int maxEntries = 250;
147: try {
148: maxEntries = Integer.parseInt(RollerConfig
149: .getProperty("comment.throttle.maxentries"));
150: } catch (Exception e) {
151: log
152: .warn(
153: "bad input for config property comment.throttle.maxentries",
154: e);
155: }
156:
157: commentThrottle = new GenericThrottle(threshold, interval,
158: maxEntries);
159:
160: log.info("Comment Throttling ENABLED");
161: } else {
162: log.info("Comment Throttling DISABLED");
163: }
164: }
165:
166: /**
167: * Handle incoming http GET requests.
168: *
169: * The CommentServlet does not support GET requests, it's a 404.
170: */
171: public void doGet(HttpServletRequest request,
172: HttpServletResponse response) throws IOException,
173: ServletException {
174:
175: response.sendError(HttpServletResponse.SC_NOT_FOUND);
176: }
177:
178: /**
179: * Service incoming POST requests.
180: *
181: * Here we handle incoming comment postings.
182: */
183: public void doPost(HttpServletRequest request,
184: HttpServletResponse response) throws IOException,
185: ServletException {
186:
187: String error = null;
188: String message = null;
189: String dispatch_url = null;
190:
191: WebsiteData weblog = null;
192: WeblogEntryData entry = null;
193:
194: // are we doing a preview? or a post?
195: String method = request.getParameter("method");
196: boolean preview = (method != null && method.equals("preview")) ? true
197: : false;
198:
199: // throttling protection against spammers
200: if (commentThrottle != null
201: && commentThrottle.processHit(request.getRemoteAddr())) {
202:
203: log.debug("ABUSIVE " + request.getRemoteAddr());
204: IPBanList.getInstance()
205: .addBannedIp(request.getRemoteAddr());
206: response.sendError(HttpServletResponse.SC_NOT_FOUND);
207: return;
208: }
209:
210: WeblogCommentRequest commentRequest = null;
211: try {
212: commentRequest = new WeblogCommentRequest(request);
213:
214: // lookup weblog specified by comment request
215: UserManager uMgr = RollerFactory.getRoller()
216: .getUserManager();
217: weblog = uMgr.getWebsiteByHandle(commentRequest
218: .getWeblogHandle());
219:
220: if (weblog == null) {
221: throw new RollerException("unable to lookup weblog: "
222: + commentRequest.getWeblogHandle());
223: }
224:
225: // lookup entry specified by comment request
226: entry = commentRequest.getWeblogEntry();
227: if (entry == null) {
228: throw new RollerException("unable to lookup entry: "
229: + commentRequest.getWeblogAnchor());
230: }
231:
232: // we know what the weblog entry is, so setup our urls
233: dispatch_url = "/roller-ui/rendering/page/"
234: + weblog.getHandle();
235: if (commentRequest.getLocale() != null) {
236: dispatch_url += "/" + commentRequest.getLocale();
237: }
238: dispatch_url += "/entry/"
239: + URLUtilities.encode(commentRequest
240: .getWeblogAnchor());
241:
242: } catch (Exception e) {
243: // some kind of error parsing the request or looking up weblog
244: log.debug("error creating page request", e);
245: response.sendError(HttpServletResponse.SC_NOT_FOUND);
246: return;
247: }
248:
249: log.debug("Doing comment posting for entry = "
250: + entry.getPermaLink());
251:
252: // collect input from request params and construct new comment object
253: // fields: name, email, url, content, notify
254: // TODO: data validation on collected comment data
255: CommentData comment = new CommentData();
256: comment.setName(commentRequest.getName());
257: comment.setEmail(commentRequest.getEmail());
258: comment.setUrl(commentRequest.getUrl());
259: comment.setContent(commentRequest.getContent());
260: comment.setNotify(new Boolean(commentRequest.isNotify()));
261: comment.setWeblogEntry(entry);
262: comment.setRemoteHost(request.getRemoteHost());
263: comment.setPostTime(new Timestamp(System.currentTimeMillis()));
264:
265: WeblogEntryCommentForm cf = new WeblogEntryCommentForm();
266: cf.setData(comment);
267:
268: // check if comments are allowed for this entry
269: // this checks site-wide settings, weblog settings, and entry settings
270: if (!entry.getCommentsStillAllowed() || !entry.isPublished()) {
271: error = bundle.getString("comments.disabled");
272:
273: // make sure comment authentication passed
274: } else if (!this .authenticator.authenticate(request)) {
275: error = bundle.getString("error.commentAuthFailed");
276: log.debug("Comment failed authentication");
277: }
278:
279: // bail now if we have already found an error
280: if (error != null) {
281: cf.setError(error);
282: request.setAttribute("commentForm", cf);
283: RequestDispatcher dispatcher = request
284: .getRequestDispatcher(dispatch_url);
285: dispatcher.forward(request, response);
286: return;
287: }
288:
289: if (preview) {
290: // TODO: i18n
291: message = "This is a comment preview only";
292: cf.setPreview(comment);
293:
294: // If comment contains blacklisted text, warn commenter
295: SpamChecker checker = new SpamChecker();
296: if (checker.checkComment(comment)) {
297: error = bundle
298: .getString("commentServlet.previewMarkedAsSpam");
299: log.debug("Comment marked as spam");
300: }
301: log.debug("Comment is a preview");
302:
303: } else {
304: // If comment contains blacklisted text, mark as spam
305: SpamChecker checker = new SpamChecker();
306: if (checker.checkComment(comment)) {
307: comment.setSpam(Boolean.TRUE);
308: error = bundle
309: .getString("commentServlet.commentMarkedAsSpam");
310: log.debug("Comment marked as spam");
311: }
312:
313: // If comment moderation is on, set comment as pending
314: if (weblog.getCommentModerationRequired()) {
315: comment.setPending(Boolean.TRUE);
316: comment.setApproved(Boolean.FALSE);
317: message = bundle
318: .getString("commentServlet.submittedToModerator");
319: } else {
320: comment.setPending(Boolean.FALSE);
321: comment.setApproved(Boolean.TRUE);
322: }
323:
324: try {
325: WeblogManager mgr = RollerFactory.getRoller()
326: .getWeblogManager();
327: mgr.saveComment(comment);
328: RollerFactory.getRoller().flush();
329:
330: // only re-index/invalidate the cache if comment isn't moderated
331: if (!weblog.getCommentModerationRequired()) {
332: reindexEntry(entry);
333:
334: // Clear all caches associated with comment
335: CacheManager.invalidate(comment);
336: }
337:
338: // Send email notifications
339: String rootURL = RollerRuntimeConfig
340: .getAbsoluteContextURL();
341: if (rootURL == null || rootURL.trim().length() == 0) {
342: rootURL = RequestUtils.serverURL(request)
343: + request.getContextPath();
344: }
345: sendEmailNotification(comment, rootURL);
346:
347: // comment was successful, clear the comment form
348: cf = new WeblogEntryCommentForm();
349:
350: } catch (RollerException re) {
351: log.error("Error saving comment", re);
352: error = re.getMessage();
353: }
354: }
355:
356: // the work has been done, now send the user back to the entry page
357: if (error != null)
358: cf.setError(error);
359: if (message != null)
360: cf.setMessage(message);
361: request.setAttribute("commentForm", cf);
362:
363: log.debug("comment processed, forwarding to " + dispatch_url);
364: RequestDispatcher dispatcher = request
365: .getRequestDispatcher(dispatch_url);
366: dispatcher.forward(request, response);
367: }
368:
369: /**
370: * Re-index the WeblogEntry so that the new comment gets indexed.
371: */
372: private void reindexEntry(WeblogEntryData entry)
373: throws RollerException {
374:
375: IndexManager manager = RollerFactory.getRoller()
376: .getIndexManager();
377:
378: // remove entry before (re)adding it, or in case it isn't Published
379: manager.removeEntryIndexOperation(entry);
380:
381: // if published, index the entry
382: if (entry.isPublished()) {
383: manager.addEntryIndexOperation(entry);
384: }
385: }
386:
387: /**
388: * Send email notification of comment.
389: *
390: * TODO: Make the addressing options configurable on a per-website basis.
391: */
392: public static void sendEmailNotification(CommentData cd,
393: String rootURL) {
394:
395: // Send commment notifications in locale of server
396: ResourceBundle resources = ResourceBundle
397: .getBundle("ApplicationResources");
398:
399: WeblogEntryData entry = cd.getWeblogEntry();
400: WebsiteData site = entry.getWebsite();
401: UserData user = entry.getCreator();
402:
403: // Send e-mail to owner and subscribed users (if enabled)
404: boolean notify = RollerRuntimeConfig
405: .getBooleanProperty("users.comments.emailnotify");
406: if (notify && site.getEmailComments().booleanValue()) {
407: log
408: .debug("Comment notification enabled ... preparing email");
409:
410: // Determine message and addressing options from init parameters
411: boolean separateMessages = RollerConfig
412: .getBooleanProperty("comment.notification.separateOwnerMessage");
413: boolean hideCommenterAddrs = RollerConfig
414: .getBooleanProperty("comment.notification.hideCommenterAddresses");
415:
416: //------------------------------------------
417: // --- Determine the "from" address
418: // --- Use either the site configured from address or the user's address
419:
420: String from = (StringUtils.isEmpty(site
421: .getEmailFromAddress())) ? user.getEmailAddress()
422: : site.getEmailFromAddress();
423:
424: //------------------------------------------
425: // --- Build list of email addresses to send notification to
426:
427: List comments = null;
428: try {
429: WeblogManager wMgr = RollerFactory.getRoller()
430: .getWeblogManager();
431: // get only approved, non spam comments
432: comments = entry.getComments(true, true);
433: } catch (RollerException re) {
434: // should never happen
435: comments = new ArrayList();
436: }
437:
438: // Get all the subscribers to this comment thread
439: Set subscribers = new TreeSet();
440: for (Iterator it = comments.iterator(); it.hasNext();) {
441: CommentData comment = (CommentData) it.next();
442: if (!StringUtils.isEmpty(comment.getEmail())) {
443: // If user has commented twice,
444: // count the most recent notify setting
445: if (comment.getNotify().booleanValue()) {
446: // only add those with valid email
447: if (comment.getEmail().matches(
448: EMAIL_ADDR_REGEXP)) {
449: subscribers.add(comment.getEmail());
450: }
451: } else {
452: // remove user who doesn't want to be notified
453: subscribers.remove(comment.getEmail());
454: }
455: }
456: }
457:
458: // Form array of commenter addrs
459: String[] commenterAddrs = (String[]) subscribers
460: .toArray(new String[0]);
461:
462: //------------------------------------------
463: // --- Form the messages to be sent -
464: // For simplicity we always build separate owner and commenter messages even if sending a single one
465:
466: // Determine with mime type to use for e-mail
467: StringBuffer msg = new StringBuffer();
468: StringBuffer ownermsg = new StringBuffer();
469: boolean escapeHtml = RollerRuntimeConfig
470: .getBooleanProperty("users.comments.escapehtml");
471:
472: if (!escapeHtml) {
473: msg.append("<html><body style=\"background: white; ");
474: msg.append(" color: black; font-size: 12px\">");
475: }
476:
477: if (!StringUtils.isEmpty(cd.getName())) {
478: msg.append(cd.getName() + " "
479: + resources.getString("email.comment.wrote")
480: + ": ");
481: } else {
482: msg.append(resources
483: .getString("email.comment.anonymous")
484: + ": ");
485: }
486:
487: msg.append((escapeHtml) ? "\n\n" : "<br /><br />");
488:
489: msg.append((escapeHtml) ? Utilities.escapeHTML(cd
490: .getContent()) : UtilitiesModel
491: .transformToHTMLSubset(Utilities.escapeHTML(cd
492: .getContent())));
493:
494: msg
495: .append((escapeHtml) ? "\n\n----\n"
496: : "<br /><br /><hr /><span style=\"font-size: 11px\">");
497: msg.append(resources.getString("email.comment.respond")
498: + ": ");
499: msg.append((escapeHtml) ? "\n" : "<br />");
500:
501: // Build link back to comment
502: StringBuffer commentURL = new StringBuffer(rootURL);
503: commentURL.append(entry.getPermaLink());
504: commentURL.append("#comments");
505:
506: if (escapeHtml) {
507: msg.append(commentURL.toString());
508: } else {
509: msg.append("<a href=\"" + commentURL + "\">"
510: + commentURL + "</a></span>");
511: }
512:
513: ownermsg.append(msg);
514:
515: // add link to weblog edit page so user can login to manage comments
516: ownermsg
517: .append((escapeHtml) ? "\n\n----\n"
518: : "<br /><br /><hr /><span style=\"font-size: 11px\">");
519: ownermsg.append("Link to comment management page:");
520: ownermsg.append((escapeHtml) ? "\n" : "<br />");
521:
522: StringBuffer deleteURL = new StringBuffer(rootURL);
523: deleteURL
524: .append("/roller-ui/authoring/commentManagement.do?method=query&entryId="
525: + entry.getId());
526:
527: if (escapeHtml) {
528: ownermsg.append(deleteURL.toString());
529: } else {
530: ownermsg.append("<a href=\"" + deleteURL + "\">"
531: + deleteURL + "</a></span>");
532: msg.append("</Body></html>");
533: ownermsg.append("</Body></html>");
534: }
535:
536: String subject = null;
537: if ((subscribers.size() > 1)
538: || (StringUtils.equals(cd.getEmail(), user
539: .getEmailAddress()))) {
540: subject = "RE: "
541: + resources.getString("email.comment.title")
542: + ": ";
543: } else {
544: subject = resources.getString("email.comment.title")
545: + ": ";
546: }
547: subject += entry.getTitle();
548:
549: //------------------------------------------
550: // --- Send message to email recipients
551: try {
552: Context ctx = (Context) new InitialContext()
553: .lookup("java:comp/env");
554: Session session = (Session) ctx.lookup("mail/Session");
555: boolean isHtml = !escapeHtml;
556: if (separateMessages) {
557: // Send separate messages to owner and commenters
558: sendMessage(session, from, new String[] { user
559: .getEmailAddress() }, null, null, subject,
560: ownermsg.toString(), isHtml);
561: if (commenterAddrs.length > 0) {
562: // If hiding commenter addrs, they go in Bcc: otherwise in the To: of the second message
563: String[] to = hideCommenterAddrs ? null
564: : commenterAddrs;
565: String[] bcc = hideCommenterAddrs ? commenterAddrs
566: : null;
567: sendMessage(session, from, to, null, bcc,
568: subject, msg.toString(), isHtml);
569:
570: }
571: } else {
572: // Single message. User in To: header, commenters in either cc or bcc depending on hiding option
573: String[] cc = hideCommenterAddrs ? null
574: : commenterAddrs;
575: String[] bcc = hideCommenterAddrs ? commenterAddrs
576: : null;
577: sendMessage(session, from, new String[] { user
578: .getEmailAddress() }, cc, bcc, subject,
579: ownermsg.toString(), isHtml);
580: }
581: } catch (NamingException ne) {
582: log
583: .error("Unable to lookup mail session. Check configuration. NamingException: "
584: + ne.getMessage());
585: } catch (Exception e) {
586: log.warn("Exception sending comment mail: "
587: + e.getMessage());
588: // This will log the stack trace if debug is enabled
589: if (log.isDebugEnabled()) {
590: log.debug(e);
591: }
592: }
593:
594: log.debug("Done sending email message");
595:
596: } // if email enabled
597: }
598:
599: /**
600: * Send message to author of approved comment
601: *
602: * TODO: Make the addressing options configurable on a per-website basis.
603: */
604: public static void sendEmailApprovalNotification(CommentData cd,
605: String rootURL) {
606:
607: // Send commment notifications in locale of server
608: ResourceBundle resources = ResourceBundle
609: .getBundle("ApplicationResources");
610:
611: WeblogEntryData entry = cd.getWeblogEntry();
612: WebsiteData site = entry.getWebsite();
613: UserData user = entry.getCreator();
614:
615: // Only send email if email notificaiton is enabled
616: boolean notify = RollerRuntimeConfig
617: .getBooleanProperty("users.comments.emailnotify");
618: if (notify && site.getEmailComments().booleanValue()) {
619: log
620: .debug("Comment notification enabled ... preparing email");
621:
622: //------------------------------------------
623: // --- Determine the "from" address
624: // --- Use either the site configured from address or the user's address
625:
626: String from = (StringUtils.isEmpty(site
627: .getEmailFromAddress())) ? user.getEmailAddress()
628: : site.getEmailFromAddress();
629:
630: //------------------------------------------
631: // --- Form the message to be sent -
632:
633: String subject = resources
634: .getString("email.comment.commentApproved");
635:
636: StringBuffer msg = new StringBuffer();
637: msg.append(resources
638: .getString("email.comment.commentApproved"));
639:
640: // Build link back to comment
641: StringBuffer commentURL = new StringBuffer(rootURL);
642: commentURL.append(entry.getPermaLink());
643: commentURL.append("#comments");
644: msg.append(commentURL.toString());
645:
646: //------------------------------------------
647: // --- Send message to author of approved comment
648: try {
649: Context ctx = (Context) new InitialContext()
650: .lookup("java:comp/env");
651: Session session = (Session) ctx.lookup("mail/Session");
652: String[] cc = null;
653: String[] bcc = null;
654: sendMessage(session, from,
655: new String[] { cd.getEmail() }, null, // cc
656: null, // bcc
657: subject, msg.toString(), false);
658: } catch (NamingException ne) {
659: log
660: .error("Unable to lookup mail session. Check configuration. NamingException: "
661: + ne.getMessage());
662: } catch (Exception e) {
663: log.warn("Exception sending comment mail: "
664: + e.getMessage());
665: // This will log the stack trace if debug is enabled
666: if (log.isDebugEnabled()) {
667: log.debug(e);
668: }
669: }
670:
671: log.debug("Done sending email message");
672:
673: } // if email enabled
674: }
675:
676: /*
677: * This is somewhat ridiculous, but avoids duplicating a bunch of logic
678: * in the already messy sendEmailNotification.
679: */
680: static void sendMessage(Session session, String from, String[] to,
681: String[] cc, String[] bcc, String subject, String msg,
682: boolean isHtml) throws MessagingException {
683: if (isHtml)
684: MailUtil.sendHTMLMessage(session, from, to, cc, bcc,
685: subject, msg);
686: else
687: MailUtil.sendTextMessage(session, from, to, cc, bcc,
688: subject, msg);
689: }
690:
691: }
|