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