1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - MoinShare library 4 5 @copyright: 2011, 2012, 2013 by Paul Boddie <paul@boddie.org.uk> 6 @copyright: 2003-2006 Edgewall Software 7 @copyright: 2006 MoinMoin:AlexanderSchremmer 8 @license: GNU GPL (v2 or later), see COPYING.txt for details. 9 """ 10 11 from ContentTypeSupport import getContentPreferences 12 from DateSupport import getCurrentTime, getDateTimeFromRFC2822 13 from MoinSupport import * 14 from MoinMoin.support.htmlmarkup import HTMLSanitizer, Markup 15 from MoinMoin import wikiutil 16 from email.parser import Parser 17 18 try: 19 from cStringIO import StringIO 20 except ImportError: 21 from StringIO import StringIO 22 23 _getFragments = getFragments 24 25 __version__ = "0.1" 26 27 # More Moin 1.9 compatibility functions. 28 29 def has_member(request, groupname, username): 30 if hasattr(request.dicts, "has_member"): 31 return request.dicts.has_member(groupname, username) 32 else: 33 return username in request.dicts.get(groupname, []) 34 35 # Fragments employ a "moinshare" attribute. 36 37 fragment_attribute = "moinshare" 38 39 def getFragments(s): 40 41 "Return all fragments in 's' having the MoinShare fragment attribute." 42 43 fragments = [] 44 for format, attributes, body in _getFragments(s): 45 if attributes.has_key(fragment_attribute): 46 fragments.append((format, attributes, body)) 47 return fragments 48 49 def getOutputTypes(request, format): 50 51 """ 52 Using the 'request' and the 'format' of a fragment, return the media types 53 available for the fragment. 54 """ 55 56 # This uses an extended parser API method if available. 57 58 parser = getParserClass(request, format) 59 if hasattr(parser, "getOutputTypes"): 60 return parser.getOutputTypes() 61 else: 62 return ["text/html"] 63 64 def getPreferredOutputTypes(request, mimetypes): 65 66 """ 67 Using the 'request', perform content negotiation, obtaining mimetypes common 68 to the fragment (given by 'mimetypes') and the client (found in the Accept 69 header). 70 """ 71 72 accept = getHeader(request, "Accept", "HTTP") 73 if accept: 74 prefs = getContentPreferences(accept) 75 return prefs.get_preferred_types(mimetypes) 76 else: 77 return mimetypes 78 79 def getUpdatedTime(metadata): 80 81 """ 82 Return the last updated time based on the given 'metadata', using the 83 current time if no explicit last modified time is specified. 84 """ 85 86 # NOTE: We could attempt to get the last edit time of a fragment. 87 88 latest_timestamp = metadata.get("last-modified") 89 if latest_timestamp: 90 return latest_timestamp 91 else: 92 return getCurrentTime() 93 94 def getUpdateSources(request, sources_page): 95 96 """ 97 Using the 'request', return the update sources defined on the given 98 'sources_page'. 99 """ 100 101 # Remote sources are accessed via dictionary page definitions. 102 103 return getWikiDict(sources_page, request) 104 105 # Entry/update classes. 106 107 class Update: 108 109 "A feed update entry." 110 111 def __init__(self): 112 self.title = None 113 self.link = None 114 self.content = None 115 self.content_type = None 116 self.updated = None 117 118 # Page-related attributes. 119 120 self.fragment = None 121 self.preferred = None 122 123 # Message-related attributes. 124 125 self.message_number = None 126 self.parts = None 127 128 # Message- and page-related attributes. 129 130 self.page = None 131 132 def __cmp__(self, other): 133 if self.updated is None and other.updated is not None: 134 return 1 135 elif self.updated is not None and other.updated is None: 136 return -1 137 else: 138 return cmp(self.updated, other.updated) 139 140 # Update retrieval from pages. 141 142 def getUpdatesFromPage(page, request): 143 144 """ 145 Get updates from the given 'page' using the 'request'. A list of update 146 objects is returned. 147 """ 148 149 updates = [] 150 151 # NOTE: Use the updated datetime from the page for updates. 152 # NOTE: The published and updated details would need to be deduced from 153 # NOTE: the page history instead of being taken from the page as a whole. 154 155 metadata = getMetadata(page) 156 updated = getUpdatedTime(metadata) 157 158 # Get the fragment regions for the page. 159 160 for n, (format, attributes, body) in enumerate(getFragments(page.get_raw_body())): 161 162 update = Update() 163 164 # Produce a fragment identifier. 165 # NOTE: Choose a more robust identifier where none is explicitly given. 166 167 update.fragment = attributes.get("fragment", str(n)) 168 update.title = attributes.get("summary", "Update #%d" % n) 169 170 # Get the preferred content types available for the fragment. 171 172 update.preferred = getPreferredOutputTypes(request, getOutputTypes(request, format)) 173 174 # Try and obtain some suitable content for the entry. 175 # NOTE: Could potentially get a summary for the fragment. 176 177 update.content = None 178 179 if "text/html" in update.preferred: 180 parser_cls = getParserClass(request, format) 181 parser = parser_cls(body, request) 182 183 if format == "html": 184 update.content = body 185 elif hasattr(parser, "formatForOutputType"): 186 s = StringIO() 187 parser.formatForOutputType("text/html", write=s.write) 188 update.content = s.getvalue() 189 else: 190 fmt = request.html_formatter 191 fmt.setPage(page) 192 update.content = formatText(body, request, fmt, parser_cls) 193 194 update.content_type = "text/html" 195 196 update.page = page 197 update.link = page.url(request) 198 update.updated = updated 199 200 updates.append(update) 201 202 return updates 203 204 # Update retrieval from message stores. 205 206 def getUpdatesFromStore(page, request): 207 208 """ 209 Get updates from the message store associated with the given 'page' using 210 the 'request'. A list of update objects is returned. 211 """ 212 213 updates = [] 214 215 metadata = getMetadata(page) 216 updated = getUpdatedTime(metadata) 217 218 store = ItemStore(page, "messages", "message-locks") 219 220 for n, message_text in enumerate(iter(store)): 221 222 update = Update() 223 message = Parser().parse(StringIO(message_text)) 224 225 # Produce a fragment identifier. 226 227 update.fragment = update.updated = getDateTimeFromRFC2822(message.get("date")) 228 update.title = message.get("subject", "Update #%d" % n) 229 230 update.page = page 231 update.message_number = n 232 233 # Determine whether the message has several representations. 234 235 # For a single part, use it as the update content. 236 237 if not message.is_multipart(): 238 update.content = message.get_payload() 239 update.content_type = message.get_content_type() 240 241 # For a collection of related parts, use the first as the update content 242 # and assume that the formatter will reference the other parts. 243 244 elif message.get_content_subtype() == "related": 245 main_part = message.get_payload()[0] 246 update.content = main_part.get_payload() 247 update.content_type = main_part.get_content_type() 248 249 # Otherwise, just obtain the parts for separate display. 250 251 else: 252 update.parts = message.get_payload() 253 update.content_type = message.get_content_type() 254 255 updates.append(update) 256 257 return updates 258 259 # Source management. 260 261 def getUpdateSources(pagename, request): 262 263 "Return the update sources from the given 'pagename' using the 'request'." 264 265 sources = {} 266 267 source_definitions = getWikiDict(pagename, request) 268 269 if source_definitions: 270 for name, value in source_definitions.items(): 271 sources[name] = getSourceParameters(value) 272 273 return sources 274 275 def getSourceParameters(source_definition): 276 277 "Return the parameters from the given 'source_definition' string." 278 279 parameters = {} 280 unqualified = ("type", "location") 281 282 for arg in source_definition.split(): 283 try: 284 argname, argvalue = arg.split("=", 1) 285 286 # Detect unlikely parameter names. 287 288 if not argname.isalpha(): 289 raise ValueError 290 291 parameters[argname] = argvalue 292 293 # Unqualified parameters are assumed to be one of a recognised set. 294 295 except ValueError: 296 for argname in unqualified: 297 if not parameters.has_key(argname): 298 parameters[argname] = arg 299 break 300 301 return parameters 302 303 # HTML parsing support. 304 305 class IncomingHTMLSanitizer(HTMLSanitizer): 306 307 "An HTML parser that rewrites references to attachments." 308 309 def __init__(self, out, request, page, message_number): 310 HTMLSanitizer.__init__(self, out) 311 self.request = request 312 self.message_number = message_number 313 self.page = page 314 315 def rewrite_reference(self, ref): 316 if ref.startswith("cid:"): 317 part = ref[len("cid:"):] 318 action_link = self.page.url(self.request, { 319 "action" : "ReadMessage", "doit" : "1", 320 "message" : self.message_number, "part" : part 321 }) 322 return action_link 323 else: 324 return ref 325 326 def handle_starttag(self, tag, attrs): 327 new_attrs = [] 328 for attrname, attrvalue in attrs: 329 if attrname in self.uri_attrs: 330 new_attrs.append((attrname, self.rewrite_reference(attrvalue))) 331 else: 332 new_attrs.append((attrname, attrvalue)) 333 HTMLSanitizer.handle_starttag(self, tag, new_attrs) 334 335 class IncomingMarkup(Markup): 336 337 "A special markup processor for incoming HTML." 338 339 def sanitize(self, request, page, message_number): 340 out = StringIO() 341 sanitizer = IncomingHTMLSanitizer(out, request, page, message_number) 342 sanitizer.feed(self.stripentities(keepxmlentities=True)) 343 return IncomingMarkup(out.getvalue()) 344 345 class IncomingHTMLParser: 346 347 "Filters and rewrites incoming HTML content." 348 349 def __init__(self, raw, request, **kw): 350 self.raw = raw 351 self.request = request 352 self.message_number = None 353 self.page = None 354 355 def format(self, formatter, **kw): 356 357 "Send the text." 358 359 try: 360 self.request.write(formatter.rawHTML(IncomingMarkup(self.raw).sanitize(self.request, self.page, self.message_number))) 361 except HTMLParseError, e: 362 self.request.write(formatter.sysmsg(1) + 363 formatter.text(u'HTML parsing error: %s in "%s"' % (e.msg, 364 self.raw.splitlines()[e.lineno - 1].strip())) + 365 formatter.sysmsg(0)) 366 367 class MakeIncomingHTMLParser: 368 369 "A class that makes parsers configured for messages." 370 371 def __init__(self, page, message_number): 372 373 "Initialise with state that is used to configure instantiated parsers." 374 375 self.message_number = message_number 376 self.page = page 377 378 def __call__(self, *args, **kw): 379 parser = IncomingHTMLParser(*args, **kw) 380 parser.message_number = self.message_number 381 parser.page = self.page 382 return parser 383 384 def get_make_parser(page, message_number): 385 386 """ 387 Return a callable that will return a parser configured for the message from 388 the given 'page' with the given 'message_number'. 389 """ 390 391 return MakeIncomingHTMLParser(page, message_number) 392 393 # vim: tabstop=4 expandtab shiftwidth=4