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