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 if self.path_info is not None: 185 return self.path_info 186 else: 187 return self.env.get("PATH_INFO") or "" 188 189 def get_query_string(self): 190 191 """ 192 Returns the query string from the path in the request. 193 """ 194 195 return self.env.get("QUERY_STRING") or "" 196 197 # Higher level request-related methods. 198 199 def get_fields_from_path(self): 200 201 """ 202 Extracts fields (or request parameters) from the path specified in the 203 transaction. The underlying framework may refuse to supply fields from 204 the path if handling a POST transaction. 205 206 Returns a dictionary mapping field names to lists of values (even if a 207 single value is associated with any given field name). 208 """ 209 210 # NOTE: Support at best ISO-8859-1 values. 211 212 fields = {} 213 for name, values in parse_qs(self.get_query_string(), keep_blank_values=1).items(): 214 fields[name] = [] 215 for value in values: 216 fields[name].append(unicode(value, "iso-8859-1")) 217 return fields 218 219 def get_fields_from_body(self, encoding=None): 220 221 """ 222 Extracts fields (or request parameters) from the message body in the 223 transaction. The optional 'encoding' parameter specifies the character 224 encoding of the message body for cases where no such information is 225 available, but where the default encoding is to be overridden. 226 227 Returns a dictionary mapping field names to lists of values (even if a 228 single value is associated with any given field name). Each value is 229 either a Unicode object (representing a simple form field, for example) 230 or a plain string (representing a file upload form field, for example). 231 """ 232 233 encoding = encoding or self.get_content_type().charset or self.default_charset 234 235 if self.storage_body is None: 236 self.storage_body = FieldStorage(fp=self.get_request_stream(), 237 headers={"content-type" : str(self.get_content_type())}, 238 environ={"REQUEST_METHOD" : self.get_request_method()}, 239 keep_blank_values=1) 240 241 # Avoid strange design issues with FieldStorage by checking the internal 242 # field list directly. 243 244 fields = {} 245 if self.storage_body.list is not None: 246 247 # Traverse the storage, finding each field value. 248 249 fields = get_body_fields(get_storage_items(self.storage_body), encoding) 250 251 return fields 252 253 def get_fields(self, encoding=None): 254 255 """ 256 Extracts fields (or request parameters) from both the path specified in 257 the transaction as well as the message body. The optional 'encoding' 258 parameter specifies the character encoding of the message body for cases 259 where no such information is available, but where the default encoding 260 is to be overridden. 261 262 Returns a dictionary mapping field names to lists of values (even if a 263 single value is associated with any given field name). Each value is 264 either a Unicode object (representing a simple form field, for example) 265 or a plain string (representing a file upload form field, for example). 266 267 Where a given field name is used in both the path and message body to 268 specify values, the values from both sources will be combined into a 269 single list associated with that field name. 270 """ 271 272 # Combine the two sources. 273 274 fields = {} 275 fields.update(self.get_fields_from_path()) 276 for name, values in self.get_fields_from_body(encoding).items(): 277 if not fields.has_key(name): 278 fields[name] = values 279 else: 280 fields[name] += values 281 return fields 282 283 def get_user(self): 284 285 """ 286 Extracts user information from the transaction. 287 288 Returns a username as a string or None if no user is defined. 289 """ 290 291 if self.user is not None: 292 return self.user 293 else: 294 return self.env.get("REMOTE_USER") 295 296 def get_cookies(self): 297 298 """ 299 Obtains cookie information from the request. 300 301 Returns a dictionary mapping cookie names to cookie objects. 302 """ 303 304 return self.process_cookies(self.cookies_in) 305 306 def get_cookie(self, cookie_name): 307 308 """ 309 Obtains cookie information from the request. 310 311 Returns a cookie object for the given 'cookie_name' or None if no such 312 cookie exists. 313 """ 314 315 cookie = self.cookies_in.get(self.encode_cookie_value(cookie_name)) 316 if cookie is not None: 317 return Cookie(cookie_name, self.decode_cookie_value(cookie.value)) 318 else: 319 return None 320 321 # Response-related methods. 322 323 def get_response_stream(self): 324 325 """ 326 Returns the response stream for the transaction. 327 """ 328 329 # Return a stream which is later emptied into the real stream. 330 # Unicode can upset this operation. Using either the specified charset 331 # or a default encoding. 332 333 encoding = self.get_response_stream_encoding() 334 return ConvertingStream(self.content, encoding) 335 336 def get_response_stream_encoding(self): 337 338 """ 339 Returns the response stream encoding. 340 """ 341 342 if self.content_type: 343 encoding = self.content_type.charset 344 else: 345 encoding = None 346 return encoding or self.default_charset 347 348 def get_response_code(self): 349 350 """ 351 Get the response code associated with the transaction. If no response 352 code is defined, None is returned. 353 """ 354 355 return self.response_code 356 357 def set_response_code(self, response_code): 358 359 """ 360 Set the 'response_code' using a numeric constant defined in the HTTP 361 specification. 362 """ 363 364 self.response_code = response_code 365 366 def set_header_value(self, header, value): 367 368 """ 369 Set the HTTP 'header' with the given 'value'. 370 """ 371 372 # The header is not written out immediately due to the buffering in use. 373 374 self.headers_out[header] = value 375 376 def set_content_type(self, content_type): 377 378 """ 379 Sets the 'content_type' for the response. 380 """ 381 382 # The content type has to be written as a header, before actual content, 383 # but after the response line. This means that some kind of buffering is 384 # required. Hence, we don't write the header out immediately. 385 386 self.content_type = content_type 387 388 # Higher level response-related methods. 389 390 def set_cookie(self, cookie): 391 392 """ 393 Stores the given 'cookie' object in the response. 394 """ 395 396 # NOTE: If multiple cookies of the same name could be specified, this 397 # NOTE: could need changing. 398 399 self.set_cookie_value(cookie.name, cookie.value) 400 401 def set_cookie_value(self, name, value, path=None, expires=None): 402 403 """ 404 Stores a cookie with the given 'name' and 'value' in the response. 405 406 The optional 'path' is a string which specifies the scope of the cookie, 407 and the optional 'expires' parameter is a value compatible with the 408 time.time function, and indicates the expiry date/time of the cookie. 409 """ 410 411 name = self.encode_cookie_value(name) 412 self.cookies_out[name] = self.encode_cookie_value(value) 413 if path is not None: 414 self.cookies_out[name]["path"] = path 415 if expires is not None: 416 self.cookies_out[name]["expires"] = expires 417 418 def delete_cookie(self, cookie_name): 419 420 """ 421 Adds to the response a request that the cookie with the given 422 'cookie_name' be deleted/discarded by the client. 423 """ 424 425 # Create a special cookie, given that we do not know whether the browser 426 # has been sent the cookie or not. 427 # NOTE: Magic discovered in Webware. 428 429 name = self.encode_cookie_value(cookie_name) 430 self.cookies_out[name] = "" 431 self.cookies_out[name]["path"] = "/" 432 self.cookies_out[name]["expires"] = 0 433 self.cookies_out[name]["max-age"] = 0 434 435 # Session-related methods. 436 437 def get_session(self, create=1): 438 439 """ 440 Gets a session corresponding to an identifier supplied in the 441 transaction. 442 443 If no session has yet been established according to information 444 provided in the transaction then the optional 'create' parameter 445 determines whether a new session will be established. 446 447 Where no session has been established and where 'create' is set to 0 448 then None is returned. In all other cases, a session object is created 449 (where appropriate) and returned. 450 """ 451 452 # NOTE: Requires configuration. 453 454 if self.session_store is None: 455 self.session_store = SessionStore(self, "WebStack-sessions") 456 return self.session_store.get_session(create) 457 458 def expire_session(self): 459 460 """ 461 Expires any session established according to information provided in the 462 transaction. 463 """ 464 465 # NOTE: Requires configuration. 466 467 if self.session_store is None: 468 self.session_store = SessionStore(self, "WebStack-sessions") 469 self.session_store.expire_session() 470 471 # vim: tabstop=4 expandtab shiftwidth=4