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