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 MoinMessage import GPG, is_encrypted, is_signed, MoinMessageError 15 from MoinMessageSupport import get_homedir 16 from MoinMoin.support.htmlmarkup import HTMLParseError, HTMLSanitizer, Markup 17 from MoinMoin import wikiutil 18 from email.parser import Parser 19 from codecs import getwriter 20 21 try: 22 from cStringIO import StringIO 23 except ImportError: 24 from StringIO import StringIO 25 26 _getFragments = getFragments 27 28 __version__ = "0.1" 29 30 # More Moin 1.9 compatibility functions. 31 32 def has_member(request, groupname, username): 33 if hasattr(request.dicts, "has_member"): 34 return request.dicts.has_member(groupname, username) 35 else: 36 return username in request.dicts.get(groupname, []) 37 38 # Fragments employ a "moinshare" attribute. 39 40 fragment_attribute = "moinshare" 41 42 def getFragments(s): 43 44 "Return all fragments in 's' having the MoinShare fragment attribute." 45 46 fragments = [] 47 for format, attributes, body in _getFragments(s): 48 if attributes.has_key(fragment_attribute): 49 fragments.append((format, attributes, body)) 50 return fragments 51 52 def getPreferredOutputTypes(request, mimetypes): 53 54 """ 55 Using the 'request', perform content negotiation, obtaining mimetypes common 56 to the fragment (given by 'mimetypes') and the client (found in the Accept 57 header). 58 """ 59 60 accept = getHeader(request, "Accept", "HTTP") 61 if accept: 62 prefs = getContentPreferences(accept) 63 return prefs.get_preferred_types(mimetypes) 64 else: 65 return mimetypes 66 67 def getUpdatedTime(metadata): 68 69 """ 70 Return the last updated time based on the given 'metadata', using the 71 current time if no explicit last modified time is specified. 72 """ 73 74 # NOTE: We could attempt to get the last edit time of a fragment. 75 76 latest_timestamp = metadata.get("last-modified") 77 if latest_timestamp: 78 return latest_timestamp 79 else: 80 return getCurrentTime() 81 82 # Entry/update classes. 83 84 class Update: 85 86 "A feed update entry." 87 88 def __init__(self): 89 self.title = None 90 self.link = None 91 self.content = None 92 self.content_type = None 93 self.updated = None 94 95 # Page-related attributes. 96 97 self.fragment = None 98 self.preferred = None 99 100 # Message-related attributes. 101 102 self.message_number = None 103 self.parts = None 104 105 # Message- and page-related attributes. 106 107 self.page = None 108 109 # Identification. 110 111 self.path = [] 112 113 def unique_id(self): 114 return "moinshare-tab-%s-%s" % (self.message_number, "-".join(map(str, self.path))) 115 116 def __cmp__(self, other): 117 if self.updated is None and other.updated is not None: 118 return 1 119 elif self.updated is not None and other.updated is None: 120 return -1 121 else: 122 return cmp(self.updated, other.updated) 123 124 def copy(self, part_number=None): 125 update = Update() 126 update.title = self.title 127 update.link = self.link 128 update.updated = self.updated 129 update.fragment = self.fragment 130 update.preferred = self.preferred 131 update.message_number = self.message_number 132 update.page = self.page 133 update.path = self.path[:] 134 if part_number is not None: 135 update.path.append(part_number) 136 return update 137 138 # Update retrieval from pages. 139 140 def getUpdatesFromPage(page, request): 141 142 """ 143 Get updates from the given 'page' using the 'request'. A list of update 144 objects is returned. 145 """ 146 147 updates = [] 148 149 # NOTE: Use the updated datetime from the page for updates. 150 # NOTE: The published and updated details would need to be deduced from 151 # NOTE: the page history instead of being taken from the page as a whole. 152 153 metadata = getMetadata(page) 154 updated = getUpdatedTime(metadata) 155 156 # Get the fragment regions for the page. 157 158 for n, (format, attributes, body) in enumerate(getFragments(page.get_raw_body())): 159 160 update = Update() 161 162 # Produce a fragment identifier. 163 # NOTE: Choose a more robust identifier where none is explicitly given. 164 165 update.fragment = attributes.get("fragment", str(n)) 166 update.title = attributes.get("summary", "Update #%d" % n) 167 168 # Get the preferred content types available for the fragment. 169 170 update.preferred = getPreferredOutputTypes(request, getOutputTypes(request, format)) 171 172 # Try and obtain some suitable content for the entry. 173 # NOTE: Could potentially get a summary for the fragment. 174 175 update.content = None 176 177 if "text/html" in update.preferred: 178 parser_cls = getParserClass(request, format) 179 180 if format == "html": 181 update.content = body 182 elif hasattr(parser_cls, "formatForOutputType"): 183 update.content = formatTextForOutputType(body, request, parser_cls, "text/html") 184 else: 185 fmt = request.html_formatter 186 fmt.setPage(page) 187 update.content = formatText(body, request, fmt, parser_cls) 188 189 update.content_type = "text/html" 190 191 update.page = page 192 193 # NOTE: The anchor would be supported in the page, but this requires 194 # NOTE: formatter modifications for the regions providing updates. 195 196 update.link = page.url(request, anchor=update.fragment) 197 update.updated = updated 198 199 updates.append(update) 200 201 return updates 202 203 # Update retrieval from message stores. 204 205 def getUpdatesFromStore(page, request): 206 207 """ 208 Get updates from the message store associated with the given 'page' using 209 the 'request'. A list of update objects is returned. 210 """ 211 212 updates = [] 213 214 metadata = getMetadata(page) 215 updated = getUpdatedTime(metadata) 216 217 store = ItemStore(page, "messages", "message-locks") 218 219 for n, message_text in enumerate(iter(store)): 220 update = getUpdateFromMessageText(message_text, n, request) 221 update.page = page 222 updates.append(update) 223 224 return updates 225 226 def getUpdateFromMessageText(message_text, message_number, request): 227 228 "Return an update for the given 'message_text' and 'message_number'." 229 230 update = Update() 231 message = Parser().parse(StringIO(message_text)) 232 233 # Produce a fragment identifier. 234 235 update.fragment = update.updated = getDateTimeFromRFC2822(message.get("date")) 236 update.title = message.get("subject", "Update #%d" % message_number) 237 238 update.message_number = message_number 239 240 update.content, update.content_type, update.parts = getUpdateContentFromPart(message, request) 241 return update 242 243 def getUpdateContentFromPart(part, request): 244 245 """ 246 Return decoded content, the content type and any subparts in a tuple for a 247 given 'part'. 248 """ 249 250 # Determine whether the part has several representations. 251 252 # For a single part, use it as the update content. 253 254 if not part.is_multipart(): 255 content, content_type = getPartContent(part) 256 return content, content_type, None 257 258 # For a collection of related parts, use the first as the update content 259 # and assume that the formatter will reference the other parts. 260 261 elif part.get_content_subtype() == "related": 262 main_part = part.get_payload()[0] 263 content, content_type = getPartContent(main_part) 264 return content, content_type, [main_part] 265 266 # Encrypted content cannot be meaningfully separated. 267 268 elif part.get_content_subtype() == "encrypted": 269 try: 270 part = getDecryptedParts(part, request) 271 return getUpdateContentFromPart(part, request) 272 except MoinMessageError: 273 return None, part.get_content_type(), part.get_payload() 274 275 # Otherwise, just obtain the parts for separate display. 276 277 else: 278 return None, part.get_content_type(), part.get_payload() 279 280 def getDecryptedParts(part, request): 281 282 "Decrypt the given 'part', returning the decoded content." 283 284 homedir = get_homedir(request) 285 gpg = GPG(homedir) 286 287 # Decrypt the part. 288 289 if is_encrypted(part): 290 text = gpg.decryptMessage(part) 291 part = Parser().parse(StringIO(text)) 292 293 # Extract any signature details. 294 # NOTE: Incorporate the signature into the output. 295 296 if is_signed(part): 297 result = gpg.verifyMessage(part) 298 if result: 299 fingerprint, identity, content = result 300 return content 301 302 return part 303 304 def getPartContent(part): 305 306 "Decode the 'part', returning the decoded payload and the content type." 307 308 charset = part.get_content_charset() 309 payload = part.get_payload(decode=True) 310 return (charset and unicode(payload, charset) or payload), part.get_content_type() 311 312 def getUpdateFromPart(parent, part, part_number, request): 313 314 "Using the 'parent' update, return an update object for the given 'part'." 315 316 update = parent.copy(part_number) 317 update.content, update.content_type, update.parts = getUpdateContentFromPart(part, request) 318 return update 319 320 def getUpdatesForFormatting(update, request): 321 322 "Get a list of updates for formatting given 'update'." 323 324 updates = [] 325 326 # Handle multipart/alternative and other non-related multiparts. 327 328 if update.parts: 329 for n, part in enumerate(update.parts): 330 update_part = getUpdateFromPart(update, part, n, request) 331 updates += getUpdatesForFormatting(update_part, request) 332 else: 333 updates.append(update) 334 335 return updates 336 337 # Update formatting. 338 339 def getFormattedUpdate(update, request, fmt): 340 341 """ 342 Return the formatted form of the given 'update' using the given 'request' 343 and 'fmt'. 344 """ 345 346 # NOTE: Some control over the HTML and XHTML should be exercised. 347 348 if update.content: 349 if update.content_type == "text/html" and update.message_number is not None: 350 parsers = [get_make_parser(update.page, update.message_number)] 351 else: 352 parsers = getParsersForContentType(request.cfg, update.content_type) 353 354 if parsers: 355 for parser_cls in parsers: 356 if hasattr(parser_cls, "formatForOutputType"): 357 return formatTextForOutputType(update.content, request, parser_cls, "text/html") 358 else: 359 return formatText(update.content, request, fmt, parser_cls=parser_cls) 360 break 361 else: 362 return None 363 else: 364 return None 365 366 def formatUpdate(update, request, fmt): 367 368 "Format the given 'update' using the given 'request' and 'fmt'." 369 370 result = [] 371 append = result.append 372 373 updates = getUpdatesForFormatting(update, request) 374 single = len(updates) == 1 375 376 # Format some navigation tabs. 377 378 if not single: 379 append(fmt.div(on=1, css_class="moinshare-alternatives")) 380 381 first = True 382 383 for update_part in updates: 384 append(fmt.url(1, "#%s" % update_part.unique_id())) 385 append(fmt.text(update_part.content_type)) 386 append(fmt.url(0)) 387 388 first = False 389 390 append(fmt.div(on=0)) 391 392 # Format the content. 393 394 first = True 395 396 for update_part in updates: 397 398 # Encapsulate each alternative if many exist. 399 400 if not single: 401 css_class = first and "moinshare-default" or "moinshare-other" 402 append(fmt.div(on=1, css_class="moinshare-alternative %s" % css_class, id=update_part.unique_id())) 403 404 # Include the content. 405 406 append(formatUpdatePart(update_part, request, fmt)) 407 408 if not single: 409 append(fmt.div(on=0)) 410 411 first = False 412 413 return "".join(result) 414 415 def formatUpdatePart(update, request, fmt): 416 417 "Format the given 'update' using the given 'request' and 'fmt'." 418 419 _ = request.getText 420 421 result = [] 422 append = result.append 423 424 # Encapsulate the content. 425 426 append(fmt.div(on=1, css_class="moinshare-content")) 427 text = getFormattedUpdate(update, request, fmt) 428 if text: 429 append(text) 430 else: 431 append(fmt.text(_("Update cannot be shown for content of type %s.") % update.content_type)) 432 append(fmt.div(on=0)) 433 434 return "".join(result) 435 436 # Source management. 437 438 def getUpdateSources(pagename, request): 439 440 "Return the update sources from the given 'pagename' using the 'request'." 441 442 sources = {} 443 444 source_definitions = getWikiDict(pagename, request) 445 446 if source_definitions: 447 for name, value in source_definitions.items(): 448 sources[name] = getSourceParameters(value) 449 450 return sources 451 452 def getSourceParameters(source_definition): 453 454 "Return the parameters from the given 'source_definition' string." 455 456 return parseDictEntry(source_definition, ("type", "location")) 457 458 # HTML parsing support. 459 460 class IncomingHTMLSanitizer(HTMLSanitizer): 461 462 "An HTML parser that rewrites references to attachments." 463 464 def __init__(self, out, request, page, message_number): 465 HTMLSanitizer.__init__(self, out) 466 self.request = request 467 self.message_number = message_number 468 self.page = page 469 470 def rewrite_reference(self, ref): 471 if ref.startswith("cid:"): 472 part = ref[len("cid:"):] 473 action_link = self.page.url(self.request, { 474 "action" : "ReadMessage", "doit" : "1", 475 "message" : self.message_number, "part" : part 476 }) 477 return action_link 478 else: 479 return ref 480 481 def handle_starttag(self, tag, attrs): 482 new_attrs = [] 483 for attrname, attrvalue in attrs: 484 if attrname in self.uri_attrs: 485 new_attrs.append((attrname, self.rewrite_reference(attrvalue))) 486 else: 487 new_attrs.append((attrname, attrvalue)) 488 HTMLSanitizer.handle_starttag(self, tag, new_attrs) 489 490 class IncomingMarkup(Markup): 491 492 "A special markup processor for incoming HTML." 493 494 def sanitize(self, request, page, message_number): 495 out = getwriter("utf-8")(StringIO()) 496 sanitizer = IncomingHTMLSanitizer(out, request, page, message_number) 497 sanitizer.feed(self.stripentities(keepxmlentities=True)) 498 return IncomingMarkup(unicode(out.getvalue(), "utf-8")) 499 500 class IncomingHTMLParser: 501 502 "Filters and rewrites incoming HTML content." 503 504 def __init__(self, raw, request, **kw): 505 self.raw = raw 506 self.request = request 507 self.message_number = None 508 self.page = None 509 510 def format(self, formatter, **kw): 511 512 "Send the text." 513 514 try: 515 self.request.write(formatter.rawHTML(IncomingMarkup(self.raw).sanitize(self.request, self.page, self.message_number))) 516 except HTMLParseError, e: 517 self.request.write(formatter.sysmsg(1) + 518 formatter.text(u'HTML parsing error: %s in "%s"' % (e.msg, 519 self.raw.splitlines()[e.lineno - 1].strip())) + 520 formatter.sysmsg(0)) 521 522 class MakeIncomingHTMLParser: 523 524 "A class that makes parsers configured for messages." 525 526 def __init__(self, page, message_number): 527 528 "Initialise with state that is used to configure instantiated parsers." 529 530 self.message_number = message_number 531 self.page = page 532 533 def __call__(self, *args, **kw): 534 parser = IncomingHTMLParser(*args, **kw) 535 parser.message_number = self.message_number 536 parser.page = self.page 537 return parser 538 539 def get_make_parser(page, message_number): 540 541 """ 542 Return a callable that will return a parser configured for the message from 543 the given 'page' with the given 'message_number'. 544 """ 545 546 return MakeIncomingHTMLParser(page, message_number) 547 548 # vim: tabstop=4 expandtab shiftwidth=4