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