1 #!/usr/bin/env python 2 3 """ 4 Zope 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 24 In places this resembles CGI a lot because Zope seems to recycle a lot of that 25 baggage. 26 """ 27 28 import WebStack.Generic 29 from WebStack.Helpers import Environment 30 from WebStack.Helpers.Request import Cookie, get_body_field_or_file, decode_value, filter_fields 31 from WebStack.Helpers.Response import ConvertingStream 32 from WebStack.Helpers.Auth import UserInfo 33 import cgi 34 35 class Transaction(WebStack.Generic.Transaction): 36 37 """ 38 Zope transaction interface. 39 """ 40 41 def __init__(self, request, adapter): 42 43 """ 44 Initialise the transaction with the Zope 'request' object and the 45 'adapter' which created this transaction. 46 """ 47 48 self.request = request 49 self.response = request.RESPONSE 50 self.adapter = adapter 51 52 # Cached information. 53 54 self._fields = None 55 56 # Attributes which may be changed later. 57 58 self.content_type = None 59 60 # Server-related methods. 61 62 def get_server_name(self): 63 64 "Returns the server name." 65 66 return self.request.environ.get("SERVER_NAME") 67 68 def get_server_port(self): 69 70 "Returns the server port as a string." 71 72 return self.request.environ.get("SERVER_PORT") 73 74 # Request-related methods. 75 76 def get_request_stream(self): 77 78 """ 79 Returns the request stream for the transaction. 80 81 NOTE: This method actually rewinds to the start of the stream, since 82 NOTE: Zope likes to read everything automatically. 83 """ 84 85 # NOTE: Possibly not safe. 86 87 stdin = self.request.stdin 88 stdin.seek(0) 89 return stdin 90 91 def get_request_method(self): 92 93 """ 94 Returns the request method. 95 """ 96 97 return self.request.environ.get("REQUEST_METHOD") 98 99 def get_headers(self): 100 101 """ 102 Returns all request headers as a dictionary-like object mapping header 103 names to values. 104 """ 105 106 return Environment.get_headers(self.request.environ) 107 108 def get_header_values(self, key): 109 110 """ 111 Returns a list of all request header values associated with the given 112 'key'. Note that according to RFC 2616, 'key' is treated as a 113 case-insensitive string. 114 """ 115 116 return self.convert_to_list(self.get_headers().get(key)) 117 118 def get_content_type(self): 119 120 """ 121 Returns the content type specified on the request, along with the 122 charset employed. 123 """ 124 125 return self.parse_content_type(self.request.environ.get("CONTENT_TYPE")) 126 127 def get_content_charsets(self): 128 129 """ 130 Returns the character set preferences. 131 132 NOTE: Not decently supported. 133 """ 134 135 return self.parse_content_preferences(None) 136 137 def get_content_languages(self): 138 139 """ 140 Returns extracted language information from the transaction. 141 142 NOTE: Not decently supported. 143 """ 144 145 return self.parse_content_preferences(None) 146 147 def get_path(self, encoding=None): 148 149 """ 150 Returns the entire path from the request as a Unicode object. Any "URL 151 encoded" character values in the part of the path before the query 152 string will be decoded and presented as genuine characters; the query 153 string will remain "URL encoded", however. 154 155 If the optional 'encoding' is set, use that in preference to the default 156 encoding to convert the path into a form not containing "URL encoded" 157 character values. 158 """ 159 160 # NOTE: Based on WebStack.CGI.get_path. 161 162 path = self.get_path_without_query(encoding) 163 qs = self.get_query_string() 164 if qs: 165 return path + "?" + qs 166 else: 167 return path 168 169 def get_path_without_query(self, encoding=None): 170 171 """ 172 Returns the entire path from the request minus the query string as a 173 Unicode object containing genuine characters (as opposed to "URL 174 encoded" character values). 175 176 If the optional 'encoding' is set, use that in preference to the default 177 encoding to convert the path into a form not containing "URL encoded" 178 character values. 179 """ 180 181 # NOTE: Based on WebStack.CGI.get_path. 182 183 path = decode_value(self.request.environ.get("SCRIPT_NAME") or "", encoding) 184 path += self.get_path_info(encoding) 185 return path 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 product_path = "/".join(self.adapter.getPhysicalPath()) 201 path_info = self.request.environ.get("PATH_INFO") or "" 202 real_path_info = path_info[len(product_path):] 203 return decode_value(real_path_info, encoding) 204 205 def get_query_string(self): 206 207 """ 208 Returns the query string from the path in the request. 209 """ 210 211 return self.request.environ.get("QUERY_STRING") or "" 212 213 # Higher level request-related methods. 214 215 def get_fields_from_path(self, encoding=None): 216 217 """ 218 Extracts fields (or request parameters) from the path specified in the 219 transaction. The underlying framework may refuse to supply fields from 220 the path if handling a POST transaction. The optional 'encoding' 221 parameter specifies the character encoding of the query string for cases 222 where the default encoding is to be overridden. 223 224 Returns a dictionary mapping field names to lists of values (even if a 225 single value is associated with any given field name). 226 """ 227 228 fields = {} 229 for name, values in cgi.parse_qs(self.get_query_string()).items(): 230 name = decode_value(name, encoding) 231 fields[name] = [] 232 for value in values: 233 value = decode_value(value, encoding) 234 fields[name].append(value) 235 return fields 236 237 def get_fields_from_body(self, encoding=None): 238 239 """ 240 Extracts fields (or request parameters) from the message body in the 241 transaction. The optional 'encoding' parameter specifies the character 242 encoding of the message body for cases where no such information is 243 available, but where the default encoding is to be overridden. 244 245 Returns a dictionary mapping field names to lists of values (even if a 246 single value is associated with any given field name). Each value is 247 either a Unicode object (representing a simple form field, for example) 248 or a WebStack.Helpers.Request.FileContent object (representing a file 249 upload form field). 250 """ 251 252 all_fields = self._get_fields(encoding) 253 fields_from_path = self.get_fields_from_path() 254 return filter_fields(all_fields, fields_from_path) 255 256 def _get_fields(self, encoding=None): 257 if self._fields is not None: 258 return self._fields 259 260 encoding = encoding or self.get_content_type().charset or self.default_charset 261 self._fields = {} 262 263 for field_name, field_values in self.request.form.items(): 264 field_name = decode_value(field_name, encoding) 265 266 # Find the body values. 267 268 if type(field_values) == type([]): 269 self._fields[field_name] = [] 270 for field_str in field_values: 271 self._fields[field_name].append(get_body_field_or_file(field_str, encoding)) 272 else: 273 self._fields[field_name] = [get_body_field_or_file(field_values, encoding)] 274 275 return self._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 # NOTE: Zope seems to provide only body fields upon POST requests. 298 299 if self.get_request_method() == "GET": 300 return self._get_fields(encoding) 301 else: 302 fields = {} 303 fields.update(self.get_fields_from_path(encoding)) 304 for name, values in self._get_fields(encoding).items(): 305 if not fields.has_key(name): 306 fields[name] = values 307 else: 308 fields[name] += values 309 return fields 310 311 def get_user(self): 312 313 """ 314 Extracts user information from the transaction. 315 316 Returns a username as a string or None if no user is defined. 317 """ 318 319 if self.user is not None: 320 return self.user 321 322 auth_header = self.request._auth 323 if auth_header: 324 return UserInfo(auth_header).username 325 else: 326 return None 327 328 def get_cookies(self): 329 330 """ 331 Obtains cookie information from the request. 332 333 Returns a dictionary mapping cookie names to cookie objects. 334 """ 335 336 return self.process_cookies(self.request.cookies, using_strings=1) 337 338 def get_cookie(self, cookie_name): 339 340 """ 341 Obtains cookie information from the request. 342 343 Returns a cookie object for the given 'cookie_name' or None if no such 344 cookie exists. 345 """ 346 347 value = self.request.cookies.get(self.encode_cookie_value(cookie_name)) 348 if value is not None: 349 return Cookie(cookie_name, self.decode_cookie_value(value)) 350 else: 351 return None 352 353 # Response-related methods. 354 355 def get_response_stream(self): 356 357 """ 358 Returns the response stream for the transaction. 359 """ 360 361 # Unicode can upset this operation. Using either the specified charset 362 # or a default encoding. 363 364 encoding = self.get_response_stream_encoding() 365 return ConvertingStream(self.response, encoding) 366 367 def get_response_stream_encoding(self): 368 369 """ 370 Returns the response stream encoding. 371 """ 372 373 if self.content_type: 374 encoding = self.content_type.charset 375 else: 376 encoding = None 377 return encoding or self.default_charset 378 379 def get_response_code(self): 380 381 """ 382 Get the response code associated with the transaction. If no response 383 code is defined, None is returned. 384 """ 385 386 return self.response.status 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.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.response.setHeader(header, value) 404 405 def set_content_type(self, content_type): 406 407 """ 408 Sets the 'content_type' for the response. 409 """ 410 411 self.content_type = content_type 412 self.response.setHeader("Content-Type", str(content_type)) 413 414 # Higher level response-related methods. 415 416 def set_cookie(self, cookie): 417 418 """ 419 Stores the given 'cookie' object in the response. 420 """ 421 422 self.set_cookie_value(cookie.name, cookie.value) 423 424 def set_cookie_value(self, name, value, path=None, expires=None): 425 426 """ 427 Stores a cookie with the given 'name' and 'value' in the response. 428 429 The optional 'path' is a string which specifies the scope of the cookie, 430 and the optional 'expires' parameter is a value compatible with the 431 time.time function, and indicates the expiry date/time of the cookie. 432 """ 433 434 self.response.setCookie(self.encode_cookie_value(name), self.encode_cookie_value(value)) 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.response.expireCookie(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 return self.request.SESSION 463 464 def expire_session(self): 465 466 """ 467 Expires any session established according to information provided in the 468 transaction. 469 """ 470 471 self.request.SESSION.invalidate() 472 473 # vim: tabstop=4 expandtab shiftwidth=4