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