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