1 #!/usr/bin/env python 2 3 """ 4 Webware classes. 5 6 Copyright (C) 2004, 2005, 2006, 2007 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 cgi import parse_qs 25 import StringIO 26 from WebStack.Helpers import Environment 27 from WebStack.Helpers.Request import Cookie, get_body_field_or_file, decode_value, filter_fields 28 from WebStack.Helpers.Response import ConvertingStream 29 30 class Transaction(WebStack.Generic.Transaction): 31 32 """ 33 Webware transaction interface. 34 """ 35 36 def __init__(self, trans): 37 38 "Initialise the transaction using the Webware transaction 'trans'." 39 40 self.trans = trans 41 self.content_type = None 42 43 def commit(self): 44 45 "Commit the transaction." 46 47 self.trans.response().commit() 48 49 # Server-related methods. 50 51 def get_server_name(self): 52 53 "Returns the server name." 54 55 return self.trans.request().serverURL().split("/")[0].split(":")[0] 56 57 def get_server_port(self): 58 59 "Returns the server port as a string." 60 61 host_and_port = self.trans.request().serverURL().split("/")[0].split(":") 62 if len(host_and_port) > 1: 63 return host_and_port[1] 64 else: 65 return "80" 66 67 # Request-related methods. 68 69 def get_request_stream(self): 70 71 """ 72 Returns the request stream for the transaction. 73 """ 74 75 request = self.trans.request() 76 try: 77 stream = request.rawInput(rewind=1) 78 if stream is None: 79 return StringIO.StringIO("") 80 81 # NOTE: Dubious catch-all situation, but it is difficult to control 82 # NOTE: cases where Webware's internals themselves fail. 83 84 except: 85 return StringIO.StringIO("") 86 87 return stream 88 89 def get_request_method(self): 90 91 """ 92 Returns the request method. 93 """ 94 95 return self.trans.request().method() 96 97 def get_headers(self): 98 99 """ 100 Returns all request headers as a dictionary-like object mapping header 101 names to values. 102 103 NOTE: If duplicate header names are permitted, then this interface will 104 NOTE: need to change. 105 """ 106 107 # Use the Webware environment and some assumptions about variable names. 108 # NOTE: Using lower case for the header names. 109 110 env = self.trans.request().environ() 111 return Environment.get_headers(env) 112 113 def get_header_values(self, key): 114 115 """ 116 Returns a list of all request header values associated with the given 117 'key'. Note that according to RFC 2616, 'key' is treated as a 118 case-insensitive string. 119 """ 120 121 # Use the Webware environment and some assumptions about variable names. 122 123 env = self.trans.request().environ() 124 cgi_key = "HTTP_" + key.replace("-", "_").upper() 125 if env.has_key(cgi_key): 126 return [env[cgi_key]] 127 else: 128 return [] 129 130 def get_content_type(self): 131 132 """ 133 Returns the content type specified on the request, along with the 134 charset employed. 135 """ 136 137 request = self.trans.request() 138 if hasattr(request, "contentType"): 139 content_type = request.contentType() 140 else: 141 content_type = self.trans.request()._environ.get('CONTENT_TYPE', None) 142 return self.parse_content_type(content_type) 143 144 def get_content_charsets(self): 145 146 """ 147 Returns the character set preferences. 148 NOTE: Requires enhancements to HTTPRequest in 0.8.1 or earlier. 149 """ 150 151 request = self.trans.request() 152 if hasattr(request, "contentCharsets"): 153 return request.contentCharsets() 154 else: 155 return request.accept("charset") 156 157 def get_content_languages(self): 158 159 """ 160 Returns extracted language information from the transaction. 161 NOTE: Requires enhancements to HTTPRequest in 0.8.1 or earlier. 162 """ 163 164 request = self.trans.request() 165 if hasattr(request, "contentLanguages"): 166 return request.contentLanguages() 167 else: 168 return request.accept("language") 169 170 def get_path(self, encoding=None): 171 172 """ 173 Returns the entire path from the request as a Unicode object. Any "URL 174 encoded" character values in the part of the path before the query 175 string will be decoded and presented as genuine characters; the query 176 string will remain "URL encoded", however. 177 178 If the optional 'encoding' is set, use that in preference to the default 179 encoding to convert the path into a form not containing "URL encoded" 180 character values. 181 """ 182 183 path = self.get_path_without_query(encoding) 184 qs = self.get_query_string() 185 if qs: 186 return path + "?" + qs 187 else: 188 return path 189 190 def get_path_without_query(self, encoding=None): 191 192 """ 193 Returns the entire path from the request minus the query string as a 194 Unicode object containing genuine characters (as opposed to "URL 195 encoded" character values). 196 197 If the optional 'encoding' is set, use that in preference to the default 198 encoding to convert the path into a form not containing "URL encoded" 199 character values. 200 """ 201 202 return self.decode_path(self.trans.request().uri().split("?")[0], encoding) 203 204 def get_path_info(self, encoding=None): 205 206 """ 207 Returns the "path info" (the part of the URL after the resource name 208 handling the current request) from the request as a Unicode object 209 containing genuine characters (as opposed to "URL encoded" character 210 values). 211 212 If the optional 'encoding' is set, use that in preference to the default 213 encoding to convert the path into a form not containing "URL encoded" 214 character values. 215 """ 216 217 encoding = encoding or self.default_charset 218 219 path_info = self.trans.request().pathInfo() 220 context_name = self.trans.request().contextName() 221 222 # Make the context name resemble the start of a path if it doesn't 223 # already do so. 224 225 if not context_name.startswith("/"): 226 context_name = "/" + context_name 227 228 # Correct the path info if appropriate. 229 230 if path_info.startswith(context_name): 231 real_path_info = path_info[len(context_name):] 232 else: 233 real_path_info = path_info 234 235 return decode_value(real_path_info, encoding) 236 237 def get_query_string(self): 238 239 """ 240 Returns the query string from the path in the request. 241 """ 242 243 return self.trans.request().queryString() 244 245 # Higher level request-related methods. 246 247 def get_fields_from_path(self, encoding=None): 248 249 """ 250 Extracts fields (or request parameters) from the path specified in the 251 transaction. The underlying framework may refuse to supply fields from 252 the path if handling a POST transaction. The optional 'encoding' 253 parameter specifies the character encoding of the query string for cases 254 where the default encoding is to be overridden. 255 256 Returns a dictionary mapping field names to lists of values (even if a 257 single value is associated with any given field name). 258 """ 259 260 encoding = encoding or self.default_charset 261 262 fields = {} 263 for name, values in parse_qs(self.get_query_string(), keep_blank_values=1).items(): 264 name = decode_value(name, encoding) 265 fields[name] = [] 266 for value in values: 267 value = decode_value(value, encoding) 268 fields[name].append(value) 269 return fields 270 271 def get_fields_from_body(self, encoding=None): 272 273 """ 274 Extracts fields (or request parameters) from the message body in the 275 transaction. The optional 'encoding' parameter specifies the character 276 encoding of the message body for cases where no such information is 277 available, but where the default encoding is to be overridden. 278 279 Returns a dictionary mapping field names to lists of values (even if a 280 single value is associated with any given field name). Each value is 281 either a Unicode object (representing a simple form field, for example) 282 or a WebStack.Helpers.Request.FileContent object (representing a file 283 upload form field). 284 """ 285 286 all_fields = self._get_fields(encoding) 287 fields_from_path = self.get_fields_from_path() 288 return filter_fields(all_fields, fields_from_path) 289 290 def _get_fields(self, encoding=None): 291 encoding = encoding or self.get_content_type().charset or self.default_charset 292 fields = {} 293 294 for field_name, field_values in self.trans.request().fields().items(): 295 field_name = decode_value(field_name, encoding) 296 297 if type(field_values) == type([]): 298 fields[field_name] = [] 299 for field_str in field_values: 300 fields[field_name].append(get_body_field_or_file(field_str, encoding)) 301 else: 302 fields[field_name] = [get_body_field_or_file(field_values, 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 # NOTE: Webware relies entirely on a CGI-style environment where the 337 # NOTE: actual headers are not available. Therefore, the Web server must 338 # NOTE: itself be set up to provide user support. 339 340 if self.user is not None: 341 return self.user 342 343 try: 344 return self.trans.request().remoteUser() 345 except KeyError, exc: 346 return None 347 348 def get_cookies(self): 349 350 """ 351 Obtains cookie information from the request. 352 353 Returns a dictionary mapping cookie names to cookie objects. 354 """ 355 356 return self.process_cookies(self.trans.request().cookies(), using_strings=1) 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 try: 368 value = self.trans.request().cookie(self.encode_cookie_value(cookie_name)) 369 return Cookie(cookie_name, self.decode_cookie_value(value)) 370 except KeyError: 371 return None 372 373 # Response-related methods. 374 375 def get_response_stream(self): 376 377 """ 378 Returns the response stream for the transaction. 379 """ 380 381 # Unicode can upset this operation. Using either the specified charset 382 # or a default encoding. 383 384 encoding = self.get_response_stream_encoding() 385 return ConvertingStream(self.trans.response(), encoding) 386 387 def get_response_stream_encoding(self): 388 389 """ 390 Returns the response stream encoding. 391 """ 392 393 if self.content_type: 394 encoding = self.content_type.charset 395 else: 396 encoding = None 397 return encoding or self.default_charset 398 399 def get_response_code(self): 400 401 """ 402 Get the response code associated with the transaction. If no response 403 code is defined, None is returned. 404 """ 405 406 # NOTE: Webware treats the response code as just another header. 407 408 status = self.trans.response().header("Status", None) 409 try: 410 if status is not None: 411 return int(status) 412 else: 413 return None 414 except ValueError: 415 return None 416 417 def set_response_code(self, response_code): 418 419 """ 420 Set the 'response_code' using a numeric constant defined in the HTTP 421 specification. 422 """ 423 424 self.trans.response().setStatus(response_code, str(response_code)) 425 426 def set_header_value(self, header, value): 427 428 """ 429 Set the HTTP 'header' with the given 'value'. 430 """ 431 432 self.trans.response().setHeader(self.format_header_value(header), self.format_header_value(value)) 433 434 def set_content_type(self, content_type): 435 436 """ 437 Sets the 'content_type' for the response. 438 """ 439 440 # Remember the content type for encoding purposes later. 441 442 self.content_type = content_type 443 return self.trans.response().setHeader("Content-Type", str(content_type)) 444 445 # Higher level response-related methods. 446 447 def set_cookie(self, cookie): 448 449 """ 450 Stores the given 'cookie' object in the response. 451 """ 452 453 self.set_cookie_value(cookie.name, cookie.value) 454 #self.trans.response().addCookie(cookie) 455 456 def set_cookie_value(self, name, value, path=None, expires=None): 457 458 """ 459 Stores a cookie with the given 'name' and 'value' in the response. 460 461 The optional 'path' is a string which specifies the scope of the cookie, 462 and the optional 'expires' parameter is a value compatible with the 463 time.time function, and indicates the expiry date/time of the cookie. 464 """ 465 466 self.trans.response().setCookie(self.encode_cookie_value(name), 467 self.encode_cookie_value(value), path, expires) 468 469 def delete_cookie(self, cookie_name): 470 471 """ 472 Adds to the response a request that the cookie with the given 473 'cookie_name' be deleted/discarded by the client. 474 """ 475 476 self.trans.response().delCookie(self.encode_cookie_value(cookie_name)) 477 478 # Session-related methods. 479 480 def get_session(self, create=1): 481 482 """ 483 Gets a session corresponding to an identifier supplied in the 484 transaction. 485 486 If no session has yet been established according to information 487 provided in the transaction then the optional 'create' parameter 488 determines whether a new session will be established. 489 490 Where no session has been established and where 'create' is set to 0 491 then None is returned. In all other cases, a session object is created 492 (where appropriate) and returned. 493 """ 494 495 # NOTE: create and hasSession() not used. 496 497 return Session(self.trans.session()) 498 499 def expire_session(self): 500 501 """ 502 Expires any session established according to information provided in the 503 transaction. 504 """ 505 506 self.trans.request().setSessionExpired(1) 507 508 class Session: 509 510 "A more dictionary-like session object than the one Webware provides." 511 512 def __init__(self, session): 513 self.session = session 514 515 def items(self): 516 return self.session.values().items() 517 518 def __getattr__(self, name): 519 return getattr(self.__dict__["session"], name) 520 521 def __delitem__(self, name): 522 del self.session[name] 523 524 def __setitem__(self, name, value): 525 self.session[name] = value 526 527 def __getitem__(self, name): 528 return self.session[name] 529 530 # vim: tabstop=4 expandtab shiftwidth=4