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