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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 21 """ 22 23 import Generic 24 import os, sys 25 from Helpers.Request import MessageBodyStream, get_body_fields, get_storage_items, Cookie 26 from Helpers.Response import ConvertingStream 27 from Helpers.Auth import UserInfo 28 from Helpers.Session import SessionStore 29 from Helpers import Environment 30 from cgi import parse_qs, FieldStorage 31 from Cookie import SimpleCookie 32 from StringIO import StringIO 33 34 class Transaction(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): 180 181 """ 182 Returns the entire path from the request. 183 """ 184 185 path = self.get_path_without_query() 186 qs = self.get_query_string() 187 if qs: 188 path += "?" 189 path += qs 190 return path 191 192 def get_path_without_query(self): 193 194 """ 195 Returns the entire path from the request minus the query string. 196 """ 197 198 path = self.env.get("SCRIPT_NAME") or "" 199 if self.env.has_key("PATH_INFO"): 200 path += self.env["PATH_INFO"] 201 return path 202 203 def get_path_info(self): 204 205 """ 206 Returns the "path info" (the part of the URL after the resource name 207 handling the current request) from the request. 208 """ 209 210 return self.env.get("PATH_INFO") or "" 211 212 def get_query_string(self): 213 214 """ 215 Returns the query string from the path in the request. 216 """ 217 218 return self.env.get("QUERY_STRING") or "" 219 220 # Higher level request-related methods. 221 222 def get_fields_from_path(self): 223 224 """ 225 Extracts fields (or request parameters) from the path specified in the 226 transaction. The underlying framework may refuse to supply fields from 227 the path if handling a POST transaction. 228 229 Returns a dictionary mapping field names to lists of values (even if a 230 single value is associated with any given field name). 231 """ 232 233 # NOTE: Support at best ISO-8859-1 values. 234 235 fields = {} 236 for name, values in parse_qs(self.get_query_string(), keep_blank_values=1).items(): 237 fields[name] = [] 238 for value in values: 239 fields[name].append(unicode(value, "iso-8859-1")) 240 return fields 241 242 def get_fields_from_body(self, encoding=None): 243 244 """ 245 Extracts fields (or request parameters) from the message body in the 246 transaction. The optional 'encoding' parameter specifies the character 247 encoding of the message body for cases where no such information is 248 available, but where the default encoding is to be overridden. 249 250 Returns a dictionary mapping field names to lists of values (even if a 251 single value is associated with any given field name). Each value is 252 either a Unicode object (representing a simple form field, for example) 253 or a plain string (representing a file upload form field, for example). 254 """ 255 256 encoding = encoding or self.get_content_type().charset or self.default_charset 257 258 if self.storage_body is None: 259 self.storage_body = FieldStorage(fp=self.get_request_stream(), 260 headers={"content-type" : str(self.get_content_type())}, 261 environ={"REQUEST_METHOD" : self.get_request_method()}, 262 keep_blank_values=1) 263 264 # Avoid strange design issues with FieldStorage by checking the internal 265 # field list directly. 266 267 fields = {} 268 if self.storage_body.list is not None: 269 270 # Traverse the storage, finding each field value. 271 272 fields = get_body_fields(get_storage_items(self.storage_body), encoding) 273 274 return fields 275 276 def get_fields(self, encoding=None): 277 278 """ 279 Extracts fields (or request parameters) from both the path specified in 280 the transaction as well as the message body. The optional 'encoding' 281 parameter specifies the character encoding of the message body for cases 282 where no such information is available, but where the default encoding 283 is to be overridden. 284 285 Returns a dictionary mapping field names to lists of values (even if a 286 single value is associated with any given field name). Each value is 287 either a Unicode object (representing a simple form field, for example) 288 or a plain string (representing a file upload form field, for example). 289 290 Where a given field name is used in both the path and message body to 291 specify values, the values from both sources will be combined into a 292 single list associated with that field name. 293 """ 294 295 # Combine the two sources. 296 297 fields = {} 298 fields.update(self.get_fields_from_path()) 299 for name, values in self.get_fields_from_body(encoding).items(): 300 if not fields.has_key(name): 301 fields[name] = values 302 else: 303 fields[name] += values 304 return fields 305 306 def get_user(self): 307 308 """ 309 Extracts user information from the transaction. 310 311 Returns a username as a string or None if no user is defined. 312 """ 313 314 if self.user is not None: 315 return self.user 316 else: 317 return self.env.get("REMOTE_USER") 318 319 def get_cookies(self): 320 321 """ 322 Obtains cookie information from the request. 323 324 Returns a dictionary mapping cookie names to cookie objects. 325 """ 326 327 return self.process_cookies(self.cookies_in) 328 329 def get_cookie(self, cookie_name): 330 331 """ 332 Obtains cookie information from the request. 333 334 Returns a cookie object for the given 'cookie_name' or None if no such 335 cookie exists. 336 """ 337 338 cookie = self.cookies_in.get(self.encode_cookie_value(cookie_name)) 339 if cookie is not None: 340 return Cookie(cookie_name, self.decode_cookie_value(cookie.value)) 341 else: 342 return None 343 344 # Response-related methods. 345 346 def get_response_stream(self): 347 348 """ 349 Returns the response stream for the transaction. 350 """ 351 352 # Return a stream which is later emptied into the real stream. 353 # Unicode can upset this operation. Using either the specified charset 354 # or a default encoding. 355 356 encoding = self.get_response_stream_encoding() 357 return ConvertingStream(self.content, encoding) 358 359 def get_response_stream_encoding(self): 360 361 """ 362 Returns the response stream encoding. 363 """ 364 365 if self.content_type: 366 encoding = self.content_type.charset 367 else: 368 encoding = None 369 return encoding or self.default_charset 370 371 def get_response_code(self): 372 373 """ 374 Get the response code associated with the transaction. If no response 375 code is defined, None is returned. 376 """ 377 378 return self.response_code 379 380 def set_response_code(self, response_code): 381 382 """ 383 Set the 'response_code' using a numeric constant defined in the HTTP 384 specification. 385 """ 386 387 self.response_code = response_code 388 389 def set_header_value(self, header, value): 390 391 """ 392 Set the HTTP 'header' with the given 'value'. 393 """ 394 395 # The header is not written out immediately due to the buffering in use. 396 397 self.headers_out[header] = value 398 399 def set_content_type(self, content_type): 400 401 """ 402 Sets the 'content_type' for the response. 403 """ 404 405 # The content type has to be written as a header, before actual content, 406 # but after the response line. This means that some kind of buffering is 407 # required. Hence, we don't write the header out immediately. 408 409 self.content_type = content_type 410 411 # Higher level response-related methods. 412 413 def set_cookie(self, cookie): 414 415 """ 416 Stores the given 'cookie' object in the response. 417 """ 418 419 # NOTE: If multiple cookies of the same name could be specified, this 420 # NOTE: could need changing. 421 422 self.set_cookie_value(cookie.name, cookie.value) 423 424 def set_cookie_value(self, name, value, path=None, expires=None): 425 426 """ 427 Stores a cookie with the given 'name' and 'value' in the response. 428 429 The optional 'path' is a string which specifies the scope of the cookie, 430 and the optional 'expires' parameter is a value compatible with the 431 time.time function, and indicates the expiry date/time of the cookie. 432 """ 433 434 name = self.encode_cookie_value(name) 435 self.cookies_out[name] = self.encode_cookie_value(value) 436 if path is not None: 437 self.cookies_out[name]["path"] = path 438 if expires is not None: 439 self.cookies_out[name]["expires"] = expires 440 441 def delete_cookie(self, cookie_name): 442 443 """ 444 Adds to the response a request that the cookie with the given 445 'cookie_name' be deleted/discarded by the client. 446 """ 447 448 # Create a special cookie, given that we do not know whether the browser 449 # has been sent the cookie or not. 450 # NOTE: Magic discovered in Webware. 451 452 name = self.encode_cookie_value(cookie_name) 453 self.cookies_out[name] = "" 454 self.cookies_out[name]["path"] = "/" 455 self.cookies_out[name]["expires"] = 0 456 self.cookies_out[name]["max-age"] = 0 457 458 # Session-related methods. 459 460 def get_session(self, create=1): 461 462 """ 463 Gets a session corresponding to an identifier supplied in the 464 transaction. 465 466 If no session has yet been established according to information 467 provided in the transaction then the optional 'create' parameter 468 determines whether a new session will be established. 469 470 Where no session has been established and where 'create' is set to 0 471 then None is returned. In all other cases, a session object is created 472 (where appropriate) and returned. 473 """ 474 475 # NOTE: Requires configuration. 476 477 if self.session_store is None: 478 self.session_store = SessionStore(self, "WebStack-sessions") 479 return self.session_store.get_session(create) 480 481 def expire_session(self): 482 483 """ 484 Expires any session established according to information provided in the 485 transaction. 486 """ 487 488 # NOTE: Requires configuration. 489 490 if self.session_store is None: 491 self.session_store = SessionStore(self, "WebStack-sessions") 492 self.session_store.expire_session() 493 494 # vim: tabstop=4 expandtab shiftwidth=4