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