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