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