MoinSupport

MoinSupport.py

17:4e984fd40300
2012-07-17 Paul Boddie Prevent getHeader from raising an exception when a header is absent, returning None instead.
     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