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