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