1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - SharedContent macro, based on the FeedReader macro 4 5 @copyright: 2008, 2012, 2013 by Paul Boddie <paul@boddie.org.uk> 6 @license: GNU GPL (v2 or later), see COPYING.txt for details. 7 """ 8 9 from DateSupport import getDateTimeFromISO8601, DateTime 10 from MoinMoin.Page import Page 11 from MoinRemoteSupport import * 12 from MoinSupport import parseMacroArguments 13 from email.utils import parsedate 14 import xml.dom.pulldom 15 16 try: 17 from cStringIO import StringIO 18 except ImportError: 19 from StringIO import StringIO 20 21 Dependencies = ["time"] 22 23 MAX_ENTRIES = 5 24 ATOM_NS = "http://www.w3.org/2005/Atom" 25 26 # Utility functions. 27 28 def text(element): 29 nodes = [] 30 for node in element.childNodes: 31 if node.nodeType == node.TEXT_NODE: 32 nodes.append(node.nodeValue) 33 return "".join(nodes) 34 35 def unescape(text): 36 return text.replace("<", "<").replace(">", ">").replace("&", "&") 37 38 def linktext(element, feed_type): 39 if feed_type == "rss": 40 return text(element) 41 else: 42 return element.getAttribute("href") 43 44 # Error classes. 45 46 class FeedError(Exception): 47 pass 48 49 class FeedMissingError(FeedError): 50 pass 51 52 class FeedContentTypeError(FeedError): 53 pass 54 55 # Entry/update classes. 56 57 class Update: 58 59 "A feed update entry." 60 61 def __init__(self): 62 self.title = None 63 self.link = None 64 self.content = None 65 self.content_type = None 66 self.updated = None 67 68 def __cmp__(self, other): 69 if self.updated is None and other.updated is not None: 70 return 1 71 elif self.updated is not None and other.updated is None: 72 return -1 73 else: 74 return cmp(self.updated, other.updated) 75 76 # Feed retrieval. 77 78 def getUpdates(request, feed_url, max_entries): 79 80 """ 81 Using the given 'request', retrieve from 'feed_url' up to the given number 82 'max_entries' of update entries. 83 84 A tuple of the form ((feed_type, channel_title, channel_link), updates) is 85 returned. 86 """ 87 88 feed_updates = [] 89 90 # Obtain the resource, using a cached version if appropriate. 91 92 max_cache_age = int(getattr(request.cfg, "moin_share_max_cache_age", "300")) 93 data = getCachedResource(request, feed_url, "MoinShare", "wiki", max_cache_age) 94 if not data: 95 raise FeedMissingError 96 97 # Interpret the cached feed. 98 99 feed = StringIO(data) 100 _url, content_type, _encoding, _metadata = getCachedResourceMetadata(feed) 101 102 if content_type not in ("application/atom+xml", "application/rss+xml", "application/xml"): 103 raise FeedContentTypeError 104 105 try: 106 # Parse each node from the feed. 107 108 channel_title = channel_link = None 109 110 feed_type = None 111 update = None 112 in_source = False 113 114 events = xml.dom.pulldom.parse(feed) 115 116 for event, value in events: 117 118 if not in_source and event == xml.dom.pulldom.START_ELEMENT: 119 tagname = value.localName 120 121 # Detect the feed type and items. 122 123 if tagname == "feed" and value.namespaceURI == ATOM_NS: 124 feed_type = "atom" 125 126 elif tagname == "rss": 127 feed_type = "rss" 128 129 # Detect items. 130 131 elif feed_type == "rss" and tagname == "item" or \ 132 feed_type == "atom" and tagname == "entry": 133 134 update = Update() 135 136 # Detect source declarations. 137 138 elif feed_type == "atom" and tagname == "source": 139 in_source = True 140 141 # Handle item elements. 142 143 elif tagname == "title": 144 events.expandNode(value) 145 if update: 146 update.title = text(value) 147 else: 148 channel_title = text(value) 149 150 elif tagname == "link": 151 events.expandNode(value) 152 if update: 153 update.link = linktext(value, feed_type) 154 else: 155 channel_link = linktext(value, feed_type) 156 157 elif feed_type == "atom" and tagname == "content": 158 events.expandNode(value) 159 if update: 160 update.content_type = value.getAttribute("type") 161 if update.content_type in ("xhtml", "application/xhtml+xml", "application/xml"): 162 update.content = value.toxml() 163 else: 164 update.content = text(value) 165 166 elif feed_type == "atom" and tagname == "updated" or \ 167 feed_type == "rss" and tagname == "pubDate": 168 events.expandNode(value) 169 170 if update: 171 if feed_type == "atom": 172 value = getDateTimeFromISO8601(text(value)) 173 else: 174 value = DateTime(parsedate(text(value))) 175 update.updated = value 176 177 elif event == xml.dom.pulldom.END_ELEMENT: 178 tagname = value.localName 179 180 if feed_type == "rss" and tagname == "item" or \ 181 feed_type == "atom" and tagname == "entry": 182 183 feed_updates.append(update) 184 185 update = None 186 187 elif feed_type == "atom" and tagname == "source": 188 in_source = False 189 190 finally: 191 feed.close() 192 193 return (feed_type, channel_title, channel_link), feed_updates 194 195 # The macro itself. 196 197 def execute(macro, args): 198 request = macro.request 199 fmt = macro.formatter 200 _ = request.getText 201 202 feed_urls = [] 203 show_content = None 204 max_entries = None 205 206 for arg, value in parseMacroArguments(args): 207 if arg == "url": 208 feed_urls.append(value) 209 elif arg == "show": 210 show_content = value in ("true", "True", "yes") 211 elif arg == "limit": 212 try: 213 max_entries = int(value) 214 except ValueError: 215 return fmt.text(_("SharedContent: limit must be set to the maximum number of entries to be shown")) 216 217 if not feed_urls: 218 return fmt.text(_("SharedContent: a feed URL must be specified")) 219 220 show_content = show_content or False 221 max_entries = max_entries or MAX_ENTRIES 222 223 # Retrieve updates from feeds, classifying them as missing or bad and 224 # excluding them if appropriate. 225 226 updates = [] 227 feeds = [] 228 missing = [] 229 bad_content = [] 230 231 for feed_url in feed_urls: 232 try: 233 feed_info, feed_updates = getUpdates(request, feed_url, max_entries) 234 updates += feed_updates 235 feeds.append((feed_url, feed_info)) 236 except FeedMissingError: 237 missing.append(feed_url) 238 except FeedContentTypeError: 239 bad_content.append(feed_url) 240 241 output = [] 242 append = output.append 243 244 # Show the updates. 245 246 if not show_content: 247 append(fmt.bullet_list(on=1)) 248 249 # NOTE: Permit configurable sorting. 250 251 updates.sort() 252 updates.reverse() 253 254 # Truncate the number of updates to the maximum number. 255 256 updates = updates[:max_entries] 257 258 for update in updates: 259 260 # Emit content where appropriate. 261 # NOTE: HTML and XHTML should be sanitised. 262 263 if show_content: 264 append(fmt.div(on=1, css_class="moinshare-update")) 265 append(fmt.div(on=1, css_class="moinshare-content")) 266 if update.content: 267 if update.content_type in ("html", "text/html"): 268 append(fmt.rawHTML(unescape(update.content))) 269 elif update.content_type in ("xhtml", "application/xhtml+xml"): 270 append(fmt.rawHTML(update.content)) 271 elif update.content_type in ("text", "text/plain"): 272 append(fmt.text(update.content)) 273 append(fmt.div(on=0)) 274 append(fmt.div(on=1, css_class="moinshare-date")) 275 append(fmt.text(str(update.updated))) 276 append(fmt.div(on=0)) 277 append(fmt.div(on=0)) 278 279 # Or emit title and link information for items. 280 281 elif update.title and update.link: 282 append(fmt.listitem(on=1, css_class="moinshare-update")) 283 append(fmt.url(on=1, href=update.link)) 284 append(fmt.icon('www')) 285 append(fmt.text(" " + update.title)) 286 append(fmt.url(on=0)) 287 append(fmt.listitem(on=0)) 288 289 if not show_content: 290 append(fmt.bullet_list(on=0)) 291 292 # Show the feeds. 293 294 for feed_url, (feed_type, channel_title, channel_link) in feeds: 295 if channel_title and channel_link: 296 append(fmt.paragraph(on=1, css_class="moinshare-feed")) 297 append(fmt.url(on=1, href=channel_link)) 298 append(fmt.text(channel_title)) 299 append(fmt.url(on=0)) 300 append(fmt.text(" ")) 301 append(fmt.url(on=1, href=feed_url)) 302 append(fmt.icon('rss')) 303 append(fmt.url(on=0)) 304 append(fmt.paragraph(on=0)) 305 306 # Show errors. 307 308 for feed_url in missing: 309 append(fmt.paragraph(on=1, css_class="moinshare-missing-feed-error")) 310 append(fmt.text(_("SharedContent: updates could not be retrieved for %s") % feed_url)) 311 append(fmt.paragraph(on=0)) 312 313 for feed_url in bad_content: 314 append(fmt.paragraph(on=1, css_class="moinshare-content-type-feed-error")) 315 return fmt.text(_("SharedContent: updates for %s were not provided in Atom or RSS format") % feed_url) 316 append(fmt.paragraph(on=0)) 317 318 return ''.join(output) 319 320 # vim: tabstop=4 expandtab shiftwidth=4