1 #!/usr/bin/env python 2 3 """ 4 mod_python classes. 5 6 Copyright (C) 2004, 2005, 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.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 60 # Support non-framework cookies. 61 62 if not have_cookies: 63 64 # Define the incoming cookies. 65 66 self.cookies_in = SimpleCookie(self.get_headers().get("cookie")) 67 68 # Cached information. 69 70 self.storage_body = None 71 72 # Special objects retained throughout the transaction. 73 74 self.session_store = None 75 self.session = None # mod_python native session 76 77 def commit(self): 78 79 """ 80 A special method, synchronising the transaction with framework-specific 81 objects. 82 """ 83 84 # Close the session store. 85 86 if self.session_store is not None: 87 self.session_store.close() 88 89 # Commit any native session. 90 91 if self.session is not None: 92 self.session.save() 93 94 # Server-related methods. 95 96 def get_server_name(self): 97 98 "Returns the server name." 99 100 return self.trans.server.server_hostname 101 102 def get_server_port(self): 103 104 "Returns the server port as a string." 105 106 return str(self.trans.connection.local_addr[1]) 107 108 # Request-related methods. 109 110 def get_request_stream(self): 111 112 """ 113 Returns the request stream for the transaction. 114 """ 115 116 return self.trans 117 118 def get_request_method(self): 119 120 """ 121 Returns the request method. 122 """ 123 124 return self.trans.method 125 126 def get_headers(self): 127 128 """ 129 Returns all request headers as a dictionary-like object mapping header 130 names to values. 131 132 NOTE: If duplicate header names are permitted, then this interface will 133 NOTE: need to change. 134 """ 135 136 return self.trans.headers_in 137 138 def get_header_values(self, key): 139 140 """ 141 Returns a list of all request header values associated with the given 142 'key'. Note that according to RFC 2616, 'key' is treated as a 143 case-insensitive string. 144 """ 145 146 return self.convert_to_list(self.trans.headers_in.get(key)) 147 148 def get_content_type(self): 149 150 """ 151 Returns the content type specified on the request, along with the 152 charset employed. 153 """ 154 155 return self.parse_content_type(self.trans.content_type) 156 157 def get_content_charsets(self): 158 159 """ 160 Returns the character set preferences. 161 """ 162 163 return self.parse_content_preferences(self.trans.headers_in.get("Accept-Charset")) 164 165 def get_content_languages(self): 166 167 """ 168 Returns extracted language information from the transaction. 169 """ 170 171 return self.parse_content_preferences(self.trans.headers_in.get("Accept-Language")) 172 173 def get_path(self, encoding=None): 174 175 """ 176 Returns the entire path from the request as a Unicode object. Any "URL 177 encoded" character values in the part of the path before the query 178 string will be decoded and presented as genuine characters; the query 179 string will remain "URL encoded", however. 180 181 If the optional 'encoding' is set, use that in preference to the default 182 encoding to convert the path into a form not containing "URL encoded" 183 character values. 184 """ 185 186 query_string = self.get_query_string() 187 if query_string: 188 return decode_value(self.trans.uri, encoding) + "?" + query_string 189 else: 190 return decode_value(self.trans.uri, encoding) 191 192 def get_path_without_query(self, encoding=None): 193 194 """ 195 Returns the entire path from the request minus the query string as a 196 Unicode object containing genuine characters (as opposed to "URL 197 encoded" character values). 198 199 If the optional 'encoding' is set, use that in preference to the default 200 encoding to convert the path into a form not containing "URL encoded" 201 character values. 202 """ 203 204 return decode_value(self.trans.uri, encoding) 205 206 def get_path_info(self, encoding=None): 207 208 """ 209 Returns the "path info" (the part of the URL after the resource name 210 handling the current request) from the request as a Unicode object 211 containing genuine characters (as opposed to "URL encoded" character 212 values). 213 214 If the optional 'encoding' is set, use that in preference to the default 215 encoding to convert the path into a form not containing "URL encoded" 216 character values. 217 """ 218 219 return decode_value(self.trans.path_info, encoding) 220 221 def get_query_string(self): 222 223 """ 224 Returns the query string from the path in the request. 225 """ 226 227 return self.trans.args or "" 228 229 # Higher level request-related methods. 230 231 def get_fields_from_path(self, encoding=None): 232 233 """ 234 Extracts fields (or request parameters) from the path specified in the 235 transaction. The underlying framework may refuse to supply fields from 236 the path if handling a POST transaction. The optional 'encoding' 237 parameter specifies the character encoding of the query string for cases 238 where the default encoding is to be overridden. 239 240 Returns a dictionary mapping field names to lists of values (even if a 241 single value is associated with any given field name). 242 """ 243 244 fields = {} 245 for name, values in parse_qs(self.get_query_string(), 1).items(): # keep_blank_values=1 246 name = decode_value(name, encoding) 247 fields[name] = [] 248 for value in values: 249 value = decode_value(value, encoding) 250 fields[name].append(value) 251 return fields 252 253 def get_fields_from_body(self, encoding=None): 254 255 """ 256 Extracts fields (or request parameters) from the message body in the 257 transaction. The optional 'encoding' parameter specifies the character 258 encoding of the message body for cases where no such information is 259 available, but where the default encoding is to be overridden. 260 261 Returns a dictionary mapping field names to lists of values (even if a 262 single value is associated with any given field name). Each value is 263 either a Unicode object (representing a simple form field, for example) 264 or a WebStack.Helpers.Request.FileContent object (representing a file 265 upload form field). 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(FileContent(field.value, parse_headers(field.headers))) 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 WebStack.Helpers.Request.FileContent object (representing a file 311 upload form field). 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