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