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