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