WebStack

WebStack/CGI.py

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