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