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