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