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., 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 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, encoding=None): 184 185 """ 186 Returns the entire path from the request as a Unicode object. Any "URL 187 encoded" character values in the part of the path before the query 188 string will be decoded and presented as genuine characters; the query 189 string will remain "URL encoded", however. 190 191 If the optional 'encoding' is set, use that in preference to the default 192 encoding to convert the path into a form not containing "URL encoded" 193 character values. 194 """ 195 196 path = self.get_path_without_query(encoding) 197 qs = self.get_query_string() 198 if qs: 199 return path + "?" + qs 200 else: 201 return path 202 203 def get_path_without_query(self, encoding=None): 204 205 """ 206 Returns the entire path from the request minus the query string as a 207 Unicode object containing genuine characters (as opposed to "URL 208 encoded" character values). 209 210 If the optional 'encoding' is set, use that in preference to the default 211 encoding to convert the path into a form not containing "URL encoded" 212 character values. 213 """ 214 215 path = decode_value(self.env.get("SCRIPT_NAME") or "", encoding) 216 path += self.get_path_info(encoding) 217 return path 218 219 def get_path_info(self, encoding=None): 220 221 """ 222 Returns the "path info" (the part of the URL after the resource name 223 handling the current request) from the request as a Unicode object 224 containing genuine characters (as opposed to "URL encoded" character 225 values). 226 227 If the optional 'encoding' is set, use that in preference to the default 228 encoding to convert the path into a form not containing "URL encoded" 229 character values. 230 """ 231 232 return decode_value(self.env.get("PATH_INFO") or "", encoding) 233 234 def get_query_string(self): 235 236 """ 237 Returns the query string from the path in the request. 238 """ 239 240 return self.env.get("QUERY_STRING") or "" 241 242 # Higher level request-related methods. 243 244 def get_fields_from_path(self, encoding=None): 245 246 """ 247 Extracts fields (or request parameters) from the path specified in the 248 transaction. The underlying framework may refuse to supply fields from 249 the path if handling a POST transaction. The optional 'encoding' 250 parameter specifies the character encoding of the query string for cases 251 where the default encoding is to be overridden. 252 253 Returns a dictionary mapping field names to lists of values (even if a 254 single value is associated with any given field name). 255 """ 256 257 fields = {} 258 for name, values in parse_qs(self.get_query_string(), keep_blank_values=1).items(): 259 name = decode_value(name, encoding) 260 fields[name] = [] 261 for value in values: 262 value = decode_value(value, encoding) 263 fields[name].append(value) 264 return fields 265 266 def get_fields_from_body(self, encoding=None): 267 268 """ 269 Extracts fields (or request parameters) from the message body in the 270 transaction. The optional 'encoding' parameter specifies the character 271 encoding of the message body for cases where no such information is 272 available, but where the default encoding is to be overridden. 273 274 Returns a dictionary mapping field names to lists of values (even if a 275 single value is associated with any given field name). Each value is 276 either a Unicode object (representing a simple form field, for example) 277 or a plain string (representing a file upload form field, for example). 278 """ 279 280 encoding = encoding or self.get_content_type().charset or self.default_charset 281 282 if self.storage_body is None: 283 self.storage_body = FieldStorage(fp=self.get_request_stream(), 284 headers={"content-type" : str(self.get_content_type())}, 285 environ={"REQUEST_METHOD" : self.get_request_method()}, 286 keep_blank_values=1) 287 288 # Avoid strange design issues with FieldStorage by checking the internal 289 # field list directly. 290 291 fields = {} 292 if self.storage_body.list is not None: 293 294 # Traverse the storage, finding each field value. 295 296 fields = get_body_fields(get_storage_items(self.storage_body), encoding) 297 298 return fields 299 300 def get_fields(self, encoding=None): 301 302 """ 303 Extracts fields (or request parameters) from both the path specified in 304 the transaction as well as the message body. The optional 'encoding' 305 parameter specifies the character encoding of the message body for cases 306 where no such information is available, but where the default encoding 307 is to be overridden. 308 309 Returns a dictionary mapping field names to lists of values (even if a 310 single value is associated with any given field name). Each value is 311 either a Unicode object (representing a simple form field, for example) 312 or a plain string (representing a file upload form field, for example). 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