WebStack

WebStack/WSGI.py

739:b65d621c16da
2007-11-17 paulb [project @ 2007-11-17 00:49:08 by paulb] Relaxed the check for OpenID 2.0-conformant fields.
     1 #!/usr/bin/env python     2      3 """     4 WSGI classes.     5      6 Copyright (C) 2004, 2005, 2006, 2007 Paul Boddie <paul@boddie.org.uk>     7      8 This library is free software; you can redistribute it and/or     9 modify it under the terms of the GNU Lesser General Public    10 License as published by the Free Software Foundation; either    11 version 2.1 of the License, or (at your option) any later version.    12     13 This library is distributed in the hope that it will be useful,    14 but WITHOUT ANY WARRANTY; without even the implied warranty of    15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU    16 Lesser General Public License for more details.    17     18 You should have received a copy of the GNU Lesser General Public    19 License along with this library; if not, write to the Free Software    20 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA    21 """    22     23 import WebStack.Generic    24 import os, sys    25 from WebStack.Helpers.Request import MessageBodyStream, get_body_fields, \    26     decode_value, get_storage_items, Cookie, HeaderDict    27 from WebStack.Helpers.Response import ConvertingStream    28 from WebStack.Helpers.Auth import UserInfo    29 from WebStack.Helpers.Session import SessionStore    30 from WebStack.Helpers import Environment    31 from cgi import parse_qs, FieldStorage    32 from Cookie import SimpleCookie    33 from StringIO import StringIO    34     35 class Transaction(WebStack.Generic.Transaction):    36     37     """    38     WSGI transaction interface.    39     """    40     41     def __init__(self, env):    42     43         """    44         Initialise the transaction using the given WSGI environment 'env'.    45         """    46     47         self.env = env    48     49         # Other attributes of interest in instances of this class.    50     51         self.content_type = None    52         self.response_code = 200    53         self.content = StringIO()    54         self.headers_out = HeaderDict()    55         self.cookies_out = SimpleCookie()    56     57         # Define the incoming cookies.    58     59         self.cookies_in = SimpleCookie(self.env.get("HTTP_COOKIE"))    60     61         # Cached information.    62     63         self.storage_body = None    64     65         # Special objects retained throughout the transaction.    66     67         self.session_store = None    68     69     def commit(self):    70     71         """    72         A special method, synchronising the transaction with framework-specific    73         objects.    74         """    75     76         # Close the session store.    77     78         if self.session_store is not None:    79             self.session_store.close()    80     81     def rollback(self):    82     83         """    84         A special method, partially synchronising the transaction with    85         framework-specific objects, but discarding previously emitted content    86         that is to be replaced by an error message.    87         """    88     89         self.content = StringIO()    90         self.headers_out = HeaderDict()    91         self.cookies_out = SimpleCookie()    92     93     def get_wsgi_headers(self):    94     95         "Provide headers for the adapter."    96     97         wsgi_headers = []    98     99         if self.content_type is not None:   100             wsgi_headers.append(("Content-type", str(self.content_type)))   101    102         for header, value in self.headers_out.items():   103             wsgi_headers.append(   104                 (self.format_header_value(header), self.format_header_value(value))   105             )   106    107         # NOTE: Nasty deconstruction of Morsel values.   108    109         for value in self.cookies_out.values():   110             parts = str(value).split(": ")   111             wsgi_headers.append(   112                 (parts[0], ": ".join(parts[1:]))   113             )   114    115         return wsgi_headers   116    117     def get_wsgi_content(self):   118         self.content.seek(0)   119         return self.content.read()   120    121     # Server-related methods.   122    123     def get_server_name(self):   124    125         "Returns the server name."   126    127         return self.env.get("SERVER_NAME")   128    129     def get_server_port(self):   130    131         "Returns the server port as a string."   132    133         return self.env.get("SERVER_PORT")   134    135     # Request-related methods.   136    137     def get_request_stream(self):   138    139         """   140         Returns the request stream for the transaction.   141         """   142    143         return self.env["wsgi.input"]   144    145     def get_request_method(self):   146    147         """   148         Returns the request method.   149         """   150    151         return self.env.get("REQUEST_METHOD")   152    153     def get_headers(self):   154    155         """   156         Returns all request headers as a dictionary-like object mapping header   157         names to values.   158         """   159    160         return Environment.get_headers(self.env)   161    162     def get_header_values(self, key):   163    164         """   165         Returns a list of all request header values associated with the given   166         'key'. Note that according to RFC 2616, 'key' is treated as a   167         case-insensitive string.   168         """   169    170         return self.convert_to_list(self.get_headers().get(key))   171    172     def get_content_type(self):   173    174         """   175         Returns the content type specified on the request, along with the   176         charset employed.   177         """   178    179         return self.parse_content_type(self.env.get("CONTENT_TYPE"))   180    181     def get_content_charsets(self):   182    183         """   184         Returns the character set preferences.   185         """   186    187         return self.parse_content_preferences(None)   188    189     def get_content_languages(self):   190    191         """   192         Returns extracted language information from the transaction.   193         """   194    195         return self.parse_content_preferences(None)   196    197     def get_path(self, encoding=None):   198    199         """   200         Returns the entire path from the request as a Unicode object. Any "URL   201         encoded" character values in the part of the path before the query   202         string will be decoded and presented as genuine characters; the query   203         string will remain "URL encoded", however.   204    205         If the optional 'encoding' is set, use that in preference to the default   206         encoding to convert the path into a form not containing "URL encoded"   207         character values.   208         """   209    210         path = self.get_path_without_query(encoding)   211         qs = self.get_query_string()   212         if qs:   213             return path + "?" + qs   214         else:   215             return path   216    217     def get_path_without_query(self, encoding=None):   218    219         """   220         Returns the entire path from the request minus the query string as a   221         Unicode object containing genuine characters (as opposed to "URL   222         encoded" character values).   223    224         If the optional 'encoding' is set, use that in preference to the default   225         encoding to convert the path into a form not containing "URL encoded"   226         character values.   227         """   228    229         encoding = encoding or self.default_charset   230    231         path = decode_value(self.env.get("SCRIPT_NAME") or "", encoding)   232         path += self.get_path_info(encoding)   233         return path   234    235     def get_path_info(self, encoding=None):   236    237         """   238         Returns the "path info" (the part of the URL after the resource name   239         handling the current request) from the request as a Unicode object   240         containing genuine characters (as opposed to "URL encoded" character   241         values).   242    243         If the optional 'encoding' is set, use that in preference to the default   244         encoding to convert the path into a form not containing "URL encoded"   245         character values.   246         """   247    248         encoding = encoding or self.default_charset   249    250         return decode_value(self.env.get("PATH_INFO") or "", encoding)   251    252     def get_query_string(self):   253    254         """   255         Returns the query string from the path in the request.   256         """   257    258         return self.env.get("QUERY_STRING") or ""   259    260     # Higher level request-related methods.   261    262     def get_fields_from_path(self, encoding=None):   263    264         """   265         Extracts fields (or request parameters) from the path specified in the   266         transaction. The underlying framework may refuse to supply fields from   267         the path if handling a POST transaction. The optional 'encoding'   268         parameter specifies the character encoding of the query string for cases   269         where the default encoding is to be overridden.   270    271         Returns a dictionary mapping field names to lists of values (even if a   272         single value is associated with any given field name).   273         """   274    275         encoding = encoding or self.default_charset   276    277         fields = {}   278         for name, values in parse_qs(self.get_query_string(), keep_blank_values=1).items():   279             name = decode_value(name, encoding)   280             fields[name] = []   281             for value in values:   282                 value = decode_value(value, encoding)   283                 fields[name].append(value)   284         return fields   285    286     def get_fields_from_body(self, encoding=None):   287    288         """   289         Extracts fields (or request parameters) from the message body in the   290         transaction. The optional 'encoding' parameter specifies the character   291         encoding of the message body for cases where no such information is   292         available, but where the default encoding is to be overridden.   293    294         Returns a dictionary mapping field names to lists of values (even if a   295         single value is associated with any given field name). Each value is   296         either a Unicode object (representing a simple form field, for example)   297         or a WebStack.Helpers.Request.FileContent object (representing a file   298         upload form field).   299         """   300    301         encoding = encoding or self.get_content_type().charset or self.default_charset   302    303         if self.storage_body is None:   304             self.storage_body = FieldStorage(fp=self.get_request_stream(),   305                 headers=self.get_headers(),   306                 environ={"REQUEST_METHOD" : self.get_request_method()},   307                 keep_blank_values=1)   308    309         # Avoid strange design issues with FieldStorage by checking the internal   310         # field list directly.   311    312         fields = {}   313         if self.storage_body.list is not None:   314    315             # Traverse the storage, finding each field value.   316    317             fields = get_body_fields(get_storage_items(self.storage_body), encoding)   318    319         return fields   320    321     def get_fields(self, encoding=None):   322    323         """   324         Extracts fields (or request parameters) from both the path specified in   325         the transaction as well as the message body. The optional 'encoding'   326         parameter specifies the character encoding of the message body for cases   327         where no such information is available, but where the default encoding   328         is to be overridden.   329    330         Returns a dictionary mapping field names to lists of values (even if a   331         single value is associated with any given field name). Each value is   332         either a Unicode object (representing a simple form field, for example)   333         or a WebStack.Helpers.Request.FileContent object (representing a file   334         upload form field).   335    336         Where a given field name is used in both the path and message body to   337         specify values, the values from both sources will be combined into a   338         single list associated with that field name.   339         """   340    341         # Combine the two sources.   342    343         fields = {}   344         fields.update(self.get_fields_from_path())   345         for name, values in self.get_fields_from_body(encoding).items():   346             if not fields.has_key(name):   347                 fields[name] = values   348             else:   349                 fields[name] += values   350         return fields   351    352     def get_user(self):   353    354         """   355         Extracts user information from the transaction.   356    357         Returns a username as a string or None if no user is defined.   358         """   359    360         if self.user is not None:   361             return self.user   362         else:   363             return self.env.get("REMOTE_USER")   364    365     def get_cookies(self):   366    367         """   368         Obtains cookie information from the request.   369    370         Returns a dictionary mapping cookie names to cookie objects.   371         """   372    373         return self.process_cookies(self.cookies_in)   374    375     def get_cookie(self, cookie_name):   376    377         """   378         Obtains cookie information from the request.   379    380         Returns a cookie object for the given 'cookie_name' or None if no such   381         cookie exists.   382         """   383    384         cookie = self.cookies_in.get(self.encode_cookie_value(cookie_name))   385         if cookie is not None:   386             return Cookie(cookie_name, self.decode_cookie_value(cookie.value))   387         else:   388             return None   389    390     # Response-related methods.   391    392     def get_response_stream(self):   393    394         """   395         Returns the response stream for the transaction.   396         """   397    398         # Return a stream which is later emptied into the real stream.   399         # Unicode can upset this operation. Using either the specified charset   400         # or a default encoding.   401    402         encoding = self.get_response_stream_encoding()   403         return ConvertingStream(self.content, encoding)   404    405     def get_response_stream_encoding(self):   406    407         """   408         Returns the response stream encoding.   409         """   410    411         if self.content_type:   412             encoding = self.content_type.charset   413         else:   414             encoding = None   415         return encoding or self.default_charset   416    417     def get_response_code(self):   418    419         """   420         Get the response code associated with the transaction. If no response   421         code is defined, None is returned.   422         """   423    424         return self.response_code   425    426     def set_response_code(self, response_code):   427    428         """   429         Set the 'response_code' using a numeric constant defined in the HTTP   430         specification.   431         """   432    433         self.response_code = response_code   434    435     def set_header_value(self, header, value):   436    437         """   438         Set the HTTP 'header' with the given 'value'.   439         """   440    441         # The header is not written out immediately due to the buffering in use.   442    443         self.headers_out[header] = value   444    445     def set_content_type(self, content_type):   446    447         """   448         Sets the 'content_type' for the response.   449         """   450    451         # The content type has to be written as a header, before actual content,   452         # but after the response line. This means that some kind of buffering is   453         # required. Hence, we don't write the header out immediately.   454    455         self.content_type = content_type   456    457     # Higher level response-related methods.   458    459     def set_cookie(self, cookie):   460    461         """   462         Stores the given 'cookie' object in the response.   463         """   464    465         # NOTE: If multiple cookies of the same name could be specified, this   466         # NOTE: could need changing.   467    468         self.set_cookie_value(cookie.name, cookie.value)   469    470     def set_cookie_value(self, name, value, path=None, expires=None):   471    472         """   473         Stores a cookie with the given 'name' and 'value' in the response.   474    475         The optional 'path' is a string which specifies the scope of the cookie,   476         and the optional 'expires' parameter is a value compatible with the   477         time.time function, and indicates the expiry date/time of the cookie.   478         """   479    480         name = self.encode_cookie_value(name)   481         self.cookies_out[name] = self.encode_cookie_value(value)   482         if path is not None:   483             self.cookies_out[name]["path"] = path   484         if expires is not None:   485             self.cookies_out[name]["expires"] = expires   486    487     def delete_cookie(self, cookie_name):   488    489         """   490         Adds to the response a request that the cookie with the given   491         'cookie_name' be deleted/discarded by the client.   492         """   493    494         # Create a special cookie, given that we do not know whether the browser   495         # has been sent the cookie or not.   496         # NOTE: Magic discovered in Webware.   497    498         name = self.encode_cookie_value(cookie_name)   499         self.cookies_out[name] = ""   500         self.cookies_out[name]["path"] = "/"   501         self.cookies_out[name]["expires"] = 0   502         self.cookies_out[name]["max-age"] = 0   503    504     # Session-related methods.   505    506     def get_session(self, create=1):   507    508         """   509         Gets a session corresponding to an identifier supplied in the   510         transaction.   511    512         If no session has yet been established according to information   513         provided in the transaction then the optional 'create' parameter   514         determines whether a new session will be established.   515    516         Where no session has been established and where 'create' is set to 0   517         then None is returned. In all other cases, a session object is created   518         (where appropriate) and returned.   519         """   520    521         # NOTE: Requires configuration.   522    523         if self.session_store is None:   524             self.session_store = SessionStore(self, "WebStack-sessions")   525         return self.session_store.get_session(create)   526    527     def expire_session(self):   528    529         """   530         Expires any session established according to information provided in the   531         transaction.   532         """   533    534         # NOTE: Requires configuration.   535    536         if self.session_store is None:   537             self.session_store = SessionStore(self, "WebStack-sessions")   538         self.session_store.expire_session()   539    540 # vim: tabstop=4 expandtab shiftwidth=4