1 #!/usr/bin/env python 2 3 """ 4 Zope classes. 5 In places this resembles CGI a lot because Zope seems to recycle a lot of that 6 baggage. 7 """ 8 9 import Generic 10 from Helpers import Environment 11 from Helpers.Request import Cookie, get_body_field, filter_fields 12 from Helpers.Response import ConvertingStream 13 from Helpers.Auth import UserInfo 14 import cgi 15 16 class Transaction(Generic.Transaction): 17 18 """ 19 Zope transaction interface. 20 """ 21 22 def __init__(self, request, adapter): 23 24 """ 25 Initialise the transaction with the Zope 'request' object and the 26 'adapter' which created this transaction. 27 """ 28 29 self.request = request 30 self.response = request.RESPONSE 31 self.adapter = adapter 32 33 # Cached information. 34 35 self._fields = None 36 37 # Attributes which may be changed later. 38 39 self.content_type = None 40 self.user = None 41 self.path_info = None 42 43 # Request-related methods. 44 45 def get_request_stream(self): 46 47 """ 48 Returns the request stream for the transaction. 49 """ 50 51 # NOTE: Possibly not safe. 52 53 return self.request.stdin 54 55 def get_request_method(self): 56 57 """ 58 Returns the request method. 59 """ 60 61 return self.request.environ.get("REQUEST_METHOD") 62 63 def get_headers(self): 64 65 """ 66 Returns all request headers as a dictionary-like object mapping header 67 names to values. 68 """ 69 70 return Environment.get_headers(self.request.environ) 71 72 def get_header_values(self, key): 73 74 """ 75 Returns a list of all request header values associated with the given 76 'key'. Note that according to RFC 2616, 'key' is treated as a 77 case-insensitive string. 78 """ 79 80 return self.convert_to_list(self.get_headers().get(key)) 81 82 def get_content_type(self): 83 84 """ 85 Returns the content type specified on the request, along with the 86 charset employed. 87 """ 88 89 return self.parse_content_type(self.request.environ.get("CONTENT_TYPE")) 90 91 def get_content_charsets(self): 92 93 """ 94 Returns the character set preferences. 95 96 NOTE: Not decently supported. 97 """ 98 99 return self.parse_content_preferences(None) 100 101 def get_content_languages(self): 102 103 """ 104 Returns extracted language information from the transaction. 105 106 NOTE: Not decently supported. 107 """ 108 109 return self.parse_content_preferences(None) 110 111 def get_path(self): 112 113 """ 114 Returns the entire path from the request. 115 """ 116 117 # NOTE: Based on WebStack.CGI.get_path. 118 119 path = self.get_path_without_query() 120 qs = self.get_query_string() 121 if qs: 122 path += "?" 123 path += qs 124 return path 125 126 def get_path_without_query(self): 127 128 """ 129 Returns the entire path from the request minus the query string. 130 """ 131 132 # NOTE: Based on WebStack.CGI.get_path. 133 134 path = self.request.environ.get("SCRIPT_NAME") or "" 135 if self.request.environ.has_key("PATH_INFO"): 136 path += self.request.environ["PATH_INFO"] 137 return path 138 139 def get_path_info(self): 140 141 """ 142 Returns the "path info" (the part of the URL after the resource name 143 handling the current request) from the request. 144 """ 145 146 if self.path_info is not None: 147 return self.path_info 148 else: 149 product_path = "/".join(self.adapter.getPhysicalPath()) 150 path_info = self.request.environ.get("PATH_INFO") or "" 151 return path_info[len(product_path):] 152 153 def get_query_string(self): 154 155 """ 156 Returns the query string from the path in the request. 157 """ 158 159 return self.request.environ.get("QUERY_STRING") or "" 160 161 # Higher level request-related methods. 162 163 def get_fields_from_path(self): 164 165 """ 166 Extracts fields (or request parameters) from the path specified in the 167 transaction. The underlying framework may refuse to supply fields from 168 the path if handling a POST transaction. 169 170 Returns a dictionary mapping field names to lists of values (even if a 171 single value is associated with any given field name). 172 """ 173 174 # NOTE: Support at best ISO-8859-1 values. 175 176 fields = {} 177 for name, values in cgi.parse_qs(self.get_query_string()).items(): 178 fields[name] = [] 179 for value in values: 180 fields[name].append(unicode(value, "iso-8859-1")) 181 return fields 182 183 def get_fields_from_body(self, encoding=None): 184 185 """ 186 Extracts fields (or request parameters) from the message body in the 187 transaction. The optional 'encoding' parameter specifies the character 188 encoding of the message body for cases where no such information is 189 available, but where the default encoding is to be overridden. 190 191 Returns a dictionary mapping field names to lists of values (even if a 192 single value is associated with any given field name). Each value is 193 either a Unicode object (representing a simple form field, for example) 194 or a plain string (representing a file upload form field, for example). 195 """ 196 197 all_fields = self._get_fields(encoding) 198 fields_from_path = self.get_fields_from_path() 199 return filter_fields(all_fields, fields_from_path) 200 201 def _get_fields(self, encoding=None): 202 if self._fields is not None: 203 return self._fields 204 205 encoding = encoding or self.get_content_type().charset or self.default_charset 206 self._fields = {} 207 for field_name, field_values in self.request.form.items(): 208 209 # Find the body values. 210 211 if type(field_values) == type([]): 212 self._fields[field_name] = [] 213 for field_str in field_values: 214 self._fields[field_name].append(get_body_field(field_str, encoding)) 215 else: 216 self._fields[field_name] = [get_body_field(field_values, encoding)] 217 218 return self._fields 219 220 def get_fields(self, encoding=None): 221 222 """ 223 Extracts fields (or request parameters) from both the path specified in 224 the transaction as well as the message body. The optional 'encoding' 225 parameter specifies the character encoding of the message body for cases 226 where no such information is available, but where the default encoding 227 is to be overridden. 228 229 Returns a dictionary mapping field names to lists of values (even if a 230 single value is associated with any given field name). Each value is 231 either a Unicode object (representing a simple form field, for example) 232 or a plain string (representing a file upload form field, for example). 233 234 Where a given field name is used in both the path and message body to 235 specify values, the values from both sources will be combined into a 236 single list associated with that field name. 237 """ 238 239 # NOTE: Zope seems to provide only body fields upon POST requests. 240 241 if self.get_request_method() == "GET": 242 return self._get_fields(encoding) 243 else: 244 fields = {} 245 fields.update(self.get_fields_from_path()) 246 for name, values in self._get_fields(encoding).items(): 247 if not fields.has_key(name): 248 fields[name] = values 249 else: 250 fields[name] += values 251 return fields 252 253 def get_user(self): 254 255 """ 256 Extracts user information from the transaction. 257 258 Returns a username as a string or None if no user is defined. 259 """ 260 261 if self.user is not None: 262 return self.user 263 264 auth_header = self.request._auth 265 if auth_header: 266 return UserInfo(auth_header).username 267 else: 268 return None 269 270 def get_cookies(self): 271 272 """ 273 Obtains cookie information from the request. 274 275 Returns a dictionary mapping cookie names to cookie objects. 276 """ 277 278 return self.process_cookies(self.request.cookies, using_strings=1) 279 280 def get_cookie(self, cookie_name): 281 282 """ 283 Obtains cookie information from the request. 284 285 Returns a cookie object for the given 'cookie_name' or None if no such 286 cookie exists. 287 """ 288 289 value = self.request.cookies.get(self.encode_cookie_value(cookie_name)) 290 if value is not None: 291 return Cookie(cookie_name, self.decode_cookie_value(value)) 292 else: 293 return None 294 295 # Response-related methods. 296 297 def get_response_stream(self): 298 299 """ 300 Returns the response stream for the transaction. 301 """ 302 303 # Unicode can upset this operation. Using either the specified charset 304 # or a default encoding. 305 306 encoding = self.get_response_stream_encoding() 307 return ConvertingStream(self.response, encoding) 308 309 def get_response_stream_encoding(self): 310 311 """ 312 Returns the response stream encoding. 313 """ 314 315 if self.content_type: 316 encoding = self.content_type.charset 317 else: 318 encoding = None 319 return encoding or self.default_charset 320 321 def get_response_code(self): 322 323 """ 324 Get the response code associated with the transaction. If no response 325 code is defined, None is returned. 326 """ 327 328 return self.response.status 329 330 def set_response_code(self, response_code): 331 332 """ 333 Set the 'response_code' using a numeric constant defined in the HTTP 334 specification. 335 """ 336 337 self.response.setStatus(response_code) 338 339 def set_header_value(self, header, value): 340 341 """ 342 Set the HTTP 'header' with the given 'value'. 343 """ 344 345 self.response.setHeader(header, value) 346 347 def set_content_type(self, content_type): 348 349 """ 350 Sets the 'content_type' for the response. 351 """ 352 353 self.content_type = content_type 354 self.response.setHeader("Content-Type", str(content_type)) 355 356 # Higher level response-related methods. 357 358 def set_cookie(self, cookie): 359 360 """ 361 Stores the given 'cookie' object in the response. 362 """ 363 364 self.set_cookie_value(cookie.name, cookie.value) 365 366 def set_cookie_value(self, name, value, path=None, expires=None): 367 368 """ 369 Stores a cookie with the given 'name' and 'value' in the response. 370 371 The optional 'path' is a string which specifies the scope of the cookie, 372 and the optional 'expires' parameter is a value compatible with the 373 time.time function, and indicates the expiry date/time of the cookie. 374 """ 375 376 self.response.setCookie(self.encode_cookie_value(name), self.encode_cookie_value(value)) 377 378 def delete_cookie(self, cookie_name): 379 380 """ 381 Adds to the response a request that the cookie with the given 382 'cookie_name' be deleted/discarded by the client. 383 """ 384 385 self.response.expireCookie(self.encode_cookie_value(cookie_name)) 386 387 # Session-related methods. 388 389 def get_session(self, create=1): 390 391 """ 392 Gets a session corresponding to an identifier supplied in the 393 transaction. 394 395 If no session has yet been established according to information 396 provided in the transaction then the optional 'create' parameter 397 determines whether a new session will be established. 398 399 Where no session has been established and where 'create' is set to 0 400 then None is returned. In all other cases, a session object is created 401 (where appropriate) and returned. 402 """ 403 404 return self.request.SESSION 405 406 def expire_session(self): 407 408 """ 409 Expires any session established according to information provided in the 410 transaction. 411 """ 412 413 self.request.SESSION.invalidate() 414 415 # vim: tabstop=4 expandtab shiftwidth=4