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