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