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, search, wikiutil 14 from StringIO import StringIO 15 from shlex import shlex 16 import re 17 import time 18 19 # Moin 1.9 request parameters. 20 21 try: 22 from MoinMoin.support.werkzeug.datastructures import MultiDict 23 except ImportError: 24 pass 25 26 __version__ = "0.2" 27 28 # Content type parsing. 29 30 encoding_regexp_str = ur'(?P<content_type>[^\s;]*)(?:;\s*charset=(?P<encoding>[-A-Za-z0-9]+))?' 31 encoding_regexp = re.compile(encoding_regexp_str) 32 33 # Accept header parsing. 34 35 accept_regexp_str = ur';\s*q=' 36 accept_regexp = re.compile(accept_regexp_str) 37 38 # Extraction of shared fragments. 39 40 marker_regexp_str = r"([{]{3,}|[}]{3,})" 41 marker_regexp = re.compile(marker_regexp_str, re.MULTILINE | re.DOTALL) # {{{... or }}}... 42 43 # Extraction of headings. 44 45 heading_regexp = re.compile(r"^(?P<level>=+)(?P<heading>.*?)(?P=level)$", re.UNICODE | re.MULTILINE) 46 47 # Category extraction from pages. 48 49 category_regexp = None 50 51 # Simple content parsing. 52 53 verbatim_regexp = re.compile(ur'(?:' 54 ur'<<Verbatim\((?P<verbatim>.*?)\)>>' 55 ur'|' 56 ur'\[\[Verbatim\((?P<verbatim2>.*?)\)\]\]' 57 ur'|' 58 ur'!(?P<verbatim3>.*?)(\s|$)?' 59 ur'|' 60 ur'`(?P<monospace>.*?)`' 61 ur'|' 62 ur'{{{(?P<preformatted>.*?)}}}' 63 ur')', re.UNICODE) 64 65 # Category discovery. 66 67 def getCategoryPattern(request): 68 global category_regexp 69 70 try: 71 return request.cfg.cache.page_category_regexact 72 except AttributeError: 73 74 # Use regular expression from MoinMoin 1.7.1 otherwise. 75 76 if category_regexp is None: 77 category_regexp = re.compile(u'^%s$' % ur'(?P<all>Category(?P<key>(?!Template)\S+))', re.UNICODE) 78 return category_regexp 79 80 def getCategories(request): 81 82 """ 83 From the AdvancedSearch macro, return a list of category page names using 84 the given 'request'. 85 """ 86 87 # This will return all pages with "Category" in the title. 88 89 cat_filter = getCategoryPattern(request).search 90 return request.rootpage.getPageList(filter=cat_filter) 91 92 def getCategoryMapping(category_pagenames, request): 93 94 """ 95 For the given 'category_pagenames' return a list of tuples of the form 96 (category name, category page name) using the given 'request'. 97 """ 98 99 cat_pattern = getCategoryPattern(request) 100 mapping = [] 101 for pagename in category_pagenames: 102 name = cat_pattern.match(pagename).group("key") 103 if name != "Category": 104 mapping.append((name, pagename)) 105 mapping.sort() 106 return mapping 107 108 def getCategoryPages(pagename, request): 109 110 """ 111 Return the pages associated with the given category 'pagename' using the 112 'request'. 113 """ 114 115 query = search.QueryParser().parse_query('category:%s' % pagename) 116 results = search.searchPages(request, query, "page_name") 117 return filterCategoryPages(results, request) 118 119 def filterCategoryPages(results, request): 120 121 "Filter category pages from the given 'results' using the 'request'." 122 123 cat_pattern = getCategoryPattern(request) 124 pages = [] 125 for page in results.hits: 126 if not cat_pattern.match(page.page_name): 127 pages.append(page) 128 return pages 129 130 def getAllCategoryPages(category_names, request): 131 132 """ 133 Return all pages belonging to the categories having the given 134 'category_names', using the given 'request'. 135 """ 136 137 pages = [] 138 pagenames = set() 139 140 for category_name in category_names: 141 142 # Get the pages and page names in the category. 143 144 pages_in_category = getCategoryPages(category_name, request) 145 146 # Visit each page in the category. 147 148 for page_in_category in pages_in_category: 149 pagename = page_in_category.page_name 150 151 # Only process each page once. 152 153 if pagename in pagenames: 154 continue 155 else: 156 pagenames.add(pagename) 157 158 pages.append(page_in_category) 159 160 return pages 161 162 def getPagesForSearch(search_pattern, request): 163 164 """ 165 Return result pages for a search employing the given 'search_pattern' and 166 using the given 'request'. 167 """ 168 169 query = search.QueryParser().parse_query(search_pattern) 170 results = search.searchPages(request, query, "page_name") 171 return filterCategoryPages(results, request) 172 173 # WikiDict functions. 174 175 def getWikiDict(pagename, request): 176 177 """ 178 Return the WikiDict provided by the given 'pagename' using the given 179 'request'. 180 """ 181 182 if pagename and Page(request, pagename).exists() and request.user.may.read(pagename): 183 if hasattr(request.dicts, "dict"): 184 return request.dicts.dict(pagename) 185 else: 186 return request.dicts[pagename] 187 else: 188 return None 189 190 # Searching-related functions. 191 192 def getPagesFromResults(result_pages, request): 193 194 "Return genuine pages for the given 'result_pages' using the 'request'." 195 196 return [Page(request, page.page_name) for page in result_pages] 197 198 # Region/section parsing. 199 200 def getRegions(s, include_non_regions=False): 201 202 """ 203 Parse the string 's', returning a list of explicitly declared regions. 204 205 If 'include_non_regions' is specified as a true value, fragments will be 206 included for text between explicitly declared regions. 207 """ 208 209 regions = [] 210 marker = None 211 is_block = True 212 213 # Start a region for exposed text, if appropriate. 214 215 if include_non_regions: 216 regions.append("") 217 218 for match_text in marker_regexp.split(s): 219 220 # Capture section text. 221 222 if is_block: 223 if marker or include_non_regions: 224 regions[-1] += match_text 225 226 # Handle section markers. 227 228 else: 229 230 # Close any open sections, returning to exposed text regions. 231 232 if marker: 233 234 # Add any marker to the current region, regardless of whether it 235 # successfully closes a section. 236 237 regions[-1] += match_text 238 239 if match_text.startswith("}") and len(marker) == len(match_text): 240 marker = None 241 242 # Start a region for exposed text, if appropriate. 243 244 if include_non_regions: 245 regions.append("") 246 247 # Without a current marker, start a new section. 248 249 else: 250 marker = match_text 251 regions.append("") 252 253 # Add the marker to the new region. 254 255 regions[-1] += match_text 256 257 # The match text alternates between text between markers and the markers 258 # themselves. 259 260 is_block = not is_block 261 262 return regions 263 264 def getFragmentsFromRegions(regions): 265 266 """ 267 Return fragments from the given 'regions', each having the form 268 (format, attributes, body text). 269 """ 270 271 fragments = [] 272 273 for region in regions: 274 format, attributes, body, header, close = getFragmentFromRegion(region) 275 fragments.append((format, attributes, body)) 276 277 return fragments 278 279 def getFragmentFromRegion(region): 280 281 """ 282 Return a fragment for the given 'region' having the form (format, 283 attributes, body text, header, close), where the 'header' is the original 284 declaration of the 'region' or None if no explicit region is defined, and 285 'close' is the closing marker of the 'region' or None if no explicit region 286 is defined. 287 """ 288 289 if region.startswith("{{{"): 290 291 body = region.lstrip("{") 292 level = len(region) - len(body) 293 body = body.rstrip("}").lstrip() 294 295 # Remove any prelude and process metadata. 296 297 if body.startswith("#!"): 298 299 try: 300 declaration, body = body.split("\n", 1) 301 except ValueError: 302 declaration = body 303 body = "" 304 305 arguments = declaration[2:] 306 307 # Get any parser/format declaration. 308 309 if arguments and not arguments[0].isspace(): 310 details = arguments.split(None, 1) 311 if len(details) == 2: 312 format, arguments = details 313 else: 314 format = details[0] 315 arguments = "" 316 else: 317 format = None 318 319 # Get the attributes/arguments for the region. 320 321 attributes = parseAttributes(arguments, False) 322 323 # Add an entry for the format in the attribute dictionary. 324 325 if format and not attributes.has_key(format): 326 attributes[format] = True 327 328 return format, attributes, body, level * "{" + declaration + "\n", level * "}" 329 330 else: 331 return None, {}, body, level * "{" + "\n", level * "}" 332 333 else: 334 return None, {}, region, None, None 335 336 def getFragments(s, include_non_regions=False): 337 338 """ 339 Return fragments for the given string 's', each having the form 340 (format, arguments, body text). 341 342 If 'include_non_regions' is specified as a true value, fragments will be 343 included for text between explicitly declared regions. 344 """ 345 346 return getFragmentsFromRegions(getRegions(s, include_non_regions)) 347 348 # Heading extraction. 349 350 def getHeadings(s): 351 352 """ 353 Return tuples of the form (level, title, span) for headings found within the 354 given string 's'. The span is itself a (start, end) tuple indicating the 355 matching region of 's' for a heading declaration. 356 """ 357 358 headings = [] 359 360 for match in heading_regexp.finditer(s): 361 headings.append( 362 (len(match.group("level")), match.group("heading"), match.span()) 363 ) 364 365 return headings 366 367 # Region/section attribute parsing. 368 369 def parseAttributes(s, escape=True): 370 371 """ 372 Parse the section attributes string 's', returning a mapping of names to 373 values. If 'escape' is set to a true value, the attributes will be suitable 374 for use with the formatter API. If 'escape' is set to a false value, the 375 attributes will have any quoting removed. 376 """ 377 378 attrs = {} 379 f = StringIO(s) 380 name = None 381 need_value = False 382 lex = shlex(f) 383 lex.wordchars += "-" 384 385 for token in lex: 386 387 # Capture the name if needed. 388 389 if name is None: 390 name = escape and wikiutil.escape(token) or strip_token(token) 391 392 # Detect either an equals sign or another name. 393 394 elif not need_value: 395 if token == "=": 396 need_value = True 397 else: 398 attrs[name.lower()] = escape and "true" or True 399 name = wikiutil.escape(token) 400 401 # Otherwise, capture a value. 402 403 else: 404 # Quoting of attributes done similarly to wikiutil.parseAttributes. 405 406 if token: 407 if escape: 408 if token[0] in ("'", '"'): 409 token = wikiutil.escape(token) 410 else: 411 token = '"%s"' % wikiutil.escape(token, 1) 412 else: 413 token = strip_token(token) 414 415 attrs[name.lower()] = token 416 name = None 417 need_value = False 418 419 # Handle any name-only attributes at the end of the collection. 420 421 if name and not need_value: 422 attrs[name.lower()] = escape and "true" or True 423 424 return attrs 425 426 def strip_token(token): 427 428 "Return the given 'token' stripped of quoting." 429 430 if token[0] in ("'", '"') and token[-1] == token[0]: 431 return token[1:-1] 432 else: 433 return token 434 435 # Request-related classes and associated functions. 436 437 class Form: 438 439 """ 440 A wrapper preserving MoinMoin 1.8.x (and earlier) behaviour in a 1.9.x 441 environment. 442 """ 443 444 def __init__(self, request): 445 self.request = request 446 self.form = request.values 447 448 def has_key(self, name): 449 return not not self.form.getlist(name) 450 451 def get(self, name, default=None): 452 values = self.form.getlist(name) 453 if not values: 454 return default 455 else: 456 return values 457 458 def __getitem__(self, name): 459 return self.form.getlist(name) 460 461 def __setitem__(self, name, value): 462 try: 463 self.form.setlist(name, value) 464 except TypeError: 465 self._write_enable() 466 self.form.setlist(name, value) 467 468 def __delitem__(self, name): 469 try: 470 del self.form[name] 471 except TypeError: 472 self._write_enable() 473 del self.form[name] 474 475 def _write_enable(self): 476 self.form = self.request.values = MultiDict(self.form) 477 478 def keys(self): 479 return self.form.keys() 480 481 def items(self): 482 return self.form.lists() 483 484 class ActionSupport: 485 486 """ 487 Work around disruptive MoinMoin changes in 1.9, and also provide useful 488 convenience methods. 489 """ 490 491 def get_form(self): 492 return get_form(self.request) 493 494 def _get_selected(self, value, input_value): 495 496 """ 497 Return the HTML attribute text indicating selection of an option (or 498 otherwise) if 'value' matches 'input_value'. 499 """ 500 501 return input_value is not None and value == input_value and 'selected="selected"' or '' 502 503 def _get_selected_for_list(self, value, input_values): 504 505 """ 506 Return the HTML attribute text indicating selection of an option (or 507 otherwise) if 'value' matches one of the 'input_values'. 508 """ 509 510 return value in input_values and 'selected="selected"' or '' 511 512 def get_option_list(self, value, values): 513 514 """ 515 Return a list of HTML element definitions for options describing the 516 given 'values', selecting the option with the specified 'value' if 517 present. 518 """ 519 520 options = [] 521 for available_value in values: 522 selected = self._get_selected(available_value, value) 523 options.append('<option value="%s" %s>%s</option>' % ( 524 escattr(available_value), selected, wikiutil.escape(available_value))) 525 return options 526 527 def _get_input(self, form, name, default=None): 528 529 """ 530 Return the input from 'form' having the given 'name', returning either 531 the input converted to an integer or the given 'default' (optional, None 532 if not specified). 533 """ 534 535 value = form.get(name, [None])[0] 536 if not value: # true if 0 obtained 537 return default 538 else: 539 return int(value) 540 541 def get_form(request): 542 543 "Work around disruptive MoinMoin changes in 1.9." 544 545 if hasattr(request, "values"): 546 return Form(request) 547 else: 548 return request.form 549 550 class send_headers_cls: 551 552 """ 553 A wrapper to preserve MoinMoin 1.8.x (and earlier) request behaviour in a 554 1.9.x environment. 555 """ 556 557 def __init__(self, request): 558 self.request = request 559 560 def __call__(self, headers): 561 for header in headers: 562 parts = header.split(":") 563 self.request.headers.add(parts[0], ":".join(parts[1:])) 564 565 def get_send_headers(request): 566 567 "Return a function that can send response headers." 568 569 if hasattr(request, "http_headers"): 570 return request.http_headers 571 elif hasattr(request, "emit_http_headers"): 572 return request.emit_http_headers 573 else: 574 return send_headers_cls(request) 575 576 def escattr(s): 577 return wikiutil.escape(s, 1) 578 579 def getPathInfo(request): 580 if hasattr(request, "getPathinfo"): 581 return request.getPathinfo() 582 else: 583 return request.path 584 585 def getHeader(request, header_name, prefix=None): 586 587 """ 588 Using the 'request', return the value of the header with the given 589 'header_name', using the optional 'prefix' to obtain protocol-specific 590 headers if necessary. 591 592 If no value is found for the given 'header_name', None is returned. 593 """ 594 595 if hasattr(request, "getHeader"): 596 return request.getHeader(header_name) 597 elif hasattr(request, "headers"): 598 return request.headers.get(header_name) 599 else: 600 return request.env.get((prefix and prefix + "_" or "") + header_name.upper()) 601 602 def writeHeaders(request, mimetype, metadata, status=None): 603 604 """ 605 Using the 'request', write resource headers using the given 'mimetype', 606 based on the given 'metadata'. If the optional 'status' is specified, set 607 the status header to the given value. 608 """ 609 610 send_headers = get_send_headers(request) 611 612 # Define headers. 613 614 headers = ["Content-Type: %s; charset=%s" % (mimetype, config.charset)] 615 616 # Define the last modified time. 617 # NOTE: Consider using request.httpDate. 618 619 latest_timestamp = metadata.get("last-modified") 620 if latest_timestamp: 621 headers.append("Last-Modified: %s" % latest_timestamp.as_HTTP_datetime_string()) 622 623 if status: 624 headers.append("Status: %s" % status) 625 626 send_headers(headers) 627 628 # Content/media type and preferences support. 629 630 class MediaRange: 631 632 "A content/media type value which supports whole categories of data." 633 634 def __init__(self, media_range, accept_parameters=None): 635 self.media_range = media_range 636 self.accept_parameters = accept_parameters or {} 637 638 parts = media_range.split(";") 639 self.media_type = parts[0] 640 self.parameters = getMappingFromParameterStrings(parts[1:]) 641 642 # The media type is divided into category and subcategory. 643 644 parts = self.media_type.split("/") 645 self.category = parts[0] 646 self.subcategory = "/".join(parts[1:]) 647 648 def get_parts(self): 649 650 "Return the category, subcategory parts." 651 652 return self.category, self.subcategory 653 654 def get_specificity(self): 655 656 """ 657 Return the specificity of the media type in terms of the scope of the 658 category and subcategory, and also in terms of any qualifying 659 parameters. 660 """ 661 662 if "*" in self.get_parts(): 663 return -list(self.get_parts()).count("*") 664 else: 665 return len(self.parameters) 666 667 def permits(self, other): 668 669 """ 670 Return whether this media type permits the use of the 'other' media type 671 if suggested as suitable content. 672 """ 673 674 if not isinstance(other, MediaRange): 675 other = MediaRange(other) 676 677 category = categoryPermits(self.category, other.category) 678 subcategory = categoryPermits(self.subcategory, other.subcategory) 679 680 if category and subcategory: 681 if "*" not in (category, subcategory): 682 return not self.parameters or self.parameters == other.parameters 683 else: 684 return True 685 else: 686 return False 687 688 def __eq__(self, other): 689 690 """ 691 Return whether this media type is effectively the same as the 'other' 692 media type. 693 """ 694 695 if not isinstance(other, MediaRange): 696 other = MediaRange(other) 697 698 category = categoryMatches(self.category, other.category) 699 subcategory = categoryMatches(self.subcategory, other.subcategory) 700 701 if category and subcategory: 702 if "*" not in (category, subcategory): 703 return self.parameters == other.parameters or \ 704 not self.parameters or not other.parameters 705 else: 706 return True 707 else: 708 return False 709 710 def __ne__(self, other): 711 return not self.__eq__(other) 712 713 def __hash__(self): 714 return hash(self.media_range) 715 716 def __repr__(self): 717 return "MediaRange(%r)" % self.media_range 718 719 def categoryMatches(this, that): 720 721 """ 722 Return the basis of a match between 'this' and 'that' or False if the given 723 categories do not match. 724 """ 725 726 return (this == "*" or this == that) and this or \ 727 that == "*" and that or False 728 729 def categoryPermits(this, that): 730 731 """ 732 Return whether 'this' category permits 'that' category. Where 'this' is a 733 wildcard ("*"), 'that' should always match. A value of False is returned if 734 the categories do not otherwise match. 735 """ 736 737 return (this == "*" or this == that) and this or False 738 739 def getMappingFromParameterStrings(l): 740 741 """ 742 Return a mapping representing the list of "name=value" strings given by 'l'. 743 """ 744 745 parameters = {} 746 747 for parameter in l: 748 parts = parameter.split("=") 749 name = parts[0].strip() 750 value = "=".join(parts[1:]).strip() 751 parameters[name] = value 752 753 return parameters 754 755 def getContentPreferences(accept): 756 757 """ 758 Return a mapping from media types to parameters for content/media types 759 extracted from the given 'accept' header value. The mapping is returned in 760 the form of a list of (media type, parameters) tuples. 761 762 See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 763 """ 764 765 preferences = [] 766 767 for field in accept.split(","): 768 769 # The media type with parameters (defined by the "media-range") is 770 # separated from any other parameters (defined as "accept-extension" 771 # parameters) by a quality parameter. 772 773 fparts = accept_regexp.split(field) 774 775 # The first part is always the media type. 776 777 media_type = fparts[0].strip() 778 779 # Any other parts can be interpreted as extension parameters. 780 781 if len(fparts) > 1: 782 fparts = ("q=" + ";q=".join(fparts[1:])).split(";") 783 else: 784 fparts = [] 785 786 # Each field in the preferences can incorporate parameters separated by 787 # semicolon characters. 788 789 parameters = getMappingFromParameterStrings(fparts) 790 media_range = MediaRange(media_type, parameters) 791 preferences.append(media_range) 792 793 return ContentPreferences(preferences) 794 795 class ContentPreferences: 796 797 "A wrapper around content preference information." 798 799 def __init__(self, preferences): 800 self.preferences = preferences 801 802 def __iter__(self): 803 return iter(self.preferences) 804 805 def get_ordered(self, by_quality=0): 806 807 """ 808 Return a list of content/media types in descending order of preference. 809 If 'by_quality' is set to a true value, the "q" value will be used as 810 the primary measure of preference; otherwise, only the specificity will 811 be considered. 812 """ 813 814 ordered = {} 815 816 for media_range in self.preferences: 817 specificity = media_range.get_specificity() 818 819 if by_quality: 820 q = float(media_range.accept_parameters.get("q", "1")) 821 key = q, specificity 822 else: 823 key = specificity 824 825 if not ordered.has_key(key): 826 ordered[key] = [] 827 828 ordered[key].append(media_range) 829 830 # Return the preferences in descending order of quality and specificity. 831 832 keys = ordered.keys() 833 keys.sort(reverse=True) 834 return [ordered[key] for key in keys] 835 836 def get_acceptable_types(self, available): 837 838 """ 839 Return content/media types from those in the 'available' list supported 840 by the known preferences grouped by preference level in descending order 841 of preference. 842 """ 843 844 matches = {} 845 available = set(available[:]) 846 847 for level in self.get_ordered(): 848 for media_range in level: 849 850 # Attempt to match available types. 851 852 found = set() 853 for available_type in available: 854 if media_range.permits(available_type): 855 q = float(media_range.accept_parameters.get("q", "1")) 856 if not matches.has_key(q): 857 matches[q] = [] 858 matches[q].append(available_type) 859 found.add(available_type) 860 861 # Stop looking for matches for matched available types. 862 863 if found: 864 available.difference_update(found) 865 866 # Sort the matches in descending order of quality. 867 868 all_q = matches.keys() 869 870 if all_q: 871 all_q.sort(reverse=True) 872 return [matches[q] for q in all_q] 873 else: 874 return [] 875 876 def get_preferred_types(self, available): 877 878 """ 879 Return the preferred content/media types from those in the 'available' 880 list, given the known preferences. 881 """ 882 883 preferred = self.get_acceptable_types(available) 884 if preferred: 885 return preferred[0] 886 else: 887 return [] 888 889 # Content type parsing. 890 891 def getContentTypeAndEncoding(content_type): 892 893 """ 894 Return a tuple with the content/media type and encoding, extracted from the 895 given 'content_type' header value. 896 """ 897 898 m = encoding_regexp.search(content_type) 899 if m: 900 return m.group("content_type"), m.group("encoding") 901 else: 902 return None, None 903 904 # Page access functions. 905 906 def getPageURL(page): 907 908 "Return the URL of the given 'page'." 909 910 request = page.request 911 return request.getQualifiedURL(page.url(request, relative=0)) 912 913 def getFormat(page): 914 915 "Get the format used on the given 'page'." 916 917 return page.pi["format"] 918 919 def getMetadata(page): 920 921 """ 922 Return a dictionary containing items describing for the given 'page' the 923 page's "created" time, "last-modified" time, "sequence" (or revision number) 924 and the "last-comment" made about the last edit. 925 """ 926 927 request = page.request 928 929 # Get the initial revision of the page. 930 931 revisions = page.getRevList() 932 933 if not revisions: 934 return {} 935 936 event_page_initial = Page(request, page.page_name, rev=revisions[-1]) 937 938 # Get the created and last modified times. 939 940 initial_revision = getPageRevision(event_page_initial) 941 942 metadata = {} 943 metadata["created"] = initial_revision["timestamp"] 944 latest_revision = getPageRevision(page) 945 metadata["last-modified"] = latest_revision["timestamp"] 946 metadata["sequence"] = len(revisions) - 1 947 metadata["last-comment"] = latest_revision["comment"] 948 949 return metadata 950 951 def getPageRevision(page): 952 953 "Return the revision details dictionary for the given 'page'." 954 955 # From Page.edit_info... 956 957 if hasattr(page, "editlog_entry"): 958 line = page.editlog_entry() 959 else: 960 line = page._last_edited(page.request) # MoinMoin 1.5.x and 1.6.x 961 962 # Similar to Page.mtime_usecs behaviour... 963 964 if line: 965 timestamp = line.ed_time_usecs 966 mtime = wikiutil.version2timestamp(long(timestamp)) # must be long for py 2.2.x 967 comment = line.comment 968 else: 969 mtime = 0 970 comment = "" 971 972 # Leave the time zone empty. 973 974 return {"timestamp" : DateTime(time.gmtime(mtime)[:6] + (None,)), "comment" : comment} 975 976 # Page parsing and formatting of embedded content. 977 978 def getPageParserClass(request): 979 980 "Using 'request', return a parser class for the current page's format." 981 982 return getParserClass(request, getFormat(request.page)) 983 984 def getParserClass(request, format): 985 986 """ 987 Return a parser class using the 'request' for the given 'format', returning 988 a plain text parser if no parser can be found for the specified 'format'. 989 """ 990 991 try: 992 return wikiutil.searchAndImportPlugin(request.cfg, "parser", format or "plain") 993 except wikiutil.PluginMissingError: 994 return wikiutil.searchAndImportPlugin(request.cfg, "parser", "plain") 995 996 def getFormatterClass(request, format): 997 998 """ 999 Return a formatter class using the 'request' for the given output 'format', 1000 returning a plain text formatter if no formatter can be found for the 1001 specified 'format'. 1002 """ 1003 1004 try: 1005 return wikiutil.searchAndImportPlugin(request.cfg, "formatter", format or "plain") 1006 except wikiutil.PluginMissingError: 1007 return wikiutil.searchAndImportPlugin(request.cfg, "formatter", "plain") 1008 1009 def formatText(text, request, fmt, inhibit_p=True, parser_cls=None): 1010 1011 """ 1012 Format the given 'text' using the specified 'request' and formatter 'fmt'. 1013 Suppress line anchors in the output, and fix lists by indicating that a 1014 paragraph has already been started. 1015 """ 1016 1017 if not parser_cls: 1018 parser_cls = getPageParserClass(request) 1019 parser = parser_cls(text, request, line_anchors=False) 1020 1021 old_fmt = request.formatter 1022 request.formatter = fmt 1023 try: 1024 return redirectedOutput(request, parser, fmt, inhibit_p=inhibit_p) 1025 finally: 1026 request.formatter = old_fmt 1027 1028 def redirectedOutput(request, parser, fmt, **kw): 1029 1030 "A fixed version of the request method of the same name." 1031 1032 buf = StringIO() 1033 request.redirect(buf) 1034 try: 1035 parser.format(fmt, **kw) 1036 if hasattr(fmt, "flush"): 1037 buf.write(fmt.flush(True)) 1038 finally: 1039 request.redirect() 1040 text = buf.getvalue() 1041 buf.close() 1042 return text 1043 1044 # Textual representations. 1045 1046 def getSimpleWikiText(text): 1047 1048 """ 1049 Return the plain text representation of the given 'text' which may employ 1050 certain Wiki syntax features, such as those providing verbatim or monospaced 1051 text. 1052 """ 1053 1054 # NOTE: Re-implementing support for verbatim text and linking avoidance. 1055 1056 return "".join([s for s in verbatim_regexp.split(text) if s is not None]) 1057 1058 def getEncodedWikiText(text): 1059 1060 "Encode the given 'text' in a verbatim representation." 1061 1062 return "<<Verbatim(%s)>>" % text 1063 1064 def getPrettyTitle(title): 1065 1066 "Return a nicely formatted version of the given 'title'." 1067 1068 return title.replace("_", " ").replace("/", u" ? ") 1069 1070 # User interface functions. 1071 1072 def getParameter(request, name, default=None): 1073 1074 """ 1075 Using the given 'request', return the value of the parameter with the given 1076 'name', returning the optional 'default' (or None) if no value was supplied 1077 in the 'request'. 1078 """ 1079 1080 return get_form(request).get(name, [default])[0] 1081 1082 def getQualifiedParameter(request, prefix, argname, default=None): 1083 1084 """ 1085 Using the given 'request', 'prefix' and 'argname', retrieve the value of the 1086 qualified parameter, returning the optional 'default' (or None) if no value 1087 was supplied in the 'request'. 1088 """ 1089 1090 argname = getQualifiedParameterName(prefix, argname) 1091 return getParameter(request, argname, default) 1092 1093 def getQualifiedParameterName(prefix, argname): 1094 1095 """ 1096 Return the qualified parameter name using the given 'prefix' and 'argname'. 1097 """ 1098 1099 if not prefix: 1100 return argname 1101 else: 1102 return "%s-%s" % (prefix, argname) 1103 1104 # Page-related functions. 1105 1106 def getPrettyPageName(page): 1107 1108 "Return a nicely formatted title/name for the given 'page'." 1109 1110 title = page.split_title(force=1) 1111 return getPrettyTitle(title) 1112 1113 def linkToPage(request, page, text, query_string=None, anchor=None, **kw): 1114 1115 """ 1116 Using 'request', return a link to 'page' with the given link 'text' and 1117 optional 'query_string' and 'anchor'. 1118 """ 1119 1120 text = wikiutil.escape(text) 1121 return page.link_to_raw(request, text, query_string, anchor, **kw) 1122 1123 def linkToResource(url, request, text, query_string=None, anchor=None): 1124 1125 """ 1126 Using 'request', return a link to 'url' with the given link 'text' and 1127 optional 'query_string' and 'anchor'. 1128 """ 1129 1130 if anchor: 1131 url += "#%s" % anchor 1132 1133 if query_string: 1134 query_string = wikiutil.makeQueryString(query_string) 1135 url += "?%s" % query_string 1136 1137 formatter = request.page and getattr(request.page, "formatter", None) or request.html_formatter 1138 1139 output = [] 1140 output.append(formatter.url(1, url)) 1141 output.append(formatter.text(text)) 1142 output.append(formatter.url(0)) 1143 return "".join(output) 1144 1145 def getFullPageName(parent, title): 1146 1147 """ 1148 Return a full page name from the given 'parent' page (can be empty or None) 1149 and 'title' (a simple page name). 1150 """ 1151 1152 if parent: 1153 return "%s/%s" % (parent.rstrip("/"), title) 1154 else: 1155 return title 1156 1157 # vim: tabstop=4 expandtab shiftwidth=4