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 289 # Detect and store file uploads. 290 291 if field.filename: 292 fields[field_name].append(field.value) 293 else: 294 fields[field_name].append(get_body_field(field.value, encoding)) 295 296 return fields 297 298 def get_fields(self, encoding=None): 299 300 """ 301 Extracts fields (or request parameters) from both the path specified in 302 the transaction as well as the message body. The optional 'encoding' 303 parameter specifies the character encoding of the message body for cases 304 where no such information is available, but where the default encoding 305 is to be overridden. 306 307 Returns a dictionary mapping field names to lists of values (even if a 308 single value is associated with any given field name). Each value is 309 either a Unicode object (representing a simple form field, for example) 310 or a plain string (representing a file upload form field, for example). 311 312 Where a given field name is used in both the path and message body to 313 specify values, the values from both sources will be combined into a 314 single list associated with that field name. 315 """ 316 317 return self._get_fields(encoding) 318 319 def get_user(self): 320 321 """ 322 Extracts user information from the transaction. 323 324 Returns a username as a string or None if no user is defined. 325 """ 326 327 if self.user is not None: 328 return self.user 329 else: 330 return self.trans.user 331 332 def get_cookies(self): 333 334 """ 335 Obtains cookie information from the request. 336 337 Returns a dictionary mapping cookie names to cookie objects. 338 339 NOTE: No additional information is passed to the underlying API despite 340 NOTE: support for enhanced cookies in mod_python. 341 """ 342 343 if have_cookies: 344 found_cookies = get_cookies(self.trans) 345 else: 346 found_cookies = self.cookies_in 347 return self.process_cookies(found_cookies) 348 349 def get_cookie(self, cookie_name): 350 351 """ 352 Obtains cookie information from the request. 353 354 Returns a cookie object for the given 'cookie_name' or None if no such 355 cookie exists. 356 """ 357 358 return self.get_cookies().get(self.encode_cookie_value(cookie_name)) 359 360 # Response-related methods. 361 362 def get_response_stream(self): 363 364 """ 365 Returns the response stream for the transaction. 366 """ 367 368 # Unicode can upset this operation. Using either the specified charset 369 # or a default encoding. 370 371 encoding = self.get_response_stream_encoding() 372 return ConvertingStream(self.trans, encoding) 373 374 def get_response_stream_encoding(self): 375 376 """ 377 Returns the response stream encoding. 378 """ 379 380 if self.content_type: 381 encoding = self.content_type.charset 382 else: 383 encoding = None 384 return encoding or self.default_charset 385 386 def get_response_code(self): 387 388 """ 389 Get the response code associated with the transaction. If no response 390 code is defined, None is returned. 391 """ 392 393 return self.response_code 394 395 def set_response_code(self, response_code): 396 397 """ 398 Set the 'response_code' using a numeric constant defined in the HTTP 399 specification. 400 """ 401 402 self.response_code = response_code 403 404 def set_header_value(self, header, value): 405 406 """ 407 Set the HTTP 'header' with the given 'value'. 408 """ 409 410 self.trans.headers_out[self.format_header_value(header)] = self.format_header_value(value) 411 412 def set_content_type(self, content_type): 413 414 """ 415 Sets the 'content_type' for the response. 416 """ 417 418 # Remember the content type for encoding purposes later. 419 420 self.content_type = content_type 421 self.trans.content_type = str(content_type) 422 423 # Higher level response-related methods. 424 425 def set_cookie(self, cookie): 426 427 """ 428 Stores the given 'cookie' object in the response. 429 """ 430 431 # NOTE: If multiple cookies of the same name could be specified, this 432 # NOTE: could need changing. 433 434 self.set_cookie_value(cookie.name, cookie.value) 435 436 def set_cookie_value(self, name, value, path=None, expires=None): 437 438 """ 439 Stores a cookie with the given 'name' and 'value' in the response. 440 441 The optional 'path' is a string which specifies the scope of the cookie, 442 and the optional 'expires' parameter is a value compatible with the 443 time.time function, and indicates the expiry date/time of the cookie. 444 """ 445 446 name = self.encode_cookie_value(name) 447 448 if have_cookies: 449 cookie = SimpleCookie(name, self.encode_cookie_value(value)) 450 if expires is not None: 451 cookie.expires = expires 452 if path is not None: 453 cookie.path = path 454 add_cookie(self.trans, cookie) 455 else: 456 cookie_out = SimpleCookie() 457 cookie_out[name] = self.encode_cookie_value(value) 458 if path is not None: 459 cookie_out[name]["path"] = path 460 if expires is not None: 461 cookie_out[name]["expires"] = expires 462 self._write_cookie(cookie_out) 463 464 def delete_cookie(self, cookie_name): 465 466 """ 467 Adds to the response a request that the cookie with the given 468 'cookie_name' be deleted/discarded by the client. 469 """ 470 471 # Create a special cookie, given that we do not know whether the browser 472 # has been sent the cookie or not. 473 # NOTE: Magic discovered in Webware. 474 475 name = self.encode_cookie_value(cookie_name) 476 477 if have_cookies: 478 cookie = SimpleCookie(name, "") 479 cookie.path = "/" 480 cookie.expires = 0 481 cookie.max_age = 0 482 add_cookie(self.trans, cookie) 483 else: 484 cookie_out = SimpleCookie() 485 cookie_out[name] = "" 486 cookie_out[name]["path"] = "/" 487 cookie_out[name]["expires"] = 0 488 cookie_out[name]["max-age"] = 0 489 self._write_cookie(cookie_out) 490 491 def _write_cookie(self, cookie): 492 493 "An internal method adding the given 'cookie' to the headers." 494 495 # NOTE: May not be using the appropriate method. 496 497 for morsel in cookie.values(): 498 self.set_header_value("Set-Cookie", morsel.OutputString()) 499 500 # Session-related methods. 501 502 def get_session(self, create=1): 503 504 """ 505 Gets a session corresponding to an identifier supplied in the 506 transaction. 507 508 If no session has yet been established according to information 509 provided in the transaction then the optional 'create' parameter 510 determines whether a new session will be established. 511 512 Where no session has been established and where 'create' is set to 0 513 then None is returned. In all other cases, a session object is created 514 (where appropriate) and returned. 515 """ 516 517 if Session: 518 # NOTE: Not exposing all functionality. 519 self.session = Session.Session(self.trans) 520 self.session.load() 521 return self.session 522 else: 523 # NOTE: Requires configuration. 524 525 if self.session_store is None: 526 self.session_store = SessionStore(self, os.path.join(apache.server_root(), "WebStack-sessions")) 527 return self.session_store.get_session(create) 528 529 def expire_session(self): 530 531 """ 532 Expires any session established according to information provided in the 533 transaction. 534 """ 535 536 if Session: 537 if self.session is None: 538 self.session = self.get_session(create=0) 539 if self.session: 540 self.session.invalidate() 541 self.session = None 542 else: 543 # NOTE: Requires configuration. 544 545 if self.session_store is None: 546 self.session_store = SessionStore(self, os.path.join(apache.server_root(), "WebStack-sessions")) 547 self.session_store.expire_session() 548 549 # vim: tabstop=4 expandtab shiftwidth=4