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): 150 151 """ 152 Returns the entire path from the request. 153 """ 154 155 # NOTE: Based on WebStack.CGI.get_path. 156 157 path = self.get_path_without_query() 158 qs = self.get_query_string() 159 if qs: 160 path += "?" 161 path += qs 162 return path 163 164 def get_path_without_query(self): 165 166 """ 167 Returns the entire path from the request minus the query string. 168 """ 169 170 # NOTE: Based on WebStack.CGI.get_path. 171 172 path = self.request.environ.get("SCRIPT_NAME") or "" 173 if self.request.environ.has_key("PATH_INFO"): 174 path += self.request.environ["PATH_INFO"] 175 return path 176 177 def get_path_info(self): 178 179 """ 180 Returns the "path info" (the part of the URL after the resource name 181 handling the current request) from the request. 182 """ 183 184 product_path = "/".join(self.adapter.getPhysicalPath()) 185 path_info = self.request.environ.get("PATH_INFO") or "" 186 return path_info[len(product_path):] 187 188 def get_query_string(self): 189 190 """ 191 Returns the query string from the path in the request. 192 """ 193 194 return self.request.environ.get("QUERY_STRING") or "" 195 196 # Higher level request-related methods. 197 198 def get_fields_from_path(self, encoding=None): 199 200 """ 201 Extracts fields (or request parameters) from the path specified in the 202 transaction. The underlying framework may refuse to supply fields from 203 the path if handling a POST transaction. The optional 'encoding' 204 parameter specifies the character encoding of the query string for cases 205 where the default encoding is to be overridden. 206 207 Returns a dictionary mapping field names to lists of values (even if a 208 single value is associated with any given field name). 209 """ 210 211 fields = {} 212 for name, values in cgi.parse_qs(self.get_query_string()).items(): 213 name = self.decode_path(name, encoding) 214 fields[name] = [] 215 for value in values: 216 value = self.decode_path(value, encoding) 217 fields[name].append(value) 218 return fields 219 220 def get_fields_from_body(self, encoding=None): 221 222 """ 223 Extracts fields (or request parameters) from the message body in the 224 transaction. The optional 'encoding' parameter specifies the character 225 encoding of the message body for cases where no such information is 226 available, but where the default encoding is to be overridden. 227 228 Returns a dictionary mapping field names to lists of values (even if a 229 single value is associated with any given field name). Each value is 230 either a Unicode object (representing a simple form field, for example) 231 or a plain string (representing a file upload form field, for example). 232 """ 233 234 all_fields = self._get_fields(encoding) 235 fields_from_path = self.get_fields_from_path() 236 return filter_fields(all_fields, fields_from_path) 237 238 def _get_fields(self, encoding=None): 239 if self._fields is not None: 240 return self._fields 241 242 encoding = encoding or self.get_content_type().charset or self.default_charset 243 self._fields = {} 244 for field_name, field_values in self.request.form.items(): 245 246 # Find the body values. 247 248 if type(field_values) == type([]): 249 self._fields[field_name] = [] 250 for field_str in field_values: 251 self._fields[field_name].append(get_body_field(field_str, encoding)) 252 else: 253 self._fields[field_name] = [get_body_field(field_values, encoding)] 254 255 return self._fields 256 257 def get_fields(self, encoding=None): 258 259 """ 260 Extracts fields (or request parameters) from both the path specified in 261 the transaction as well as the message body. The optional 'encoding' 262 parameter specifies the character encoding of the message body for cases 263 where no such information is available, but where the default encoding 264 is to be overridden. 265 266 Returns a dictionary mapping field names to lists of values (even if a 267 single value is associated with any given field name). Each value is 268 either a Unicode object (representing a simple form field, for example) 269 or a plain string (representing a file upload form field, for example). 270 271 Where a given field name is used in both the path and message body to 272 specify values, the values from both sources will be combined into a 273 single list associated with that field name. 274 """ 275 276 # NOTE: Zope seems to provide only body fields upon POST requests. 277 278 if self.get_request_method() == "GET": 279 return self._get_fields(encoding) 280 else: 281 fields = {} 282 fields.update(self.get_fields_from_path()) 283 for name, values in self._get_fields(encoding).items(): 284 if not fields.has_key(name): 285 fields[name] = values 286 else: 287 fields[name] += values 288 return fields 289 290 def get_user(self): 291 292 """ 293 Extracts user information from the transaction. 294 295 Returns a username as a string or None if no user is defined. 296 """ 297 298 if self.user is not None: 299 return self.user 300 301 auth_header = self.request._auth 302 if auth_header: 303 return UserInfo(auth_header).username 304 else: 305 return None 306 307 def get_cookies(self): 308 309 """ 310 Obtains cookie information from the request. 311 312 Returns a dictionary mapping cookie names to cookie objects. 313 """ 314 315 return self.process_cookies(self.request.cookies, using_strings=1) 316 317 def get_cookie(self, cookie_name): 318 319 """ 320 Obtains cookie information from the request. 321 322 Returns a cookie object for the given 'cookie_name' or None if no such 323 cookie exists. 324 """ 325 326 value = self.request.cookies.get(self.encode_cookie_value(cookie_name)) 327 if value is not None: 328 return Cookie(cookie_name, self.decode_cookie_value(value)) 329 else: 330 return None 331 332 # Response-related methods. 333 334 def get_response_stream(self): 335 336 """ 337 Returns the response stream for the transaction. 338 """ 339 340 # Unicode can upset this operation. Using either the specified charset 341 # or a default encoding. 342 343 encoding = self.get_response_stream_encoding() 344 return ConvertingStream(self.response, encoding) 345 346 def get_response_stream_encoding(self): 347 348 """ 349 Returns the response stream encoding. 350 """ 351 352 if self.content_type: 353 encoding = self.content_type.charset 354 else: 355 encoding = None 356 return encoding or self.default_charset 357 358 def get_response_code(self): 359 360 """ 361 Get the response code associated with the transaction. If no response 362 code is defined, None is returned. 363 """ 364 365 return self.response.status 366 367 def set_response_code(self, response_code): 368 369 """ 370 Set the 'response_code' using a numeric constant defined in the HTTP 371 specification. 372 """ 373 374 self.response.setStatus(response_code) 375 376 def set_header_value(self, header, value): 377 378 """ 379 Set the HTTP 'header' with the given 'value'. 380 """ 381 382 self.response.setHeader(header, value) 383 384 def set_content_type(self, content_type): 385 386 """ 387 Sets the 'content_type' for the response. 388 """ 389 390 self.content_type = content_type 391 self.response.setHeader("Content-Type", str(content_type)) 392 393 # Higher level response-related methods. 394 395 def set_cookie(self, cookie): 396 397 """ 398 Stores the given 'cookie' object in the response. 399 """ 400 401 self.set_cookie_value(cookie.name, cookie.value) 402 403 def set_cookie_value(self, name, value, path=None, expires=None): 404 405 """ 406 Stores a cookie with the given 'name' and 'value' in the response. 407 408 The optional 'path' is a string which specifies the scope of the cookie, 409 and the optional 'expires' parameter is a value compatible with the 410 time.time function, and indicates the expiry date/time of the cookie. 411 """ 412 413 self.response.setCookie(self.encode_cookie_value(name), self.encode_cookie_value(value)) 414 415 def delete_cookie(self, cookie_name): 416 417 """ 418 Adds to the response a request that the cookie with the given 419 'cookie_name' be deleted/discarded by the client. 420 """ 421 422 self.response.expireCookie(self.encode_cookie_value(cookie_name)) 423 424 # Session-related methods. 425 426 def get_session(self, create=1): 427 428 """ 429 Gets a session corresponding to an identifier supplied in the 430 transaction. 431 432 If no session has yet been established according to information 433 provided in the transaction then the optional 'create' parameter 434 determines whether a new session will be established. 435 436 Where no session has been established and where 'create' is set to 0 437 then None is returned. In all other cases, a session object is created 438 (where appropriate) and returned. 439 """ 440 441 return self.request.SESSION 442 443 def expire_session(self): 444 445 """ 446 Expires any session established according to information provided in the 447 transaction. 448 """ 449 450 self.request.SESSION.invalidate() 451 452 # vim: tabstop=4 expandtab shiftwidth=4