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, 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): 175 176 """ 177 Returns the entire path from the request. 178 """ 179 180 query_string = self.get_query_string() 181 if query_string: 182 return self.trans.uri + "?" + query_string 183 else: 184 return self.trans.uri 185 186 def get_path_without_query(self): 187 188 """ 189 Returns the entire path from the request minus the query string. 190 """ 191 192 return self.trans.uri 193 194 def get_path_info(self): 195 196 """ 197 Returns the "path info" (the part of the URL after the resource name 198 handling the current request) from the request. 199 """ 200 201 return self.trans.path_info 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.trans.args or "" 210 211 # Higher level request-related methods. 212 213 def get_fields_from_path(self): 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. 219 220 Returns a dictionary mapping field names to lists of values (even if a 221 single value is associated with any given field name). 222 """ 223 224 # NOTE: Support at best ISO-8859-1 values. 225 226 fields = {} 227 for name, values in parse_qs(self.get_query_string(), 1).items(): # keep_blank_values=1 228 fields[name] = [] 229 for value in values: 230 fields[name].append(unicode(value, "iso-8859-1")) 231 return fields 232 233 def get_fields_from_body(self, encoding=None): 234 235 """ 236 Extracts fields (or request parameters) from the message body in the 237 transaction. The optional 'encoding' parameter specifies the character 238 encoding of the message body for cases where no such information is 239 available, but 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). Each value is 243 either a Unicode object (representing a simple form field, for example) 244 or a plain string (representing a file upload form field, for example). 245 246 The mod_python.util.FieldStorage class may augment the fields from the 247 body with fields found in the path. 248 """ 249 250 all_fields = self._get_fields(encoding) 251 fields_from_path = self.get_fields_from_path() 252 return filter_fields(all_fields, fields_from_path) 253 254 def _get_fields(self, encoding=None): 255 encoding = encoding or self.get_content_type().charset or self.default_charset 256 257 if self.storage_body is None: 258 self.storage_body = FieldStorage(self.trans, keep_blank_values=1) 259 260 # Traverse the storage, finding each field value. 261 262 fields = {} 263 for field in self.storage_body.list: 264 if not fields.has_key(field.name): 265 fields[field.name] = [] 266 fields[field.name].append(get_body_field(field.value, encoding)) 267 return fields 268 269 def get_fields(self, encoding=None): 270 271 """ 272 Extracts fields (or request parameters) from both the path specified in 273 the transaction as well as the message body. The optional 'encoding' 274 parameter specifies the character encoding of the message body for cases 275 where no such information is available, but where the default encoding 276 is to be overridden. 277 278 Returns a dictionary mapping field names to lists of values (even if a 279 single value is associated with any given field name). Each value is 280 either a Unicode object (representing a simple form field, for example) 281 or a plain string (representing a file upload form field, for example). 282 283 Where a given field name is used in both the path and message body to 284 specify values, the values from both sources will be combined into a 285 single list associated with that field name. 286 """ 287 288 return self._get_fields(encoding) 289 290 def get_user(self): 291 292 """ 293 Extracts user information from the transaction. 294 295 Returns a username as a string or None if no user is defined. 296 """ 297 298 if self.user is not None: 299 return self.user 300 else: 301 return self.trans.user 302 303 def get_cookies(self): 304 305 """ 306 Obtains cookie information from the request. 307 308 Returns a dictionary mapping cookie names to cookie objects. 309 310 NOTE: No additional information is passed to the underlying API despite 311 NOTE: support for enhanced cookies in mod_python. 312 """ 313 314 if have_cookies: 315 found_cookies = get_cookies(self.trans) 316 else: 317 found_cookies = self.cookies_in 318 return self.process_cookies(found_cookies) 319 320 def get_cookie(self, cookie_name): 321 322 """ 323 Obtains cookie information from the request. 324 325 Returns a cookie object for the given 'cookie_name' or None if no such 326 cookie exists. 327 """ 328 329 return self.get_cookies().get(self.encode_cookie_value(cookie_name)) 330 331 # Response-related methods. 332 333 def get_response_stream(self): 334 335 """ 336 Returns the response stream for the transaction. 337 """ 338 339 # Unicode can upset this operation. Using either the specified charset 340 # or a default encoding. 341 342 encoding = self.get_response_stream_encoding() 343 return ConvertingStream(self.trans, encoding) 344 345 def get_response_stream_encoding(self): 346 347 """ 348 Returns the response stream encoding. 349 """ 350 351 if self.content_type: 352 encoding = self.content_type.charset 353 else: 354 encoding = None 355 return encoding or self.default_charset 356 357 def get_response_code(self): 358 359 """ 360 Get the response code associated with the transaction. If no response 361 code is defined, None is returned. 362 """ 363 364 return self.response_code 365 366 def set_response_code(self, response_code): 367 368 """ 369 Set the 'response_code' using a numeric constant defined in the HTTP 370 specification. 371 """ 372 373 self.response_code = response_code 374 375 def set_header_value(self, header, value): 376 377 """ 378 Set the HTTP 'header' with the given 'value'. 379 """ 380 381 self.trans.headers_out[self.format_header_value(header)] = self.format_header_value(value) 382 383 def set_content_type(self, content_type): 384 385 """ 386 Sets the 'content_type' for the response. 387 """ 388 389 # Remember the content type for encoding purposes later. 390 391 self.content_type = content_type 392 self.trans.content_type = str(content_type) 393 394 # Higher level response-related methods. 395 396 def set_cookie(self, cookie): 397 398 """ 399 Stores the given 'cookie' object in the response. 400 """ 401 402 # NOTE: If multiple cookies of the same name could be specified, this 403 # NOTE: could need changing. 404 405 self.set_cookie_value(cookie.name, cookie.value) 406 407 def set_cookie_value(self, name, value, path=None, expires=None): 408 409 """ 410 Stores a cookie with the given 'name' and 'value' in the response. 411 412 The optional 'path' is a string which specifies the scope of the cookie, 413 and the optional 'expires' parameter is a value compatible with the 414 time.time function, and indicates the expiry date/time of the cookie. 415 """ 416 417 name = self.encode_cookie_value(name) 418 419 if have_cookies: 420 cookie = SimpleCookie(name, self.encode_cookie_value(value)) 421 if expires is not None: 422 cookie.expires = expires 423 if path is not None: 424 cookie.path = path 425 add_cookie(self.trans, cookie) 426 else: 427 cookie_out = SimpleCookie() 428 cookie_out[name] = self.encode_cookie_value(value) 429 if path is not None: 430 cookie_out[name]["path"] = path 431 if expires is not None: 432 cookie_out[name]["expires"] = expires 433 self._write_cookie(cookie_out) 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 # Create a special cookie, given that we do not know whether the browser 443 # has been sent the cookie or not. 444 # NOTE: Magic discovered in Webware. 445 446 name = self.encode_cookie_value(cookie_name) 447 448 if have_cookies: 449 cookie = SimpleCookie(name, "") 450 cookie.path = "/" 451 cookie.expires = 0 452 cookie.max_age = 0 453 add_cookie(self.trans, cookie) 454 else: 455 cookie_out = SimpleCookie() 456 cookie_out[name] = "" 457 cookie_out[name]["path"] = "/" 458 cookie_out[name]["expires"] = 0 459 cookie_out[name]["max-age"] = 0 460 self._write_cookie(cookie_out) 461 462 def _write_cookie(self, cookie): 463 464 "An internal method adding the given 'cookie' to the headers." 465 466 # NOTE: May not be using the appropriate method. 467 468 for morsel in cookie.values(): 469 self.set_header_value("Set-Cookie", morsel.OutputString()) 470 471 # Session-related methods. 472 473 def get_session(self, create=1): 474 475 """ 476 Gets a session corresponding to an identifier supplied in the 477 transaction. 478 479 If no session has yet been established according to information 480 provided in the transaction then the optional 'create' parameter 481 determines whether a new session will be established. 482 483 Where no session has been established and where 'create' is set to 0 484 then None is returned. In all other cases, a session object is created 485 (where appropriate) and returned. 486 """ 487 488 if Session: 489 # NOTE: Not exposing all functionality. 490 self.session = Session.Session(self.trans) 491 self.session.load() 492 return self.session 493 else: 494 # NOTE: Requires configuration. 495 496 if self.session_store is None: 497 self.session_store = SessionStore(self, os.path.join(apache.server_root(), "WebStack-sessions")) 498 return self.session_store.get_session(create) 499 500 def expire_session(self): 501 502 """ 503 Expires any session established according to information provided in the 504 transaction. 505 """ 506 507 if Session: 508 if self.session is None: 509 self.session = self.get_session(create=0) 510 if self.session: 511 self.session.invalidate() 512 self.session = None 513 else: 514 # NOTE: Requires configuration. 515 516 if self.session_store is None: 517 self.session_store = SessionStore(self, os.path.join(apache.server_root(), "WebStack-sessions")) 518 self.session_store.expire_session() 519 520 # vim: tabstop=4 expandtab shiftwidth=4