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