1 #!/usr/bin/env python 2 3 """ 4 Twisted classes. 5 """ 6 7 import Generic 8 from Helpers.Auth import UserInfo 9 from Helpers.Request import Cookie, get_body_field, filter_fields 10 from Helpers.Response import ConvertingStream 11 from Helpers.Session import SessionStore 12 from cgi import parse_qs 13 14 class Transaction(Generic.Transaction): 15 16 """ 17 Twisted transaction interface. 18 """ 19 20 def __init__(self, trans): 21 22 "Initialise the transaction using the Twisted transaction 'trans'." 23 24 self.trans = trans 25 self.content_type = None 26 self.user = None 27 self.path_info = None 28 29 # Special objects retained throughout the transaction. 30 31 self.session_store = None 32 33 def commit(self): 34 35 """ 36 A special method, synchronising the transaction with framework-specific 37 objects. 38 """ 39 40 # Close the session store. 41 42 if self.session_store is not None: 43 self.session_store.close() 44 45 # Server-related methods. 46 47 def get_server_name(self): 48 49 "Returns the server name." 50 51 return self.trans.getRequestHostname() 52 53 def get_server_port(self): 54 55 "Returns the server port as a string." 56 57 return str(self.trans.getHost()[2]) 58 59 # Request-related methods. 60 61 def get_request_stream(self): 62 63 """ 64 Returns the request stream for the transaction. 65 """ 66 67 return self.trans.content 68 69 def get_request_method(self): 70 71 """ 72 Returns the request method. 73 """ 74 75 return self.trans.method 76 77 def get_headers(self): 78 79 """ 80 Returns all request headers as a dictionary-like object mapping header 81 names to values. 82 83 NOTE: If duplicate header names are permitted, then this interface will 84 NOTE: need to change. 85 """ 86 87 return self.trans.received_headers 88 89 def get_header_values(self, key): 90 91 """ 92 Returns a list of all request header values associated with the given 93 'key'. Note that according to RFC 2616, 'key' is treated as a 94 case-insensitive string. 95 """ 96 97 # Twisted does not convert the header key to lower case (which is the 98 # stored representation). 99 100 return self.convert_to_list(self.trans.received_headers.get(key.lower())) 101 102 def get_content_type(self): 103 104 """ 105 Returns the content type specified on the request, along with the 106 charset employed. 107 """ 108 109 return self.parse_content_type(self.trans.getHeader("Content-Type")) 110 111 def get_content_charsets(self): 112 113 """ 114 Returns the character set preferences. 115 """ 116 117 return self.parse_content_preferences(self.trans.getHeader("Accept-Language")) 118 119 def get_content_languages(self): 120 121 """ 122 Returns extracted language information from the transaction. 123 """ 124 125 return self.parse_content_preferences(self.trans.getHeader("Accept-Charset")) 126 127 def get_path(self): 128 129 """ 130 Returns the entire path from the request. 131 """ 132 133 return self.trans.uri 134 135 def get_path_without_query(self): 136 137 """ 138 Returns the entire path from the request minus the query string. 139 """ 140 141 return self.get_path().split("?")[0] 142 143 def get_path_info(self): 144 145 """ 146 Returns the "path info" (the part of the URL after the resource name 147 handling the current request) from the request. 148 """ 149 150 return "/%s" % "/".join(self.trans.postpath) 151 152 def get_query_string(self): 153 154 """ 155 Returns the query string from the path in the request. 156 """ 157 158 t = self.get_path().split("?") 159 if len(t) == 1: 160 return "" 161 else: 162 163 # NOTE: Overlook erroneous usage of "?" characters in the path. 164 165 return "?".join(t[1:]) 166 167 # Higher level request-related methods. 168 169 def get_fields_from_path(self): 170 171 """ 172 Extracts fields (or request parameters) from the path specified in the 173 transaction. The underlying framework may refuse to supply fields from 174 the path if handling a POST transaction. 175 176 Returns a dictionary mapping field names to lists of values (even if a 177 single value is associated with any given field name). 178 """ 179 180 # NOTE: Support at best ISO-8859-1 values. 181 182 fields = {} 183 for name, values in parse_qs(self.get_query_string(), keep_blank_values=1).items(): 184 fields[name] = [] 185 for value in values: 186 fields[name].append(unicode(value, "iso-8859-1")) 187 return fields 188 189 def get_fields_from_body(self, encoding=None): 190 191 """ 192 Extracts fields (or request parameters) from the message body in the 193 transaction. The optional 'encoding' parameter specifies the character 194 encoding of the message body for cases where no such information is 195 available, but where the default encoding is to be overridden. 196 197 Returns a dictionary mapping field names to lists of values (even if a 198 single value is associated with any given field name). Each value is 199 either a Unicode object (representing a simple form field, for example) 200 or a plain string (representing a file upload form field, for example). 201 """ 202 203 # There may not be a reliable means of extracting only the fields 204 # the message body using the API. Remove fields originating from the 205 # path in the mixture provided by the API. 206 207 all_fields = self._get_fields(encoding) 208 fields_from_path = self.get_fields_from_path() 209 return filter_fields(all_fields, fields_from_path) 210 211 def _get_fields(self, encoding=None): 212 encoding = encoding or self.get_content_type().charset or self.default_charset 213 fields = {} 214 for field_name, field_values in self.trans.args.items(): 215 216 # Find the body values. 217 218 if type(field_values) == type([]): 219 fields[field_name] = [] 220 221 # Twisted stores plain strings. 222 223 for field_str in field_values: 224 fields[field_name].append(get_body_field(field_str, encoding)) 225 else: 226 fields[field_name] = get_body_field(field_values, encoding) 227 228 return fields 229 230 def get_fields(self, encoding=None): 231 232 """ 233 Extracts fields (or request parameters) from both the path specified in 234 the transaction as well as the message body. The optional 'encoding' 235 parameter specifies the character encoding of the message body for cases 236 where no such information is available, but where the default encoding 237 is to be overridden. 238 239 Returns a dictionary mapping field names to lists of values (even if a 240 single value is associated with any given field name). Each value is 241 either a Unicode object (representing a simple form field, for example) 242 or a plain string (representing a file upload form field, for example). 243 244 Where a given field name is used in both the path and message body to 245 specify values, the values from both sources will be combined into a 246 single list associated with that field name. 247 """ 248 249 return self._get_fields(encoding) 250 251 def get_user(self): 252 253 """ 254 Extracts user information from the transaction. 255 256 Returns a username as a string or None if no user is defined. 257 """ 258 259 # Twisted makes headers lower case. 260 261 if self.user is not None: 262 return self.user 263 264 auth_header = self.get_headers().get("authorization") 265 if auth_header: 266 return UserInfo(auth_header).username 267 else: 268 return None 269 270 def get_cookies(self): 271 272 """ 273 Obtains cookie information from the request. 274 275 Returns a dictionary mapping cookie names to cookie objects. 276 NOTE: Twisted does not seem to support this operation via methods. Thus, 277 NOTE: direct access has been employed to get the dictionary. 278 NOTE: Twisted also returns a plain string - a Cookie object is therefore 279 NOTE: introduced. 280 """ 281 282 return self.process_cookies(self.trans.received_cookies, using_strings=1) 283 284 def get_cookie(self, cookie_name): 285 286 """ 287 Obtains cookie information from the request. 288 289 Returns a cookie object for the given 'cookie_name' or None if no such 290 cookie exists. 291 NOTE: Twisted also returns a plain string - a Cookie object is therefore 292 NOTE: introduced. 293 """ 294 295 value = self.trans.getCookie(self.encode_cookie_value(cookie_name)) 296 if value is not None: 297 return Cookie(cookie_name, self.decode_cookie_value(value)) 298 else: 299 return None 300 301 # Response-related methods. 302 303 def get_response_stream(self): 304 305 """ 306 Returns the response stream for the transaction. 307 """ 308 309 # Unicode can upset this operation. Using either the specified charset 310 # or a default encoding. 311 312 encoding = self.get_response_stream_encoding() 313 return ConvertingStream(self.trans, encoding) 314 315 def get_response_stream_encoding(self): 316 317 """ 318 Returns the response stream encoding. 319 """ 320 321 if self.content_type: 322 encoding = self.content_type.charset 323 else: 324 encoding = None 325 return encoding or self.default_charset 326 327 def get_response_code(self): 328 329 """ 330 Get the response code associated with the transaction. If no response 331 code is defined, None is returned. 332 """ 333 334 # NOTE: Accessing the request attribute directly. 335 336 return self.trans.code 337 338 def set_response_code(self, response_code): 339 340 """ 341 Set the 'response_code' using a numeric constant defined in the HTTP 342 specification. 343 """ 344 345 self.trans.setResponseCode(response_code) 346 347 def set_header_value(self, header, value): 348 349 """ 350 Set the HTTP 'header' with the given 'value'. 351 """ 352 353 self.trans.setHeader(self.format_header_value(header), self.format_header_value(value)) 354 355 def set_content_type(self, content_type): 356 357 """ 358 Sets the 'content_type' for the response. 359 """ 360 361 # Remember the content type for encoding purposes later. 362 363 self.content_type = content_type 364 self.trans.setHeader("Content-Type", str(content_type)) 365 366 # Higher level response-related methods. 367 368 def set_cookie(self, cookie): 369 370 """ 371 Stores the given 'cookie' object in the response. 372 """ 373 374 self.set_cookie_value(cookie.name, cookie.value, path=cookie.path, expires=cookie.expires) 375 376 def set_cookie_value(self, name, value, path=None, expires=None): 377 378 """ 379 Stores a cookie with the given 'name' and 'value' in the response. 380 381 The optional 'path' is a string which specifies the scope of the cookie, 382 and the optional 'expires' parameter is a value compatible with the 383 time.time function, and indicates the expiry date/time of the cookie. 384 """ 385 386 self.trans.addCookie(self.encode_cookie_value(name), 387 self.encode_cookie_value(value), expires=expires, path=path) 388 389 def delete_cookie(self, cookie_name): 390 391 """ 392 Adds to the response a request that the cookie with the given 393 'cookie_name' be deleted/discarded by the client. 394 """ 395 396 # Create a special cookie, given that we do not know whether the browser 397 # has been sent the cookie or not. 398 # NOTE: Magic discovered in Webware. 399 400 self.trans.addCookie(self.encode_cookie_value(cookie_name), "", expires=0, path="/", max_age=0) 401 402 # Session-related methods. 403 404 def get_session(self, create=1): 405 406 """ 407 Gets a session corresponding to an identifier supplied in the 408 transaction. 409 410 If no session has yet been established according to information 411 provided in the transaction then the optional 'create' parameter 412 determines whether a new session will be established. 413 414 Where no session has been established and where 'create' is set to 0 415 then None is returned. In all other cases, a session object is created 416 (where appropriate) and returned. 417 """ 418 419 # NOTE: Requires configuration. 420 421 if self.session_store is None: 422 self.session_store = SessionStore(self, "WebStack-sessions") 423 return self.session_store.get_session(create) 424 425 def expire_session(self): 426 427 """ 428 Expires any session established according to information provided in the 429 transaction. 430 """ 431 432 # NOTE: Requires configuration. 433 434 if self.session_store is None: 435 self.session_store = SessionStore(self, "WebStack-sessions") 436 self.session_store.expire_session() 437 438 # vim: tabstop=4 expandtab shiftwidth=4