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