1 #!/usr/bin/env python 2 3 """ 4 Zope 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 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 self.user = None 60 self.path_info = None 61 62 # Server-related methods. 63 64 def get_server_name(self): 65 66 "Returns the server name." 67 68 return self.request.environ.get("SERVER_NAME") 69 70 def get_server_port(self): 71 72 "Returns the server port as a string." 73 74 return self.request.environ.get("SERVER_PORT") 75 76 # Request-related methods. 77 78 def get_request_stream(self): 79 80 """ 81 Returns the request stream for the transaction. 82 83 NOTE: This method actually rewinds to the start of the stream, since 84 NOTE: Zope likes to read everything automatically. 85 """ 86 87 # NOTE: Possibly not safe. 88 89 stdin = self.request.stdin 90 stdin.seek(0) 91 return stdin 92 93 def get_request_method(self): 94 95 """ 96 Returns the request method. 97 """ 98 99 return self.request.environ.get("REQUEST_METHOD") 100 101 def get_headers(self): 102 103 """ 104 Returns all request headers as a dictionary-like object mapping header 105 names to values. 106 """ 107 108 return Environment.get_headers(self.request.environ) 109 110 def get_header_values(self, key): 111 112 """ 113 Returns a list of all request header values associated with the given 114 'key'. Note that according to RFC 2616, 'key' is treated as a 115 case-insensitive string. 116 """ 117 118 return self.convert_to_list(self.get_headers().get(key)) 119 120 def get_content_type(self): 121 122 """ 123 Returns the content type specified on the request, along with the 124 charset employed. 125 """ 126 127 return self.parse_content_type(self.request.environ.get("CONTENT_TYPE")) 128 129 def get_content_charsets(self): 130 131 """ 132 Returns the character set preferences. 133 134 NOTE: Not decently supported. 135 """ 136 137 return self.parse_content_preferences(None) 138 139 def get_content_languages(self): 140 141 """ 142 Returns extracted language information from the transaction. 143 144 NOTE: Not decently supported. 145 """ 146 147 return self.parse_content_preferences(None) 148 149 def get_path(self, encoding=None): 150 151 """ 152 Returns the entire path from the request as a Unicode object. Any "URL 153 encoded" character values in the part of the path before the query 154 string will be decoded and presented as genuine characters; the query 155 string will remain "URL encoded", however. 156 157 If the optional 'encoding' is set, use that in preference to the default 158 encoding to convert the path into a form not containing "URL encoded" 159 character values. 160 """ 161 162 # NOTE: Based on WebStack.CGI.get_path. 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 # NOTE: Based on WebStack.CGI.get_path. 184 185 path = decode_value(self.request.environ.get("SCRIPT_NAME") or "", encoding) 186 path += self.get_path_info(encoding) 187 return path 188 189 def get_path_info(self, encoding=None): 190 191 """ 192 Returns the "path info" (the part of the URL after the resource name 193 handling the current request) from the request as a Unicode object 194 containing genuine characters (as opposed to "URL encoded" character 195 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 product_path = "/".join(self.adapter.getPhysicalPath()) 203 path_info = self.request.environ.get("PATH_INFO") or "" 204 real_path_info = path_info[len(product_path):] 205 return decode_value(real_path_info, encoding) 206 207 def get_query_string(self): 208 209 """ 210 Returns the query string from the path in the request. 211 """ 212 213 return self.request.environ.get("QUERY_STRING") or "" 214 215 # Higher level request-related methods. 216 217 def get_fields_from_path(self, encoding=None): 218 219 """ 220 Extracts fields (or request parameters) from the path specified in the 221 transaction. The underlying framework may refuse to supply fields from 222 the path if handling a POST transaction. The optional 'encoding' 223 parameter specifies the character encoding of the query string for cases 224 where the default encoding is to be overridden. 225 226 Returns a dictionary mapping field names to lists of values (even if a 227 single value is associated with any given field name). 228 """ 229 230 fields = {} 231 for name, values in cgi.parse_qs(self.get_query_string()).items(): 232 name = decode_value(name, encoding) 233 fields[name] = [] 234 for value in values: 235 value = decode_value(value, encoding) 236 fields[name].append(value) 237 return fields 238 239 def get_fields_from_body(self, encoding=None): 240 241 """ 242 Extracts fields (or request parameters) from the message body in the 243 transaction. The optional 'encoding' parameter specifies the character 244 encoding of the message body for cases where no such information is 245 available, but where the default encoding is to be overridden. 246 247 Returns a dictionary mapping field names to lists of values (even if a 248 single value is associated with any given field name). Each value is 249 either a Unicode object (representing a simple form field, for example) 250 or a plain string (representing a file upload form field, for example). 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 if self._fields is not None: 259 return self._fields 260 261 encoding = encoding or self.get_content_type().charset or self.default_charset 262 self._fields = {} 263 264 for field_name, field_values in self.request.form.items(): 265 field_name = decode_value(field_name, encoding) 266 267 # Find the body values. 268 269 if type(field_values) == type([]): 270 self._fields[field_name] = [] 271 for field_str in field_values: 272 self._fields[field_name].append(get_body_field_or_file(field_str, encoding)) 273 else: 274 self._fields[field_name] = [get_body_field_or_file(field_values, encoding)] 275 276 return self._fields 277 278 def get_fields(self, encoding=None): 279 280 """ 281 Extracts fields (or request parameters) from both the path specified in 282 the transaction as well as the message body. The optional 'encoding' 283 parameter specifies the character encoding of the message body for cases 284 where no such information is available, but where the default encoding 285 is to be overridden. 286 287 Returns a dictionary mapping field names to lists of values (even if a 288 single value is associated with any given field name). Each value is 289 either a Unicode object (representing a simple form field, for example) 290 or a plain string (representing a file upload form field, for example). 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()) 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