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