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 files[name] = [FileContent(file.get("content", ""), { 269 "Content-Type" : file.get("content-type", ""), 270 "Content-Disposition" : "%s; filename=%s" % (name, file.get("filename", "")) 271 })] 272 return files 273 274 def get_fields(self, encoding=None): 275 276 """ 277 Extracts fields (or request parameters) from both the path specified in 278 the transaction as well as the message body. The optional 'encoding' 279 parameter specifies the character encoding of the message body for cases 280 where no such information is available, but where the default encoding 281 is to be overridden. 282 283 Returns a dictionary mapping field names to lists of values (even if a 284 single value is associated with any given field name). Each value is 285 either a Unicode object (representing a simple form field, for example) 286 or a WebStack.Helpers.Request.FileContent object (representing a file 287 upload form field). 288 289 Where a given field name is used in both the path and message body to 290 specify values, the values from both sources will be combined into a 291 single list associated with that field name. 292 """ 293 294 fields = {} 295 fields.update(self.get_fields_from_path(encoding)) 296 self._update_fields(fields, self.get_fields_from_body(encoding)) 297 return fields 298 299 def _update_fields(self, fields, new_fields): 300 for name, values in new_fields.items(): 301 if not fields.has_key(name): 302 fields[name] = values 303 else: 304 fields[name] += values 305 306 def get_user(self): 307 308 """ 309 Extracts user information from the transaction. 310 311 Returns a username as a string or None if no user is defined. 312 """ 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 """ 330 331 return self.process_cookies(self.request.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 """ 341 342 value = self.request.COOKIES.get(self.encode_cookie_value(cookie_name)) 343 if value is not None: 344 return Cookie(cookie_name, self.decode_cookie_value(value)) 345 else: 346 return None 347 348 # Response-related methods. 349 350 def get_response_stream(self): 351 352 """ 353 Returns the response stream for the transaction. 354 """ 355 356 # Unicode can upset this operation. Using either the specified charset 357 # or a default encoding. 358 359 encoding = self.get_response_stream_encoding() 360 return ConvertingStream(self.content, encoding) 361 362 def get_response_stream_encoding(self): 363 364 """ 365 Returns the response stream encoding. 366 """ 367 368 if self.content_type: 369 encoding = self.content_type.charset 370 else: 371 encoding = None 372 return encoding or self.default_charset 373 374 def get_response_code(self): 375 376 """ 377 Get the response code associated with the transaction. If no response 378 code is defined, None is returned. 379 """ 380 381 return self.response.status_code 382 383 def set_response_code(self, response_code): 384 385 """ 386 Set the 'response_code' using a numeric constant defined in the HTTP 387 specification. 388 """ 389 390 self.response.status_code = response_code 391 392 def set_header_value(self, header, value): 393 394 """ 395 Set the HTTP 'header' with the given 'value'. 396 """ 397 398 self.response.headers[header] = value 399 400 def set_content_type(self, content_type): 401 402 """ 403 Sets the 'content_type' for the response. 404 """ 405 406 self.content_type = content_type 407 self.response.headers["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) 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.response.set_cookie(self.encode_cookie_value(name), self.encode_cookie_value(value), path=path, expires=expires) 430 431 def delete_cookie(self, cookie_name): 432 433 """ 434 Adds to the response a request that the cookie with the given 435 'cookie_name' be deleted/discarded by the client. 436 """ 437 438 #self.response.delete_cookie(self.encode_cookie_value(cookie_name)) 439 440 # Create a special cookie, given that we do not know whether the browser 441 # has been sent the cookie or not. 442 # NOTE: Magic discovered in Webware. 443 444 name = self.encode_cookie_value(cookie_name) 445 self.response.set_cookie(name, "", path="/", expires=0, max_age=0) 446 447 # Session-related methods. 448 449 def get_session(self, create=1): 450 451 """ 452 Gets a session corresponding to an identifier supplied in the 453 transaction. 454 455 If no session has yet been established according to information 456 provided in the transaction then the optional 'create' parameter 457 determines whether a new session will be established. 458 459 Where no session has been established and where 'create' is set to 0 460 then None is returned. In all other cases, a session object is created 461 (where appropriate) and returned. 462 """ 463 464 # NOTE: Dubious access to a more dictionary-like object. 465 466 if create: 467 self.request.session["_hack"] = "created" 468 return Session(self.request.session) 469 470 def expire_session(self): 471 472 """ 473 Expires any session established according to information provided in the 474 transaction. 475 """ 476 477 # NOTE: Not trivially supported! 478 479 class Session: 480 def __init__(self, session): 481 self.session = session 482 def __getattr__(self, name): 483 return getattr(self.session, name) 484 def keys(self): 485 return self.session._session.keys() 486 def values(self): 487 return self.session._session.values() 488 def items(self): 489 return self.session._session.items() 490 491 # vim: tabstop=4 expandtab shiftwidth=4