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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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 Generic 29 from Helpers import Environment 30 from Helpers.Request import Cookie, get_body_field, filter_fields 31 from Helpers.Response import ConvertingStream 32 from Helpers.Auth import UserInfo 33 import cgi 34 35 class Transaction(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 = self.decode_path(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 self.decode_path(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 = self.decode_path(name, encoding) 233 fields[name] = [] 234 for value in values: 235 value = self.decode_path(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 for field_name, field_values in self.request.form.items(): 264 265 # Find the body values. 266 267 if type(field_values) == type([]): 268 self._fields[field_name] = [] 269 for field_str in field_values: 270 self._fields[field_name].append(get_body_field(field_str, encoding)) 271 else: 272 self._fields[field_name] = [get_body_field(field_values, encoding)] 273 274 return self._fields 275 276 def get_fields(self, encoding=None): 277 278 """ 279 Extracts fields (or request parameters) from both the path specified in 280 the transaction as well as the message body. The optional 'encoding' 281 parameter specifies the character encoding of the message body for cases 282 where no such information is available, but where the default encoding 283 is to be overridden. 284 285 Returns a dictionary mapping field names to lists of values (even if a 286 single value is associated with any given field name). Each value is 287 either a Unicode object (representing a simple form field, for example) 288 or a plain string (representing a file upload form field, for example). 289 290 Where a given field name is used in both the path and message body to 291 specify values, the values from both sources will be combined into a 292 single list associated with that field name. 293 """ 294 295 # NOTE: Zope seems to provide only body fields upon POST requests. 296 297 if self.get_request_method() == "GET": 298 return self._get_fields(encoding) 299 else: 300 fields = {} 301 fields.update(self.get_fields_from_path()) 302 for name, values in self._get_fields(encoding).items(): 303 if not fields.has_key(name): 304 fields[name] = values 305 else: 306 fields[name] += values 307 return fields 308 309 def get_user(self): 310 311 """ 312 Extracts user information from the transaction. 313 314 Returns a username as a string or None if no user is defined. 315 """ 316 317 if self.user is not None: 318 return self.user 319 320 auth_header = self.request._auth 321 if auth_header: 322 return UserInfo(auth_header).username 323 else: 324 return None 325 326 def get_cookies(self): 327 328 """ 329 Obtains cookie information from the request. 330 331 Returns a dictionary mapping cookie names to cookie objects. 332 """ 333 334 return self.process_cookies(self.request.cookies, using_strings=1) 335 336 def get_cookie(self, cookie_name): 337 338 """ 339 Obtains cookie information from the request. 340 341 Returns a cookie object for the given 'cookie_name' or None if no such 342 cookie exists. 343 """ 344 345 value = self.request.cookies.get(self.encode_cookie_value(cookie_name)) 346 if value is not None: 347 return Cookie(cookie_name, self.decode_cookie_value(value)) 348 else: 349 return None 350 351 # Response-related methods. 352 353 def get_response_stream(self): 354 355 """ 356 Returns the response stream for the transaction. 357 """ 358 359 # Unicode can upset this operation. Using either the specified charset 360 # or a default encoding. 361 362 encoding = self.get_response_stream_encoding() 363 return ConvertingStream(self.response, encoding) 364 365 def get_response_stream_encoding(self): 366 367 """ 368 Returns the response stream encoding. 369 """ 370 371 if self.content_type: 372 encoding = self.content_type.charset 373 else: 374 encoding = None 375 return encoding or self.default_charset 376 377 def get_response_code(self): 378 379 """ 380 Get the response code associated with the transaction. If no response 381 code is defined, None is returned. 382 """ 383 384 return self.response.status 385 386 def set_response_code(self, response_code): 387 388 """ 389 Set the 'response_code' using a numeric constant defined in the HTTP 390 specification. 391 """ 392 393 self.response.setStatus(response_code) 394 395 def set_header_value(self, header, value): 396 397 """ 398 Set the HTTP 'header' with the given 'value'. 399 """ 400 401 self.response.setHeader(header, value) 402 403 def set_content_type(self, content_type): 404 405 """ 406 Sets the 'content_type' for the response. 407 """ 408 409 self.content_type = content_type 410 self.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 422 def set_cookie_value(self, name, value, path=None, expires=None): 423 424 """ 425 Stores a cookie with the given 'name' and 'value' in the response. 426 427 The optional 'path' is a string which specifies the scope of the cookie, 428 and the optional 'expires' parameter is a value compatible with the 429 time.time function, and indicates the expiry date/time of the cookie. 430 """ 431 432 self.response.setCookie(self.encode_cookie_value(name), self.encode_cookie_value(value)) 433 434 def delete_cookie(self, cookie_name): 435 436 """ 437 Adds to the response a request that the cookie with the given 438 'cookie_name' be deleted/discarded by the client. 439 """ 440 441 self.response.expireCookie(self.encode_cookie_value(cookie_name)) 442 443 # Session-related methods. 444 445 def get_session(self, create=1): 446 447 """ 448 Gets a session corresponding to an identifier supplied in the 449 transaction. 450 451 If no session has yet been established according to information 452 provided in the transaction then the optional 'create' parameter 453 determines whether a new session will be established. 454 455 Where no session has been established and where 'create' is set to 0 456 then None is returned. In all other cases, a session object is created 457 (where appropriate) and returned. 458 """ 459 460 return self.request.SESSION 461 462 def expire_session(self): 463 464 """ 465 Expires any session established according to information provided in the 466 transaction. 467 """ 468 469 self.request.SESSION.invalidate() 470 471 # vim: tabstop=4 expandtab shiftwidth=4