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