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