1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/WebStack/WSGI.py Wed Dec 01 00:56:03 2004 +0000
1.3 @@ -0,0 +1,479 @@
1.4 +#!/usr/bin/env python
1.5 +
1.6 +"""
1.7 +WSGI classes.
1.8 +"""
1.9 +
1.10 +import Generic
1.11 +import os, sys
1.12 +from Helpers.Request import MessageBodyStream, get_body_fields, get_storage_items, Cookie
1.13 +from Helpers.Response import ConvertingStream
1.14 +from Helpers.Auth import UserInfo
1.15 +from Helpers.Session import SessionStore
1.16 +from Helpers import Environment
1.17 +from cgi import parse_qs, FieldStorage
1.18 +from Cookie import SimpleCookie
1.19 +from StringIO import StringIO
1.20 +
1.21 +class Transaction(Generic.Transaction):
1.22 +
1.23 + """
1.24 + WSGI transaction interface.
1.25 + """
1.26 +
1.27 + def __init__(self, env):
1.28 +
1.29 + """
1.30 + Initialise the transaction using the given WSGI environment 'env'.
1.31 + """
1.32 +
1.33 + self.env = env
1.34 +
1.35 + # Other attributes of interest in instances of this class.
1.36 +
1.37 + self.content_type = None
1.38 + self.response_code = 200
1.39 + self.content = StringIO()
1.40 + self.headers_out = {}
1.41 + self.cookies_out = SimpleCookie()
1.42 + self.user = None
1.43 +
1.44 + # Define the incoming cookies.
1.45 +
1.46 + self.cookies_in = SimpleCookie(self.env.get("HTTP_COOKIE"))
1.47 +
1.48 + # Cached information.
1.49 +
1.50 + self.storage_body = None
1.51 +
1.52 + # Special objects retained throughout the transaction.
1.53 +
1.54 + self.session_store = None
1.55 +
1.56 + def commit(self):
1.57 +
1.58 + """
1.59 + A special method, synchronising the transaction with framework-specific
1.60 + objects.
1.61 + """
1.62 +
1.63 + # Close the session store.
1.64 +
1.65 + if self.session_store is not None:
1.66 + self.session_store.close()
1.67 +
1.68 + def get_wsgi_headers(self):
1.69 + wsgi_headers = []
1.70 +
1.71 + if self.content_type is not None:
1.72 + wsgi_headers.append(("Content-type", str(self.content_type)))
1.73 +
1.74 + for header, value in self.headers_out.items():
1.75 + wsgi_headers.append(
1.76 + (self.format_header_value(header), self.format_header_value(value))
1.77 + )
1.78 +
1.79 + # NOTE: Nasty deconstruction of Morsel values.
1.80 +
1.81 + for value in self.cookies_out.values():
1.82 + parts = str(value).split(": ")
1.83 + wsgi_headers.append(
1.84 + (parts[0], ": ".join(parts[1:]))
1.85 + )
1.86 +
1.87 + return wsgi_headers
1.88 +
1.89 + def get_wsgi_content(self):
1.90 + self.content.seek(0)
1.91 + return self.content.read()
1.92 +
1.93 + # Request-related methods.
1.94 +
1.95 + def get_request_stream(self):
1.96 +
1.97 + """
1.98 + Returns the request stream for the transaction.
1.99 + """
1.100 +
1.101 + return self.env["wsgi.input"]
1.102 +
1.103 + def get_request_method(self):
1.104 +
1.105 + """
1.106 + Returns the request method.
1.107 + """
1.108 +
1.109 + return self.env.get("REQUEST_METHOD")
1.110 +
1.111 + def get_headers(self):
1.112 +
1.113 + """
1.114 + Returns all request headers as a dictionary-like object mapping header
1.115 + names to values.
1.116 + """
1.117 +
1.118 + return Environment.get_headers(self.env)
1.119 +
1.120 + def get_header_values(self, key):
1.121 +
1.122 + """
1.123 + Returns a list of all request header values associated with the given
1.124 + 'key'. Note that according to RFC 2616, 'key' is treated as a
1.125 + case-insensitive string.
1.126 + """
1.127 +
1.128 + return self.convert_to_list(self.get_headers().get(key))
1.129 +
1.130 + def get_content_type(self):
1.131 +
1.132 + """
1.133 + Returns the content type specified on the request, along with the
1.134 + charset employed.
1.135 + """
1.136 +
1.137 + return self.parse_content_type(self.env.get("CONTENT_TYPE"))
1.138 +
1.139 + def get_content_charsets(self):
1.140 +
1.141 + """
1.142 + Returns the character set preferences.
1.143 + """
1.144 +
1.145 + return self.parse_content_preferences(None)
1.146 +
1.147 + def get_content_languages(self):
1.148 +
1.149 + """
1.150 + Returns extracted language information from the transaction.
1.151 + """
1.152 +
1.153 + return self.parse_content_preferences(None)
1.154 +
1.155 + def get_path(self):
1.156 +
1.157 + """
1.158 + Returns the entire path from the request.
1.159 + """
1.160 +
1.161 + path = self.get_path_without_query()
1.162 + qs = self.get_query_string()
1.163 + if qs:
1.164 + path += "?"
1.165 + path += qs
1.166 + return path
1.167 +
1.168 + def get_path_without_query(self):
1.169 +
1.170 + """
1.171 + Returns the entire path from the request minus the query string.
1.172 + """
1.173 +
1.174 + path = self.env.get("SCRIPT_NAME") or ""
1.175 + if self.env.has_key("PATH_INFO"):
1.176 + path += self.env["PATH_INFO"]
1.177 + return path
1.178 +
1.179 + def get_path_info(self):
1.180 +
1.181 + """
1.182 + Returns the "path info" (the part of the URL after the resource name
1.183 + handling the current request) from the request.
1.184 + """
1.185 +
1.186 + return self.env.get("PATH_INFO") or ""
1.187 +
1.188 + def get_query_string(self):
1.189 +
1.190 + """
1.191 + Returns the query string from the path in the request.
1.192 + """
1.193 +
1.194 + return self.env.get("QUERY_STRING") or ""
1.195 +
1.196 + # Higher level request-related methods.
1.197 +
1.198 + def get_fields_from_path(self):
1.199 +
1.200 + """
1.201 + Extracts fields (or request parameters) from the path specified in the
1.202 + transaction. The underlying framework may refuse to supply fields from
1.203 + the path if handling a POST transaction.
1.204 +
1.205 + Returns a dictionary mapping field names to lists of values (even if a
1.206 + single value is associated with any given field name).
1.207 + """
1.208 +
1.209 + # NOTE: Support at best ISO-8859-1 values.
1.210 +
1.211 + fields = {}
1.212 + for name, values in parse_qs(self.get_query_string(), keep_blank_values=1).items():
1.213 + fields[name] = []
1.214 + for value in values:
1.215 + fields[name].append(unicode(value, "iso-8859-1"))
1.216 + return fields
1.217 +
1.218 + def get_fields_from_body(self, encoding=None):
1.219 +
1.220 + """
1.221 + Extracts fields (or request parameters) from the message body in the
1.222 + transaction. The optional 'encoding' parameter specifies the character
1.223 + encoding of the message body for cases where no such information is
1.224 + available, but where the default encoding is to be overridden.
1.225 +
1.226 + Returns a dictionary mapping field names to lists of values (even if a
1.227 + single value is associated with any given field name). Each value is
1.228 + either a Unicode object (representing a simple form field, for example)
1.229 + or a plain string (representing a file upload form field, for example).
1.230 + """
1.231 +
1.232 + encoding = encoding or self.get_content_type().charset or self.default_charset
1.233 +
1.234 + if self.storage_body is None:
1.235 + self.storage_body = FieldStorage(fp=self.get_request_stream(),
1.236 + headers={"content-type" : str(self.get_content_type())},
1.237 + environ={"REQUEST_METHOD" : self.get_request_method()},
1.238 + keep_blank_values=1)
1.239 +
1.240 + # Avoid strange design issues with FieldStorage by checking the internal
1.241 + # field list directly.
1.242 +
1.243 + fields = {}
1.244 + if self.storage_body.list is not None:
1.245 +
1.246 + # Traverse the storage, finding each field value.
1.247 +
1.248 + fields = get_body_fields(get_storage_items(self.storage_body), encoding)
1.249 +
1.250 + return fields
1.251 +
1.252 + def get_fields(self, encoding=None):
1.253 +
1.254 + """
1.255 + Extracts fields (or request parameters) from both the path specified in
1.256 + the transaction as well as the message body. The optional 'encoding'
1.257 + parameter specifies the character encoding of the message body for cases
1.258 + where no such information is available, but where the default encoding
1.259 + is to be overridden.
1.260 +
1.261 + Returns a dictionary mapping field names to lists of values (even if a
1.262 + single value is associated with any given field name). Each value is
1.263 + either a Unicode object (representing a simple form field, for example)
1.264 + or a plain string (representing a file upload form field, for example).
1.265 +
1.266 + Where a given field name is used in both the path and message body to
1.267 + specify values, the values from both sources will be combined into a
1.268 + single list associated with that field name.
1.269 + """
1.270 +
1.271 + # Combine the two sources.
1.272 +
1.273 + fields = {}
1.274 + fields.update(self.get_fields_from_path())
1.275 + for name, values in self.get_fields_from_body(encoding).items():
1.276 + if not fields.has_key(name):
1.277 + fields[name] = values
1.278 + else:
1.279 + fields[name] += values
1.280 + return fields
1.281 +
1.282 + def get_user(self):
1.283 +
1.284 + """
1.285 + Extracts user information from the transaction.
1.286 +
1.287 + Returns a username as a string or None if no user is defined.
1.288 + """
1.289 +
1.290 + if self.user is not None:
1.291 + return self.user
1.292 + else:
1.293 + return self.env.get("REMOTE_USER")
1.294 +
1.295 + def get_cookies(self):
1.296 +
1.297 + """
1.298 + Obtains cookie information from the request.
1.299 +
1.300 + Returns a dictionary mapping cookie names to cookie objects.
1.301 + """
1.302 +
1.303 + return self.process_cookies(self.cookies_in)
1.304 +
1.305 + def get_cookie(self, cookie_name):
1.306 +
1.307 + """
1.308 + Obtains cookie information from the request.
1.309 +
1.310 + Returns a cookie object for the given 'cookie_name' or None if no such
1.311 + cookie exists.
1.312 + """
1.313 +
1.314 + cookie = self.cookies_in.get(self.encode_cookie_value(cookie_name))
1.315 + if cookie is not None:
1.316 + return Cookie(cookie_name, self.decode_cookie_value(cookie.value))
1.317 + else:
1.318 + return None
1.319 +
1.320 + # Response-related methods.
1.321 +
1.322 + def get_response_stream(self):
1.323 +
1.324 + """
1.325 + Returns the response stream for the transaction.
1.326 + """
1.327 +
1.328 + # Return a stream which is later emptied into the real stream.
1.329 + # Unicode can upset this operation. Using either the specified charset
1.330 + # or a default encoding.
1.331 +
1.332 + encoding = self.get_response_stream_encoding()
1.333 + return ConvertingStream(self.content, encoding)
1.334 +
1.335 + def get_response_stream_encoding(self):
1.336 +
1.337 + """
1.338 + Returns the response stream encoding.
1.339 + """
1.340 +
1.341 + if self.content_type:
1.342 + encoding = self.content_type.charset
1.343 + else:
1.344 + encoding = None
1.345 + return encoding or self.default_charset
1.346 +
1.347 + def get_response_code(self):
1.348 +
1.349 + """
1.350 + Get the response code associated with the transaction. If no response
1.351 + code is defined, None is returned.
1.352 + """
1.353 +
1.354 + return self.response_code
1.355 +
1.356 + def set_response_code(self, response_code):
1.357 +
1.358 + """
1.359 + Set the 'response_code' using a numeric constant defined in the HTTP
1.360 + specification.
1.361 + """
1.362 +
1.363 + self.response_code = response_code
1.364 +
1.365 + def set_header_value(self, header, value):
1.366 +
1.367 + """
1.368 + Set the HTTP 'header' with the given 'value'.
1.369 + """
1.370 +
1.371 + # The header is not written out immediately due to the buffering in use.
1.372 +
1.373 + self.headers_out[header] = value
1.374 +
1.375 + def set_content_type(self, content_type):
1.376 +
1.377 + """
1.378 + Sets the 'content_type' for the response.
1.379 + """
1.380 +
1.381 + # The content type has to be written as a header, before actual content,
1.382 + # but after the response line. This means that some kind of buffering is
1.383 + # required. Hence, we don't write the header out immediately.
1.384 +
1.385 + self.content_type = content_type
1.386 +
1.387 + # Higher level response-related methods.
1.388 +
1.389 + def set_cookie(self, cookie):
1.390 +
1.391 + """
1.392 + Stores the given 'cookie' object in the response.
1.393 + """
1.394 +
1.395 + # NOTE: If multiple cookies of the same name could be specified, this
1.396 + # NOTE: could need changing.
1.397 +
1.398 + self.set_cookie_value(cookie.name, cookie.value)
1.399 +
1.400 + def set_cookie_value(self, name, value, path=None, expires=None):
1.401 +
1.402 + """
1.403 + Stores a cookie with the given 'name' and 'value' in the response.
1.404 +
1.405 + The optional 'path' is a string which specifies the scope of the cookie,
1.406 + and the optional 'expires' parameter is a value compatible with the
1.407 + time.time function, and indicates the expiry date/time of the cookie.
1.408 + """
1.409 +
1.410 + name = self.encode_cookie_value(name)
1.411 + self.cookies_out[name] = self.encode_cookie_value(value)
1.412 + if path is not None:
1.413 + self.cookies_out[name]["path"] = path
1.414 + if expires is not None:
1.415 + self.cookies_out[name]["expires"] = expires
1.416 +
1.417 + def delete_cookie(self, cookie_name):
1.418 +
1.419 + """
1.420 + Adds to the response a request that the cookie with the given
1.421 + 'cookie_name' be deleted/discarded by the client.
1.422 + """
1.423 +
1.424 + # Create a special cookie, given that we do not know whether the browser
1.425 + # has been sent the cookie or not.
1.426 + # NOTE: Magic discovered in Webware.
1.427 +
1.428 + name = self.encode_cookie_value(cookie_name)
1.429 + self.cookies_out[name] = ""
1.430 + self.cookies_out[name]["path"] = "/"
1.431 + self.cookies_out[name]["expires"] = 0
1.432 + self.cookies_out[name]["max-age"] = 0
1.433 +
1.434 + # Session-related methods.
1.435 +
1.436 + def get_session(self, create=1):
1.437 +
1.438 + """
1.439 + Gets a session corresponding to an identifier supplied in the
1.440 + transaction.
1.441 +
1.442 + If no session has yet been established according to information
1.443 + provided in the transaction then the optional 'create' parameter
1.444 + determines whether a new session will be established.
1.445 +
1.446 + Where no session has been established and where 'create' is set to 0
1.447 + then None is returned. In all other cases, a session object is created
1.448 + (where appropriate) and returned.
1.449 + """
1.450 +
1.451 + # NOTE: Requires configuration.
1.452 +
1.453 + if self.session_store is None:
1.454 + self.session_store = SessionStore(self, "WebStack-sessions")
1.455 + return self.session_store.get_session(create)
1.456 +
1.457 + def expire_session(self):
1.458 +
1.459 + """
1.460 + Expires any session established according to information provided in the
1.461 + transaction.
1.462 + """
1.463 +
1.464 + # NOTE: Requires configuration.
1.465 +
1.466 + if self.session_store is None:
1.467 + self.session_store = SessionStore(self, "WebStack-sessions")
1.468 + self.session_store.expire_session()
1.469 +
1.470 + # Application-specific methods.
1.471 +
1.472 + def set_user(self, username):
1.473 +
1.474 + """
1.475 + An application-specific method which sets the user information with
1.476 + 'username' in the transaction. This affects subsequent calls to
1.477 + 'get_user'.
1.478 + """
1.479 +
1.480 + self.user = username
1.481 +
1.482 +# vim: tabstop=4 expandtab shiftwidth=4