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