1 #!/usr/bin/env python 2 3 """ 4 Webware 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 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 path_info = self.trans.request().pathInfo() 199 context_name = self.trans.request().contextName() 200 if path_info.startswith(context_name): 201 real_path_info = path_info[len(context_name):] 202 else: 203 real_path_info = path_info 204 return decode_value(real_path_info, encoding) 205 206 def get_query_string(self): 207 208 """ 209 Returns the query string from the path in the request. 210 """ 211 212 return self.trans.request().queryString() 213 214 # Higher level request-related methods. 215 216 def get_fields_from_path(self, encoding=None): 217 218 """ 219 Extracts fields (or request parameters) from the path specified in the 220 transaction. The underlying framework may refuse to supply fields from 221 the path if handling a POST transaction. The optional 'encoding' 222 parameter specifies the character encoding of the query string for cases 223 where the default encoding is to be overridden. 224 225 Returns a dictionary mapping field names to lists of values (even if a 226 single value is associated with any given field name). 227 """ 228 229 fields = {} 230 for name, values in parse_qs(self.get_query_string(), keep_blank_values=1).items(): 231 name = decode_value(name, encoding) 232 fields[name] = [] 233 for value in values: 234 value = decode_value(value, encoding) 235 fields[name].append(value) 236 return fields 237 238 def get_fields_from_body(self, encoding=None): 239 240 """ 241 Extracts fields (or request parameters) from the message body in the 242 transaction. The optional 'encoding' parameter specifies the character 243 encoding of the message body for cases where no such information is 244 available, but 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). Each value is 248 either a Unicode object (representing a simple form field, for example) 249 or a WebStack.Helpers.Request.FileContent object (representing a file 250 upload form field). 251 """ 252 253 all_fields = self._get_fields(encoding) 254 fields_from_path = self.get_fields_from_path() 255 return filter_fields(all_fields, fields_from_path) 256 257 def _get_fields(self, encoding=None): 258 encoding = encoding or self.get_content_type().charset or self.default_charset 259 fields = {} 260 261 for field_name, field_values in self.trans.request().fields().items(): 262 field_name = decode_value(field_name, encoding) 263 264 if type(field_values) == type([]): 265 fields[field_name] = [] 266 for field_str in field_values: 267 fields[field_name].append(get_body_field_or_file(field_str, encoding)) 268 else: 269 fields[field_name] = [get_body_field_or_file(field_values, encoding)] 270 271 return fields 272 273 def get_fields(self, encoding=None): 274 275 """ 276 Extracts fields (or request parameters) from both the path specified in 277 the transaction as well as the message body. The optional 'encoding' 278 parameter specifies the character encoding of the message body for cases 279 where no such information is available, but where the default encoding 280 is to be overridden. 281 282 Returns a dictionary mapping field names to lists of values (even if a 283 single value is associated with any given field name). Each value is 284 either a Unicode object (representing a simple form field, for example) 285 or a WebStack.Helpers.Request.FileContent object (representing a file 286 upload form field). 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