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