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 cookies = {} 268 for name, value in self.trans.received_cookies.items(): 269 cookies[name] = Cookie(name, value) 270 return cookies 271 272 def get_cookie(self, cookie_name): 273 274 """ 275 Obtains cookie information from the request. 276 277 Returns a cookie object for the given 'cookie_name' or None if no such 278 cookie exists. 279 NOTE: Twisted also returns a plain string - a Cookie object is therefore 280 NOTE: introduced. 281 """ 282 283 return Cookie(cookie_name, self.trans.getCookie(cookie_name)) 284 285 # Response-related methods. 286 287 def get_response_stream(self): 288 289 """ 290 Returns the response stream for the transaction. 291 """ 292 293 # Unicode can upset this operation. Using either the specified charset 294 # or a default encoding. 295 296 encoding = self.get_response_stream_encoding() 297 return ConvertingStream(self.trans, encoding) 298 299 def get_response_stream_encoding(self): 300 301 """ 302 Returns the response stream encoding. 303 """ 304 305 if self.content_type: 306 encoding = self.content_type.charset 307 else: 308 encoding = None 309 return encoding or self.default_charset 310 311 def get_response_code(self): 312 313 """ 314 Get the response code associated with the transaction. If no response 315 code is defined, None is returned. 316 """ 317 318 # NOTE: Accessing the request attribute directly. 319 320 return self.trans.code 321 322 def set_response_code(self, response_code): 323 324 """ 325 Set the 'response_code' using a numeric constant defined in the HTTP 326 specification. 327 """ 328 329 self.trans.setResponseCode(response_code) 330 331 def set_header_value(self, header, value): 332 333 """ 334 Set the HTTP 'header' with the given 'value'. 335 """ 336 337 self.trans.setHeader(self.format_header_value(header), self.format_header_value(value)) 338 339 def set_content_type(self, content_type): 340 341 """ 342 Sets the 'content_type' for the response. 343 """ 344 345 # Remember the content type for encoding purposes later. 346 347 self.content_type = content_type 348 self.trans.setHeader("Content-Type", str(content_type)) 349 350 # Higher level response-related methods. 351 352 def set_cookie(self, cookie): 353 354 """ 355 Stores the given 'cookie' object in the response. 356 """ 357 358 self.trans.addCookie(cookie.name, cookie.value, expires=cookie.expires, path=cookie.path) 359 360 def set_cookie_value(self, name, value, path=None, expires=None): 361 362 """ 363 Stores a cookie with the given 'name' and 'value' in the response. 364 365 The optional 'path' is a string which specifies the scope of the cookie, 366 and the optional 'expires' parameter is a value compatible with the 367 time.time function, and indicates the expiry date/time of the cookie. 368 """ 369 370 self.trans.addCookie(self.format_header_value(name), 371 self.format_header_value(value), expires=expires, path=path) 372 373 def delete_cookie(self, cookie_name): 374 375 """ 376 Adds to the response a request that the cookie with the given 377 'cookie_name' be deleted/discarded by the client. 378 """ 379 380 # Create a special cookie, given that we do not know whether the browser 381 # has been sent the cookie or not. 382 # NOTE: Magic discovered in Webware. 383 384 self.trans.addCookie(cookie_name, "", expires=0, path="/", max_age=0) 385 386 # Session-related methods. 387 388 def get_session(self, create=1): 389 390 """ 391 Gets a session corresponding to an identifier supplied in the 392 transaction. 393 394 If no session has yet been established according to information 395 provided in the transaction then the optional 'create' parameter 396 determines whether a new session will be established. 397 398 Where no session has been established and where 'create' is set to 0 399 then None is returned. In all other cases, a session object is created 400 (where appropriate) and returned. 401 """ 402 403 # NOTE: Requires configuration. 404 405 if self.session_store is None: 406 self.session_store = SessionStore(self, "WebStack-sessions") 407 return self.session_store.get_session(create) 408 409 def expire_session(self): 410 411 """ 412 Expires any session established according to information provided in the 413 transaction. 414 """ 415 416 # NOTE: Requires configuration. 417 418 if self.session_store is None: 419 self.session_store = SessionStore(self, "WebStack-sessions") 420 self.session_store.expire_session() 421 422 # Application-specific methods. 423 424 def set_user(self, username): 425 426 """ 427 An application-specific method which sets the user information with 428 'username' in the transaction. This affects subsequent calls to 429 'get_user'. 430 """ 431 432 self.user = username 433 434 # vim: tabstop=4 expandtab shiftwidth=4