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, getParsersForContentType, formatText 13 from MoinShare import getUpdateSources, getUpdatesFromPage, \ 14 getUpdatesFromStore, Update, get_make_parser 15 from email.utils import parsedate 16 import xml.dom.pulldom 17 18 try: 19 from cStringIO import StringIO 20 except ImportError: 21 from StringIO import StringIO 22 23 Dependencies = ["time"] 24 25 MAX_ENTRIES = 5 26 ATOM_NS = "http://www.w3.org/2005/Atom" 27 28 # Utility functions. 29 30 def text(element): 31 nodes = [] 32 for node in element.childNodes: 33 if node.nodeType == node.TEXT_NODE: 34 nodes.append(node.nodeValue) 35 return "".join(nodes) 36 37 def children(element): 38 nodes = [] 39 for node in element.childNodes: 40 nodes.append(node.toxml()) 41 return "".join(nodes) 42 43 def unescape(text): 44 return text.replace("<", "<").replace(">", ">").replace("&", "&") 45 46 def linktext(element, feed_type): 47 if feed_type == "rss": 48 return text(element) 49 else: 50 return element.getAttribute("href") 51 52 def need_content(show_content, tagname): 53 return show_content in ("content", "description") and tagname in ("content", "description") 54 55 # Error classes. 56 57 class FeedError(Exception): 58 pass 59 60 class FeedMissingError(FeedError): 61 pass 62 63 class FeedContentTypeError(FeedError): 64 pass 65 66 # Feed retrieval. 67 68 def getUpdates(request, feed_url, max_entries, show_content): 69 70 """ 71 Using the given 'request', retrieve from 'feed_url' up to the given number 72 'max_entries' of update entries. The 'show_content' parameter can indicate 73 that a "summary" is to be obtained for each update, that the "content" of 74 each update is to be obtained (falling back to a summary if no content is 75 provided), or no content (indicated by a false value) is to be obtained. 76 77 A tuple of the form ((feed_type, channel_title, channel_link), updates) is 78 returned. 79 """ 80 81 feed_updates = [] 82 83 # Obtain the resource, using a cached version if appropriate. 84 85 max_cache_age = int(getattr(request.cfg, "moin_share_max_cache_age", "300")) 86 data = getCachedResource(request, feed_url, "MoinShare", "wiki", max_cache_age) 87 if not data: 88 raise FeedMissingError 89 90 # Interpret the cached feed. 91 92 feed = StringIO(data) 93 _url, content_type, _encoding, _metadata = getCachedResourceMetadata(feed) 94 95 if content_type not in ("application/atom+xml", "application/rss+xml", "application/xml"): 96 raise FeedContentTypeError 97 98 try: 99 # Parse each node from the feed. 100 101 channel_title = channel_link = None 102 103 feed_type = None 104 update = None 105 in_source = False 106 107 events = xml.dom.pulldom.parse(feed) 108 109 for event, value in events: 110 111 if not in_source and event == xml.dom.pulldom.START_ELEMENT: 112 tagname = value.localName 113 114 # Detect the feed type and items. 115 116 if tagname == "feed" and value.namespaceURI == ATOM_NS: 117 feed_type = "atom" 118 119 elif tagname == "rss": 120 feed_type = "rss" 121 122 # Detect items. 123 124 elif feed_type == "rss" and tagname == "item" or \ 125 feed_type == "atom" and tagname == "entry": 126 127 update = Update() 128 129 # Detect source declarations. 130 131 elif feed_type == "atom" and tagname == "source": 132 in_source = True 133 134 # Handle item elements. 135 136 elif tagname == "title": 137 events.expandNode(value) 138 if update: 139 update.title = text(value) 140 else: 141 channel_title = text(value) 142 143 elif tagname == "link": 144 events.expandNode(value) 145 if update: 146 update.link = linktext(value, feed_type) 147 else: 148 channel_link = linktext(value, feed_type) 149 150 elif show_content and ( 151 feed_type == "atom" and tagname in ("content", "summary") or 152 feed_type == "rss" and tagname == "description"): 153 154 events.expandNode(value) 155 156 # Obtain content where requested or, failing that, a 157 # summary. 158 159 if update and (need_content(show_content, tagname) or tagname == "summary" and not update.content): 160 if feed_type == "atom": 161 update.content_type = value.getAttribute("type") or "text" 162 163 # Normalise the content types and extract the 164 # content. 165 166 if update.content_type in ("xhtml", "application/xhtml+xml", "application/xml"): 167 update.content = children(value) 168 update.content_type = "application/xhtml+xml" 169 elif update.content_type in ("html", "text/html"): 170 update.content = text(value) 171 update.content_type = "text/html" 172 else: 173 update.content = text(value) 174 update.content_type = "text/plain" 175 else: 176 update.content_type = "text/html" 177 update.content = text(value) 178 179 elif feed_type == "atom" and tagname == "updated" or \ 180 feed_type == "rss" and tagname == "pubDate": 181 182 events.expandNode(value) 183 184 if update: 185 if feed_type == "atom": 186 value = getDateTimeFromISO8601(text(value)) 187 else: 188 value = DateTime(parsedate(text(value))) 189 update.updated = value 190 191 elif event == xml.dom.pulldom.END_ELEMENT: 192 tagname = value.localName 193 194 if feed_type == "rss" and tagname == "item" or \ 195 feed_type == "atom" and tagname == "entry": 196 197 feed_updates.append(update) 198 199 update = None 200 201 elif feed_type == "atom" and tagname == "source": 202 in_source = False 203 204 finally: 205 feed.close() 206 207 return (feed_type, channel_title, channel_link), feed_updates 208 209 # The macro itself. 210 211 def execute(macro, args): 212 request = macro.request 213 fmt = macro.formatter 214 _ = request.getText 215 216 source_pages = [] 217 show_content = None 218 max_entries = None 219 220 for arg, value in parseMacroArguments(args): 221 if arg == "sources": 222 source_pages.append(value) 223 elif arg == "show": 224 show_content = value.lower() 225 elif arg == "limit": 226 try: 227 max_entries = int(value) 228 except ValueError: 229 return fmt.text(_("SharedContent: limit must be set to the maximum number of entries to be shown")) 230 231 if not source_pages: 232 return fmt.text(_("SharedContent: at least one sources page must be specified")) 233 234 sources = {} 235 236 for source_page in source_pages: 237 sources.update(getUpdateSources(source_page, request)) 238 239 if not sources: 240 return fmt.text(_("SharedContent: at least one update source must be specified")) 241 242 show_content = show_content or False 243 max_entries = max_entries or MAX_ENTRIES 244 245 # Retrieve updates, classifying them as missing or bad and excluding them if 246 # appropriate. 247 248 updates = [] 249 feeds = [] 250 unspecified = [] 251 missing = [] 252 bad_content = [] 253 254 for source_name, source_parameters in sources.items(): 255 location = source_parameters.get("location") 256 if not location: 257 unspecified.append(source_name) 258 continue 259 260 show_feed_content = source_parameters.get("show", show_content) 261 try: 262 max_entries_for_feed = int(source_parameters["limit"]) 263 except (KeyError, ValueError): 264 max_entries_for_feed = None 265 266 # Retrieve updates from feeds. 267 268 if source_parameters.get("type") == "url": 269 try: 270 feed_info, feed_updates = getUpdates(request, location, max_entries_for_feed, show_feed_content) 271 updates += feed_updates 272 feeds.append((location, feed_info)) 273 except FeedMissingError: 274 missing.append(location) 275 except FeedContentTypeError: 276 bad_content.append(location) 277 278 # Retrieve updates from pages. 279 280 elif source_parameters.get("type") == "page": 281 page = Page(request, location) 282 updates += getUpdatesFromPage(page, request) 283 284 # Build feed-equivalent information for the update source. 285 286 feeds.append(( 287 page.url(request, {"action" : "SharedUpdates", "doit" : "1"}), ( 288 "internal", _("Updates from page %s") % location, 289 page.url(request) 290 ) 291 )) 292 293 # Retrieve updates from message stores. 294 295 elif source_parameters.get("type") == "store": 296 page = Page(request, location) 297 updates += getUpdatesFromStore(page, request) 298 299 # Build feed-equivalent information for the update source. 300 301 feeds.append(( 302 page.url(request, {"action" : "SharedUpdates", "store" : "1", "doit" : "1"}), ( 303 "internal", _("Updates from message store on page %s") % location, 304 page.url(request) 305 ) 306 )) 307 308 # Prepare the output. 309 310 output = [] 311 append = output.append 312 313 # Show the updates. 314 315 if not show_content: 316 append(fmt.bullet_list(on=1)) 317 318 # NOTE: Permit configurable sorting. 319 320 updates.sort() 321 updates.reverse() 322 323 # Truncate the number of updates to the maximum number. 324 325 updates = updates[:max_entries] 326 327 for update in updates: 328 329 # Emit content where appropriate. 330 # NOTE: Some control over the HTML and XHTML should be exercised. 331 332 if show_content: 333 append(fmt.div(on=1, css_class="moinshare-update")) 334 append(fmt.div(on=1, css_class="moinshare-content")) 335 336 if update.content: 337 if update.content_type == "text/html" and update.message_number is not None: 338 parsers = [get_make_parser(update.page, update.message_number)] 339 else: 340 parsers = getParsersForContentType(request.cfg, update.content_type) 341 342 if parsers: 343 for parser_cls in parsers: 344 append(formatText(update.content, request, fmt, parser_cls=parser_cls)) 345 break 346 else: 347 append(fmt.text(_("Update cannot be shown for content of type %s.") % update.content_type)) 348 349 append(fmt.div(on=0)) 350 append(fmt.div(on=1, css_class="moinshare-date")) 351 append(fmt.text(str(update.updated))) 352 append(fmt.div(on=0)) 353 append(fmt.div(on=0)) 354 355 # Or emit title and link information for items. 356 357 elif update.title and update.link: 358 append(fmt.listitem(on=1, css_class="moinshare-update")) 359 append(fmt.url(on=1, href=update.link)) 360 append(fmt.icon('www')) 361 append(fmt.text(" " + update.title)) 362 append(fmt.url(on=0)) 363 append(fmt.listitem(on=0)) 364 365 if not show_content: 366 append(fmt.bullet_list(on=0)) 367 368 # Show the feeds. 369 370 for feed_url, (feed_type, channel_title, channel_link) in feeds: 371 if channel_title and channel_link: 372 append(fmt.paragraph(on=1, css_class="moinshare-feed")) 373 append(fmt.url(on=1, href=channel_link)) 374 append(fmt.text(channel_title)) 375 append(fmt.url(on=0)) 376 append(fmt.text(" ")) 377 append(fmt.url(on=1, href=feed_url)) 378 append(fmt.icon('rss')) 379 append(fmt.url(on=0)) 380 append(fmt.paragraph(on=0)) 381 382 # Show errors. 383 384 for feed_url in missing: 385 append(fmt.paragraph(on=1, css_class="moinshare-missing-feed-error")) 386 append(fmt.text(_("SharedContent: updates could not be retrieved for %s") % feed_url)) 387 append(fmt.paragraph(on=0)) 388 389 for feed_url in bad_content: 390 append(fmt.paragraph(on=1, css_class="moinshare-content-type-feed-error")) 391 return fmt.text(_("SharedContent: updates for %s were not provided in Atom or RSS format") % feed_url) 392 append(fmt.paragraph(on=0)) 393 394 return ''.join(output) 395 396 # vim: tabstop=4 expandtab shiftwidth=4