1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - MoinSupport library (derived from EventAggregatorSupport) 4 5 @copyright: 2008, 2009, 2010, 2011, 2012 by Paul Boddie <paul@boddie.org.uk> 6 @copyright: 2000-2004 Juergen Hermann <jh@web.de>, 7 2005-2008 MoinMoin:ThomasWaldmann. 8 @license: GNU GPL (v2 or later), see COPYING.txt for details. 9 """ 10 11 from DateSupport import * 12 from MoinMoin.Page import Page 13 from MoinMoin import config, wikiutil 14 from StringIO import StringIO 15 from shlex import shlex 16 import re 17 import time 18 19 __version__ = "0.2" 20 21 # Content type parsing. 22 23 encoding_regexp_str = ur'(?P<content_type>[^\s;]*)(?:;\s*charset=(?P<encoding>[-A-Za-z0-9]+))?' 24 encoding_regexp = re.compile(encoding_regexp_str) 25 26 # Accept header parsing. 27 28 accept_regexp_str = ur';\s*q=' 29 accept_regexp = re.compile(accept_regexp_str) 30 31 # Utility functions. 32 33 def getContentTypeAndEncoding(content_type): 34 35 """ 36 Return a tuple with the content/media type and encoding, extracted from the 37 given 'content_type' header value. 38 """ 39 40 m = encoding_regexp.search(content_type) 41 if m: 42 return m.group("content_type"), m.group("encoding") 43 else: 44 return None, None 45 46 def int_or_none(x): 47 if x is None: 48 return x 49 else: 50 return int(x) 51 52 def parseAttributes(s, escape=True): 53 54 """ 55 Parse the section attributes string 's', returning a mapping of names to 56 values. If 'escape' is set to a true value, the attributes will be suitable 57 for use with the formatter API. If 'escape' is set to a false value, the 58 attributes will have any quoting removed. 59 """ 60 61 attrs = {} 62 f = StringIO(s) 63 name = None 64 need_value = False 65 66 for token in shlex(f): 67 68 # Capture the name if needed. 69 70 if name is None: 71 name = escape and wikiutil.escape(token) or strip_token(token) 72 73 # Detect either an equals sign or another name. 74 75 elif not need_value: 76 if token == "=": 77 need_value = True 78 else: 79 attrs[name.lower()] = escape and "true" or True 80 name = wikiutil.escape(token) 81 82 # Otherwise, capture a value. 83 84 else: 85 # Quoting of attributes done similarly to wikiutil.parseAttributes. 86 87 if token: 88 if escape: 89 if token[0] in ("'", '"'): 90 token = wikiutil.escape(token) 91 else: 92 token = '"%s"' % wikiutil.escape(token, 1) 93 else: 94 token = strip_token(token) 95 96 attrs[name.lower()] = token 97 name = None 98 need_value = False 99 100 # Handle any name-only attributes at the end of the collection. 101 102 if name and not need_value: 103 attrs[name.lower()] = escape and "true" or True 104 105 return attrs 106 107 def strip_token(token): 108 109 "Return the given 'token' stripped of quoting." 110 111 if token[0] in ("'", '"') and token[-1] == token[0]: 112 return token[1:-1] 113 else: 114 return token 115 116 # Utility classes and associated functions. 117 118 class Form: 119 120 """ 121 A wrapper preserving MoinMoin 1.8.x (and earlier) behaviour in a 1.9.x 122 environment. 123 """ 124 125 def __init__(self, form): 126 self.form = form 127 128 def has_key(self, name): 129 return not not self.form.getlist(name) 130 131 def get(self, name, default=None): 132 values = self.form.getlist(name) 133 if not values: 134 return default 135 else: 136 return values 137 138 def __getitem__(self, name): 139 return self.form.getlist(name) 140 141 class ActionSupport: 142 143 """ 144 Work around disruptive MoinMoin changes in 1.9, and also provide useful 145 convenience methods. 146 """ 147 148 def get_form(self): 149 return get_form(self.request) 150 151 def _get_selected(self, value, input_value): 152 153 """ 154 Return the HTML attribute text indicating selection of an option (or 155 otherwise) if 'value' matches 'input_value'. 156 """ 157 158 return input_value is not None and value == input_value and 'selected="selected"' or '' 159 160 def _get_selected_for_list(self, value, input_values): 161 162 """ 163 Return the HTML attribute text indicating selection of an option (or 164 otherwise) if 'value' matches one of the 'input_values'. 165 """ 166 167 return value in input_values and 'selected="selected"' or '' 168 169 def _get_input(self, form, name, default=None): 170 171 """ 172 Return the input from 'form' having the given 'name', returning either 173 the input converted to an integer or the given 'default' (optional, None 174 if not specified). 175 """ 176 177 value = form.get(name, [None])[0] 178 if not value: # true if 0 obtained 179 return default 180 else: 181 return int(value) 182 183 def get_form(request): 184 185 "Work around disruptive MoinMoin changes in 1.9." 186 187 if hasattr(request, "values"): 188 return Form(request.values) 189 else: 190 return request.form 191 192 class send_headers_cls: 193 194 """ 195 A wrapper to preserve MoinMoin 1.8.x (and earlier) request behaviour in a 196 1.9.x environment. 197 """ 198 199 def __init__(self, request): 200 self.request = request 201 202 def __call__(self, headers): 203 for header in headers: 204 parts = header.split(":") 205 self.request.headers.add(parts[0], ":".join(parts[1:])) 206 207 def get_send_headers(request): 208 209 "Return a function that can send response headers." 210 211 if hasattr(request, "http_headers"): 212 return request.http_headers 213 elif hasattr(request, "emit_http_headers"): 214 return request.emit_http_headers 215 else: 216 return send_headers_cls(request) 217 218 def escattr(s): 219 return wikiutil.escape(s, 1) 220 221 def getPathInfo(request): 222 if hasattr(request, "getPathinfo"): 223 return request.getPathinfo() 224 else: 225 return request.path 226 227 def getHeader(request, header_name, prefix=None): 228 229 """ 230 Using the 'request', return the value of the header with the given 231 'header_name', using the optional 'prefix' to obtain protocol-specific 232 headers if necessary. 233 234 If no value is found for the given 'header_name', None is returned. 235 """ 236 237 if hasattr(request, "getHeader"): 238 return request.getHeader(header_name) 239 elif hasattr(request, "headers"): 240 return request.headers.get(header_name) 241 else: 242 return request.env.get((prefix and prefix + "_" or "") + header_name.upper()) 243 244 def writeHeaders(request, mimetype, metadata, status=None): 245 246 """ 247 Using the 'request', write resource headers using the given 'mimetype', 248 based on the given 'metadata'. If the optional 'status' is specified, set 249 the status header to the given value. 250 """ 251 252 send_headers = get_send_headers(request) 253 254 # Define headers. 255 256 headers = ["Content-Type: %s; charset=%s" % (mimetype, config.charset)] 257 258 # Define the last modified time. 259 # NOTE: Consider using request.httpDate. 260 261 latest_timestamp = metadata.get("last-modified") 262 if latest_timestamp: 263 headers.append("Last-Modified: %s" % latest_timestamp.as_HTTP_datetime_string()) 264 265 if status: 266 headers.append("Status: %s" % status) 267 268 send_headers(headers) 269 270 # Content/media type and preferences support. 271 272 class MediaRange: 273 274 "A content/media type value which supports whole categories of data." 275 276 def __init__(self, media_range, accept_parameters=None): 277 self.media_range = media_range 278 self.accept_parameters = accept_parameters or {} 279 280 parts = media_range.split(";") 281 self.media_type = parts[0] 282 self.parameters = getMappingFromParameterStrings(parts[1:]) 283 284 # The media type is divided into category and subcategory. 285 286 parts = self.media_type.split("/") 287 self.category = parts[0] 288 self.subcategory = "/".join(parts[1:]) 289 290 def get_parts(self): 291 292 "Return the category, subcategory parts." 293 294 return self.category, self.subcategory 295 296 def get_specificity(self): 297 298 """ 299 Return the specificity of the media type in terms of the scope of the 300 category and subcategory, and also in terms of any qualifying 301 parameters. 302 """ 303 304 if "*" in self.get_parts(): 305 return -list(self.get_parts()).count("*") 306 else: 307 return len(self.parameters) 308 309 def permits(self, other): 310 311 """ 312 Return whether this media type permits the use of the 'other' media type 313 if suggested as suitable content. 314 """ 315 316 if not isinstance(other, MediaRange): 317 other = MediaRange(other) 318 319 category = categoryPermits(self.category, other.category) 320 subcategory = categoryPermits(self.subcategory, other.subcategory) 321 322 if category and subcategory: 323 if "*" not in (category, subcategory): 324 return not self.parameters or self.parameters == other.parameters 325 else: 326 return True 327 else: 328 return False 329 330 def __eq__(self, other): 331 332 """ 333 Return whether this media type is effectively the same as the 'other' 334 media type. 335 """ 336 337 if not isinstance(other, MediaRange): 338 other = MediaRange(other) 339 340 category = categoryMatches(self.category, other.category) 341 subcategory = categoryMatches(self.subcategory, other.subcategory) 342 343 if category and subcategory: 344 if "*" not in (category, subcategory): 345 return self.parameters == other.parameters or \ 346 not self.parameters or not other.parameters 347 else: 348 return True 349 else: 350 return False 351 352 def __ne__(self, other): 353 return not self.__eq__(other) 354 355 def __hash__(self): 356 return hash(self.media_range) 357 358 def __repr__(self): 359 return "MediaRange(%r)" % self.media_range 360 361 def categoryMatches(this, that): 362 363 """ 364 Return the basis of a match between 'this' and 'that' or False if the given 365 categories do not match. 366 """ 367 368 return (this == "*" or this == that) and this or \ 369 that == "*" and that or False 370 371 def categoryPermits(this, that): 372 373 """ 374 Return whether 'this' category permits 'that' category. Where 'this' is a 375 wildcard ("*"), 'that' should always match. A value of False is returned if 376 the categories do not otherwise match. 377 """ 378 379 return (this == "*" or this == that) and this or False 380 381 def getMappingFromParameterStrings(l): 382 383 """ 384 Return a mapping representing the list of "name=value" strings given by 'l'. 385 """ 386 387 parameters = {} 388 389 for parameter in l: 390 parts = parameter.split("=") 391 name = parts[0].strip() 392 value = "=".join(parts[1:]).strip() 393 parameters[name] = value 394 395 return parameters 396 397 def getContentPreferences(accept): 398 399 """ 400 Return a mapping from media types to parameters for content/media types 401 extracted from the given 'accept' header value. The mapping is returned in 402 the form of a list of (media type, parameters) tuples. 403 404 See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 405 """ 406 407 preferences = [] 408 409 for field in accept.split(","): 410 411 # The media type with parameters (defined by the "media-range") is 412 # separated from any other parameters (defined as "accept-extension" 413 # parameters) by a quality parameter. 414 415 fparts = accept_regexp.split(field) 416 417 # The first part is always the media type. 418 419 media_type = fparts[0].strip() 420 421 # Any other parts can be interpreted as extension parameters. 422 423 if len(fparts) > 1: 424 fparts = ("q=" + ";q=".join(fparts[1:])).split(";") 425 else: 426 fparts = [] 427 428 # Each field in the preferences can incorporate parameters separated by 429 # semicolon characters. 430 431 parameters = getMappingFromParameterStrings(fparts) 432 media_range = MediaRange(media_type, parameters) 433 preferences.append(media_range) 434 435 return ContentPreferences(preferences) 436 437 class ContentPreferences: 438 439 "A wrapper around content preference information." 440 441 def __init__(self, preferences): 442 self.preferences = preferences 443 444 def __iter__(self): 445 return iter(self.preferences) 446 447 def get_ordered(self, by_quality=0): 448 449 """ 450 Return a list of content/media types in descending order of preference. 451 If 'by_quality' is set to a true value, the "q" value will be used as 452 the primary measure of preference; otherwise, only the specificity will 453 be considered. 454 """ 455 456 ordered = {} 457 458 for media_range in self.preferences: 459 specificity = media_range.get_specificity() 460 461 if by_quality: 462 q = float(media_range.accept_parameters.get("q", "1")) 463 key = q, specificity 464 else: 465 key = specificity 466 467 if not ordered.has_key(key): 468 ordered[key] = [] 469 470 ordered[key].append(media_range) 471 472 # Return the preferences in descending order of quality and specificity. 473 474 keys = ordered.keys() 475 keys.sort(reverse=True) 476 return [ordered[key] for key in keys] 477 478 def get_acceptable_types(self, available): 479 480 """ 481 Return content/media types from those in the 'available' list supported 482 by the known preferences grouped by preference level in descending order 483 of preference. 484 """ 485 486 matches = {} 487 available = set(available[:]) 488 489 for level in self.get_ordered(): 490 for media_range in level: 491 492 # Attempt to match available types. 493 494 found = set() 495 for available_type in available: 496 if media_range.permits(available_type): 497 q = float(media_range.accept_parameters.get("q", "1")) 498 if not matches.has_key(q): 499 matches[q] = [] 500 matches[q].append(available_type) 501 found.add(available_type) 502 503 # Stop looking for matches for matched available types. 504 505 if found: 506 available.difference_update(found) 507 508 # Sort the matches in descending order of quality. 509 510 all_q = matches.keys() 511 512 if all_q: 513 all_q.sort(reverse=True) 514 return [matches[q] for q in all_q] 515 else: 516 return [] 517 518 def get_preferred_types(self, available): 519 520 """ 521 Return the preferred content/media types from those in the 'available' 522 list, given the known preferences. 523 """ 524 525 preferred = self.get_acceptable_types(available) 526 if preferred: 527 return preferred[0] 528 else: 529 return [] 530 531 # Page access functions. 532 533 def getPageURL(page): 534 535 "Return the URL of the given 'page'." 536 537 request = page.request 538 return request.getQualifiedURL(page.url(request, relative=0)) 539 540 def getFormat(page): 541 542 "Get the format used on the given 'page'." 543 544 return page.pi["format"] 545 546 def getMetadata(page): 547 548 """ 549 Return a dictionary containing items describing for the given 'page' the 550 page's "created" time, "last-modified" time, "sequence" (or revision number) 551 and the "last-comment" made about the last edit. 552 """ 553 554 request = page.request 555 556 # Get the initial revision of the page. 557 558 revisions = page.getRevList() 559 event_page_initial = Page(request, page.page_name, rev=revisions[-1]) 560 561 # Get the created and last modified times. 562 563 initial_revision = getPageRevision(event_page_initial) 564 565 metadata = {} 566 metadata["created"] = initial_revision["timestamp"] 567 latest_revision = getPageRevision(page) 568 metadata["last-modified"] = latest_revision["timestamp"] 569 metadata["sequence"] = len(revisions) - 1 570 metadata["last-comment"] = latest_revision["comment"] 571 572 return metadata 573 574 def getPageRevision(page): 575 576 "Return the revision details dictionary for the given 'page'." 577 578 # From Page.edit_info... 579 580 if hasattr(page, "editlog_entry"): 581 line = page.editlog_entry() 582 else: 583 line = page._last_edited(page.request) # MoinMoin 1.5.x and 1.6.x 584 585 # Similar to Page.mtime_usecs behaviour... 586 587 if line: 588 timestamp = line.ed_time_usecs 589 mtime = wikiutil.version2timestamp(long(timestamp)) # must be long for py 2.2.x 590 comment = line.comment 591 else: 592 mtime = 0 593 comment = "" 594 595 # Leave the time zone empty. 596 597 return {"timestamp" : DateTime(time.gmtime(mtime)[:6] + (None,)), "comment" : comment} 598 599 # Page parsing and formatting of embedded content. 600 601 def getPageParserClass(request): 602 603 "Using 'request', return a parser class for the current page's format." 604 605 return getParserClass(request, getFormat(request.page)) 606 607 def getParserClass(request, format): 608 609 """ 610 Return a parser class using the 'request' for the given 'format', returning 611 a plain text parser if no parser can be found for the specified 'format'. 612 """ 613 614 try: 615 return wikiutil.searchAndImportPlugin(request.cfg, "parser", format or "plain") 616 except wikiutil.PluginMissingError: 617 return wikiutil.searchAndImportPlugin(request.cfg, "parser", "plain") 618 619 def getFormatterClass(request, format): 620 621 """ 622 Return a formatter class using the 'request' for the given output 'format', 623 returning a plain text formatter if no formatter can be found for the 624 specified 'format'. 625 """ 626 627 try: 628 return wikiutil.searchAndImportPlugin(request.cfg, "formatter", format or "plain") 629 except wikiutil.PluginMissingError: 630 return wikiutil.searchAndImportPlugin(request.cfg, "formatter", "plain") 631 632 def formatText(text, request, fmt, parser_cls=None): 633 634 """ 635 Format the given 'text' using the specified 'request' and formatter 'fmt'. 636 Suppress line anchors in the output, and fix lists by indicating that a 637 paragraph has already been started. 638 """ 639 640 if not parser_cls: 641 parser_cls = getPageParserClass(request) 642 parser = parser_cls(text, request, line_anchors=False) 643 644 old_fmt = request.formatter 645 request.formatter = fmt 646 try: 647 return redirectedOutput(request, parser, fmt, inhibit_p=True) 648 finally: 649 request.formatter = old_fmt 650 651 def redirectedOutput(request, parser, fmt, **kw): 652 653 "A fixed version of the request method of the same name." 654 655 buf = StringIO() 656 request.redirect(buf) 657 try: 658 parser.format(fmt, **kw) 659 if hasattr(fmt, "flush"): 660 buf.write(fmt.flush(True)) 661 finally: 662 request.redirect() 663 text = buf.getvalue() 664 buf.close() 665 return text 666 667 # User interface functions. 668 669 def getParameter(request, name, default=None): 670 671 """ 672 Using the given 'request', return the value of the parameter with the given 673 'name', returning the optional 'default' (or None) if no value was supplied 674 in the 'request'. 675 """ 676 677 return get_form(request).get(name, [default])[0] 678 679 def getQualifiedParameter(request, prefix, argname, default=None): 680 681 """ 682 Using the given 'request', 'prefix' and 'argname', retrieve the value of the 683 qualified parameter, returning the optional 'default' (or None) if no value 684 was supplied in the 'request'. 685 """ 686 687 argname = getQualifiedParameterName(prefix, argname) 688 return getParameter(request, argname, default) 689 690 def getQualifiedParameterName(prefix, argname): 691 692 """ 693 Return the qualified parameter name using the given 'prefix' and 'argname'. 694 """ 695 696 if prefix is None: 697 return argname 698 else: 699 return "%s-%s" % (prefix, argname) 700 701 # Page-related functions. 702 703 def getPrettyPageName(page): 704 705 "Return a nicely formatted title/name for the given 'page'." 706 707 title = page.split_title(force=1) 708 return getPrettyTitle(title) 709 710 def linkToPage(request, page, text, query_string=None, **kw): 711 712 """ 713 Using 'request', return a link to 'page' with the given link 'text' and 714 optional 'query_string'. 715 """ 716 717 text = wikiutil.escape(text) 718 return page.link_to_raw(request, text, query_string, **kw) 719 720 def linkToResource(url, request, text, query_string=None): 721 722 """ 723 Using 'request', return a link to 'url' with the given link 'text' and 724 optional 'query_string'. 725 """ 726 727 if query_string: 728 query_string = wikiutil.makeQueryString(query_string) 729 url = "%s?%s" % (url, query_string) 730 731 formatter = request.page and getattr(request.page, "formatter", None) or request.html_formatter 732 733 output = [] 734 output.append(formatter.url(1, url)) 735 output.append(formatter.text(text)) 736 output.append(formatter.url(0)) 737 return "".join(output) 738 739 def getFullPageName(parent, title): 740 741 """ 742 Return a full page name from the given 'parent' page (can be empty or None) 743 and 'title' (a simple page name). 744 """ 745 746 if parent: 747 return "%s/%s" % (parent.rstrip("/"), title) 748 else: 749 return title 750 751 # vim: tabstop=4 expandtab shiftwidth=4