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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 21 """ 22 23 import WebStack.Generic 24 from WebStack.Helpers.Auth import UserInfo 25 from WebStack.Helpers.Request import Cookie, decode_value, filter_fields 26 from WebStack.Helpers.Response import ConvertingStream 27 from WebStack.Helpers.Session import SessionStore 28 from cgi import parse_qs 29 30 class Transaction(WebStack.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 decode_value("/%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 = decode_value(name, encoding) 225 fields[name] = [] 226 for value in values: 227 value = decode_value(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 field_name = decode_value(field_name, encoding) 258 259 # Find the body values. 260 261 if type(field_values) == type([]): 262 fields[field_name] = [] 263 264 # Twisted stores plain strings. 265 266 for field_str in field_values: 267 fields[field_name].append(decode_value(field_str, encoding)) 268 else: 269 fields[field_name] = decode_value(field_values, encoding) 270 271 return fields 272 273 def get_fields(self, encoding=None): 274 275 """ 276 Extracts fields (or request parameters) from both the path specified in 277 the transaction as well as the message body. The optional 'encoding' 278 parameter specifies the character encoding of the message body for cases 279 where no such information is available, but where the default encoding 280 is to be overridden. 281 282 Returns a dictionary mapping field names to lists of values (even if a 283 single value is associated with any given field name). Each value is 284 either a Unicode object (representing a simple form field, for example) 285 or a plain string (representing a file upload form field, for example). 286 287 Where a given field name is used in both the path and message body to 288 specify values, the values from both sources will be combined into a 289 single list associated with that field name. 290 """ 291 292 return self._get_fields(encoding) 293 294 def get_user(self): 295 296 """ 297 Extracts user information from the transaction. 298 299 Returns a username as a string or None if no user is defined. 300 """ 301 302 # Twisted makes headers lower case. 303 304 if self.user is not None: 305 return self.user 306 307 auth_header = self.get_headers().get("authorization") 308 if auth_header: 309 return UserInfo(auth_header).username 310 else: 311 return None 312 313 def get_cookies(self): 314 315 """ 316 Obtains cookie information from the request. 317 318 Returns a dictionary mapping cookie names to cookie objects. 319 NOTE: Twisted does not seem to support this operation via methods. Thus, 320 NOTE: direct access has been employed to get the dictionary. 321 NOTE: Twisted also returns a plain string - a Cookie object is therefore 322 NOTE: introduced. 323 """ 324 325 return self.process_cookies(self.trans.received_cookies, using_strings=1) 326 327 def get_cookie(self, cookie_name): 328 329 """ 330 Obtains cookie information from the request. 331 332 Returns a cookie object for the given 'cookie_name' or None if no such 333 cookie exists. 334 NOTE: Twisted also returns a plain string - a Cookie object is therefore 335 NOTE: introduced. 336 """ 337 338 value = self.trans.getCookie(self.encode_cookie_value(cookie_name)) 339 if value is not None: 340 return Cookie(cookie_name, self.decode_cookie_value(value)) 341 else: 342 return None 343 344 # Response-related methods. 345 346 def get_response_stream(self): 347 348 """ 349 Returns the response stream for the transaction. 350 """ 351 352 # Unicode can upset this operation. Using either the specified charset 353 # or a default encoding. 354 355 encoding = self.get_response_stream_encoding() 356 return ConvertingStream(self.trans, encoding) 357 358 def get_response_stream_encoding(self): 359 360 """ 361 Returns the response stream encoding. 362 """ 363 364 if self.content_type: 365 encoding = self.content_type.charset 366 else: 367 encoding = None 368 return encoding or self.default_charset 369 370 def get_response_code(self): 371 372 """ 373 Get the response code associated with the transaction. If no response 374 code is defined, None is returned. 375 """ 376 377 # NOTE: Accessing the request attribute directly. 378 379 return self.trans.code 380 381 def set_response_code(self, response_code): 382 383 """ 384 Set the 'response_code' using a numeric constant defined in the HTTP 385 specification. 386 """ 387 388 self.trans.setResponseCode(response_code) 389 390 def set_header_value(self, header, value): 391 392 """ 393 Set the HTTP 'header' with the given 'value'. 394 """ 395 396 self.trans.setHeader(self.format_header_value(header), self.format_header_value(value)) 397 398 def set_content_type(self, content_type): 399 400 """ 401 Sets the 'content_type' for the response. 402 """ 403 404 # Remember the content type for encoding purposes later. 405 406 self.content_type = content_type 407 self.trans.setHeader("Content-Type", str(content_type)) 408 409 # Higher level response-related methods. 410 411 def set_cookie(self, cookie): 412 413 """ 414 Stores the given 'cookie' object in the response. 415 """ 416 417 self.set_cookie_value(cookie.name, cookie.value, path=cookie.path, expires=cookie.expires) 418 419 def set_cookie_value(self, name, value, path=None, expires=None): 420 421 """ 422 Stores a cookie with the given 'name' and 'value' in the response. 423 424 The optional 'path' is a string which specifies the scope of the cookie, 425 and the optional 'expires' parameter is a value compatible with the 426 time.time function, and indicates the expiry date/time of the cookie. 427 """ 428 429 self.trans.addCookie(self.encode_cookie_value(name), 430 self.encode_cookie_value(value), expires=expires, path=path) 431 432 def delete_cookie(self, cookie_name): 433 434 """ 435 Adds to the response a request that the cookie with the given 436 'cookie_name' be deleted/discarded by the client. 437 """ 438 439 # Create a special cookie, given that we do not know whether the browser 440 # has been sent the cookie or not. 441 # NOTE: Magic discovered in Webware. 442 443 self.trans.addCookie(self.encode_cookie_value(cookie_name), "", expires=0, path="/", max_age=0) 444 445 # Session-related methods. 446 447 def get_session(self, create=1): 448 449 """ 450 Gets a session corresponding to an identifier supplied in the 451 transaction. 452 453 If no session has yet been established according to information 454 provided in the transaction then the optional 'create' parameter 455 determines whether a new session will be established. 456 457 Where no session has been established and where 'create' is set to 0 458 then None is returned. In all other cases, a session object is created 459 (where appropriate) and returned. 460 """ 461 462 # NOTE: Requires configuration. 463 464 if self.session_store is None: 465 self.session_store = SessionStore(self, "WebStack-sessions") 466 return self.session_store.get_session(create) 467 468 def expire_session(self): 469 470 """ 471 Expires any session established according to information provided in the 472 transaction. 473 """ 474 475 # NOTE: Requires configuration. 476 477 if self.session_store is None: 478 self.session_store = SessionStore(self, "WebStack-sessions") 479 self.session_store.expire_session() 480 481 # vim: tabstop=4 expandtab shiftwidth=4