001: /* Copyright 2001, 2006, 2007 The JA-SIG Collaborative. All rights reserved.
002: * See license distributed with this file and
003: * available online at http://www.uportal.org/license.html
004: */
005:
006: package org.jasig.portal.channels;
007:
008: import java.util.Iterator;
009: import java.util.List;
010: import java.io.IOException;
011: import java.io.InputStream;
012: import java.io.InputStreamReader;
013: import java.io.StringReader;
014: import java.net.MalformedURLException;
015: import java.net.URI;
016: import java.net.URISyntaxException;
017: import java.net.URL;
018:
019: import org.apache.commons.httpclient.HttpClient;
020: import org.apache.commons.httpclient.HttpStatus;
021: import org.apache.commons.httpclient.methods.GetMethod;
022: import org.ccil.cowan.tagsoup.Parser;
023: import org.jasig.portal.ChannelCacheKey;
024: import org.jasig.portal.ChannelRuntimeData;
025: import org.jasig.portal.ChannelStaticData;
026: import org.jasig.portal.ICacheable;
027: import org.jasig.portal.PortalException;
028: import org.jasig.portal.ResourceMissingException;
029: import org.jasig.portal.services.HttpClientManager;
030: import org.jasig.portal.utils.DocumentFactory;
031: import org.jasig.portal.utils.ResourceLoader;
032: import org.jasig.portal.utils.XSLT;
033: import org.jasig.portal.utils.uri.BlockedUriException;
034: import org.jasig.portal.utils.uri.IUriScrutinizer;
035: import org.jasig.portal.utils.uri.PrefixUriScrutinizer;
036: import org.w3c.dom.Document;
037: import org.w3c.dom.Node;
038: import org.xml.sax.ContentHandler;
039: import org.xml.sax.InputSource;
040: import org.xml.sax.SAXException;
041:
042: import com.sun.syndication.feed.synd.SyndContent;
043: import com.sun.syndication.feed.synd.SyndEntry;
044: import com.sun.syndication.feed.synd.SyndFeed;
045: import com.sun.syndication.feed.synd.SyndImage;
046: import com.sun.syndication.io.FeedException;
047: import com.sun.syndication.io.SyndFeedInput;
048: import com.sun.syndication.io.XmlReader;
049:
050: /**
051: * <p>A channel which renders news feeds in the portal.</p>
052: *
053: * <p>Static channel parameters to be supplied:
054: *
055: * 1) "xmlUri" - URI representing the news feed. RSS and Atom are supported.
056: * (See Rome's documentation for specific version information.)
057: * 2) "viewNum" - Maximum number of news articles to list.
058: * 3) "cacheTimeout" - the amount of time (in seconds) that the contents of the
059: * channel should be cached (optional). If this parameter is left
060: * out, a default timeout value will be used.
061: * 4) "upc_localConnContext" - The class name of the ILocalConnectionContext
062: * implementation.
063: * <i>Use when local data needs to be sent with the
064: * request for the URL.</i>
065: * 5) "upc_allow_xmlUri_prefixes" - Optional parameter specifying as a whitespace
066: * delimited String the allowable xmlUri prefixes.
067: * <i>Defaults to "http:// https://"</i>
068: * 6) "upc_deny_xmlUri_prefixes" - Optional parameter specifying as a whitespace
069: * delimited String URI prefixes that should block a URI
070: * as xmlUri even if it matched one of the allow prefixes.
071: * <i>Defaults to ""</i>
072: * 7) "restrict_xmlUri_inStaticData" - Optional parameter specifying whether
073: * the xmlUri should be restricted according to the allow and
074: * deny prefix rules above as presented in ChannelStaticData
075: * or just as presented in ChannelRuntimeData. "true" means
076: * both ChannelStaticData and ChannelRuntimeData will be restricted.
077: * Any other value or the parameter not being present means
078: * only ChannelRuntimeData will be restricted. It is important
079: * to set this value to true when using subscribe-time
080: * channel parameter configuration of the xmlUri.
081: * </p>
082: * <p>
083: * As of uPortal 2.5.1, the xmlUri must match an allowed URI prefix.
084: * By default http:// and https:// URIs are allowed. If you are using the
085: * empty document or another XML file from the classpath or from the filesystem,
086: * you will need to allow a prefix to or the full path of that resource.
087: * </p>
088: */
089:
090: public class CSyndFeed extends BaseChannel implements ICacheable {
091:
092: private static final String SSL_LOCATION = "CSyndFeed/CSyndFeed.ssl";
093:
094: private SyndFeed feed = null;
095: private int cacheTimeout = 500;
096: private IUriScrutinizer uriScrutinizer;
097:
098: private int limit = 15;
099: private String xmlUri = null;
100:
101: public void renderXML(ContentHandler out) throws PortalException {
102: Document doc;
103:
104: feed = getFeed(xmlUri);
105:
106: if (feed == null) {
107: doc = buildErrorDocument();
108: } else {
109: doc = buildNewsDocument(feed, limit);
110: }
111:
112: if (log.isDebugEnabled()) {
113: log.debug("XML: "
114: + org.jasig.portal.utils.XML.serializeNode(doc));
115: }
116:
117: // Now perform the transformation
118: XSLT xslt = XSLT.getTransformer(this );
119: xslt.setXML(doc);
120: xslt.setXSL(SSL_LOCATION, runtimeData.getBrowserInfo());
121: xslt.setStylesheetParameter("baseActionURL", runtimeData
122: .getBaseActionURL());
123: xslt.setTarget(out);
124: xslt.transform();
125: }
126:
127: private static Document buildErrorDocument() {
128: Document doc;
129: doc = DocumentFactory.getNewDocument();
130: // display something if there was an error with the feed
131: Node newsNode = doc.createElement("error");
132: newsNode
133: .setTextContent("There was an error retrieving the news source. Please try back later.");
134: doc.appendChild(newsNode);
135: return doc;
136: }
137:
138: private static Document buildNewsDocument(SyndFeed feed, int limit) {
139: Document doc = DocumentFactory.getNewDocument();
140: Node newsNode = doc.createElement("news");
141: doc.appendChild(newsNode);
142:
143: Node temp = doc.createElement("desc");
144: temp.setTextContent(feed.getDescription());
145: newsNode.appendChild(temp);
146:
147: temp = doc.createElement("link");
148: // make sure link uses a safe URL scheme
149: temp.setTextContent(SaferHTMLHandler
150: .sanitizeURL(feed.getLink()));
151: newsNode.appendChild(temp);
152:
153: SyndImage image = feed.getImage();
154: if (image != null) {
155: Node imageNode = doc.createElement("image");
156: newsNode.appendChild(imageNode);
157:
158: temp = doc.createElement("url");
159: // make sure link uses a safe URL scheme
160: temp.setTextContent(SaferHTMLHandler.sanitizeURL(image
161: .getUrl()));
162: imageNode.appendChild(temp);
163:
164: temp = doc.createElement("title");
165: temp.setTextContent(image.getTitle());
166: imageNode.appendChild(temp);
167:
168: temp = doc.createElement("description");
169: temp.setTextContent(image.getDescription());
170: imageNode.appendChild(temp);
171:
172: temp = doc.createElement("link");
173: // make sure link uses a safe URL scheme
174: temp.setTextContent(SaferHTMLHandler.sanitizeURL(image
175: .getLink()));
176: imageNode.appendChild(temp);
177: }
178:
179: Node itemsNode = doc.createElement("items");
180: newsNode.appendChild(itemsNode);
181:
182: List entries = feed.getEntries();
183: int count = 0;
184: for (Iterator i = entries.iterator(); i.hasNext()
185: && count < limit; count++) {
186: SyndEntry item = (SyndEntry) i.next();
187:
188: Node itemNode = doc.createElement("item");
189: itemsNode.appendChild(itemNode);
190: Node n;
191:
192: n = doc.createElement("title");
193: itemNode.appendChild(n);
194: n.setTextContent(item.getTitle());
195:
196: n = doc.createElement("link");
197: itemNode.appendChild(n);
198: // make sure link uses a safe URL scheme
199: n.setTextContent(SaferHTMLHandler.sanitizeURL(item
200: .getLink()));
201:
202: SyndContent sc = item.getDescription();
203: if (sc != null) {
204: String text = sc.getValue();
205: n = doc.createElement("description");
206: itemNode.appendChild(n);
207:
208: // for now we always assume html: see Rome bug #26
209: // if (sc.getType().equals("text/html")){
210: Parser p = new Parser();
211: try {
212:
213: SaferHTMLHandler c = new SaferHTMLHandler(doc, n);
214: p.setContentHandler(c);
215: p.parse(new InputSource(new StringReader(text)));
216:
217: } catch (IOException e) {
218: throw new RuntimeException(e);
219: } catch (SAXException e) {
220: throw new RuntimeException(e);
221: }
222: // }
223: }
224: }
225: return doc;
226: }
227:
228: public void setStaticData(ChannelStaticData sd)
229: throws PortalException {
230: staticData = sd;
231:
232: String allowXmlUriPrefixesParam = sd
233: .getParameter("upc_allow_xmlUri_prefixes");
234: String denyXmlUriPrefixesParam = sd
235: .getParameter("upc_deny_xmlUri_prefixes");
236:
237: uriScrutinizer = PrefixUriScrutinizer.instanceFromParameters(
238: allowXmlUriPrefixesParam, denyXmlUriPrefixesParam);
239:
240: // determine whether we should restrict what URIs we accept as the xmlUri from
241: // ChannelStaticData
242: String scrutinizeXmlUriAsStaticDataString = sd
243: .getParameter("restrict_xmlUri_inStaticData");
244: boolean scrutinizeXmlUriAsStaticData = "true"
245: .equals(scrutinizeXmlUriAsStaticDataString);
246:
247: String xmlUriParam = sd.getParameter("xmlUri");
248: if (scrutinizeXmlUriAsStaticData) {
249: // apply configured xmlUri restrictions
250: setXmlUri(xmlUriParam);
251: } else {
252: // set the field directly to avoid applying xmlUri restrictions
253: xmlUri = xmlUriParam;
254: }
255:
256: String param = sd.getParameter("cacheTimeout");
257: try {
258: cacheTimeout = Integer.parseInt(param);
259: } catch (NumberFormatException e) {
260: cacheTimeout = 500;
261: }
262: param = sd.getParameter("viewNum");
263: try {
264: limit = Integer.parseInt(param);
265: } catch (NumberFormatException e) {
266: limit = 15;
267: }
268: }
269:
270: public void setRuntimeData(ChannelRuntimeData rd)
271: throws PortalException {
272: runtimeData = rd;
273: }
274:
275: private static SyndFeed getFeed(String xmlUri)
276: throws PortalException {
277: SyndFeed feed;
278: try {
279: final SyndFeedInput input = new SyndFeedInput();
280: if (xmlUri.substring(0, 7).equalsIgnoreCase("http://")
281: || xmlUri.substring(0, 8).equalsIgnoreCase(
282: "https://")) {
283: final HttpClient client = HttpClientManager
284: .getNewHTTPClient();
285: final GetMethod get = new GetMethod(xmlUri);
286: try {
287: get.setFollowRedirects(true);
288: final int rc = client.executeMethod(get);
289: if (rc != HttpStatus.SC_OK) {
290: throw new PortalException("HttpStatus:" + rc
291: + " url: " + xmlUri);
292: }
293: final InputStream in = get
294: .getResponseBodyAsStream();
295: feed = input.build(new InputStreamReader(in));
296: } finally {
297: get.releaseConnection();
298: }
299: } else {
300: URL feedUrl;
301: feedUrl = new URL(xmlUri);
302: feed = input.build(new XmlReader(feedUrl));
303: }
304: } catch (MalformedURLException e) {
305: throw new PortalException(e);
306: } catch (IllegalArgumentException e) {
307: throw new PortalException(e);
308: } catch (FeedException e) {
309: throw new PortalException(e);
310: } catch (IOException e) {
311: throw new PortalException(e);
312: } catch (BlockedUriException e) {
313: throw new PortalException(e);
314: }
315: return feed;
316: }
317:
318: /*
319: public final ChannelRuntimeProperties getRuntimeProperties() {
320: // this channel returns ChannelRuntimeProperties that specify the
321: // dynamic channel title to be the title of the feed.
322:
323: String title = null;
324: if (feed != null)
325: title = feed.getTitle();
326: return new TitledChannelRuntimeProperties(title);
327: }
328: */
329:
330: public ChannelCacheKey generateKey() {
331: ChannelCacheKey k = null;
332: k = new ChannelCacheKey();
333: k.setKeyScope(ChannelCacheKey.SYSTEM_KEY_SCOPE);
334: k.setKey("RSS:xmlUri:" + xmlUri + ",limit:" + limit);
335: k.setKeyValidity(new Long(System.currentTimeMillis()));
336: return k;
337: }
338:
339: public boolean isCacheValid(Object validity) {
340:
341: if (!(validity instanceof Long))
342: return false;
343: return (System.currentTimeMillis()
344: - ((Long) validity).longValue() < cacheTimeout * 1000);
345: }
346:
347: /**
348: * Set the URI or resource-relative-path of the XML this channel should
349: * render.
350: * @param xmlUriArg URI or local resource path to the XML this channel should render.
351: * @throws IllegalArgumentException if xmlUriArg specifies a missing resource
352: * or if the URI has bad syntax
353: * @throws BlockedUriException if the xmlUriArg is blocked for policy reasons
354: */
355: private void setXmlUri(String xmlUriArg) {
356: URL url = null;
357: try {
358: url = ResourceLoader.getResourceAsURL(this .getClass(),
359: xmlUriArg);
360: } catch (ResourceMissingException e) {
361: IllegalArgumentException iae = new IllegalArgumentException(
362: "Resource [" + xmlUriArg + "] missing.");
363: iae.initCause(e);
364: throw iae;
365: }
366:
367: String urlString = url.toExternalForm();
368: try {
369: this .uriScrutinizer.scrutinize(new URI(urlString));
370: } catch (URISyntaxException e1) {
371: throw new IllegalArgumentException("xmlUri [" + xmlUriArg
372: + "] resolved to a URI with bad syntax.");
373: }
374:
375: this.xmlUri = xmlUriArg;
376: }
377:
378: }
|