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