1 #!/usr/bin/env python 2 3 """ 4 CGI classes. 5 """ 6 7 import Generic 8 import os, sys 9 from Helpers.Request import MessageBodyStream, get_body_fields, get_storage_items, Cookie 10 from Helpers.Response import ConvertingStream 11 from Helpers.Auth import UserInfo 12 from Helpers.Session import SessionStore 13 from Helpers import Environment 14 from cgi import parse_qs, FieldStorage 15 from Cookie import SimpleCookie 16 from StringIO import StringIO 17 18 class Transaction(Generic.Transaction): 19 20 """ 21 CGI transaction interface. 22 """ 23 24 def __init__(self, input=None, output=None, env=None): 25 26 """ 27 Initialise the transaction using the CGI 'input' and 'output' streams. 28 These streams are optional and default to standard input and standard 29 output respectively. 30 """ 31 32 self.input = input or sys.stdin 33 self.output = output or sys.stdout 34 self.env = env or os.environ 35 36 # Other attributes of interest in instances of this class. 37 38 self.content_type = None 39 self.response_code = 200 40 self.content = StringIO() 41 self.headers_out = {} 42 self.cookies_out = SimpleCookie() 43 self.user = None 44 45 # Define the incoming cookies. 46 47 self.cookies_in = SimpleCookie(self.env.get("HTTP_COOKIE")) 48 49 # Cached information. 50 51 self.storage_body = None 52 53 # Special objects retained throughout the transaction. 54 55 self.session_store = None 56 57 def commit(self): 58 59 """ 60 A special method, synchronising the transaction with framework-specific 61 objects. 62 63 See draft-coar-cgi-v11-03, section 7. 64 """ 65 66 # Close the session store. 67 68 if self.session_store is not None: 69 self.session_store.close() 70 71 # NOTE: Provide sensible messages. 72 73 self.output.write("Status: %s %s\n" % (self.response_code, "WebStack status")) 74 if self.content_type is not None: 75 self.output.write("Content-type: %s\n" % str(self.content_type)) 76 for header, value in self.headers_out.items(): 77 self.output.write("%s: %s\n" % 78 (self.format_header_value(header), self.format_header_value(value)) 79 ) 80 self.output.write(str(self.cookies_out)) 81 self.output.write("\n") 82 self.output.write("\n") 83 84 self.content.seek(0) 85 self.output.write(self.content.read()) 86 87 # Request-related methods. 88 89 def get_request_stream(self): 90 91 """ 92 Returns the request stream for the transaction. 93 """ 94 95 return self.input 96 97 def get_request_method(self): 98 99 """ 100 Returns the request method. 101 """ 102 103 return self.env.get("REQUEST_METHOD") 104 105 def get_headers(self): 106 107 """ 108 Returns all request headers as a dictionary-like object mapping header 109 names to values. 110 """ 111 112 return Environment.get_headers(self.env) 113 114 def get_header_values(self, key): 115 116 """ 117 Returns a list of all request header values associated with the given 118 'key'. Note that according to RFC 2616, 'key' is treated as a 119 case-insensitive string. 120 """ 121 122 return self.convert_to_list(self.get_headers().get(key)) 123 124 def get_content_type(self): 125 126 """ 127 Returns the content type specified on the request, along with the 128 charset employed. 129 """ 130 131 return self.parse_content_type(self.env.get("CONTENT_TYPE")) 132 133 def get_content_charsets(self): 134 135 """ 136 Returns the character set preferences. 137 """ 138 139 return self.parse_content_preferences(None) 140 141 def get_content_languages(self): 142 143 """ 144 Returns extracted language information from the transaction. 145 """ 146 147 return self.parse_content_preferences(None) 148 149 def get_path(self): 150 151 """ 152 Returns the entire path from the request. 153 """ 154 155 path = self.get_path_without_query() 156 qs = self.get_query_string() 157 if qs: 158 path += "?" 159 path += qs 160 return path 161 162 def get_path_without_query(self): 163 164 """ 165 Returns the entire path from the request minus the query string. 166 """ 167 168 path = self.env.get("SCRIPT_NAME") or "" 169 if self.env.has_key("PATH_INFO"): 170 path += self.env["PATH_INFO"] 171 return path 172 173 def get_path_info(self): 174 175 """ 176 Returns the "path info" (the part of the URL after the resource name 177 handling the current request) from the request. 178 """ 179 180 return self.env.get("PATH_INFO") or "" 181 182 def get_query_string(self): 183 184 """ 185 Returns the query string from the path in the request. 186 """ 187 188 return self.env.get("QUERY_STRING") or "" 189 190 # Higher level request-related methods. 191 192 def get_fields_from_path(self): 193 194 """ 195 Extracts fields (or request parameters) from the path specified in the 196 transaction. The underlying framework may refuse to supply fields from 197 the path if handling a POST transaction. 198 199 Returns a dictionary mapping field names to lists of values (even if a 200 single value is associated with any given field name). 201 """ 202 203 # NOTE: Support at best ISO-8859-1 values. 204 205 fields = {} 206 for name, values in parse_qs(self.get_query_string(), keep_blank_values=1).items(): 207 fields[name] = [] 208 for value in values: 209 fields[name].append(unicode(value, "iso-8859-1")) 210 return fields 211 212 def get_fields_from_body(self, encoding=None): 213 214 """ 215 Extracts fields (or request parameters) from the message body in the 216 transaction. The optional 'encoding' parameter specifies the character 217 encoding of the message body for cases where no such information is 218 available, but where the default encoding is to be overridden. 219 220 Returns a dictionary mapping field names to lists of values (even if a 221 single value is associated with any given field name). Each value is 222 either a Unicode object (representing a simple form field, for example) 223 or a plain string (representing a file upload form field, for example). 224 """ 225 226 encoding = encoding or self.get_content_type().charset or self.default_charset 227 228 if self.storage_body is None: 229 self.storage_body = FieldStorage(fp=self.get_request_stream(), 230 headers={"content-type" : str(self.get_content_type())}, 231 environ={"REQUEST_METHOD" : self.get_request_method()}, 232 keep_blank_values=1) 233 234 # Avoid strange design issues with FieldStorage by checking the internal 235 # field list directly. 236 237 fields = {} 238 if self.storage_body.list is not None: 239 240 # Traverse the storage, finding each field value. 241 242 fields = get_body_fields(get_storage_items(self.storage_body), encoding) 243 244 return fields 245 246 def get_fields(self, encoding=None): 247 248 """ 249 Extracts fields (or request parameters) from both the path specified in 250 the transaction as well as the message body. The optional 'encoding' 251 parameter specifies the character encoding of the message body for cases 252 where no such information is available, but where the default encoding 253 is to be overridden. 254 255 Returns a dictionary mapping field names to lists of values (even if a 256 single value is associated with any given field name). Each value is 257 either a Unicode object (representing a simple form field, for example) 258 or a plain string (representing a file upload form field, for example). 259 260 Where a given field name is used in both the path and message body to 261 specify values, the values from both sources will be combined into a 262 single list associated with that field name. 263 """ 264 265 # Combine the two sources. 266 267 fields = {} 268 fields.update(self.get_fields_from_path()) 269 for name, values in self.get_fields_from_body(encoding).items(): 270 if not fields.has_key(name): 271 fields[name] = values 272 else: 273 fields[name] += values 274 return fields 275 276 def get_user(self): 277 278 """ 279 Extracts user information from the transaction. 280 281 Returns a username as a string or None if no user is defined. 282 """ 283 284 if self.user is not None: 285 return self.user 286 else: 287 return self.env.get("REMOTE_USER") 288 289 def get_cookies(self): 290 291 """ 292 Obtains cookie information from the request. 293 294 Returns a dictionary mapping cookie names to cookie objects. 295 """ 296 297 return self.process_cookies(self.cookies_in) 298 299 def get_cookie(self, cookie_name): 300 301 """ 302 Obtains cookie information from the request. 303 304 Returns a cookie object for the given 'cookie_name' or None if no such 305 cookie exists. 306 """ 307 308 cookie = self.cookies_in.get(self.encode_cookie_value(cookie_name)) 309 if cookie is not None: 310 return Cookie(cookie_name, self.decode_cookie_value(cookie.value)) 311 else: 312 return None 313 314 # Response-related methods. 315 316 def get_response_stream(self): 317 318 """ 319 Returns the response stream for the transaction. 320 """ 321 322 # Return a stream which is later emptied into the real stream. 323 # Unicode can upset this operation. Using either the specified charset 324 # or a default encoding. 325 326 encoding = self.get_response_stream_encoding() 327 return ConvertingStream(self.content, encoding) 328 329 def get_response_stream_encoding(self): 330 331 """ 332 Returns the response stream encoding. 333 """ 334 335 if self.content_type: 336 encoding = self.content_type.charset 337 else: 338 encoding = None 339 return encoding or self.default_charset 340 341 def get_response_code(self): 342 343 """ 344 Get the response code associated with the transaction. If no response 345 code is defined, None is returned. 346 """ 347 348 return self.response_code 349 350 def set_response_code(self, response_code): 351 352 """ 353 Set the 'response_code' using a numeric constant defined in the HTTP 354 specification. 355 """ 356 357 self.response_code = response_code 358 359 def set_header_value(self, header, value): 360 361 """ 362 Set the HTTP 'header' with the given 'value'. 363 """ 364 365 # The header is not written out immediately due to the buffering in use. 366 367 self.headers_out[header] = value 368 369 def set_content_type(self, content_type): 370 371 """ 372 Sets the 'content_type' for the response. 373 """ 374 375 # The content type has to be written as a header, before actual content, 376 # but after the response line. This means that some kind of buffering is 377 # required. Hence, we don't write the header out immediately. 378 379 self.content_type = content_type 380 381 # Higher level response-related methods. 382 383 def set_cookie(self, cookie): 384 385 """ 386 Stores the given 'cookie' object in the response. 387 """ 388 389 # NOTE: If multiple cookies of the same name could be specified, this 390 # NOTE: could need changing. 391 392 self.set_cookie_value(cookie.name, cookie.value) 393 394 def set_cookie_value(self, name, value, path=None, expires=None): 395 396 """ 397 Stores a cookie with the given 'name' and 'value' in the response. 398 399 The optional 'path' is a string which specifies the scope of the cookie, 400 and the optional 'expires' parameter is a value compatible with the 401 time.time function, and indicates the expiry date/time of the cookie. 402 """ 403 404 name = self.encode_cookie_value(name) 405 self.cookies_out[name] = self.encode_cookie_value(value) 406 if path is not None: 407 self.cookies_out[name]["path"] = path 408 if expires is not None: 409 self.cookies_out[name]["expires"] = expires 410 411 def delete_cookie(self, cookie_name): 412 413 """ 414 Adds to the response a request that the cookie with the given 415 'cookie_name' be deleted/discarded by the client. 416 """ 417 418 # Create a special cookie, given that we do not know whether the browser 419 # has been sent the cookie or not. 420 # NOTE: Magic discovered in Webware. 421 422 name = self.encode_cookie_value(cookie_name) 423 self.cookies_out[name] = "" 424 self.cookies_out[name]["path"] = "/" 425 self.cookies_out[name]["expires"] = 0 426 self.cookies_out[name]["max-age"] = 0 427 428 # Session-related methods. 429 430 def get_session(self, create=1): 431 432 """ 433 Gets a session corresponding to an identifier supplied in the 434 transaction. 435 436 If no session has yet been established according to information 437 provided in the transaction then the optional 'create' parameter 438 determines whether a new session will be established. 439 440 Where no session has been established and where 'create' is set to 0 441 then None is returned. In all other cases, a session object is created 442 (where appropriate) and returned. 443 """ 444 445 # NOTE: Requires configuration. 446 447 if self.session_store is None: 448 self.session_store = SessionStore(self, "WebStack-sessions") 449 return self.session_store.get_session(create) 450 451 def expire_session(self): 452 453 """ 454 Expires any session established according to information provided in the 455 transaction. 456 """ 457 458 # NOTE: Requires configuration. 459 460 if self.session_store is None: 461 self.session_store = SessionStore(self, "WebStack-sessions") 462 self.session_store.expire_session() 463 464 # Application-specific methods. 465 466 def set_user(self, username): 467 468 """ 469 An application-specific method which sets the user information with 470 'username' in the transaction. This affects subsequent calls to 471 'get_user'. 472 """ 473 474 self.user = username 475 476 # vim: tabstop=4 expandtab shiftwidth=4