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