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