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