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