1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - MoinSupport library (derived from EventAggregatorSupport) 4 5 @copyright: 2008, 2009, 2010, 2011, 2012, 2013 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 ItemSupport import ItemDirectoryStore 13 from MoinMoin.Page import Page 14 from MoinMoin.util import lock 15 from MoinMoin import config, search, wikiutil 16 from StringIO import StringIO 17 from shlex import shlex 18 import re 19 import time 20 import os 21 22 # Moin 1.9 request parameters. 23 24 try: 25 from MoinMoin.support.werkzeug.datastructures import MultiDict 26 except ImportError: 27 pass 28 29 __version__ = "0.2" 30 31 # Extraction of shared fragments. 32 33 marker_regexp_str = r"([{]{3,}|[}]{3,})" 34 marker_regexp = re.compile(marker_regexp_str, re.MULTILINE | re.DOTALL) # {{{... or }}}... 35 36 # Extraction of headings. 37 38 heading_regexp = re.compile(r"^(?P<level>=+)(?P<heading>.*?)(?P=level)$", re.UNICODE | re.MULTILINE) 39 40 # Category extraction from pages. 41 42 category_regexp = None 43 44 # Simple content parsing. 45 46 verbatim_regexp = re.compile(ur'(?:' 47 ur'<<Verbatim\((?P<verbatim>.*?)\)>>' 48 ur'|' 49 ur'\[\[Verbatim\((?P<verbatim2>.*?)\)\]\]' 50 ur'|' 51 ur'!(?P<verbatim3>.*?)(\s|$)?' 52 ur'|' 53 ur'`(?P<monospace>.*?)`' 54 ur'|' 55 ur'{{{(?P<preformatted>.*?)}}}' 56 ur')', re.UNICODE) 57 58 # Category discovery. 59 60 def getCategoryPattern(request): 61 global category_regexp 62 63 try: 64 return request.cfg.cache.page_category_regexact 65 except AttributeError: 66 67 # Use regular expression from MoinMoin 1.7.1 otherwise. 68 69 if category_regexp is None: 70 category_regexp = re.compile(u'^%s$' % ur'(?P<all>Category(?P<key>(?!Template)\S+))', re.UNICODE) 71 return category_regexp 72 73 def getCategories(request): 74 75 """ 76 From the AdvancedSearch macro, return a list of category page names using 77 the given 'request'. 78 """ 79 80 # This will return all pages with "Category" in the title. 81 82 cat_filter = getCategoryPattern(request).search 83 return request.rootpage.getPageList(filter=cat_filter) 84 85 def getCategoryMapping(category_pagenames, request): 86 87 """ 88 For the given 'category_pagenames' return a list of tuples of the form 89 (category name, category page name) using the given 'request'. 90 """ 91 92 cat_pattern = getCategoryPattern(request) 93 mapping = [] 94 for pagename in category_pagenames: 95 name = cat_pattern.match(pagename).group("key") 96 if name != "Category": 97 mapping.append((name, pagename)) 98 mapping.sort() 99 return mapping 100 101 def getCategoryPages(pagename, request): 102 103 """ 104 Return the pages associated with the given category 'pagename' using the 105 'request'. 106 """ 107 108 query = search.QueryParser().parse_query('category:%s' % pagename) 109 results = search.searchPages(request, query, "page_name") 110 return filterCategoryPages(results, request) 111 112 def filterCategoryPages(results, request): 113 114 "Filter category pages from the given 'results' using the 'request'." 115 116 cat_pattern = getCategoryPattern(request) 117 pages = [] 118 for page in results.hits: 119 if not cat_pattern.match(page.page_name): 120 pages.append(page) 121 return pages 122 123 def getAllCategoryPages(category_names, request): 124 125 """ 126 Return all pages belonging to the categories having the given 127 'category_names', using the given 'request'. 128 """ 129 130 pages = [] 131 pagenames = set() 132 133 for category_name in category_names: 134 135 # Get the pages and page names in the category. 136 137 pages_in_category = getCategoryPages(category_name, request) 138 139 # Visit each page in the category. 140 141 for page_in_category in pages_in_category: 142 pagename = page_in_category.page_name 143 144 # Only process each page once. 145 146 if pagename in pagenames: 147 continue 148 else: 149 pagenames.add(pagename) 150 151 pages.append(page_in_category) 152 153 return pages 154 155 def getPagesForSearch(search_pattern, request): 156 157 """ 158 Return result pages for a search employing the given 'search_pattern' and 159 using the given 'request'. 160 """ 161 162 query = search.QueryParser().parse_query(search_pattern) 163 results = search.searchPages(request, query, "page_name") 164 return filterCategoryPages(results, request) 165 166 # WikiDict functions. 167 168 def getWikiDict(pagename, request): 169 170 """ 171 Return the WikiDict provided by the given 'pagename' using the given 172 'request'. 173 """ 174 175 if pagename and Page(request, pagename).exists() and request.user.may.read(pagename): 176 if hasattr(request.dicts, "dict"): 177 return request.dicts.dict(pagename) 178 else: 179 return request.dicts[pagename] 180 else: 181 return None 182 183 # Searching-related functions. 184 185 def getPagesFromResults(result_pages, request): 186 187 "Return genuine pages for the given 'result_pages' using the 'request'." 188 189 return [Page(request, page.page_name) for page in result_pages] 190 191 # Region/section parsing. 192 193 def getRegions(s, include_non_regions=False): 194 195 """ 196 Parse the string 's', returning a list of explicitly declared regions. 197 198 If 'include_non_regions' is specified as a true value, fragments will be 199 included for text between explicitly declared regions. 200 """ 201 202 regions = [] 203 marker = None 204 is_block = True 205 206 # Start a region for exposed text, if appropriate. 207 208 if include_non_regions: 209 regions.append("") 210 211 for match_text in marker_regexp.split(s): 212 213 # Capture section text. 214 215 if is_block: 216 if marker or include_non_regions: 217 regions[-1] += match_text 218 219 # Handle section markers. 220 221 else: 222 223 # Close any open sections, returning to exposed text regions. 224 225 if marker: 226 227 # Add any marker to the current region, regardless of whether it 228 # successfully closes a section. 229 230 regions[-1] += match_text 231 232 if match_text.startswith("}") and len(marker) == len(match_text): 233 marker = None 234 235 # Start a region for exposed text, if appropriate. 236 237 if include_non_regions: 238 regions.append("") 239 240 # Without a current marker, start a new section. 241 242 else: 243 marker = match_text 244 regions.append("") 245 246 # Add the marker to the new region. 247 248 regions[-1] += match_text 249 250 # The match text alternates between text between markers and the markers 251 # themselves. 252 253 is_block = not is_block 254 255 return regions 256 257 def getFragmentsFromRegions(regions): 258 259 """ 260 Return fragments from the given 'regions', each having the form 261 (format, attributes, body text). 262 """ 263 264 fragments = [] 265 266 for region in regions: 267 format, attributes, body, header, close = getFragmentFromRegion(region) 268 fragments.append((format, attributes, body)) 269 270 return fragments 271 272 def getFragmentFromRegion(region): 273 274 """ 275 Return a fragment for the given 'region' having the form (format, 276 attributes, body text, header, close), where the 'header' is the original 277 declaration of the 'region' or None if no explicit region is defined, and 278 'close' is the closing marker of the 'region' or None if no explicit region 279 is defined. 280 """ 281 282 if region.startswith("{{{"): 283 284 body = region.lstrip("{") 285 level = len(region) - len(body) 286 body = body.rstrip("}").lstrip() 287 288 # Remove any prelude and process metadata. 289 290 if body.startswith("#!"): 291 292 try: 293 declaration, body = body.split("\n", 1) 294 except ValueError: 295 declaration = body 296 body = "" 297 298 arguments = declaration[2:] 299 300 # Get any parser/format declaration. 301 302 if arguments and not arguments[0].isspace(): 303 details = arguments.split(None, 1) 304 if len(details) == 2: 305 format, arguments = details 306 else: 307 format = details[0] 308 arguments = "" 309 else: 310 format = None 311 312 # Get the attributes/arguments for the region. 313 314 attributes = parseAttributes(arguments, False) 315 316 # Add an entry for the format in the attribute dictionary. 317 318 if format and not attributes.has_key(format): 319 attributes[format] = True 320 321 return format, attributes, body, level * "{" + declaration + "\n", level * "}" 322 323 else: 324 return None, {}, body, level * "{" + "\n", level * "}" 325 326 else: 327 return None, {}, region, None, None 328 329 def getFragments(s, include_non_regions=False): 330 331 """ 332 Return fragments for the given string 's', each having the form 333 (format, arguments, body text). 334 335 If 'include_non_regions' is specified as a true value, fragments will be 336 included for text between explicitly declared regions. 337 """ 338 339 return getFragmentsFromRegions(getRegions(s, include_non_regions)) 340 341 # Heading extraction. 342 343 def getHeadings(s): 344 345 """ 346 Return tuples of the form (level, title, span) for headings found within the 347 given string 's'. The span is itself a (start, end) tuple indicating the 348 matching region of 's' for a heading declaration. 349 """ 350 351 headings = [] 352 353 for match in heading_regexp.finditer(s): 354 headings.append( 355 (len(match.group("level")), match.group("heading"), match.span()) 356 ) 357 358 return headings 359 360 # Region/section attribute parsing. 361 362 def parseAttributes(s, escape=True): 363 364 """ 365 Parse the section attributes string 's', returning a mapping of names to 366 values. If 'escape' is set to a true value, the attributes will be suitable 367 for use with the formatter API. If 'escape' is set to a false value, the 368 attributes will have any quoting removed. 369 """ 370 371 attrs = {} 372 f = StringIO(s) 373 name = None 374 need_value = False 375 lex = shlex(f) 376 lex.wordchars += "-" 377 378 for token in lex: 379 380 # Capture the name if needed. 381 382 if name is None: 383 name = escape and wikiutil.escape(token) or strip_token(token) 384 385 # Detect either an equals sign or another name. 386 387 elif not need_value: 388 if token == "=": 389 need_value = True 390 else: 391 attrs[name.lower()] = escape and "true" or True 392 name = wikiutil.escape(token) 393 394 # Otherwise, capture a value. 395 396 else: 397 # Quoting of attributes done similarly to wikiutil.parseAttributes. 398 399 if token: 400 if escape: 401 if token[0] in ("'", '"'): 402 token = wikiutil.escape(token) 403 else: 404 token = '"%s"' % wikiutil.escape(token, 1) 405 else: 406 token = strip_token(token) 407 408 attrs[name.lower()] = token 409 name = None 410 need_value = False 411 412 # Handle any name-only attributes at the end of the collection. 413 414 if name and not need_value: 415 attrs[name.lower()] = escape and "true" or True 416 417 return attrs 418 419 def strip_token(token): 420 421 "Return the given 'token' stripped of quoting." 422 423 if token[0] in ("'", '"') and token[-1] == token[0]: 424 return token[1:-1] 425 else: 426 return token 427 428 # Request-related classes and associated functions. 429 430 class Form: 431 432 """ 433 A wrapper preserving MoinMoin 1.8.x (and earlier) behaviour in a 1.9.x 434 environment. 435 """ 436 437 def __init__(self, request): 438 self.request = request 439 self.form = request.values 440 441 def has_key(self, name): 442 return not not self.form.getlist(name) 443 444 def get(self, name, default=None): 445 values = self.form.getlist(name) 446 if not values: 447 return default 448 else: 449 return values 450 451 def __getitem__(self, name): 452 return self.form.getlist(name) 453 454 def __setitem__(self, name, value): 455 try: 456 self.form.setlist(name, value) 457 except TypeError: 458 self._write_enable() 459 self.form.setlist(name, value) 460 461 def __delitem__(self, name): 462 try: 463 del self.form[name] 464 except TypeError: 465 self._write_enable() 466 del self.form[name] 467 468 def _write_enable(self): 469 self.form = self.request.values = MultiDict(self.form) 470 471 def keys(self): 472 return self.form.keys() 473 474 def items(self): 475 return self.form.lists() 476 477 class ActionSupport: 478 479 """ 480 Work around disruptive MoinMoin changes in 1.9, and also provide useful 481 convenience methods. 482 """ 483 484 def get_form(self): 485 return get_form(self.request) 486 487 def _get_selected(self, value, input_value): 488 489 """ 490 Return the HTML attribute text indicating selection of an option (or 491 otherwise) if 'value' matches 'input_value'. 492 """ 493 494 return input_value is not None and value == input_value and 'selected="selected"' or '' 495 496 def _get_selected_for_list(self, value, input_values): 497 498 """ 499 Return the HTML attribute text indicating selection of an option (or 500 otherwise) if 'value' matches one of the 'input_values'. 501 """ 502 503 return value in input_values and 'selected="selected"' or '' 504 505 def get_option_list(self, value, values): 506 507 """ 508 Return a list of HTML element definitions for options describing the 509 given 'values', selecting the option with the specified 'value' if 510 present. 511 """ 512 513 options = [] 514 for available_value in values: 515 selected = self._get_selected(available_value, value) 516 options.append('<option value="%s" %s>%s</option>' % ( 517 escattr(available_value), selected, wikiutil.escape(available_value))) 518 return options 519 520 def _get_input(self, form, name, default=None): 521 522 """ 523 Return the input from 'form' having the given 'name', returning either 524 the input converted to an integer or the given 'default' (optional, None 525 if not specified). 526 """ 527 528 value = form.get(name, [None])[0] 529 if not value: # true if 0 obtained 530 return default 531 else: 532 return int(value) 533 534 def get_form(request): 535 536 "Work around disruptive MoinMoin changes in 1.9." 537 538 if hasattr(request, "values"): 539 return Form(request) 540 else: 541 return request.form 542 543 class send_headers_cls: 544 545 """ 546 A wrapper to preserve MoinMoin 1.8.x (and earlier) request behaviour in a 547 1.9.x environment. 548 """ 549 550 def __init__(self, request): 551 self.request = request 552 553 def __call__(self, headers): 554 for header in headers: 555 parts = header.split(":") 556 self.request.headers.add(parts[0], ":".join(parts[1:])) 557 558 def get_send_headers(request): 559 560 "Return a function that can send response headers." 561 562 if hasattr(request, "http_headers"): 563 return request.http_headers 564 elif hasattr(request, "emit_http_headers"): 565 return request.emit_http_headers 566 else: 567 return send_headers_cls(request) 568 569 def escattr(s): 570 return wikiutil.escape(s, 1) 571 572 def getPathInfo(request): 573 if hasattr(request, "getPathinfo"): 574 return request.getPathinfo() 575 else: 576 return request.path 577 578 def getHeader(request, header_name, prefix=None): 579 580 """ 581 Using the 'request', return the value of the header with the given 582 'header_name', using the optional 'prefix' to obtain protocol-specific 583 headers if necessary. 584 585 If no value is found for the given 'header_name', None is returned. 586 """ 587 588 if hasattr(request, "getHeader"): 589 return request.getHeader(header_name) 590 elif hasattr(request, "headers"): 591 return request.headers.get(header_name) 592 else: 593 return request.env.get((prefix and prefix + "_" or "") + header_name.upper()) 594 595 def writeHeaders(request, mimetype, metadata, status=None): 596 597 """ 598 Using the 'request', write resource headers using the given 'mimetype', 599 based on the given 'metadata'. If the optional 'status' is specified, set 600 the status header to the given value. 601 """ 602 603 send_headers = get_send_headers(request) 604 605 # Define headers. 606 607 headers = ["Content-Type: %s; charset=%s" % (mimetype, config.charset)] 608 609 # Define the last modified time. 610 # NOTE: Consider using request.httpDate. 611 612 latest_timestamp = metadata.get("last-modified") 613 if latest_timestamp: 614 headers.append("Last-Modified: %s" % latest_timestamp.as_HTTP_datetime_string()) 615 616 if status: 617 headers.append("Status: %s" % status) 618 619 send_headers(headers) 620 621 # Page access functions. 622 623 def getPageURL(page): 624 625 "Return the URL of the given 'page'." 626 627 request = page.request 628 return request.getQualifiedURL(page.url(request, relative=0)) 629 630 def getFormat(page): 631 632 "Get the format used on the given 'page'." 633 634 return page.pi["format"] 635 636 def getMetadata(page): 637 638 """ 639 Return a dictionary containing items describing for the given 'page' the 640 page's "created" time, "last-modified" time, "sequence" (or revision number) 641 and the "last-comment" made about the last edit. 642 """ 643 644 request = page.request 645 646 # Get the initial revision of the page. 647 648 revisions = page.getRevList() 649 650 if not revisions: 651 return {} 652 653 event_page_initial = Page(request, page.page_name, rev=revisions[-1]) 654 655 # Get the created and last modified times. 656 657 initial_revision = getPageRevision(event_page_initial) 658 659 metadata = {} 660 metadata["created"] = initial_revision["timestamp"] 661 latest_revision = getPageRevision(page) 662 metadata["last-modified"] = latest_revision["timestamp"] 663 metadata["sequence"] = len(revisions) - 1 664 metadata["last-comment"] = latest_revision["comment"] 665 666 return metadata 667 668 def getPageRevision(page): 669 670 "Return the revision details dictionary for the given 'page'." 671 672 # From Page.edit_info... 673 674 if hasattr(page, "editlog_entry"): 675 line = page.editlog_entry() 676 else: 677 line = page._last_edited(page.request) # MoinMoin 1.5.x and 1.6.x 678 679 # Similar to Page.mtime_usecs behaviour... 680 681 if line: 682 timestamp = line.ed_time_usecs 683 mtime = wikiutil.version2timestamp(long(timestamp)) # must be long for py 2.2.x 684 comment = line.comment 685 else: 686 mtime = 0 687 comment = "" 688 689 # Leave the time zone empty. 690 691 return {"timestamp" : DateTime(time.gmtime(mtime)[:6] + (None,)), "comment" : comment} 692 693 # Page parsing and formatting of embedded content. 694 695 def getPageParserClass(request): 696 697 "Using 'request', return a parser class for the current page's format." 698 699 return getParserClass(request, getFormat(request.page)) 700 701 def getParserClass(request, format): 702 703 """ 704 Return a parser class using the 'request' for the given 'format', returning 705 a plain text parser if no parser can be found for the specified 'format'. 706 """ 707 708 try: 709 return wikiutil.searchAndImportPlugin(request.cfg, "parser", format or "plain") 710 except wikiutil.PluginMissingError: 711 return wikiutil.searchAndImportPlugin(request.cfg, "parser", "plain") 712 713 def getFormatterClass(request, format): 714 715 """ 716 Return a formatter class using the 'request' for the given output 'format', 717 returning a plain text formatter if no formatter can be found for the 718 specified 'format'. 719 """ 720 721 try: 722 return wikiutil.searchAndImportPlugin(request.cfg, "formatter", format or "plain") 723 except wikiutil.PluginMissingError: 724 return wikiutil.searchAndImportPlugin(request.cfg, "formatter", "plain") 725 726 def formatText(text, request, fmt, inhibit_p=True, parser_cls=None): 727 728 """ 729 Format the given 'text' using the specified 'request' and formatter 'fmt'. 730 Suppress line anchors in the output, and fix lists by indicating that a 731 paragraph has already been started. 732 """ 733 734 if not parser_cls: 735 parser_cls = getPageParserClass(request) 736 parser = parser_cls(text, request, line_anchors=False) 737 738 old_fmt = request.formatter 739 request.formatter = fmt 740 try: 741 return redirectedOutput(request, parser, fmt, inhibit_p=inhibit_p) 742 finally: 743 request.formatter = old_fmt 744 745 def redirectedOutput(request, parser, fmt, **kw): 746 747 "A fixed version of the request method of the same name." 748 749 buf = StringIO() 750 request.redirect(buf) 751 try: 752 parser.format(fmt, **kw) 753 if hasattr(fmt, "flush"): 754 buf.write(fmt.flush(True)) 755 finally: 756 request.redirect() 757 text = buf.getvalue() 758 buf.close() 759 return text 760 761 # Textual representations. 762 763 def getSimpleWikiText(text): 764 765 """ 766 Return the plain text representation of the given 'text' which may employ 767 certain Wiki syntax features, such as those providing verbatim or monospaced 768 text. 769 """ 770 771 # NOTE: Re-implementing support for verbatim text and linking avoidance. 772 773 return "".join([s for s in verbatim_regexp.split(text) if s is not None]) 774 775 def getEncodedWikiText(text): 776 777 "Encode the given 'text' in a verbatim representation." 778 779 return "<<Verbatim(%s)>>" % text 780 781 def getPrettyTitle(title): 782 783 "Return a nicely formatted version of the given 'title'." 784 785 return title.replace("_", " ").replace("/", u" ? ") 786 787 # User interface functions. 788 789 def getParameter(request, name, default=None): 790 791 """ 792 Using the given 'request', return the value of the parameter with the given 793 'name', returning the optional 'default' (or None) if no value was supplied 794 in the 'request'. 795 """ 796 797 return get_form(request).get(name, [default])[0] 798 799 def getQualifiedParameter(request, prefix, argname, default=None): 800 801 """ 802 Using the given 'request', 'prefix' and 'argname', retrieve the value of the 803 qualified parameter, returning the optional 'default' (or None) if no value 804 was supplied in the 'request'. 805 """ 806 807 argname = getQualifiedParameterName(prefix, argname) 808 return getParameter(request, argname, default) 809 810 def getQualifiedParameterName(prefix, argname): 811 812 """ 813 Return the qualified parameter name using the given 'prefix' and 'argname'. 814 """ 815 816 if not prefix: 817 return argname 818 else: 819 return "%s-%s" % (prefix, argname) 820 821 # Page-related functions. 822 823 def getPrettyPageName(page): 824 825 "Return a nicely formatted title/name for the given 'page'." 826 827 title = page.split_title(force=1) 828 return getPrettyTitle(title) 829 830 def linkToPage(request, page, text, query_string=None, anchor=None, **kw): 831 832 """ 833 Using 'request', return a link to 'page' with the given link 'text' and 834 optional 'query_string' and 'anchor'. 835 """ 836 837 text = wikiutil.escape(text) 838 return page.link_to_raw(request, text, query_string, anchor, **kw) 839 840 def linkToResource(url, request, text, query_string=None, anchor=None): 841 842 """ 843 Using 'request', return a link to 'url' with the given link 'text' and 844 optional 'query_string' and 'anchor'. 845 """ 846 847 if anchor: 848 url += "#%s" % anchor 849 850 if query_string: 851 query_string = wikiutil.makeQueryString(query_string) 852 url += "?%s" % query_string 853 854 formatter = request.page and getattr(request.page, "formatter", None) or request.html_formatter 855 856 output = [] 857 output.append(formatter.url(1, url)) 858 output.append(formatter.text(text)) 859 output.append(formatter.url(0)) 860 return "".join(output) 861 862 def getFullPageName(parent, title): 863 864 """ 865 Return a full page name from the given 'parent' page (can be empty or None) 866 and 'title' (a simple page name). 867 """ 868 869 if parent: 870 return "%s/%s" % (parent.rstrip("/"), title) 871 else: 872 return title 873 874 # Content storage support. 875 876 class ItemStore(ItemDirectoryStore): 877 878 "A page-specific item store." 879 880 def __init__(self, page, item_dir_name="items", lock_dir_name="item-locks"): 881 882 "Initialise an item store for the given 'page'." 883 884 ItemDirectoryStore.__init__(self, page.getPagePath(item_dir_name), page.getPagePath(lock_dir_name)) 885 self.page = page 886 887 def can_write(self): 888 889 """ 890 Return whether the user associated with the request can write to the 891 page owning this store. 892 """ 893 894 user = self.page.request.user 895 return user and user.may.write(self.page.page_name) 896 897 # High-level methods. 898 899 def append(self, item): 900 901 "Append the given 'item' to the store." 902 903 if not self.can_write(): 904 return 905 906 ItemDirectoryStore.append(self, item) 907 908 # vim: tabstop=4 expandtab shiftwidth=4