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