1 #!/usr/bin/env python 2 3 """ 4 CGI classes. 5 6 Copyright (C) 2004, 2005 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 self.user = None 60 self.path_info = None 61 62 # Define the incoming cookies. 63 64 self.cookies_in = SimpleCookie(self.env.get("HTTP_COOKIE")) 65 66 # Cached information. 67 68 self.storage_body = None 69 70 # Special objects retained throughout the transaction. 71 72 self.session_store = None 73 74 def commit(self): 75 76 """ 77 A special method, synchronising the transaction with framework-specific 78 objects. 79 80 See draft-coar-cgi-v11-03, section 7. 81 """ 82 83 # Close the session store. 84 85 if self.session_store is not None: 86 self.session_store.close() 87 88 # NOTE: Provide sensible messages. 89 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 self.output.write(str(self.cookies_out)) 98 self.output.write("\n") 99 100 self.content.seek(0) 101 self.output.write(self.content.read()) 102 103 # Server-related methods. 104 105 def get_server_name(self): 106 107 "Returns the server name." 108 109 return self.env.get("SERVER_NAME") 110 111 def get_server_port(self): 112 113 "Returns the server port as a string." 114 115 return self.env.get("SERVER_PORT") 116 117 # Request-related methods. 118 119 def get_request_stream(self): 120 121 """ 122 Returns the request stream for the transaction. 123 """ 124 125 return self.input 126 127 def get_request_method(self): 128 129 """ 130 Returns the request method. 131 """ 132 133 return self.env.get("REQUEST_METHOD") 134 135 def get_headers(self): 136 137 """ 138 Returns all request headers as a dictionary-like object mapping header 139 names to values. 140 """ 141 142 return Environment.get_headers(self.env) 143 144 def get_header_values(self, key): 145 146 """ 147 Returns a list of all request header values associated with the given 148 'key'. Note that according to RFC 2616, 'key' is treated as a 149 case-insensitive string. 150 """ 151 152 return self.convert_to_list(self.get_headers().get(key)) 153 154 def get_content_type(self): 155 156 """ 157 Returns the content type specified on the request, along with the 158 charset employed. 159 """ 160 161 return self.parse_content_type(self.env.get("CONTENT_TYPE")) 162 163 def get_content_charsets(self): 164 165 """ 166 Returns the character set preferences. 167 """ 168 169 return self.parse_content_preferences(None) 170 171 def get_content_languages(self): 172 173 """ 174 Returns extracted language information from the transaction. 175 """ 176 177 return self.parse_content_preferences(None) 178 179 def get_path(self, encoding=None): 180 181 """ 182 Returns the entire path from the request as a Unicode object. Any "URL 183 encoded" character values in the part of the path before the query 184 string will be decoded and presented as genuine characters; the query 185 string will remain "URL encoded", however. 186 187 If the optional 'encoding' is set, use that in preference to the default 188 encoding to convert the path into a form not containing "URL encoded" 189 character values. 190 """ 191 192 path = self.get_path_without_query(encoding) 193 qs = self.get_query_string() 194 if qs: 195 return path + "?" + qs 196 else: 197 return path 198 199 def get_path_without_query(self, encoding=None): 200 201 """ 202 Returns the entire path from the request minus the query string as a 203 Unicode object containing genuine characters (as opposed to "URL 204 encoded" character values). 205 206 If the optional 'encoding' is set, use that in preference to the default 207 encoding to convert the path into a form not containing "URL encoded" 208 character values. 209 """ 210 211 path = decode_value(self.env.get("SCRIPT_NAME") or "", encoding) 212 path += self.get_path_info(encoding) 213 return path 214 215 def get_path_info(self, encoding=None): 216 217 """ 218 Returns the "path info" (the part of the URL after the resource name 219 handling the current request) from the request as a Unicode object 220 containing genuine characters (as opposed to "URL encoded" character 221 values). 222 223 If the optional 'encoding' is set, use that in preference to the default 224 encoding to convert the path into a form not containing "URL encoded" 225 character values. 226 """ 227 228 return decode_value(self.env.get("PATH_INFO") or "", encoding) 229 230 def get_query_string(self): 231 232 """ 233 Returns the query string from the path in the request. 234 """ 235 236 return self.env.get("QUERY_STRING") or "" 237 238 # Higher level request-related methods. 239 240 def get_fields_from_path(self, encoding=None): 241 242 """ 243 Extracts fields (or request parameters) from the path specified in the 244 transaction. The underlying framework may refuse to supply fields from 245 the path if handling a POST transaction. The optional 'encoding' 246 parameter specifies the character encoding of the query string for cases 247 where the default encoding is to be overridden. 248 249 Returns a dictionary mapping field names to lists of values (even if a 250 single value is associated with any given field name). 251 """ 252 253 fields = {} 254 for name, values in parse_qs(self.get_query_string(), keep_blank_values=1).items(): 255 name = decode_value(name, encoding) 256 fields[name] = [] 257 for value in values: 258 value = decode_value(value, encoding) 259 fields[name].append(value) 260 return fields 261 262 def get_fields_from_body(self, encoding=None): 263 264 """ 265 Extracts fields (or request parameters) from the message body in the 266 transaction. The optional 'encoding' parameter specifies the character 267 encoding of the message body for cases where no such information is 268 available, but where the default encoding is to be overridden. 269 270 Returns a dictionary mapping field names to lists of values (even if a 271 single value is associated with any given field name). Each value is 272 either a Unicode object (representing a simple form field, for example) 273 or a plain string (representing a file upload form field, for example). 274 """ 275 276 encoding = encoding or self.get_content_type().charset or self.default_charset 277 278 if self.storage_body is None: 279 self.storage_body = FieldStorage(fp=self.get_request_stream(), 280 headers={"content-type" : str(self.get_content_type())}, 281 environ={"REQUEST_METHOD" : self.get_request_method()}, 282 keep_blank_values=1) 283 284 # Avoid strange design issues with FieldStorage by checking the internal 285 # field list directly. 286 287 fields = {} 288 if self.storage_body.list is not None: 289 290 # Traverse the storage, finding each field value. 291 292 fields = get_body_fields(get_storage_items(self.storage_body), encoding) 293 294 return fields 295 296 def get_fields(self, encoding=None): 297 298 """ 299 Extracts fields (or request parameters) from both the path specified in 300 the transaction as well as the message body. The optional 'encoding' 301 parameter specifies the character encoding of the message body for cases 302 where no such information is available, but where the default encoding 303 is to be overridden. 304 305 Returns a dictionary mapping field names to lists of values (even if a 306 single value is associated with any given field name). Each value is 307 either a Unicode object (representing a simple form field, for example) 308 or a plain string (representing a file upload form field, for example). 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