WebStack

WebStack/CGI.py

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