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