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