001: /**
002: * Copyright (c) 2003-2007, David A. Czarnecki
003: * All rights reserved.
004: *
005: * Redistribution and use in source and binary forms, with or without
006: * modification, are permitted provided that the following conditions are met:
007: *
008: * Redistributions of source code must retain the above copyright notice, this list of conditions and the
009: * following disclaimer.
010: * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
011: * following disclaimer in the documentation and/or other materials provided with the distribution.
012: * Neither the name of "David A. Czarnecki" and "blojsom" nor the names of its contributors may be used to
013: * endorse or promote products derived from this software without specific prior written permission.
014: * Products derived from this software may not be called "blojsom", nor may "blojsom" appear in their name,
015: * without prior written permission of David A. Czarnecki.
016: *
017: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
018: * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
019: * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
020: * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
021: * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
022: * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
023: * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
024: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
025: * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
026: * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
027: * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
028: * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
029: * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
030: */package org.blojsom.plugin.pingback;
031:
032: import org.apache.commons.logging.Log;
033: import org.apache.commons.logging.LogFactory;
034: import org.apache.commons.mail.Email;
035: import org.apache.commons.mail.EmailException;
036: import org.apache.commons.mail.HtmlEmail;
037: import org.apache.xmlrpc.AsyncCallback;
038: import org.apache.xmlrpc.XmlRpcClient;
039: import org.blojsom.blog.Blog;
040: import org.blojsom.blog.Entry;
041: import org.blojsom.blog.User;
042: import org.blojsom.event.Event;
043: import org.blojsom.event.EventBroadcaster;
044: import org.blojsom.event.Listener;
045: import org.blojsom.plugin.Plugin;
046: import org.blojsom.plugin.PluginException;
047: import org.blojsom.plugin.admin.event.EntryAddedEvent;
048: import org.blojsom.plugin.admin.event.EntryEvent;
049: import org.blojsom.plugin.admin.event.EntryUpdatedEvent;
050: import org.blojsom.plugin.email.EmailConstants;
051: import org.blojsom.plugin.pingback.event.PingbackAddedEvent;
052: import org.blojsom.plugin.velocity.StandaloneVelocityPlugin;
053: import org.blojsom.util.BlojsomConstants;
054: import org.blojsom.util.BlojsomUtils;
055: import org.blojsom.fetcher.FetcherException;
056: import org.blojsom.fetcher.Fetcher;
057:
058: import javax.mail.Session;
059: import javax.naming.Context;
060: import javax.naming.InitialContext;
061: import javax.naming.NamingException;
062: import javax.servlet.http.HttpServletRequest;
063: import javax.servlet.http.HttpServletResponse;
064: import java.io.BufferedReader;
065: import java.io.IOException;
066: import java.io.InputStreamReader;
067: import java.net.HttpURLConnection;
068: import java.net.MalformedURLException;
069: import java.net.URL;
070: import java.util.Date;
071: import java.util.HashMap;
072: import java.util.Map;
073: import java.util.Vector;
074: import java.util.regex.Matcher;
075: import java.util.regex.Pattern;
076:
077: /**
078: * Pingback plugin implements a pingback client to send pingbacks to any URLs in a blog entry according to the
079: * <a href="http://www.hixie.ch/specs/pingback/pingback">Pingback 1.0</a> specification.
080: *
081: * @author David Czarnecki
082: * @version $Id: PingbackPlugin.java,v 1.8 2007/01/17 02:35:13 czarneckid Exp $
083: * @since blojsom 3.0
084: */
085: public class PingbackPlugin extends StandaloneVelocityPlugin implements
086: Plugin, Listener {
087:
088: private static Log _logger = LogFactory
089: .getLog(PingbackPlugin.class);
090:
091: private static final String PINGBACK_PLUGIN_EMAIL_TEMPLATE_HTML = "org/blojsom/plugin/pingback/pingback-plugin-email-template-html.vm";
092: private static final String PINGBACK_PLUGIN_EMAIL_TEMPLATE_TEXT = "org/blojsom/plugin/pingback/pingback-plugin-email-template-text.vm";
093:
094: private static final String DEFAULT_PINGBACK_PREFIX = "[blojsom] Pingback on: ";
095: public static final String PINGBACK_PREFIX_IP = "plugin-pingback-email-prefix";
096: private static final String BLOJSOM_PINGBACK_PLUGIN_BLOG_ENTRY = "BLOJSOM_PINGBACK_PLUGIN_BLOG_ENTRY";
097: private static final String BLOJSOM_PINGBACK_PLUGIN_PINGBACK = "BLOJSOM_PINGBACK_PLUGIN_PINGBACK";
098:
099: private static final String PINGBACK_METHOD = "pingback.ping";
100: private static final String X_PINGBACK_HEADER = "X-Pingback";
101: private static final String PINGBACK_LINK_REGEX = "<link rel=\"pingback\" href=\"([^\"]+)\" ?/?>";
102: private static final String HREF_REGEX = "href\\s*=\\s*\"(.*?)\"";
103:
104: /**
105: * IP address meta-data
106: */
107: public static final String BLOJSOM_PINGBACK_PLUGIN_METADATA_IP = "BLOJSOM_PINGBACK_PLUGIN_METADATA_IP";
108:
109: // blog.properties property
110: public static final String PINGBACK_MODERATION_ENABLED = "pingback-moderation-enabled";
111:
112: public static final String BLOJSOM_PINGBACK_PLUGIN_APPROVED = "BLOJSOM_PINGBACK_PLUGIN_APPROVED";
113: public static final String BLOJSOM_PLUGIN_PINGBACK_METADATA_DESTROY = "BLOJSOM_PLUGIN_PINGBACK_METADATA_DESTROY";
114: public static final String PINGBACK_PLUGIN_METADATA_SEND_PINGBACKS = "send-pingbacks";
115:
116: private String _mailServer;
117: private String _mailServerUsername;
118: private String _mailServerPassword;
119: private Session _session;
120: private EventBroadcaster _eventBroadcaster;
121: private Fetcher _fetcher;
122:
123: private PingbackPluginAsyncCallback _callbackHandler;
124:
125: /**
126: * Create a new instance of the Pingback plugin
127: */
128: public PingbackPlugin() {
129: }
130:
131: /**
132: * Set the {@link EventBroadcaster} event broadcaster
133: *
134: * @param eventBroadcaster {@link EventBroadcaster}
135: */
136: public void setEventBroadcaster(EventBroadcaster eventBroadcaster) {
137: _eventBroadcaster = eventBroadcaster;
138: }
139:
140: /**
141: * Set the {@link Fetcher}
142: *
143: * @param fetcher {@link Fetcher}
144: */
145: public void setFetcher(Fetcher fetcher) {
146: _fetcher = fetcher;
147: }
148:
149: /**
150: * Initialize this plugin. This method only called when the plugin is instantiated.
151: *
152: * @throws PluginException If there is an error initializing the plugin
153: */
154: public void init() throws PluginException {
155: super .init();
156:
157: _callbackHandler = new PingbackPluginAsyncCallback();
158:
159: _mailServer = _servletConfig
160: .getInitParameter(EmailConstants.SMTPSERVER_IP);
161:
162: if (_mailServer != null) {
163: if (_mailServer.startsWith("java:comp/env")) {
164: try {
165: Context context = new InitialContext();
166: _session = (Session) context.lookup(_mailServer);
167: } catch (NamingException e) {
168: _logger.error(e);
169: throw new PluginException(e);
170: }
171: } else {
172: _mailServerUsername = _servletConfig
173: .getInitParameter(EmailConstants.SMTPSERVER_USERNAME_IP);
174: _mailServerPassword = _servletConfig
175: .getInitParameter(EmailConstants.SMTPSERVER_PASSWORD_IP);
176: }
177: } else {
178: throw new PluginException(
179: "Missing SMTP servername servlet initialization parameter: "
180: + EmailConstants.SMTPSERVER_IP);
181: }
182:
183: _eventBroadcaster.addListener(this );
184:
185: _logger.debug("Initialized pingback plugin");
186: }
187:
188: /**
189: * Process the blog entries
190: *
191: * @param httpServletRequest Request
192: * @param httpServletResponse Response
193: * @param blog {@link Blog} instance
194: * @param context Context
195: * @param entries Blog entries retrieved for the particular request
196: * @return Modified set of blog entries
197: * @throws PluginException If there is an error processing the blog entries
198: */
199: public Entry[] process(HttpServletRequest httpServletRequest,
200: HttpServletResponse httpServletResponse, Blog blog,
201: Map context, Entry[] entries) throws PluginException {
202: return entries;
203: }
204:
205: /**
206: * Perform any cleanup for the plugin. Called after {@link #process}.
207: *
208: * @throws org.blojsom.plugin.PluginException
209: * If there is an error performing cleanup for this plugin
210: */
211: public void cleanup() throws PluginException {
212: }
213:
214: /**
215: * Called when BlojsomServlet is taken out of service
216: *
217: * @throws org.blojsom.plugin.PluginException
218: * If there is an error in finalizing this plugin
219: */
220: public void destroy() throws PluginException {
221: }
222:
223: /**
224: * Setup the pingback e-mail
225: *
226: * @param blog {@link org.blojsom.blog.Blog} information
227: * @param email Email message
228: * @throws org.apache.commons.mail.EmailException
229: * If there is an error preparing the e-mail message
230: */
231: protected void setupEmail(Blog blog, Entry entry, Email email)
232: throws EmailException {
233: email.setCharset(BlojsomConstants.UTF8);
234:
235: // If we have a mail session for the environment, use that
236: if (_session != null) {
237: email.setMailSession(_session);
238: } else {
239: // Otherwise, if there is a username and password for the mail server, use that
240: if (!BlojsomUtils.checkNullOrBlank(_mailServerUsername)
241: && !BlojsomUtils
242: .checkNullOrBlank(_mailServerPassword)) {
243: email.setHostName(_mailServer);
244: email.setAuthentication(_mailServerUsername,
245: _mailServerPassword);
246: } else {
247: email.setHostName(_mailServer);
248: }
249: }
250:
251: email.setFrom(blog.getBlogOwnerEmail(), "Blojsom Pingback");
252:
253: String author = entry.getAuthor();
254: if (BlojsomUtils.checkNullOrBlank(author)) {
255: author = blog.getBlogOwner();
256: }
257:
258: String authorEmail = blog.getBlogOwnerEmail();
259:
260: if (author != null) {
261: try {
262: User user = _fetcher.loadUser(blog, author);
263:
264: if (user == null) {
265: authorEmail = blog.getBlogOwnerEmail();
266: } else {
267: authorEmail = user.getUserEmail();
268: if (BlojsomUtils.checkNullOrBlank(authorEmail)) {
269: authorEmail = blog.getBlogOwnerEmail();
270: }
271: }
272: } catch (FetcherException e) {
273: }
274: }
275:
276: email.addTo(authorEmail, author);
277: email.setSentDate(new Date());
278: }
279:
280: /**
281: * Handle an event broadcast from another component
282: *
283: * @param event {@link org.blojsom.event.Event} to be handled
284: */
285: public void handleEvent(Event event) {
286: if (event instanceof EntryAddedEvent
287: || event instanceof EntryUpdatedEvent) {
288: EntryEvent entryEvent = (EntryEvent) event;
289:
290: String text = entryEvent.getEntry().getDescription();
291: if (!BlojsomUtils.checkNullOrBlank(text)
292: && BlojsomUtils.checkMapForKey(entryEvent
293: .getEntry().getMetaData(),
294: PINGBACK_PLUGIN_METADATA_SEND_PINGBACKS)) {
295: String pingbackURL;
296: StringBuffer sourceURI = new StringBuffer().append(
297: entryEvent.getBlog().getBlogURL()).append(
298: entryEvent.getEntry().getBlogCategory()
299: .getName()).append(
300: entryEvent.getEntry().getPostSlug());
301: String targetURI;
302:
303: Pattern hrefPattern = Pattern
304: .compile(HREF_REGEX, Pattern.CASE_INSENSITIVE
305: | Pattern.MULTILINE
306: | Pattern.UNICODE_CASE | Pattern.DOTALL);
307: Matcher hrefMatcher = hrefPattern.matcher(text);
308: if (_logger.isDebugEnabled()) {
309: _logger.debug("Checking for href's in entry: "
310: + entryEvent.getEntry().getId());
311: }
312: while (hrefMatcher.find()) {
313: targetURI = hrefMatcher.group(1);
314: if (_logger.isDebugEnabled()) {
315: _logger.debug("Found potential targetURI: "
316: + targetURI);
317: }
318:
319: // Perform an HTTP request and first see if the X-Pingback header is available
320: try {
321: HttpURLConnection urlConnection = (HttpURLConnection) new URL(
322: targetURI).openConnection();
323: urlConnection.setRequestMethod("GET");
324: urlConnection.connect();
325: pingbackURL = urlConnection
326: .getHeaderField(X_PINGBACK_HEADER);
327:
328: // If the header is not available, look for the link in the URL content
329: if (pingbackURL == null) {
330: BufferedReader bufferedReader = new BufferedReader(
331: new InputStreamReader(urlConnection
332: .getInputStream(),
333: BlojsomConstants.UTF8));
334: StringBuffer content = new StringBuffer();
335: String input;
336: while ((input = bufferedReader.readLine()) != null) {
337: content
338: .append(input)
339: .append(
340: BlojsomConstants.LINE_SEPARATOR);
341: }
342: bufferedReader.close();
343:
344: Pattern pingbackLinkPattern = Pattern
345: .compile(
346: PINGBACK_LINK_REGEX,
347: Pattern.CASE_INSENSITIVE
348: | Pattern.MULTILINE
349: | Pattern.UNICODE_CASE
350: | Pattern.DOTALL);
351: Matcher pingbackLinkMatcher = pingbackLinkPattern
352: .matcher(content.toString());
353: if (pingbackLinkMatcher.find()) {
354: pingbackURL = pingbackLinkMatcher
355: .group(1);
356: }
357: }
358:
359: // Finally, send the pingback
360: if (pingbackURL != null && targetURI != null) {
361: Vector parameters = new Vector();
362: parameters.add(sourceURI.toString());
363: parameters.add(targetURI);
364: try {
365: if (_logger.isDebugEnabled()) {
366: _logger
367: .debug("Sending pingback to: "
368: + pingbackURL
369: + " sourceURI: "
370: + sourceURI
371: + " targetURI: "
372: + targetURI);
373: }
374:
375: XmlRpcClient xmlRpcClient = new XmlRpcClient(
376: pingbackURL);
377: xmlRpcClient.executeAsync(
378: PINGBACK_METHOD, parameters,
379: _callbackHandler);
380: } catch (MalformedURLException e) {
381: if (_logger.isErrorEnabled()) {
382: _logger.error(e);
383: }
384: }
385: }
386: } catch (IOException e) {
387: if (_logger.isErrorEnabled()) {
388: _logger.error(e);
389: }
390: }
391: }
392: } else {
393: if (_logger.isDebugEnabled()) {
394: _logger.debug("No text in blog entry or "
395: + PINGBACK_PLUGIN_METADATA_SEND_PINGBACKS
396: + " not enabled.");
397: }
398: }
399: } else if (event instanceof PingbackAddedEvent) {
400: HtmlEmail email = new HtmlEmail();
401: PingbackAddedEvent pingbackAddedEvent = (PingbackAddedEvent) event;
402:
403: if (pingbackAddedEvent.getBlog().getBlogEmailEnabled()
404: .booleanValue()) {
405: try {
406: setupEmail(pingbackAddedEvent.getBlog(),
407: pingbackAddedEvent.getEntry(), email);
408:
409: Map emailTemplateContext = new HashMap();
410: emailTemplateContext.put(
411: BlojsomConstants.BLOJSOM_BLOG,
412: pingbackAddedEvent.getBlog());
413: emailTemplateContext.put(
414: BLOJSOM_PINGBACK_PLUGIN_PINGBACK,
415: pingbackAddedEvent.getPingback());
416: emailTemplateContext.put(
417: BLOJSOM_PINGBACK_PLUGIN_BLOG_ENTRY,
418: pingbackAddedEvent.getEntry());
419:
420: String htmlText = mergeTemplate(
421: PINGBACK_PLUGIN_EMAIL_TEMPLATE_HTML,
422: pingbackAddedEvent.getBlog(),
423: emailTemplateContext);
424: String plainText = mergeTemplate(
425: PINGBACK_PLUGIN_EMAIL_TEMPLATE_TEXT,
426: pingbackAddedEvent.getBlog(),
427: emailTemplateContext);
428:
429: email.setHtmlMsg(htmlText);
430: email.setTextMsg(plainText);
431:
432: String emailPrefix = (String) pingbackAddedEvent
433: .getBlog().getProperties().get(
434: PINGBACK_PREFIX_IP);
435: if (BlojsomUtils.checkNullOrBlank(emailPrefix)) {
436: emailPrefix = DEFAULT_PINGBACK_PREFIX;
437: }
438:
439: email = (HtmlEmail) email.setSubject(emailPrefix
440: + pingbackAddedEvent.getEntry().getTitle());
441:
442: email.send();
443: } catch (EmailException e) {
444: if (_logger.isErrorEnabled()) {
445: _logger.error(e);
446: }
447: }
448: }
449: }
450: }
451:
452: /**
453: * Process an event from another component
454: *
455: * @param event {@link Event} to be handled
456: */
457: public void processEvent(Event event) {
458: }
459:
460: /**
461: * Asynchronous callback handler for the pingback ping
462: */
463: private class PingbackPluginAsyncCallback implements AsyncCallback {
464:
465: /**
466: * Default constructor
467: */
468: public PingbackPluginAsyncCallback() {
469: }
470:
471: /**
472: * Call went ok, handle result.
473: *
474: * @param o Return object
475: * @param url URL
476: * @param s String
477: */
478: public void handleResult(Object o, URL url, String s) {
479: if (_logger.isDebugEnabled()) {
480: _logger.debug(o.toString());
481: }
482: }
483:
484: /**
485: * Something went wrong, handle error.
486: *
487: * @param e Exception containing error from XML-RPC call
488: * @param url URL
489: * @param s String
490: */
491: public void handleError(Exception e, URL url, String s) {
492: if (_logger.isErrorEnabled()) {
493: _logger.error(e);
494: }
495: }
496: }
497: }
|