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.extension.xmlrpc.handler;
031:
032: import org.apache.commons.logging.Log;
033: import org.apache.commons.logging.LogFactory;
034: import org.apache.xmlrpc.XmlRpcException;
035: import org.blojsom.blog.Entry;
036: import org.blojsom.blog.Pingback;
037: import org.blojsom.fetcher.FetcherException;
038: import org.blojsom.plugin.common.ResponseConstants;
039: import org.blojsom.plugin.pingback.PingbackPlugin;
040: import org.blojsom.plugin.pingback.event.PingbackAddedEvent;
041: import org.blojsom.plugin.pingback.event.PingbackResponseSubmissionEvent;
042: import org.blojsom.util.BlojsomConstants;
043: import org.blojsom.util.BlojsomUtils;
044:
045: import java.io.BufferedReader;
046: import java.io.IOException;
047: import java.io.InputStreamReader;
048: import java.net.HttpURLConnection;
049: import java.net.URL;
050: import java.util.Date;
051: import java.util.HashMap;
052: import java.util.Map;
053: import java.util.regex.Matcher;
054: import java.util.regex.Pattern;
055:
056: /**
057: * Pingback handler provides support for the <a href="http://www.hixie.ch/specs/pingback/pingback">Pingback 1.0</a>
058: * specification.
059: *
060: * @author David Czarnecki
061: * @version $Id: PingbackHandler.java,v 1.8 2007/01/17 02:35:07 czarneckid Exp $
062: * @since blojsom 3.0
063: */
064: public class PingbackHandler extends APIHandler {
065:
066: private static final Log _logger = LogFactory
067: .getLog(PingbackHandler.class);
068:
069: private static final String TITLE_PATTERN = "<title>(.*)</title>";
070:
071: protected static final String API_NAME = "pingback";
072:
073: protected static final int PINGBACK_GENERIC_FAULT_CODE = 0;
074: protected static final int PINGBACK_SOURCE_URI_NON_EXISTENT_CODE = 16;
075: protected static final int PINGBACK_NO_LINK_TO_TARGET_URI_CODE = 17;
076: protected static final int PINGBACK_TARGET_URI_NON_EXISTENT_CODE = 32;
077: protected static final int PINGBACK_TARGET_URI_NOT_ENABLED_CODE = 33;
078: protected static final int PINGBACK_ALREADY_REGISTERED_CODE = 48;
079: protected static final int PINGBACK_ACCESS_DENIED_CODE = 49;
080: protected static final int PINGBACK_UPSTREAM_SERVER_ERROR_CODE = 50;
081:
082: protected static final String PINGBACK_SOURCE_URI_METADATA = "pingback-source-uri";
083: protected static final String PINGBACK_TARGET_URI_METADATA = "pingback-target-uri";
084:
085: /**
086: * Construct a new Pingback handler
087: */
088: public PingbackHandler() {
089: }
090:
091: /**
092: * Gets the name of API Handler. Used to bind to XML-RPC
093: *
094: * @return The API Name (ie: pingback)
095: */
096: public String getName() {
097: return API_NAME;
098: }
099:
100: /**
101: * Try to find the <title></title> tags from the source text
102: *
103: * @param source Source URI text
104: * @return Title of text or <code>null</code> if title tags are not found
105: */
106: protected String getTitleFromSource(String source) {
107: String title = null;
108: Pattern titlePattern = Pattern.compile(TITLE_PATTERN,
109: Pattern.CASE_INSENSITIVE | Pattern.MULTILINE
110: | Pattern.DOTALL | Pattern.UNICODE_CASE);
111: Matcher titleMatcher = titlePattern.matcher(source);
112: if (titleMatcher.find()) {
113: title = titleMatcher.group(1);
114: }
115:
116: return title;
117: }
118:
119: /**
120: * Try to extract an excerpt from the source text. Currently looks ahead 200 and ahead 200 characters from
121: * the location of the targetURI within the source.
122: *
123: * @param source Source URI text
124: * @param targetURI Target URI from which to start the excerpt
125: * @return Excerpt of text or <code>null</code> if we cannot find the targetURI
126: */
127: protected String getExcerptFromSource(String source,
128: String targetURI) {
129: String excerpt = null;
130:
131: int startOfTarget = source.indexOf(targetURI);
132: if (startOfTarget != -1) {
133: int startOfExcerpt = startOfTarget - 200;
134: if (startOfExcerpt < 0) {
135: startOfExcerpt = 0;
136: }
137:
138: int endOfExcerpt = startOfTarget + 200;
139: if (endOfExcerpt > source.length()) {
140: endOfExcerpt = source.length();
141: }
142:
143: excerpt = source.substring(startOfExcerpt, endOfExcerpt);
144: excerpt = BlojsomUtils.stripHTML(excerpt);
145: int firstSpace = excerpt.indexOf(' ') + 1;
146: int lastSpace = excerpt.lastIndexOf(' ');
147: if (-1 == lastSpace || lastSpace < firstSpace)
148: lastSpace = excerpt.length();
149: excerpt = excerpt.substring(firstSpace, lastSpace);
150: }
151:
152: return excerpt;
153: }
154:
155: /**
156: * Notifies the server that a link has been added to sourceURI, pointing to targetURI.
157: *
158: * @param sourceURI The absolute URI of the post on the source page containing the link to the target site.
159: * @param targetURI The absolute URI of the target of the link, as given on the source page.
160: * @return
161: */
162: public String ping(String sourceURI, String targetURI)
163: throws XmlRpcException {
164: if (_logger.isDebugEnabled()) {
165: _logger.debug("Pingback from: " + sourceURI + " to: "
166: + targetURI);
167: }
168:
169: if (BlojsomUtils.checkNullOrBlank(sourceURI)) {
170: if (_logger.isErrorEnabled()) {
171: _logger.error("Pingback must include a source URI");
172: }
173:
174: throw new XmlRpcException(
175: PINGBACK_SOURCE_URI_NON_EXISTENT_CODE,
176: "Pingback must include a source URI");
177: }
178:
179: // Fetch sourceURI to make sure there is a link to the targetURI
180: StringBuffer sourcePage;
181: try {
182: URL source = new URL(sourceURI);
183: HttpURLConnection sourceConnection = (HttpURLConnection) source
184: .openConnection();
185: sourceConnection.setRequestMethod("GET");
186: sourceConnection.connect();
187: BufferedReader sourceReader = new BufferedReader(
188: new InputStreamReader(sourceConnection
189: .getInputStream(), BlojsomConstants.UTF8));
190: String line;
191: sourcePage = new StringBuffer();
192:
193: while ((line = sourceReader.readLine()) != null) {
194: sourcePage.append(line);
195: sourcePage.append(BlojsomConstants.LINE_SEPARATOR);
196: }
197: } catch (IOException e) {
198: if (_logger.isErrorEnabled()) {
199: _logger.error(e);
200: }
201:
202: throw new XmlRpcException(PINGBACK_GENERIC_FAULT_CODE,
203: "Unable to retrieve source URI");
204: }
205:
206: // Check that the sourceURI contains a link to the targetURI
207: if (sourcePage.indexOf(targetURI) == -1) {
208: if (_logger.isErrorEnabled()) {
209: _logger.error("Target URI not found in Source URI");
210: }
211:
212: throw new XmlRpcException(
213: PINGBACK_NO_LINK_TO_TARGET_URI_CODE,
214: "Target URI not found in source URI");
215: }
216:
217: // Check targetURI exists and is a valid entry
218: try {
219: URL target = new URL(targetURI);
220: HttpURLConnection httpURLConnection = (HttpURLConnection) target
221: .openConnection();
222: httpURLConnection.setRequestMethod("HEAD");
223: httpURLConnection.connect();
224:
225: if (httpURLConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
226: if (_logger.isErrorEnabled()) {
227: _logger.error("Target URI does not exist");
228: }
229:
230: throw new XmlRpcException(
231: PINGBACK_TARGET_URI_NON_EXISTENT_CODE,
232: "Target URI does not exist");
233: }
234: } catch (IOException e) {
235: if (_logger.isErrorEnabled()) {
236: _logger.error(e);
237: }
238:
239: throw new XmlRpcException(PINGBACK_GENERIC_FAULT_CODE,
240: "Unable to retrieve target URI");
241: }
242:
243: String permalink = BlojsomUtils.getRequestValue(
244: BlojsomConstants.PERMALINK_PARAM, _httpServletRequest);
245: if (BlojsomUtils.checkNullOrBlank(permalink)) {
246: _logger.error("Permalink is null or blank: " + permalink);
247: throw new XmlRpcException(PINGBACK_GENERIC_FAULT_CODE,
248: "Unable to retrieve target URI");
249: }
250:
251: // Check that the resource hasn't already been registered
252: try {
253: Pingback pingback = _fetcher.loadPingback(_blog, sourceURI,
254: targetURI);
255:
256: if (pingback != null) {
257: throw new XmlRpcException(
258: PINGBACK_ALREADY_REGISTERED_CODE,
259: "Pingback already registered");
260: }
261: } catch (FetcherException e) {
262: }
263:
264: // Check the resource is pingback-enabled
265: try {
266: Entry entry = _fetcher.loadEntry(_blog, permalink);
267:
268: if (_blog.getBlogPingbacksEnabled().booleanValue()
269: && entry.allowsPingbacks().booleanValue()) {
270: // Record pingback
271: Pingback pingback = _fetcher.newPingback();
272: pingback.setBlogEntryId(entry.getId());
273: pingback.setBlogId(_blog.getId());
274: pingback.setEntry(entry);
275: pingback.setIp(_httpServletRequest.getRemoteAddr());
276: pingback.setSourceURI(sourceURI);
277: pingback.setTargetURI(targetURI);
278:
279: Map pingbackMetaData = new HashMap();
280:
281: _eventBroadcaster
282: .processEvent(new PingbackResponseSubmissionEvent(
283: this , new Date(), _blog,
284: _httpServletRequest,
285: _httpServletResponse,
286: getTitleFromSource(sourcePage
287: .toString()),
288: getTitleFromSource(sourcePage
289: .toString()), sourceURI,
290: getExcerptFromSource(sourcePage
291: .toString(), targetURI), entry,
292: pingbackMetaData));
293:
294: // Check to see if the trackback should be destroyed (not saved) automatically
295: if (!pingbackMetaData
296: .containsKey(PingbackPlugin.BLOJSOM_PLUGIN_PINGBACK_METADATA_DESTROY)) {
297:
298: pingback.setMetaData(pingbackMetaData);
299: if (pingbackMetaData
300: .containsKey(PingbackPlugin.BLOJSOM_PINGBACK_PLUGIN_APPROVED)
301: && "true"
302: .equals(pingbackMetaData
303: .get(PingbackPlugin.BLOJSOM_PINGBACK_PLUGIN_APPROVED))) {
304: pingback
305: .setStatus(ResponseConstants.APPROVED_STATUS);
306: } else {
307: if ("true"
308: .equals(_blog
309: .getProperty(PingbackPlugin.PINGBACK_MODERATION_ENABLED))) {
310: pingback
311: .setStatus(ResponseConstants.NEW_STATUS);
312: } else {
313: pingback
314: .setStatus(ResponseConstants.APPROVED_STATUS);
315: }
316: }
317:
318: Integer status = addPingback(entry.getTitle(),
319: getExcerptFromSource(sourcePage.toString(),
320: targetURI), sourceURI,
321: getTitleFromSource(sourcePage.toString()),
322: pingbackMetaData, pingback);
323:
324: if (status.intValue() != 0) {
325: throw new XmlRpcException(status.intValue(),
326: "Unknown exception occurred");
327: } else {
328: _eventBroadcaster
329: .broadcastEvent(new PingbackAddedEvent(
330: this , new Date(), pingback,
331: _blog));
332: }
333: } else {
334: if (_logger.isInfoEnabled()) {
335: _logger
336: .info("Pingback meta-data contained destroy key. Pingback was not saved");
337: }
338:
339: throw new XmlRpcException(
340: PINGBACK_ACCESS_DENIED_CODE,
341: "Pingback meta-data contained destroy key. Pingback was not saved.");
342: }
343: } else {
344: if (_logger.isDebugEnabled()) {
345: _logger
346: .debug("Target URI does not support pingbacks");
347: }
348:
349: throw new XmlRpcException(
350: PINGBACK_TARGET_URI_NOT_ENABLED_CODE,
351: "Target URI does not support pingbacks");
352: }
353: } catch (FetcherException e) {
354: if (_logger.isErrorEnabled()) {
355: _logger.error(e);
356: }
357:
358: throw new XmlRpcException(
359: PINGBACK_TARGET_URI_NON_EXISTENT_CODE,
360: "Target URI does not exist");
361: }
362:
363: // Update notification
364: return "Registered pingback from: " + sourceURI + " to: "
365: + targetURI;
366: }
367:
368: /**
369: * Add a pingback for a given blog ID
370: *
371: * @param title Pingback title
372: * @param excerpt Pingback excerpt
373: * @param url Pingback URL
374: * @param blogName Pingback blog name
375: * @param pingbackMetaData Pingback meta-data
376: * @param pingback {@link Pingback}
377: * @return <code>0</code> if the pingback was registered, otherwise a fault code is returned
378: */
379: protected Integer addPingback(String title, String excerpt,
380: String url, String blogName, Map pingbackMetaData,
381: Pingback pingback) throws XmlRpcException {
382: title = BlojsomUtils.escapeStringSimple(title);
383: title = BlojsomUtils.stripLineTerminators(title, " ");
384: pingback.setTitle(title);
385:
386: excerpt = BlojsomUtils.escapeStringSimple(excerpt);
387: excerpt = BlojsomUtils.stripLineTerminators(excerpt, " ");
388: pingback.setExcerpt(excerpt);
389:
390: url = BlojsomUtils.escapeStringSimple(url);
391: url = BlojsomUtils.stripLineTerminators(url, " ");
392: pingback.setUrl(url);
393:
394: blogName = BlojsomUtils.escapeStringSimple(blogName);
395: blogName = BlojsomUtils.stripLineTerminators(blogName, " ");
396: pingback.setBlogName(blogName);
397:
398: pingback.setTrackbackDate(new Date());
399: pingback.setMetaData(pingbackMetaData);
400:
401: try {
402: _fetcher.savePingback(_blog, pingback);
403: } catch (FetcherException e) {
404: if (_logger.isErrorEnabled()) {
405: _logger.error(e);
406: }
407:
408: if (e.getCause() instanceof XmlRpcException) {
409: throw (XmlRpcException) e.getCause();
410: }
411: }
412:
413: return new Integer(0);
414: }
415: }
|