1 #!/usr/bin/env python 2 3 """ 4 mod_python classes. 5 6 Copyright (C) 2004, 2005 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 21 """ 22 23 import Generic 24 from Helpers.Request import get_body_field, decode_value, filter_fields, Cookie 25 from Helpers.Response import ConvertingStream 26 from mod_python.util import parse_qs, FieldStorage 27 from mod_python import apache 28 29 # Provide alternative implementations. 30 # The alternative session support requires cookie support of some kind. 31 32 try: 33 from mod_python.Cookie import get_cookies, add_cookie, Cookie as SimpleCookie 34 have_cookies = 1 35 except ImportError: 36 from Cookie import SimpleCookie 37 have_cookies = 0 38 try: 39 from mod_python import Session 40 except ImportError: 41 from Helpers.Session import SessionStore 42 import os 43 Session = None 44 45 class Transaction(Generic.Transaction): 46 47 """ 48 mod_python transaction interface. 49 """ 50 51 def __init__(self, trans): 52 53 "Initialise the transaction using the mod_python transaction 'trans'." 54 55 self.trans = trans 56 self.response_code = apache.OK 57 self.content_type = None 58 self.user = None 59 self.path_info = None 60 61 # Support non-framework cookies. 62 63 if not have_cookies: 64 65 # Define the incoming cookies. 66 67 self.cookies_in = SimpleCookie(self.get_headers().get("cookie")) 68 69 # Cached information. 70 71 self.storage_body = None 72 73 # Special objects retained throughout the transaction. 74 75 self.session_store = None 76 self.session = None # mod_python native session 77 78 def commit(self): 79 80 """ 81 A special method, synchronising the transaction with framework-specific 82 objects. 83 """ 84 85 # Close the session store. 86 87 if self.session_store is not None: 88 self.session_store.close() 89 90 # Commit any native session. 91 92 if self.session is not None: 93 self.session.save() 94 95 # Server-related methods. 96 97 def get_server_name(self): 98 99 "Returns the server name." 100 101 return self.trans.server.server_hostname 102 103 def get_server_port(self): 104 105 "Returns the server port as a string." 106 107 return str(self.trans.connection.local_addr[1]) 108 109 # Request-related methods. 110 111 def get_request_stream(self): 112 113 """ 114 Returns the request stream for the transaction. 115 """ 116 117 return self.trans 118 119 def get_request_method(self): 120 121 """ 122 Returns the request method. 123 """ 124 125 return self.trans.method 126 127 def get_headers(self): 128 129 """ 130 Returns all request headers as a dictionary-like object mapping header 131 names to values. 132 133 NOTE: If duplicate header names are permitted, then this interface will 134 NOTE: need to change. 135 """ 136 137 return self.trans.headers_in 138 139 def get_header_values(self, key): 140 141 """ 142 Returns a list of all request header values associated with the given 143 'key'. Note that according to RFC 2616, 'key' is treated as a 144 case-insensitive string. 145 """ 146 147 return self.convert_to_list(self.trans.headers_in.get(key)) 148 149 def get_content_type(self): 150 151 """ 152 Returns the content type specified on the request, along with the 153 charset employed. 154 """ 155 156 return self.parse_content_type(self.trans.content_type) 157 158 def get_content_charsets(self): 159 160 """ 161 Returns the character set preferences. 162 """ 163 164 return self.parse_content_preferences(self.trans.headers_in.get("Accept-Charset")) 165 166 def get_content_languages(self): 167 168 """ 169 Returns extracted language information from the transaction. 170 """ 171 172 return self.parse_content_preferences(self.trans.headers_in.get("Accept-Language")) 173 174 def get_path(self, encoding=None): 175 176 """ 177 Returns the entire path from the request as a Unicode object. Any "URL 178 encoded" character values in the part of the path before the query 179 string will be decoded and presented as genuine characters; the query 180 string will remain "URL encoded", however. 181 182 If the optional 'encoding' is set, use that in preference to the default 183 encoding to convert the path into a form not containing "URL encoded" 184 character values. 185 """ 186 187 query_string = self.get_query_string() 188 if query_string: 189 return decode_value(self.trans.uri, encoding) + "?" + query_string 190 else: 191 return decode_value(self.trans.uri, encoding) 192 193 def get_path_without_query(self, encoding=None): 194 195 """ 196 Returns the entire path from the request minus the query string as a 197 Unicode object containing genuine characters (as opposed to "URL 198 encoded" character values). 199 200 If the optional 'encoding' is set, use that in preference to the default 201 encoding to convert the path into a form not containing "URL encoded" 202 character values. 203 """ 204 205 return decode_value(self.trans.uri, encoding) 206 207 def get_path_info(self, encoding=None): 208 209 """ 210 Returns the "path info" (the part of the URL after the resource name 211 handling the current request) from the request as a Unicode object 212 containing genuine characters (as opposed to "URL encoded" character 213 values). 214 215 If the optional 'encoding' is set, use that in preference to the default 216 encoding to convert the path into a form not containing "URL encoded" 217 character values. 218 """ 219 220 return decode_value(self.trans.path_info, encoding) 221 222 def get_query_string(self): 223 224 """ 225 Returns the query string from the path in the request. 226 """ 227 228 return self.trans.args or "" 229 230 # Higher level request-related methods. 231 232 def get_fields_from_path(self, encoding=None): 233 234 """ 235 Extracts fields (or request parameters) from the path specified in the 236 transaction. The underlying framework may refuse to supply fields from 237 the path if handling a POST transaction. The optional 'encoding' 238 parameter specifies the character encoding of the query string for cases 239 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). 243 """ 244 245 fields = {} 246 for name, values in parse_qs(self.get_query_string(), 1).items(): # keep_blank_values=1 247 name = decode_value(name, encoding) 248 fields[name] = [] 249 for value in values: 250 value = decode_value(value, encoding) 251 fields[name].append(value) 252 return fields 253 254 def get_fields_from_body(self, encoding=None): 255 256 """ 257 Extracts fields (or request parameters) from the message body in the 258 transaction. The optional 'encoding' parameter specifies the character 259 encoding of the message body for cases where no such information is 260 available, but where the default encoding is to be overridden. 261 262 Returns a dictionary mapping field names to lists of values (even if a 263 single value is associated with any given field name). Each value is 264 either a Unicode object (representing a simple form field, for example) 265 or a plain string (representing a file upload form field, for example). 266 267 The mod_python.util.FieldStorage class may augment the fields from the 268 body with fields found in the path. 269 """ 270 271 all_fields = self._get_fields(encoding) 272 fields_from_path = self.get_fields_from_path() 273 return filter_fields(all_fields, fields_from_path) 274 275 def _get_fields(self, encoding=None): 276 encoding = encoding or self.get_content_type().charset or self.default_charset 277 278 if self.storage_body is None: 279 self.storage_body = FieldStorage(self.trans, keep_blank_values=1) 280 281 # Traverse the storage, finding each field value. 282 283 fields = {} 284 for field in self.storage_body.list: 285 field_name = decode_value(field.name, encoding) 286 if not fields.has_key(field_name): 287 fields[field_name] = [] 288 fields[field_name].append(get_body_field(field.value, encoding)) 289 return fields 290 291 def get_fields(self, encoding=None): 292 293 """ 294 Extracts fields (or request parameters) from both the path specified in 295 the transaction as well as the message body. The optional 'encoding' 296 parameter specifies the character encoding of the message body for cases 297 where no such information is available, but where the default encoding 298 is to be overridden. 299 300 Returns a dictionary mapping field names to lists of values (even if a 301 single value is associated with any given field name). Each value is 302 either a Unicode object (representing a simple form field, for example) 303 or a plain string (representing a file upload form field, for example). 304 305 Where a given field name is used in both the path and message body to 306 specify values, the values from both sources will be combined into a 307 single list associated with that field name. 308 """ 309 310 return self._get_fields(encoding) 311 312 def get_user(self): 313 314 """ 315 Extracts user information from the transaction. 316 317 Returns a username as a string or None if no user is defined. 318 """ 319 320 if self.user is not None: 321 return self.user 322 else: 323 return self.trans.user 324 325 def get_cookies(self): 326 327 """ 328 Obtains cookie information from the request. 329 330 Returns a dictionary mapping cookie names to cookie objects. 331 332 NOTE: No additional information is passed to the underlying API despite 333 NOTE: support for enhanced cookies in mod_python. 334 """ 335 336 if have_cookies: 337 found_cookies = get_cookies(self.trans) 338 else: 339 found_cookies = self.cookies_in 340 return self.process_cookies(found_cookies) 341 342 def get_cookie(self, cookie_name): 343 344 """ 345 Obtains cookie information from the request. 346 347 Returns a cookie object for the given 'cookie_name' or None if no such 348 cookie exists. 349 """ 350 351 return self.get_cookies().get(self.encode_cookie_value(cookie_name)) 352 353 # Response-related methods. 354 355 def get_response_stream(self): 356 357 """ 358 Returns the response stream for the transaction. 359 """ 360 361 # Unicode can upset this operation. Using either the specified charset 362 # or a default encoding. 363 364 encoding = self.get_response_stream_encoding() 365 return ConvertingStream(self.trans, encoding) 366 367 def get_response_stream_encoding(self): 368 369 """ 370 Returns the response stream encoding. 371 """ 372 373 if self.content_type: 374 encoding = self.content_type.charset 375 else: 376 encoding = None 377 return encoding or self.default_charset 378 379 def get_response_code(self): 380 381 """ 382 Get the response code associated with the transaction. If no response 383 code is defined, None is returned. 384 """ 385 386 return self.response_code 387 388 def set_response_code(self, response_code): 389 390 """ 391 Set the 'response_code' using a numeric constant defined in the HTTP 392 specification. 393 """ 394 395 self.response_code = response_code 396 397 def set_header_value(self, header, value): 398 399 """ 400 Set the HTTP 'header' with the given 'value'. 401 """ 402 403 self.trans.headers_out[self.format_header_value(header)] = self.format_header_value(value) 404 405 def set_content_type(self, content_type): 406 407 """ 408 Sets the 'content_type' for the response. 409 """ 410 411 # Remember the content type for encoding purposes later. 412 413 self.content_type = content_type 414 self.trans.content_type = str(content_type) 415 416 # Higher level response-related methods. 417 418 def set_cookie(self, cookie): 419 420 """ 421 Stores the given 'cookie' object in the response. 422 """ 423 424 # NOTE: If multiple cookies of the same name could be specified, this 425 # NOTE: could need changing. 426 427 self.set_cookie_value(cookie.name, cookie.value) 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 name = self.encode_cookie_value(name) 440 441 if have_cookies: 442 cookie = SimpleCookie(name, self.encode_cookie_value(value)) 443 if expires is not None: 444 cookie.expires = expires 445 if path is not None: 446 cookie.path = path 447 add_cookie(self.trans, cookie) 448 else: 449 cookie_out = SimpleCookie() 450 cookie_out[name] = self.encode_cookie_value(value) 451 if path is not None: 452 cookie_out[name]["path"] = path 453 if expires is not None: 454 cookie_out[name]["expires"] = expires 455 self._write_cookie(cookie_out) 456 457 def delete_cookie(self, cookie_name): 458 459 """ 460 Adds to the response a request that the cookie with the given 461 'cookie_name' be deleted/discarded by the client. 462 """ 463 464 # Create a special cookie, given that we do not know whether the browser 465 # has been sent the cookie or not. 466 # NOTE: Magic discovered in Webware. 467 468 name = self.encode_cookie_value(cookie_name) 469 470 if have_cookies: 471 cookie = SimpleCookie(name, "") 472 cookie.path = "/" 473 cookie.expires = 0 474 cookie.max_age = 0 475 add_cookie(self.trans, cookie) 476 else: 477 cookie_out = SimpleCookie() 478 cookie_out[name] = "" 479 cookie_out[name]["path"] = "/" 480 cookie_out[name]["expires"] = 0 481 cookie_out[name]["max-age"] = 0 482 self._write_cookie(cookie_out) 483 484 def _write_cookie(self, cookie): 485 486 "An internal method adding the given 'cookie' to the headers." 487 488 # NOTE: May not be using the appropriate method. 489 490 for morsel in cookie.values(): 491 self.set_header_value("Set-Cookie", morsel.OutputString()) 492 493 # Session-related methods. 494 495 def get_session(self, create=1): 496 497 """ 498 Gets a session corresponding to an identifier supplied in the 499 transaction. 500 501 If no session has yet been established according to information 502 provided in the transaction then the optional 'create' parameter 503 determines whether a new session will be established. 504 505 Where no session has been established and where 'create' is set to 0 506 then None is returned. In all other cases, a session object is created 507 (where appropriate) and returned. 508 """ 509 510 if Session: 511 # NOTE: Not exposing all functionality. 512 self.session = Session.Session(self.trans) 513 self.session.load() 514 return self.session 515 else: 516 # NOTE: Requires configuration. 517 518 if self.session_store is None: 519 self.session_store = SessionStore(self, os.path.join(apache.server_root(), "WebStack-sessions")) 520 return self.session_store.get_session(create) 521 522 def expire_session(self): 523 524 """ 525 Expires any session established according to information provided in the 526 transaction. 527 """ 528 529 if Session: 530 if self.session is None: 531 self.session = self.get_session(create=0) 532 if self.session: 533 self.session.invalidate() 534 self.session = None 535 else: 536 # NOTE: Requires configuration. 537 538 if self.session_store is None: 539 self.session_store = SessionStore(self, os.path.join(apache.server_root(), "WebStack-sessions")) 540 self.session_store.expire_session() 541 542 # vim: tabstop=4 expandtab shiftwidth=4