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