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