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