1 #!/usr/bin/env python 2 3 """ 4 Java Servlet classes. 5 6 Copyright (C) 2004, 2005 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 21 """ 22 23 import Generic 24 from StringIO import StringIO 25 from Helpers.Request import Cookie, get_body_fields, get_storage_items, get_fields_from_query_string, filter_fields 26 import javax.servlet.http 27 28 # Form data decoding. 29 30 import javax.mail.internet 31 import javax.mail 32 import java.util 33 import java.net 34 from WebStack.Generic import HeaderValue 35 36 class Stream: 37 38 """ 39 Wrapper around java.io.BufferedReader. 40 """ 41 42 def __init__(self, stream): 43 44 "Initialise the stream with the given underlying 'stream'." 45 46 self.stream = stream 47 48 def read(self): 49 50 "Read the entire message, returning it as a string." 51 52 characters = StringIO() 53 while 1: 54 c = self.stream.read() 55 if c == -1: 56 return characters.getvalue() 57 else: 58 characters.write(chr(c)) 59 60 def readline(self): 61 62 "Read a line from the stream, returning it as a string." 63 64 line = self.stream.readLine() 65 if line is not None: 66 return line + "\n" 67 else: 68 return "" 69 70 class Transaction(Generic.Transaction): 71 72 """ 73 Java Servlet transaction interface. 74 """ 75 76 def __init__(self, request, response): 77 78 """ 79 Initialise the transaction using the Java Servlet HTTP 'request' and 80 'response'. 81 """ 82 83 self.request = request 84 self.response = response 85 self.status = None 86 self.user = None 87 self.path_info = None 88 89 # Remember the cookies received in the request. 90 # NOTE: Discarding much of the information received. 91 92 self.cookies_in = {} 93 for cookie in self.request.getCookies() or []: 94 cookie_name = self.decode_cookie_value(cookie.getName()) 95 self.cookies_in[cookie_name] = Cookie(cookie_name, self.decode_cookie_value(cookie.getValue())) 96 97 # Cached information. 98 99 self.message_fields = None 100 101 def commit(self): 102 103 """ 104 A special method, synchronising the transaction with framework-specific 105 objects. 106 """ 107 108 self.get_response_stream().close() 109 110 # Server-related methods. 111 112 def get_server_name(self): 113 114 "Returns the server name." 115 116 return self.request.getServerName() 117 118 def get_server_port(self): 119 120 "Returns the server port as a string." 121 122 return str(self.request.getServerPort()) 123 124 # Request-related methods. 125 126 def get_request_stream(self): 127 128 """ 129 Returns the request stream for the transaction. 130 """ 131 132 return Stream(self.request.getReader()) 133 134 def get_request_method(self): 135 136 """ 137 Returns the request method. 138 """ 139 140 return self.request.getMethod() 141 142 def get_headers(self): 143 144 """ 145 Returns all request headers as a dictionary-like object mapping header 146 names to values. 147 148 NOTE: If duplicate header names are permitted, then this interface will 149 NOTE: need to change. 150 """ 151 152 headers = {} 153 header_names_enum = self.request.getHeaderNames() 154 while header_names_enum.hasMoreElements(): 155 156 # NOTE: Retrieve only a single value (not using getHeaders). 157 158 header_name = header_names_enum.nextElement() 159 headers[header_name] = self.request.getHeader(header_name) 160 161 return headers 162 163 def get_header_values(self, key): 164 165 """ 166 Returns a list of all request header values associated with the given 167 'key'. Note that according to RFC 2616, 'key' is treated as a 168 case-insensitive string. 169 """ 170 171 values = [] 172 headers_enum = self.request.getHeaders(key) 173 while headers_enum.hasMoreElements(): 174 values.append(headers_enum.nextElement()) 175 return values 176 177 def get_content_type(self): 178 179 """ 180 Returns the content type specified on the request, along with the 181 charset employed. 182 """ 183 184 content_types = self.get_header_values("Content-Type") or [] 185 if len(content_types) >= 1: 186 return self.parse_content_type(content_types[0]) 187 else: 188 return None 189 190 def get_content_charsets(self): 191 192 """ 193 Returns the character set preferences. 194 """ 195 196 accept_charsets = self.get_header_values("Accept-Charset") or [] 197 if len(accept_charsets) >= 1: 198 return self.parse_content_preferences(accept_charsets[0]) 199 else: 200 return None 201 202 def get_content_languages(self): 203 204 """ 205 Returns extracted language information from the transaction. 206 """ 207 208 accept_languages = self.get_header_values("Accept-Language") or [] 209 if len(accept_languages) >= 1: 210 return self.parse_content_preferences(accept_languages[0]) 211 else: 212 return None 213 214 def get_path(self, encoding=None): 215 216 """ 217 Returns the entire path from the request as a Unicode object. Any "URL 218 encoded" character values in the part of the path before the query 219 string will be decoded and presented as genuine characters; the query 220 string will remain "URL encoded", however. 221 222 If the optional 'encoding' is set, use that in preference to the default 223 encoding to convert the path into a form not containing "URL encoded" 224 character values. 225 """ 226 227 path = self.get_path_without_query(encoding) 228 qs = self.get_query_string() 229 if qs: 230 return path + "?" + qs 231 else: 232 return path 233 234 def get_path_without_query(self, encoding=None): 235 236 """ 237 Returns the entire path from the request minus the query string as a 238 Unicode object containing genuine characters (as opposed to "URL 239 encoded" character values). 240 241 If the optional 'encoding' is set, use that in preference to the default 242 encoding to convert the path into a form not containing "URL encoded" 243 character values. 244 """ 245 246 # NOTE: We do not actually use the encoding - this may be a servlet 247 # NOTE: container option. 248 249 return self.request.getContextPath() + self.request.getServletPath() + self.get_path_info(encoding) 250 251 def get_path_info(self, encoding=None): 252 253 """ 254 Returns the "path info" (the part of the URL after the resource name 255 handling the current request) from the request as a Unicode object 256 containing genuine characters (as opposed to "URL encoded" character 257 values). 258 259 If the optional 'encoding' is set, use that in preference to the default 260 encoding to convert the path into a form not containing "URL encoded" 261 character values. 262 """ 263 264 # NOTE: We do not actually use the encoding - this may be a servlet 265 # NOTE: container option. 266 267 return self.request.getPathInfo() or "" 268 269 def get_query_string(self): 270 271 """ 272 Returns the query string from the path in the request. 273 """ 274 275 return self.request.getQueryString() or "" 276 277 # Higher level request-related methods. 278 279 def get_fields_from_path(self, encoding=None): 280 281 """ 282 Extracts fields (or request parameters) from the path specified in the 283 transaction. The underlying framework may refuse to supply fields from 284 the path if handling a POST transaction. The optional 'encoding' 285 parameter specifies the character encoding of the query string for cases 286 where the default encoding is to be overridden. 287 288 Returns a dictionary mapping field names to lists of values (even if a 289 single value is associated with any given field name). 290 """ 291 292 # There may not be a reliable means of extracting only the fields from 293 # the path using the API. Moreover, any access to the request parameters 294 # disrupts the proper extraction and decoding of the request parameters 295 # which originated in the request body. 296 297 return get_fields_from_query_string(self.get_query_string(), java.net.URLDecoder().decode) 298 299 def get_fields_from_body(self, encoding=None): 300 301 """ 302 Extracts fields (or request parameters) from the message body in the 303 transaction. The optional 'encoding' parameter specifies the character 304 encoding of the message body for cases where no such information is 305 available, but where the default encoding is to be overridden. 306 307 Returns a dictionary mapping field names to lists of values (even if a 308 single value is associated with any given field name). Each value is 309 either a Unicode object (representing a simple form field, for example) 310 or a plain string (representing a file upload form field, for example). 311 """ 312 313 # There may not be a reliable means of extracting only the fields 314 # the message body using the API. Remove fields originating from the 315 # path in the mixture provided by the API. 316 317 all_fields = self._get_fields(encoding) 318 fields_from_path = self.get_fields_from_path() 319 return filter_fields(all_fields, fields_from_path) 320 321 def _get_fields(self, encoding=None): 322 323 # Override the default encoding if requested. 324 325 if encoding is not None: 326 self.request.setCharacterEncoding(encoding) 327 328 # Where the content type is "multipart/form-data", we use javax.mail 329 # functionality. Otherwise, we use the Servlet API's parameter access 330 # methods. 331 332 if self.get_content_type() and self.get_content_type().media_type == "multipart/form-data": 333 if self.message_fields is not None: 334 return self.message_fields 335 else: 336 fields = self.message_fields = self._get_fields_from_message() 337 else: 338 fields = {} 339 parameter_map = self.request.getParameterMap() 340 if parameter_map: 341 for field_name in parameter_map.keySet(): 342 fields[field_name] = parameter_map[field_name] 343 344 return fields 345 346 def get_fields(self, encoding=None): 347 348 """ 349 Extracts fields (or request parameters) from both the path specified in 350 the transaction as well as the message body. The optional 'encoding' 351 parameter specifies the character encoding of the message body for cases 352 where no such information is available, but where the default encoding 353 is to be overridden. 354 355 Returns a dictionary mapping field names to lists of values (even if a 356 single value is associated with any given field name). Each value is 357 either a Unicode object (representing a simple form field, for example) 358 or a plain string (representing a file upload form field, for example). 359 360 Where a given field name is used in both the path and message body to 361 specify values, the values from both sources will be combined into a 362 single list associated with that field name. 363 """ 364 365 # NOTE: The Java Servlet API (like Zope) seems to provide only body 366 # NOTE: fields upon POST requests. 367 368 if self.get_request_method() == "GET": 369 return self._get_fields(encoding) 370 else: 371 fields = {} 372 fields.update(self.get_fields_from_path()) 373 for name, values in self._get_fields(encoding).items(): 374 if not fields.has_key(name): 375 fields[name] = values 376 else: 377 fields[name] += values 378 return fields 379 380 def get_user(self): 381 382 """ 383 Extracts user information from the transaction. 384 385 Returns a username as a string or None if no user is defined. 386 """ 387 388 if self.user is not None: 389 return self.user 390 else: 391 return self.request.getRemoteUser() 392 393 def get_cookies(self): 394 395 """ 396 Obtains cookie information from the request. 397 398 Returns a dictionary mapping cookie names to cookie objects. 399 """ 400 401 return self.cookies_in 402 403 def get_cookie(self, cookie_name): 404 405 """ 406 Obtains cookie information from the request. 407 408 Returns a cookie object for the given 'cookie_name' or None if no such 409 cookie exists. 410 """ 411 412 return self.cookies_in.get(cookie_name) 413 414 # Response-related methods. 415 416 def get_response_stream(self): 417 418 """ 419 Returns the response stream for the transaction. 420 """ 421 422 return self.response.getWriter() 423 424 def get_response_stream_encoding(self): 425 426 """ 427 Returns the response stream encoding. 428 """ 429 430 return self.response.getCharacterEncoding() 431 432 def get_response_code(self): 433 434 """ 435 Get the response code associated with the transaction. If no response 436 code is defined, None is returned. 437 """ 438 439 return self.status 440 441 def set_response_code(self, response_code): 442 443 """ 444 Set the 'response_code' using a numeric constant defined in the HTTP 445 specification. 446 """ 447 448 self.status = response_code 449 self.response.setStatus(self.status) 450 451 def set_header_value(self, header, value): 452 453 """ 454 Set the HTTP 'header' with the given 'value'. 455 """ 456 457 self.response.setHeader(self.format_header_value(header), self.format_header_value(value)) 458 459 def set_content_type(self, content_type): 460 461 """ 462 Sets the 'content_type' for the response. 463 """ 464 465 self.response.setContentType(str(content_type)) 466 467 # Higher level response-related methods. 468 469 def set_cookie(self, cookie): 470 471 """ 472 Stores the given 'cookie' object in the response. 473 """ 474 475 self.set_cookie_value(cookie.name, cookie.value) 476 477 def set_cookie_value(self, name, value, path=None, expires=None): 478 479 """ 480 Stores a cookie with the given 'name' and 'value' in the response. 481 482 The optional 'path' is a string which specifies the scope of the cookie, 483 and the optional 'expires' parameter is a value compatible with the 484 time.time function, and indicates the expiry date/time of the cookie. 485 """ 486 487 cookie = javax.servlet.http.Cookie(self.encode_cookie_value(name), 488 self.encode_cookie_value(value)) 489 if path is not None: 490 cookie.setPath(path) 491 492 # NOTE: The expires parameter seems not to be supported. 493 494 self.response.addCookie(cookie) 495 496 def delete_cookie(self, cookie_name): 497 498 """ 499 Adds to the response a request that the cookie with the given 500 'cookie_name' be deleted/discarded by the client. 501 """ 502 503 # Create a special cookie, given that we do not know whether the browser 504 # has been sent the cookie or not. 505 # NOTE: Magic discovered in Webware. 506 507 cookie = javax.servlet.http.Cookie(self.encode_cookie_value(cookie_name), "") 508 cookie.setPath("/") 509 cookie.setMaxAge(0) 510 self.response.addCookie(cookie) 511 512 # Session-related methods. 513 514 def get_session(self, create=1): 515 516 """ 517 Gets a session corresponding to an identifier supplied in the 518 transaction. 519 520 If no session has yet been established according to information 521 provided in the transaction then the optional 'create' parameter 522 determines whether a new session will be established. 523 524 Where no session has been established and where 'create' is set to 0 525 then None is returned. In all other cases, a session object is created 526 (where appropriate) and returned. 527 """ 528 529 session = self.request.getSession(create) 530 if session: 531 return Session(session) 532 else: 533 return None 534 535 def expire_session(self): 536 537 """ 538 Expires any session established according to information provided in the 539 transaction. 540 """ 541 542 session = self.request.getSession(0) 543 if session: 544 session.invalidate() 545 546 # Special Java-specific methods. 547 548 def _get_fields_from_message(self): 549 550 "Get fields from a multipart message." 551 552 session = javax.mail.Session.getDefaultInstance(java.util.Properties()) 553 554 # Fake a multipart message. 555 556 str_buffer = java.io.StringWriter() 557 fp = self.get_request_stream() 558 boundary = fp.readline() 559 str_buffer.write('Content-Type: multipart/mixed; boundary="%s"\n\n' % boundary[2:-1]) 560 str_buffer.write(boundary) 561 str_buffer.write(fp.read()) 562 str_buffer.close() 563 564 # Re-read that message. 565 566 input_stream = java.io.StringBufferInputStream(str_buffer.toString()) 567 message = javax.mail.internet.MimeMessage(session, input_stream) 568 content = message.getContent() 569 return self._get_fields_from_multipart(content) 570 571 def _get_fields_from_multipart(self, content): 572 573 "Get fields from multipart 'content'." 574 575 fields = {} 576 for i in range(0, content.getCount()): 577 part = content.getBodyPart(i) 578 subcontent = part.getContent() 579 580 # Convert input stream content. 581 582 if isinstance(subcontent, java.io.InputStream): 583 subcontent = Stream(subcontent).read() 584 585 # Record string content. 586 587 if type(subcontent) == type(""): 588 589 # Should get: form-data; name="x" 590 591 disposition = self.parse_header_value(HeaderValue, part.getHeader("Content-Disposition")[0]) 592 593 # Store and optionally convert the field. 594 595 if disposition.name is not None: 596 if not fields.has_key(disposition.name[1:-1]): 597 fields[disposition.name[1:-1]] = [] 598 fields[disposition.name[1:-1]].append(subcontent) 599 600 # Otherwise, descend deeper into the multipart hierarchy. 601 602 else: 603 fields.update(self._get_fields_from_multipart(subcontent)) 604 605 return fields 606 607 class Session: 608 609 """ 610 A simple session class with behaviour more similar to the Python framework 611 session classes. 612 """ 613 614 def __init__(self, session): 615 616 "Initialise the session object with the framework 'session' object." 617 618 self.session = session 619 620 def keys(self): 621 keys = [] 622 keys_enum = self.session.getAttributeNames() 623 while keys_enum.hasMoreElements(): 624 keys.append(keys_enum.nextElement()) 625 return keys 626 627 def values(self): 628 values = [] 629 for key in self.keys(): 630 values.append(self[key]) 631 return values 632 633 def items(self): 634 items = [] 635 for key in self.keys(): 636 items.append((key, self[key])) 637 return items 638 639 def __getitem__(self, key): 640 return self.session.getAttribute(key) 641 642 def __setitem__(self, key, value): 643 self.session.setAttribute(key, value) 644 645 def __delitem__(self, key): 646 self.session.removeAttribute(key) 647 648 # vim: tabstop=4 expandtab shiftwidth=4