1 #!/usr/bin/env python 2 3 """ 4 CGI classes. 5 6 Copyright (C) 2004, 2005, 2006, 2007, 2009 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 # set_content_type(self, content_type) not reimplemented 442 443 # The content type has to be written as a header, before actual content, 444 # but after the response line. This means that some kind of buffering is 445 # required. Hence, we don't write the header out immediately. 446 447 # Higher level response-related methods. 448 449 def set_cookie(self, cookie): 450 451 """ 452 Stores the given 'cookie' object in the response. 453 """ 454 455 # NOTE: If multiple cookies of the same name could be specified, this 456 # NOTE: could need changing. 457 458 self.set_cookie_value(cookie.name, cookie.value) 459 460 def set_cookie_value(self, name, value, path=None, expires=None): 461 462 """ 463 Stores a cookie with the given 'name' and 'value' in the response. 464 465 The optional 'path' is a string which specifies the scope of the cookie, 466 and the optional 'expires' parameter is a value compatible with the 467 time.time function, and indicates the expiry date/time of the cookie. 468 """ 469 470 name = self.encode_cookie_value(name) 471 self.cookies_out[name] = self.encode_cookie_value(value) 472 if path is not None: 473 self.cookies_out[name]["path"] = path 474 if expires is not None: 475 self.cookies_out[name]["expires"] = expires 476 477 def delete_cookie(self, cookie_name): 478 479 """ 480 Adds to the response a request that the cookie with the given 481 'cookie_name' be deleted/discarded by the client. 482 """ 483 484 # Create a special cookie, given that we do not know whether the browser 485 # has been sent the cookie or not. 486 # NOTE: Magic discovered in Webware. 487 488 name = self.encode_cookie_value(cookie_name) 489 self.cookies_out[name] = "" 490 self.cookies_out[name]["path"] = "/" 491 self.cookies_out[name]["expires"] = 0 492 self.cookies_out[name]["max-age"] = 0 493 494 # Session-related methods. 495 496 def get_session(self, create=1): 497 498 """ 499 Gets a session corresponding to an identifier supplied in the 500 transaction. 501 502 If no session has yet been established according to information 503 provided in the transaction then the optional 'create' parameter 504 determines whether a new session will be established. 505 506 Where no session has been established and where 'create' is set to 0 507 then None is returned. In all other cases, a session object is created 508 (where appropriate) and returned. 509 """ 510 511 # NOTE: Requires configuration. 512 513 if self.session_store is None: 514 self.session_store = SessionStore(self, "WebStack-sessions") 515 return self.session_store.get_session(create) 516 517 def expire_session(self): 518 519 """ 520 Expires any session established according to information provided in the 521 transaction. 522 """ 523 524 # NOTE: Requires configuration. 525 526 if self.session_store is None: 527 self.session_store = SessionStore(self, "WebStack-sessions") 528 self.session_store.expire_session() 529 530 # vim: tabstop=4 expandtab shiftwidth=4