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 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 if hasattr(request, "getHeader"): 229 return request.getHeader(header_name) 230 elif hasattr(request, "headers"): 231 return request.headers[header_name] 232 else: 233 return request.env[(prefix and prefix + "_" or "") + header_name.upper()] 234 235 # Content/media type and preferences support. 236 237 class MediaRange: 238 239 "A content/media type value which supports whole categories of data." 240 241 def __init__(self, media_range, accept_parameters=None): 242 self.media_range = media_range 243 self.accept_parameters = accept_parameters or {} 244 245 parts = media_range.split(";") 246 self.media_type = parts[0] 247 self.parameters = getMappingFromParameterStrings(parts[1:]) 248 249 # The media type is divided into category and subcategory. 250 251 parts = self.media_type.split("/") 252 self.category = parts[0] 253 self.subcategory = "/".join(parts[1:]) 254 255 def get_parts(self): 256 257 "Return the category, subcategory parts." 258 259 return self.category, self.subcategory 260 261 def get_specificity(self): 262 263 """ 264 Return the specificity of the media type in terms of the scope of the 265 category and subcategory, and also in terms of any qualifying 266 parameters. 267 """ 268 269 if "*" in self.get_parts(): 270 return -list(self.get_parts()).count("*") 271 else: 272 return len(self.parameters) 273 274 def permits(self, other): 275 276 """ 277 Return whether this media type permits the use of the 'other' media type 278 if suggested as suitable content. 279 """ 280 281 if not isinstance(other, MediaRange): 282 other = MediaRange(other) 283 284 category = categoryPermits(self.category, other.category) 285 subcategory = categoryPermits(self.subcategory, other.subcategory) 286 287 if category and subcategory: 288 if "*" not in (category, subcategory): 289 return not self.parameters or self.parameters == other.parameters 290 else: 291 return True 292 else: 293 return False 294 295 def __eq__(self, other): 296 297 """ 298 Return whether this media type is effectively the same as the 'other' 299 media type. 300 """ 301 302 if not isinstance(other, MediaRange): 303 other = MediaRange(other) 304 305 category = categoryMatches(self.category, other.category) 306 subcategory = categoryMatches(self.subcategory, other.subcategory) 307 308 if category and subcategory: 309 if "*" not in (category, subcategory): 310 return self.parameters == other.parameters or \ 311 not self.parameters or not other.parameters 312 else: 313 return True 314 else: 315 return False 316 317 def __ne__(self, other): 318 return not self.__eq__(other) 319 320 def __hash__(self): 321 return hash(self.media_range) 322 323 def __repr__(self): 324 return "MediaRange(%r)" % self.media_range 325 326 def categoryMatches(this, that): 327 328 """ 329 Return the basis of a match between 'this' and 'that' or False if the given 330 categories do not match. 331 """ 332 333 return (this == "*" or this == that) and this or \ 334 that == "*" and that or False 335 336 def categoryPermits(this, that): 337 338 """ 339 Return whether 'this' category permits 'that' category. Where 'this' is a 340 wildcard ("*"), 'that' should always match. A value of False is returned if 341 the categories do not otherwise match. 342 """ 343 344 return (this == "*" or this == that) and this or False 345 346 def getMappingFromParameterStrings(l): 347 348 """ 349 Return a mapping representing the list of "name=value" strings given by 'l'. 350 """ 351 352 parameters = {} 353 354 for parameter in l: 355 parts = parameter.split("=") 356 name = parts[0].strip() 357 value = "=".join(parts[1:]).strip() 358 parameters[name] = value 359 360 return parameters 361 362 def getContentPreferences(accept): 363 364 """ 365 Return a mapping from media types to parameters for content/media types 366 extracted from the given 'accept' header value. The mapping is returned in 367 the form of a list of (media type, parameters) tuples. 368 369 See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 370 """ 371 372 preferences = [] 373 374 for field in accept.split(","): 375 376 # The media type with parameters (defined by the "media-range") is 377 # separated from any other parameters (defined as "accept-extension" 378 # parameters) by a quality parameter. 379 380 fparts = accept_regexp.split(field) 381 382 # The first part is always the media type. 383 384 media_type = fparts[0].strip() 385 386 # Any other parts can be interpreted as extension parameters. 387 388 if len(fparts) > 1: 389 fparts = ("q=" + ";q=".join(fparts[1:])).split(";") 390 else: 391 fparts = [] 392 393 # Each field in the preferences can incorporate parameters separated by 394 # semicolon characters. 395 396 parameters = getMappingFromParameterStrings(fparts) 397 media_range = MediaRange(media_type, parameters) 398 preferences.append(media_range) 399 400 return ContentPreferences(preferences) 401 402 class ContentPreferences: 403 404 "A wrapper around content preference information." 405 406 def __init__(self, preferences): 407 self.preferences = preferences 408 409 def __iter__(self): 410 return iter(self.preferences) 411 412 def get_ordered(self, by_quality=0): 413 414 """ 415 Return a list of content/media types in descending order of preference. 416 If 'by_quality' is set to a true value, the "q" value will be used as 417 the primary measure of preference; otherwise, only the specificity will 418 be considered. 419 """ 420 421 ordered = {} 422 423 for media_range in self.preferences: 424 specificity = media_range.get_specificity() 425 426 if by_quality: 427 q = float(media_range.accept_parameters.get("q", "1")) 428 key = q, specificity 429 else: 430 key = specificity 431 432 if not ordered.has_key(key): 433 ordered[key] = [] 434 435 ordered[key].append(media_range) 436 437 # Return the preferences in descending order of quality and specificity. 438 439 keys = ordered.keys() 440 keys.sort(reverse=True) 441 return [ordered[key] for key in keys] 442 443 def get_acceptable_types(self, available): 444 445 """ 446 Return content/media types from those in the 'available' list supported 447 by the known preferences grouped by preference level in descending order 448 of preference. 449 """ 450 451 matches = {} 452 available = set(available[:]) 453 454 for level in self.get_ordered(): 455 for media_range in level: 456 457 # Attempt to match available types. 458 459 found = set() 460 for available_type in available: 461 if media_range.permits(available_type): 462 q = float(media_range.accept_parameters.get("q", "1")) 463 if not matches.has_key(q): 464 matches[q] = [] 465 matches[q].append(available_type) 466 found.add(available_type) 467 468 # Stop looking for matches for matched available types. 469 470 if found: 471 available.difference_update(found) 472 473 # Sort the matches in descending order of quality. 474 475 all_q = matches.keys() 476 477 if all_q: 478 all_q.sort(reverse=True) 479 return [matches[q] for q in all_q] 480 else: 481 return [] 482 483 def get_preferred_types(self, available): 484 485 """ 486 Return the preferred content/media types from those in the 'available' 487 list, given the known preferences. 488 """ 489 490 preferred = self.get_acceptable_types(available) 491 if preferred: 492 return preferred[0] 493 else: 494 return [] 495 496 # Page access functions. 497 498 def getPageURL(page): 499 500 "Return the URL of the given 'page'." 501 502 request = page.request 503 return request.getQualifiedURL(page.url(request, relative=0)) 504 505 def getFormat(page): 506 507 "Get the format used on the given 'page'." 508 509 return page.pi["format"] 510 511 def getMetadata(page): 512 513 """ 514 Return a dictionary containing items describing for the given 'page' the 515 page's "created" time, "last-modified" time, "sequence" (or revision number) 516 and the "last-comment" made about the last edit. 517 """ 518 519 request = page.request 520 521 # Get the initial revision of the page. 522 523 revisions = page.getRevList() 524 event_page_initial = Page(request, page.page_name, rev=revisions[-1]) 525 526 # Get the created and last modified times. 527 528 initial_revision = getPageRevision(event_page_initial) 529 530 metadata = {} 531 metadata["created"] = initial_revision["timestamp"] 532 latest_revision = getPageRevision(page) 533 metadata["last-modified"] = latest_revision["timestamp"] 534 metadata["sequence"] = len(revisions) - 1 535 metadata["last-comment"] = latest_revision["comment"] 536 537 return metadata 538 539 def getPageRevision(page): 540 541 "Return the revision details dictionary for the given 'page'." 542 543 # From Page.edit_info... 544 545 if hasattr(page, "editlog_entry"): 546 line = page.editlog_entry() 547 else: 548 line = page._last_edited(page.request) # MoinMoin 1.5.x and 1.6.x 549 550 # Similar to Page.mtime_usecs behaviour... 551 552 if line: 553 timestamp = line.ed_time_usecs 554 mtime = wikiutil.version2timestamp(long(timestamp)) # must be long for py 2.2.x 555 comment = line.comment 556 else: 557 mtime = 0 558 comment = "" 559 560 # Leave the time zone empty. 561 562 return {"timestamp" : DateTime(time.gmtime(mtime)[:6] + (None,)), "comment" : comment} 563 564 # Page parsing and formatting of embedded content. 565 566 def getPageParserClass(request): 567 568 "Using 'request', return a parser class for the current page's format." 569 570 return getParserClass(request, request.page.pi["format"]) 571 572 def getParserClass(request, format): 573 574 """ 575 Return a parser class using the 'request' for the given 'format', returning 576 a plain text parser if no parser can be found for the specified 'format'. 577 """ 578 579 try: 580 return wikiutil.searchAndImportPlugin(request.cfg, "parser", format or "plain") 581 except wikiutil.PluginMissingError: 582 return wikiutil.searchAndImportPlugin(request.cfg, "parser", "plain") 583 584 def getFormatterClass(request, format): 585 586 """ 587 Return a formatter class using the 'request' for the given output 'format', 588 returning a plain text formatter if no formatter can be found for the 589 specified 'format'. 590 """ 591 592 try: 593 return wikiutil.searchAndImportPlugin(request.cfg, "formatter", format or "plain") 594 except wikiutil.PluginMissingError: 595 return wikiutil.searchAndImportPlugin(request.cfg, "formatter", "plain") 596 597 def formatText(text, request, fmt, parser_cls=None): 598 599 """ 600 Format the given 'text' using the specified 'request' and formatter 'fmt'. 601 Suppress line anchors in the output, and fix lists by indicating that a 602 paragraph has already been started. 603 """ 604 605 if not parser_cls: 606 parser_cls = getPageParserClass(request) 607 parser = parser_cls(text, request, line_anchors=False) 608 609 old_fmt = request.formatter 610 request.formatter = fmt 611 try: 612 return redirectedOutput(request, parser, fmt, inhibit_p=True) 613 finally: 614 request.formatter = old_fmt 615 616 def redirectedOutput(request, parser, fmt, **kw): 617 618 "A fixed version of the request method of the same name." 619 620 buf = StringIO() 621 request.redirect(buf) 622 try: 623 parser.format(fmt, **kw) 624 if hasattr(fmt, "flush"): 625 buf.write(fmt.flush(True)) 626 finally: 627 request.redirect() 628 text = buf.getvalue() 629 buf.close() 630 return text 631 632 # User interface functions. 633 634 def getParameter(request, name, default=None): 635 636 """ 637 Using the given 'request', return the value of the parameter with the given 638 'name', returning the optional 'default' (or None) if no value was supplied 639 in the 'request'. 640 """ 641 642 return get_form(request).get(name, [default])[0] 643 644 def getQualifiedParameter(request, prefix, argname, default=None): 645 646 """ 647 Using the given 'request', 'prefix' and 'argname', retrieve the value of the 648 qualified parameter, returning the optional 'default' (or None) if no value 649 was supplied in the 'request'. 650 """ 651 652 argname = getQualifiedParameterName(prefix, argname) 653 return getParameter(request, argname, default) 654 655 def getQualifiedParameterName(prefix, argname): 656 657 """ 658 Return the qualified parameter name using the given 'prefix' and 'argname'. 659 """ 660 661 if prefix is None: 662 return argname 663 else: 664 return "%s-%s" % (prefix, argname) 665 666 # Page-related functions. 667 668 def getPrettyPageName(page): 669 670 "Return a nicely formatted title/name for the given 'page'." 671 672 title = page.split_title(force=1) 673 return getPrettyTitle(title) 674 675 def linkToPage(request, page, text, query_string=None, **kw): 676 677 """ 678 Using 'request', return a link to 'page' with the given link 'text' and 679 optional 'query_string'. 680 """ 681 682 text = wikiutil.escape(text) 683 return page.link_to_raw(request, text, query_string, **kw) 684 685 def linkToResource(url, request, text, query_string=None): 686 687 """ 688 Using 'request', return a link to 'url' with the given link 'text' and 689 optional 'query_string'. 690 """ 691 692 if query_string: 693 query_string = wikiutil.makeQueryString(query_string) 694 url = "%s?%s" % (url, query_string) 695 696 formatter = request.page and getattr(request.page, "formatter", None) or request.html_formatter 697 698 output = [] 699 output.append(formatter.url(1, url)) 700 output.append(formatter.text(text)) 701 output.append(formatter.url(0)) 702 return "".join(output) 703 704 def getFullPageName(parent, title): 705 706 """ 707 Return a full page name from the given 'parent' page (can be empty or None) 708 and 'title' (a simple page name). 709 """ 710 711 if parent: 712 return "%s/%s" % (parent.rstrip("/"), title) 713 else: 714 return title 715 716 # vim: tabstop=4 expandtab shiftwidth=4