1 #!/usr/bin/env python 2 3 """ 4 Django classes. 5 6 Copyright (C) 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 import Environment 25 from WebStack.Helpers.Request import decode_value, FileContent, Cookie 26 from WebStack.Helpers.Response import ConvertingStream 27 from WebStack.Helpers.Auth import UserInfo 28 from django.http import HttpResponse 29 from StringIO import StringIO 30 31 class Transaction(WebStack.Generic.Transaction): 32 33 """ 34 Django transaction interface. 35 """ 36 37 def __init__(self, request): 38 39 """ 40 Initialise the transaction with the Django 'request' object. 41 """ 42 43 self.request = request 44 45 # Attributes which may be changed later. 46 47 self.content_type = None 48 49 # The response is created here but must be modified later. 50 # NOTE: It is unfortunate that Django wants to initialise the response 51 # NOTE: with the content type immediately. 52 53 self.response = HttpResponse() 54 self.content = StringIO() 55 56 def commit(self): 57 58 "Commit the transaction by finishing some things off." 59 60 self.content.seek(0) 61 self.response.content = self.content.read() 62 63 def rollback(self): 64 65 """ 66 A special method, partially synchronising the transaction with 67 framework-specific objects, but discarding previously emitted content 68 that is to be replaced by an error message. 69 """ 70 71 self.response = HttpResponse() 72 self.content = StringIO() 73 74 # Server-related methods. 75 76 def get_server_name(self): 77 78 "Returns the server name." 79 80 return self.request.META.get("SERVER_NAME") 81 82 def get_server_port(self): 83 84 "Returns the server port as a string." 85 86 return self.request.META.get("SERVER_PORT") 87 88 # Request-related methods. 89 90 def get_request_stream(self): 91 92 """ 93 Returns the request stream for the transaction. 94 """ 95 96 # Unfortunately, we get given a string from Django. Thus, we need to 97 # create a stream around that string. 98 99 return StringIO(self.request.raw_post_data) 100 101 def get_request_method(self): 102 103 """ 104 Returns the request method. 105 """ 106 107 return self.request.META.get("REQUEST_METHOD") 108 109 def get_headers(self): 110 111 """ 112 Returns all request headers as a dictionary-like object mapping header 113 names to values. 114 """ 115 116 return Environment.get_headers(self.request.META) 117 118 def get_header_values(self, key): 119 120 """ 121 Returns a list of all request header values associated with the given 122 'key'. Note that according to RFC 2616, 'key' is treated as a 123 case-insensitive string. 124 """ 125 126 return self.convert_to_list(self.get_headers().get(key)) 127 128 def get_content_type(self): 129 130 """ 131 Returns the content type specified on the request, along with the 132 charset employed. 133 """ 134 135 return self.parse_content_type(self.request.META.get("CONTENT_TYPE")) 136 137 def get_content_charsets(self): 138 139 """ 140 Returns the character set preferences. 141 142 NOTE: Not decently supported. 143 """ 144 145 return self.parse_content_preferences(None) 146 147 def get_content_languages(self): 148 149 """ 150 Returns extracted language information from the transaction. 151 152 NOTE: Not decently supported. 153 """ 154 155 return self.parse_content_preferences(None) 156 157 def get_path(self, encoding=None): 158 159 """ 160 Returns the entire path from the request as a Unicode object. Any "URL 161 encoded" character values in the part of the path before the query 162 string will be decoded and presented as genuine characters; the query 163 string will remain "URL encoded", however. 164 165 If the optional 'encoding' is set, use that in preference to the default 166 encoding to convert the path into a form not containing "URL encoded" 167 character values. 168 """ 169 170 encoding = encoding or self.default_charset 171 172 return decode_value(self.request.get_full_path(), encoding) 173 174 def get_path_without_query(self, encoding=None): 175 176 """ 177 Returns the entire path from the request minus the query string as a 178 Unicode object containing genuine characters (as opposed to "URL 179 encoded" character values). 180 181 If the optional 'encoding' is set, use that in preference to the default 182 encoding to convert the path into a form not containing "URL encoded" 183 character values. 184 """ 185 186 path = self.get_path(encoding) 187 return path.split("?")[0] 188 189 def get_path_info(self, encoding=None): 190 191 """ 192 Returns the "path info" (the part of the URL after the resource name 193 handling the current request) from the request as a Unicode object 194 containing genuine characters (as opposed to "URL encoded" character 195 values). 196 197 If the optional 'encoding' is set, use that in preference to the default 198 encoding to convert the path into a form not containing "URL encoded" 199 character values. 200 """ 201 202 encoding = encoding or self.default_charset 203 204 path_info = self.request.META.get("PATH_INFO") or "" 205 return decode_value(path_info, encoding) 206 207 def get_query_string(self): 208 209 """ 210 Returns the query string from the path in the request. 211 """ 212 213 return self.request.META.get("QUERY_STRING") or "" 214 215 # Higher level request-related methods. 216 217 def get_fields_from_path(self, encoding=None): 218 219 """ 220 Extracts fields (or request parameters) from the path specified in the 221 transaction. The underlying framework may refuse to supply fields from 222 the path if handling a POST transaction. The optional 'encoding' 223 parameter specifies the character encoding of the query string for cases 224 where the default encoding is to be overridden. 225 226 Returns a dictionary mapping field names to lists of values (even if a 227 single value is associated with any given field name). 228 """ 229 230 return self._get_fields(self.request.GET, encoding) 231 232 def get_fields_from_body(self, encoding=None): 233 234 """ 235 Extracts fields (or request parameters) from the message body in the 236 transaction. The optional 'encoding' parameter specifies the character 237 encoding of the message body for cases where no such information is 238 available, but where the default encoding is to be overridden. 239 240 Returns a dictionary mapping field names to lists of values (even if a 241 single value is associated with any given field name). Each value is 242 either a Unicode object (representing a simple form field, for example) 243 or a WebStack.Helpers.Request.FileContent object (representing a file 244 upload form field). 245 """ 246 247 fields = {} 248 self._update_fields(fields, self._get_fields(self.request.POST, encoding)) 249 self._update_fields(fields, self._get_files()) 250 return fields 251 252 def _get_fields(self, source, encoding=None): 253 254 encoding = encoding or self.get_content_type().charset or self.default_charset 255 256 fields = {} 257 for name in source.keys(): 258 name = decode_value(name, encoding) 259 fields[name] = [] 260 for value in source.getlist(name): 261 value = decode_value(value, encoding) 262 fields[name].append(value) 263 return fields 264 265 def _get_files(self): 266 files = {} 267 for name, file in self.request.FILES.items(): 268 269 # NOTE: Django does not seem to expose a stream for a file upload. 270 271 files[name] = [FileContent( 272 StringIO(file.get("content", "")), { 273 "Content-Type" : file.get("content-type", ""), 274 "Content-Disposition" : "%s; filename=%s" % (name, file.get("filename", "")) 275 })] 276 return files 277 278 def get_fields(self, encoding=None): 279 280 """ 281 Extracts fields (or request parameters) from both the path specified in 282 the transaction as well as the message body. The optional 'encoding' 283 parameter specifies the character encoding of the message body for cases 284 where no such information is available, but where the default encoding 285 is to be overridden. 286 287 Returns a dictionary mapping field names to lists of values (even if a 288 single value is associated with any given field name). Each value is 289 either a Unicode object (representing a simple form field, for example) 290 or a WebStack.Helpers.Request.FileContent object (representing a file 291 upload form field). 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 fields = {} 299 fields.update(self.get_fields_from_path(encoding)) 300 self._update_fields(fields, self.get_fields_from_body(encoding)) 301 return fields 302 303 def _update_fields(self, fields, new_fields): 304 for name, values in new_fields.items(): 305 if not fields.has_key(name): 306 fields[name] = values 307 else: 308 fields[name] += values 309 310 def get_user(self): 311 312 """ 313 Extracts user information from the transaction. 314 315 Returns a username as a string or None if no user is defined. 316 """ 317 318 if self.user is not None: 319 return self.user 320 321 auth_header = self.get_headers().get("authorization") 322 if auth_header: 323 return UserInfo(auth_header).username 324 else: 325 return None 326 327 def get_cookies(self): 328 329 """ 330 Obtains cookie information from the request. 331 332 Returns a dictionary mapping cookie names to cookie objects. 333 """ 334 335 return self.process_cookies(self.request.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 """ 345 346 value = self.request.COOKIES.get(self.encode_cookie_value(cookie_name)) 347 if value is not None: 348 return Cookie(cookie_name, self.decode_cookie_value(value)) 349 else: 350 return None 351 352 # Response-related methods. 353 354 def get_response_stream(self): 355 356 """ 357 Returns the response stream for the transaction. 358 """ 359 360 # Unicode can upset this operation. Using either the specified charset 361 # or a default encoding. 362 363 encoding = self.get_response_stream_encoding() 364 return ConvertingStream(self.content, encoding) 365 366 def get_response_stream_encoding(self): 367 368 """ 369 Returns the response stream encoding. 370 """ 371 372 if self.content_type: 373 encoding = self.content_type.charset 374 else: 375 encoding = None 376 return encoding or self.default_charset 377 378 def get_response_code(self): 379 380 """ 381 Get the response code associated with the transaction. If no response 382 code is defined, None is returned. 383 """ 384 385 return self.response.status_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.response.status_code = 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.response.headers[header] = value 403 404 def set_content_type(self, content_type): 405 406 """ 407 Sets the 'content_type' for the response. 408 """ 409 410 self.content_type = content_type 411 self.response.headers["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) 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.response.set_cookie(self.encode_cookie_value(name), self.encode_cookie_value(value), path=path, expires=expires) 434 435 def delete_cookie(self, cookie_name): 436 437 """ 438 Adds to the response a request that the cookie with the given 439 'cookie_name' be deleted/discarded by the client. 440 """ 441 442 #self.response.delete_cookie(self.encode_cookie_value(cookie_name)) 443 444 # Create a special cookie, given that we do not know whether the browser 445 # has been sent the cookie or not. 446 # NOTE: Magic discovered in Webware. 447 448 name = self.encode_cookie_value(cookie_name) 449 self.response.set_cookie(name, "", path="/", expires=0, 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: Dubious access to a more dictionary-like object. 469 470 if create: 471 self.request.session["_hack"] = "created" 472 return Session(self.request.session) 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: Not trivially supported! 482 483 class Session: 484 def __init__(self, session): 485 self.session = session 486 def __getattr__(self, name): 487 return getattr(self.session, name) 488 def keys(self): 489 return self.session._session.keys() 490 def values(self): 491 return self.session._session.values() 492 def items(self): 493 return self.session._session.items() 494 495 # vim: tabstop=4 expandtab shiftwidth=4