Package qm :: Module web
[hide private]
[frames] | no frames]

Source Code for Module qm.web

   1  ######################################################################## 
   2  # 
   3  # File:   web.py 
   4  # Author: Alex Samuel 
   5  # Date:   2001-02-08 
   6  # 
   7  # Contents: 
   8  #   Common code for implementing web user interfaces. 
   9  # 
  10  # Copyright (c) 2001, 2002, 2003 by CodeSourcery, LLC.  All rights reserved.  
  11  # 
  12  # For license terms see the file COPYING. 
  13  # 
  14  ######################################################################## 
  15   
  16  """Common code for implementing web user interfaces.""" 
  17   
  18  ######################################################################## 
  19  # imports 
  20  ######################################################################## 
  21   
  22  import BaseHTTPServer 
  23  import cgi 
  24  import diagnostic 
  25  import errno 
  26  import htmlentitydefs 
  27  import os 
  28  import os.path 
  29  import common 
  30  import qm.platform 
  31  import qm.user 
  32  import re 
  33  import SimpleHTTPServer 
  34  import SocketServer 
  35  import socket 
  36  import string 
  37  import structured_text 
  38  import sys 
  39  import temporary_directory 
  40  import time 
  41  import traceback 
  42  import types 
  43  import urllib 
  44  import user 
  45  import random 
  46   
  47  import qm.external.DocumentTemplate as DocumentTemplate 
  48  sys.path.insert(1, os.path.dirname(DocumentTemplate.__file__)) 
  49   
  50  ######################################################################## 
  51  # constants 
  52  ######################################################################## 
  53   
  54  session_id_field = "session" 
  55  """The name of the form field used to store the session ID.""" 
  56   
  57  ######################################################################## 
  58  # exception classes 
  59  ######################################################################## 
  60   
61 -class AddressInUseError(common.QMException):
62 pass
63 64 65
66 -class PrivilegedPortError(common.QMException):
67 pass
68 69 70
71 -class NoSessionError(common.QMException):
72 pass
73 74 75
76 -class InvalidSessionError(common.QMException):
77 pass
78 79 80 81 82 ######################################################################## 83 # classes 84 ######################################################################## 85
86 -class DtmlPage:
87 """Base class for classes to generate web pages from DTML. 88 89 The 'DtmlPage' object is used as the variable context when 90 generating HTML from DTML. Attributes and methods are available as 91 variables in DTML expressions. 92 93 This base class contains common variables and functions that are 94 available when generating all DTML files. 95 96 To generate HTML from a DTML template, instantiate a 'DtmlPage' 97 object, passing the name of the DTML template file to the 98 initializer function (or create a subclass which does this 99 automatically). Additional named attributes specified in the 100 initializer functions are set as attributes of the 'DtmlPage' 101 object, and thus available as variables in DTML Python expressions. 102 103 To generate HTML from the template, use the '__call__' method, 104 passing a 'WebRequest' object representing the request in response 105 to which the HTML page is being generated. The request set as the 106 'request' attribute of the 'DtmlPage' object. The 'WebRequest' 107 object may be omitted, if the generated HTML page is generic and 108 requires no information specific to the request or web session; in 109 this case, an empty request object is used. 110 111 This class also has an attribute, 'default_class', which is the 112 default 'DtmlPage' subclass to use when generating HTML. By 113 default, it is initialized to 'DtmlPage' itself, but applications 114 may derive a 'DtmlPage' subclass and point 'default_class' to it to 115 obtain customized versions of standard pages.""" 116 117 html_stylesheet = "/stylesheets/qm.css" 118 """The URL for the cascading stylesheet to use with generated pages.""" 119 120 common_javascript = "/common.js" 121 122 qm_bug_system_url = "mailto:qmtest@codesourcery.com" 123 """The public URL for the bug tracking system for the QM tools.""" 124 125
126 - def __init__(self, dtml_template, **attributes):
127 """Create a new page. 128 129 'dtml_template' -- The file name of the DTML template from which 130 the page is generated. The file is assumed to reside in the 131 'dtml' subdirectory of the configured share directory. 132 133 '**attributes' -- Additional attributes to include in the 134 variable context.""" 135 136 self.__dtml_template = dtml_template 137 for key, value in attributes.items(): 138 setattr(self, key, value)
139 140
141 - def __call__(self, request=None):
142 """Generate an HTML page from the DTML template. 143 144 'request' -- A 'WebRequest' object containing a page request in 145 response to which an HTML page is being generated. Session 146 information from the request may be used when generating the 147 page. The request may be 'None', if none is available. 148 149 returns -- The generated HTML text.""" 150 151 # Use an empty request if none was specified. 152 if request is None: 153 request = WebRequest("?") 154 self.request = request 155 # Construct the path to the template file. DTML templates are 156 # stored in the 'dtml' subdirectory of the share directory. 157 template_path = os.path.join(qm.get_share_directory(), "dtml", 158 self.__dtml_template) 159 # Generate HTML from the template. 160 html_file = DocumentTemplate.HTMLFile(template_path) 161 return html_file(self)
162 163
164 - def GetProgramName(self):
165 """Return the name of this application program.""" 166 167 return common.program_name
168 169
170 - def GetMainPageUrl(self):
171 """Return the URL for the main page.""" 172 173 return "/"
174 175
176 - def WebRequest(self, script_url, **fields):
177 """Convenience constructor for 'WebRequest' objects. 178 179 Constructs a 'WebRequest' using the specified 'script_url' and 180 'fields', using the request associated with this object as the 181 base request.""" 182 183 return apply(WebRequest, (script_url, self.request), fields)
184 185
186 - def GenerateXMLHeader(self):
187 """Return the XML header for the document.""" 188 189 return \ 190 '''<?xml version="1.0" encoding="iso-8859-1"?> 191 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 192 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 193 <html xmlns="http://www.w3.org/1999/xhtml">'''
194 195
196 - def GenerateHtmlHeader(self, description, headers=""):
197 """Return the header for an HTML document. 198 199 'description' -- A string describing this page. 200 201 'headers' -- Any additional HTML headers to place in the 202 '<head>' section of the HTML document.""" 203 204 return \ 205 '''<head> 206 <meta http-equiv="Content-Type" 207 content="text/html; charset=iso-8859-1"/> 208 %s 209 <meta http-equiv="Content-Style-Type" 210 content="text/css"/> 211 <link rel="stylesheet" 212 type="text/css" 213 href="%s"/> 214 <meta name="Generator" 215 content="%s"/> 216 <title>%s: %s</title> 217 </head> 218 ''' % (headers, self.html_stylesheet, self.GetProgramName(), 219 self.GetProgramName(), description)
220 221
222 - def GenerateStartBody(self, decorations=1):
223 """Return markup to start the body of the HTML document.""" 224 225 return "<body>"
226 227
228 - def GenerateEndBody(self, decorations=1):
229 """Return markup to end the body of the HTML document.""" 230 231 result = """ 232 <br /><br /> 233 """ 234 235 return result + self.GenerateStartScript(self.common_javascript) \ 236 + self.GenerateEndScript() + "</body>"
237 238
239 - def GenerateStartScript(self, uri=None):
240 """Return the HTML for beginning a script. 241 242 'uri' -- If not None, a string giving the URI of the script. 243 244 returns -- A string consisting of HTML for beginning an 245 embedded script. 246 247 'GenerateEndScript' must be called later to terminate the script.""" 248 249 # XHTML does not allow the "language" attribute but Netscape 4 250 # requires it. Also, in XHTML we should bracked the included 251 # script as CDATA, but that does not work with Netscape 4 252 # either. 253 result = '<script language="javascript" type="text/javascript"' 254 if uri is not None: 255 result = result + ' src="%s"' % uri 256 result = result + '>' 257 258 return result
259 260
261 - def GenerateEndScript(self):
262 """Return the HTML for ending an embedded script. 263 264 returns -- A string consisting of HTML for ending an 265 embedded script.""" 266 267 return '</script>'
268 269
270 - def MakeLoginForm(self, redirect_request=None, default_user_id=""):
271 if redirect_request is None: 272 # No redirection specified, so redirect back to this page. 273 redirect_request = self.request 274 request = redirect_request.copy("login") 275 request["_redirect_url"] = redirect_request.GetUrl() 276 # Use a POST method to submit the login form, so that passwords 277 # don't appear in web logs. 278 form = request.AsForm(method="post", name="login_form") 279 form = form + \ 280 ''' 281 <table cellpadding="0" cellspacing="0"> 282 <tr><td>User name:</td></tr> 283 <tr><td> 284 <input type="text" 285 size="16" 286 name="_login_user_name" 287 value="%s"/> 288 </td></tr> 289 <tr><td>Password:</td></tr> 290 <tr><td> 291 <input type="password" 292 size="16" 293 name="_login_password"/> 294 </td></tr> 295 <tr><td> 296 <input type="button" 297 value=" Log In " 298 onclick="document.login_form.submit();"/> 299 </td></tr> 300 </table> 301 </form> 302 ''' % default_user_id 303 return form
304 305
306 - def MakeButton(self, title, script_url, css_class=None, **fields):
307 """Generate HTML for a button to load a URL. 308 309 'title' -- The button title. 310 311 'script_url' -- The URL of the script. 312 313 'fields' -- Additional fields to add to the script request. 314 315 'css_class' -- The CSS class to use for the button, or 'None'. 316 317 The resulting HTML must be included in a form.""" 318 319 request = apply(WebRequest, [script_url, self.request], fields) 320 return make_button_for_request(title, request, css_class)
321 322
323 - def MakeImageUrl(self, image):
324 """Generate a URL for an image.""" 325 326 return "/images/%s" % image
327 328
329 - def MakeSpacer(self, width=1, height=1):
330 """Generate a spacer. 331 332 'width' -- The width of the spacer, in pixels. 333 334 'height' -- The height of the spacer, in pixels. 335 336 returns -- A transparent image of the requested size.""" 337 338 # 'clear.gif' is an image file containing a single transparent 339 # pixel, used for generating fixed spacers 340 return '<img border="0" width="%d" height="%d" src="%s"/>' \ 341 % (width, height, self.MakeImageUrl("clear.gif"))
342 343
344 - def MakeRule(self, color="black"):
345 """Generate a plain horizontal rule.""" 346 347 return ''' 348 <table border="0" cellpadding="0" cellspacing="0" width="100%%"> 349 <tr bgcolor="%s"><td>%s</td></tr> 350 </table> 351 ''' % (color, self.MakeSpacer())
352 353
354 - def UserIsInGroup(self, group_id):
355 """Return true if the user is a member of group 'group_id'. 356 357 Checks the group membership of the user associated with the 358 current session. 359 360 If there is no group named 'group_id' in the user database, 361 returns a false result.""" 362 363 user_id = self.request.GetSession().GetUserId() 364 try: 365 group = user.database.GetGroup(group_id) 366 except KeyError: 367 # No such group. 368 return 0 369 else: 370 return user_id in group
371 372 373 374 DtmlPage.default_class = DtmlPage 375 """Set the default DtmlPage implementation to the base class.""" 376 377 DtmlPage.web = sys.modules[DtmlPage.__module__] 378 """Make the functions in this module accessible.""" 379 380 381
382 -class HttpRedirect(Exception):
383 """Exception signalling an HTTP redirect response. 384 385 A script registered with a 'WebServer' instance can raise this 386 exception instead of returning HTML source text, to indicate that 387 the server should send an HTTP redirect (code 302) response to the 388 client instead of the usual code 202 response. 389 390 The exception argument is the URL of the redirect target. The 391 'request' attribute contains a 'WebRequest' for the redirect 392 target.""" 393
394 - def __init__(self, redirect_target_request):
395 """Construct a redirection exception. 396 397 'redirect_target_request' -- The 'WebRequest' to which to 398 redirect the client.""" 399 400 # Initialize the base class. 401 Exception.__init__(self, redirect_target_request.AsUrl()) 402 # Store the request itself. 403 self.request = redirect_target_request
404 405 406
407 -class WebRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
408 """Handler for HTTP requests. 409 410 This class groups callback functions that are invoked in response 411 to HTTP requests by 'WebServer'. 412 413 Don't define '__init__' or store any persistent information in 414 this class or subclasses; a new instance is created for each 415 request. Instead, store the information in the server instance, 416 available through the 'server' attribute.""" 417 418 # Update the extensions_map so that files are mapped to the correct 419 # content-types. 420 SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map.update( 421 { '.css' : 'text/css', 422 '.js' : 'text/javascript' } 423 ) 424
425 - def do_GET(self):
426 """Process HTTP GET requests.""" 427 428 # Parse the query string encoded in the URL, if any. 429 script_url, fields = parse_url_query(self.path) 430 # Build a request object and hand it off. 431 request = apply(WebRequest, (script_url, ), fields) 432 # Store the client's IP address with the request. 433 request.client_address = self.client_address[0] 434 435 self.__HandleRequest(request)
436
437 - def do_POST(self):
438 """Process HTTP POST requests.""" 439 440 # Determine the post's content type. 441 if self.headers.typeheader is None: 442 content_type_header = self.headers.type 443 else: 444 content_type_header = self.headers.typeheader 445 content_type, params = cgi.parse_header(content_type_header) 446 # We only know how to handle form-data submissions. 447 if content_type == "multipart/form-data": 448 # Parse the form data. 449 fields = cgi.parse_multipart(self.rfile, params) 450 # For each field, take the first value, discarding others. 451 # We don't support multi-valued fields. 452 for name, value in fields.items(): 453 if len(value) == 1: 454 fields[name] = value[0] 455 # There may be additional query arguments in the URL, so 456 # parse that too. 457 script_url, url_fields = parse_url_query(self.path) 458 # Merge query arguments from the form and from the URL. 459 fields.update(url_fields) 460 # Create and process a request. 461 request = apply(WebRequest, (script_url, ), fields) 462 # Store the client's IP address with the request. 463 request.client_address = self.client_address[0] 464 self.__HandleRequest(request) 465 else: 466 self.send_response(400, 467 "Unexpected request (POST of %s)." 468 % content_type)
469 470
471 - def __HandleScriptRequest(self, request):
472 try: 473 # Execute the script. The script returns the HTML 474 # text to return to the client. 475 try: 476 script_output = self.server.ProcessScript(request) 477 except NoSessionError, msg: 478 script_output = self.server.HandleNoSessionError(request, msg) 479 except InvalidSessionError, msg: 480 script_output = generate_login_form(request, msg) 481 except HttpRedirect, redirection: 482 # The script requested an HTTP redirect response to 483 # the client. 484 self.send_response(302) 485 self.send_header("Location", str(redirection)) 486 self.end_headers() 487 return 488 except SystemExit: 489 self.server.RequestShutdown() 490 script_output = ("<html><b>%s shutdown.</b></html>" 491 % qm.common.program_name) 492 except: 493 # Oops, the script raised an exception. Show 494 # information about the exception instead. 495 script_output = format_exception(sys.exc_info()) 496 # Send its output. 497 if isinstance(script_output, types.StringType): 498 # The return value from the script is a string. Assume it's 499 # HTML text, and send it appropriate.ly. 500 mime_type = "text/html" 501 data = script_output 502 elif isinstance(script_output, types.TupleType): 503 # The return value from the script is a tuple. Assume the 504 # first element is a MIME type and the second is result 505 # data. 506 mime_type, data = script_output 507 else: 508 raise ValueError 509 self.send_response(200) 510 self.send_header("Content-Type", mime_type) 511 self.send_header("Content-Length", len(data)) 512 # Since this is a dynamically-generated page, indicate that it 513 # should not be cached. The second header is necessary to support 514 # HTTP/1.0 clients. 515 self.send_header("Cache-Control", "no-cache") 516 self.send_header("Pragma", "no-cache") 517 self.end_headers() 518 try: 519 self.wfile.write(data) 520 except IOError: 521 # Couldn't write to the client. Oh well, it's probably a 522 # nework problem, or the user cancelled the operation, or 523 # the browser crashed... 524 pass
525 526
527 - def __HandleFileRequest(self, request, path):
528 # There should be no query arguments to a request for an 529 # ordinary file. 530 if len(request.keys()) > 0: 531 self.send_error(400, "Unexpected request.") 532 return 533 # Open the file. 534 try: 535 file = open(path, "rb") 536 except IOError: 537 # Send a generic 404 if there's a problem opening the file. 538 self.send_error(404, "File not found.") 539 return 540 # Send the file. 541 self.send_response(200) 542 self.send_header("Content-Type", self.guess_type(path)) 543 self.send_header("Cache-Control", "public") 544 self.end_headers() 545 self.copyfile(file, self.wfile)
546 547
548 - def __HandlePageCacheRequest(self, request):
549 """Process a retrieval request from the global page cache.""" 550 551 # Get the page from the cache. 552 page = self.server.GetCachedPage(request) 553 # Send it. 554 self.send_response(200) 555 self.send_header("Content-Type", "text/html") 556 self.send_header("Content-Length", str(len(page))) 557 self.send_header("Cache-Control", "public") 558 self.end_headers() 559 self.wfile.write(page)
560 561
562 - def __HandleSessionCacheRequest(self, request):
563 """Process a retrieval request from the session page cache.""" 564 565 # Extract the session ID. 566 session_id = request.GetSessionId() 567 if session_id is None: 568 # We should never get request for pages from the session 569 # cache without a session ID. 570 self.send_error(400, "Missing session ID.") 571 return 572 # Get the page from the cache. 573 page = self.server.GetCachedPage(request, session_id) 574 # Send it. 575 self.send_response(200) 576 self.send_header("Content-Type", "text/html") 577 self.send_header("Content-Length", str(len(page))) 578 self.send_header("Cache-Control", "private") 579 self.end_headers() 580 self.wfile.write(page)
581 582
583 - def __HandleRequest(self, request):
584 """Process a request from a GET or POST operation. 585 586 'request' -- A 'WebRequest' object.""" 587 588 if request.GetScriptName() == _page_cache_name: 589 # It's a request from the global page cache. 590 self.__HandlePageCacheRequest(request) 591 elif request.GetScriptName() == _session_cache_name: 592 # It's a request from the session page cache. 593 self.__HandleSessionCacheRequest(request) 594 # Check if this request corresponds to a script. 595 elif self.server.IsScript(request): 596 # It is, so run it. 597 self.__HandleScriptRequest(request) 598 else: 599 # Now check if it maps onto a file. Translate the script URL 600 # into a file system path. 601 path = self.server.TranslateRequest(request) 602 # Is it a file? 603 if path is not None and os.path.isfile(path): 604 self.__HandleFileRequest(request, path) 605 606 else: 607 # The server doesn't know about this URL. 608 self.send_error(404, "File not found.")
609 610
611 - def log_message(self, format, *args):
612 """Log a message; overrides 'BaseHTTPRequestHandler.log_message'.""" 613 614 # Write an Apache-style log entry via the server instance. 615 message = "%s - - [%s] %s\n" \ 616 % (self.address_string(), 617 self.log_date_time_string(), 618 format%args) 619 self.server.LogMessage(message)
620 621 622
623 -class HTTPServer(BaseHTTPServer.HTTPServer):
624 """Workaround for problems in 'BaseHTTPServer.HTTPServer'. 625 626 The Python 1.5.2 library's implementation of 627 'BaseHTTPServer.HTTPServer.server_bind' seems to have difficulties 628 when the local host address cannot be resolved by 'gethostbyaddr'. 629 This may happen for a variety of reasons, such as reverse DNS 630 misconfiguration. This subclass fixes that problem.""" 631
632 - def server_bind(self):
633 """Override 'server_bind' to store the server name.""" 634 635 # The problem occurs when an empty host name is specified as the 636 # local address to which the socket binds. Specifying an empty 637 # host name causes the socket to bind to 'INADDR_ANY', which 638 # indicates that the socket should be bound to all interfaces. 639 # 640 # If the socket is bound to 'INADDR_ANY', 'gethostname' returns 641 # '0.0.0.0'. In this case, 'BaseHTTPServer' tries unhelpfully 642 # to obtain a host name to associate with the socket by calling 643 # 'gethostname' and then 'gethostbyaddr' on the result. This 644 # will raise a socket error if reverse lookup on the (primary) 645 # host address fails. So, we use our own method to retrieve the 646 # local host name, which fails more gracefully under this 647 # circumstance. 648 649 SocketServer.TCPServer.server_bind(self) 650 host, port = self.socket.getsockname() 651 652 # Use the primary host name if we're bound to all interfaces. 653 # This is a bit misleading, because the primary host name may 654 # not be bound to all interfaces. 655 if not host or host == '0.0.0.0': 656 host = socket.gethostname() 657 658 # Try the broken 'BaseHTTPServer' implementation. 659 try: 660 hostname, hostnames, hostaddrs = socket.gethostbyaddr(host) 661 if '.' not in hostname: 662 for host in hostnames: 663 if '.' in host: 664 hostname = host 665 break 666 except socket.error: 667 # If it bombs, use our more lenient method. 668 hostname = qm.platform.get_host_name() 669 670 self.server_name = hostname 671 self.server_port = port
672 673 674
675 -class WebServer(HTTPServer):
676 """A web server that serves ordinary files and dynamic content. 677 678 To configure the server to serve ordinary files, register the 679 directories containing those files with 680 'RegisterPathTranslations'. An arbitrary number of directories 681 may be specified, and all files in each directory and under it 682 are made available. 683 684 To congifure the server to serve dynamic content, register dynamic 685 URLs with 'RegisterScript'. A request matching the URL exactly 686 will cause the server to invoke the provided function. 687 688 The web server resolves request URLs in a two-step process. 689 690 1. The server checks if the URL matches exactly a script URL. If 691 a match is found, the corresponding function is invoked, and 692 its return value is sent to the client. 693 694 2. The server checks whether any registered path translation is a 695 prefix of the reqest URL. If it is, the path is translated 696 into a file system path, and the corresponding file is 697 returned. 698 699 The server also provides a rudimentary manual caching mechanism for 700 generated pages. The application may insert a generated page into 701 the page cache, if it is expected not to change. The application 702 can use this mechanism: 703 704 - to supress duplicate generation of the same page, 705 706 - or to pre-generate a page that may be requested later. This is 707 particularly handy if generating the page requires state 708 information that would be difficult to reconstruct later. 709 710 Pages may be shared across sessions, or may be specific to a 711 particular session.""" 712 713
714 - def __init__(self, 715 port, 716 address="", 717 log_file=sys.stderr):
718 """Create a new web server. 719 720 'port' -- The port on which to accept connections. If 'port' 721 is '0', then any port will do. 722 723 'address' -- The local address to which to bind. An empty 724 string means bind to all local addresses. 725 726 'log_file' -- A file object to which to write log messages. 727 If it's 'None', no logging. 728 729 The server is not started until the 'Bind' and 'Run' methods are 730 invoked.""" 731 732 self.__port = port 733 self.__address = address 734 self.__log_file = log_file 735 self.__scripts = {} 736 self.__translations = {} 737 self.__shutdown_requested = 0 738 739 self.RegisterScript("/problems.html", self._HandleProblems) 740 self.RegisterScript("/", self._HandleRoot) 741 742 # Register the common JavaScript. 743 self.RegisterPathTranslation( 744 "/common.js", qm.get_share_directory("web", "common.js")) 745 746 self.__cache_dir = temporary_directory.TemporaryDirectory() 747 self.__cache_path = self.__cache_dir.GetPath() 748 os.mkdir(os.path.join(self.__cache_path, "sessions"), 0700) 749 750 # Create a temporary attachment store to process attachment data 751 # uploads. 752 self.__temporary_store = qm.attachment.TemporaryAttachmentStore() 753 self.RegisterScript(qm.fields.AttachmentField.upload_url, 754 self.__temporary_store.HandleUploadRequest)
755 756 # Don't call the base class __init__ here, since we don't want 757 # to create the web server just yet. Instead, we'll call it 758 # when it's time to run the server. 759 760
761 - def RegisterScript(self, script_path, script):
762 """Register a dynamic URL. 763 764 'script_path' -- The URL for this script. A request must 765 match this path exactly. 766 767 'script' -- A callable to invoke to generate the page 768 content. 769 770 If you register 771 772 web_server.RegisterScript('/cgi-bin/myscript', make_page) 773 774 then the URL 'http://my.server.com/cgi-bin/myscript' will 775 respond with the output of calling 'make_page'. 776 777 The script is passed a single argument, a 'WebRequest' 778 instance. It returns the HTML source, as a string, of the 779 page it generates. If it returns a tuple instead, the first 780 element is taken to be a MIME type and the second is the data. 781 782 The script may instead raise an 'HttpRedirect' instance, 783 indicating an HTTP redirect response should be sent to the 784 client.""" 785 786 self.__scripts[script_path] = script
787 788
789 - def RegisterPathTranslation(self, url_path, file_path):
790 """Register a path translation. 791 792 'url_path' -- The path in URL-space to map from. URLs of 793 which 'url_path' is a prefix can be translated. 794 795 'file_path' -- The file system path corresponding to 796 'url_path'. 797 798 For example, if you register 799 800 web_server.RegisterPathTranslation('/images', '/path/to/pictures') 801 802 the URL 'http://my.server.com/images/big/tree.gif' will be 803 mapped to the file path '/path/to/pictures/big/tree.gif'.""" 804 805 self.__translations[url_path] = file_path
806 807
808 - def IsScript(self, request):
809 """Return a true value if 'request' corresponds to a script.""" 810 811 return self.__scripts.has_key(request.GetUrl())
812 813
814 - def ProcessScript(self, request):
815 """Process 'request' as a script. 816 817 'request' -- A 'WebRequest' object. 818 819 returns -- The output of the script.""" 820 821 return self.__scripts[request.GetUrl()](request)
822 823
824 - def TranslateRequest(self, request):
825 """Translate the URL in 'request' to a file system path. 826 827 'request' -- A 'WebRequest' object. 828 829 returns -- A path to the corresponding file, or 'None' if the 830 request URL didn't match any translations.""" 831 832 path = request.GetUrl() 833 # Loop over translations. 834 for url_path, file_path in self.__translations.items(): 835 # Is this translation a prefix of the URL? 836 if path[:len(url_path)] == url_path: 837 # Yes. First cut off the prefix that matched. 838 sub_path = path[len(url_path):] 839 # Make sure what's left doesn't look like an absolute path. 840 if os.path.isabs(sub_path): 841 sub_path = sub_path[1:] 842 # Construct the file system path. 843 if sub_path: 844 file_path = os.path.join(file_path, sub_path) 845 return file_path 846 # No match was found. 847 return None
848 849
850 - def Bind(self):
851 """Bind the server to the specified address and port. 852 853 Does not start serving.""" 854 855 # Initialize the base class here. This binds the server 856 # socket. 857 try: 858 # Base class initialization. Unfortunately, the base 859 # class's initializer function (actually, its own base 860 # class's initializer function, 'TCPServer.__init__') 861 # doesn't provide a way to set options on the server socket 862 # after it's created but before it's bound. 863 # 864 # If the SO_REUSEADDR option is not set before the socket is 865 # bound, the bind operation will fail if there is alreay a 866 # socket on the same port in the TIME_WAIT state. This 867 # happens most frequently if a server is terminated and then 868 # promptly restarted on the same port. Eventually, the 869 # socket is cleaned up and the port number is available 870 # again, but it's a big nuisance. The SO_REUSEADDR option 871 # allows the new socket to be bound to the same port 872 # immediately. 873 # 874 # So that we can insert the call to 'setsockopt' between the 875 # socket creation and bind, we duplicate the body of 876 # 'TCPServer.__init__' here and add the call. 877 self.RequestHandlerClass = WebRequestHandler 878 self.socket = socket.socket(self.address_family, 879 self.socket_type) 880 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 881 self.server_address = (self.__address, self.__port) 882 self.server_bind() 883 self.server_activate() 884 except socket.error, error: 885 error_number, message = error 886 if error_number == errno.EADDRINUSE: 887 # The specified address/port is already in use. 888 if self.__address == "": 889 address = "port %d" % self.__port 890 else: 891 address = "%s:%d" % (self.__address, self.__port) 892 raise AddressInUseError, address 893 elif error_number == errno.EACCES: 894 # Permission denied. 895 raise PrivilegedPortError, "port %d" % self.__port 896 else: 897 # Propagate other exceptions. 898 raise
899 900
901 - def Run(self):
902 """Start the web server. 903 904 preconditions -- The server must be bound.""" 905 906 while not self.__shutdown_requested: 907 self.handle_request()
908 909
910 - def RequestShutdown(self):
911 """Shut the server down after processing the current request.""" 912 913 self.__shutdown_requested = 1
914 915
916 - def LogMessage(self, message):
917 """Log a message.""" 918 919 if self.__log_file is not None: 920 self.__log_file.write(message) 921 self.__log_file.flush()
922 923
924 - def GetServerAddress(self):
925 """Return the host address on which this server is running. 926 927 returns -- A pair '(hostname, port)'.""" 928 929 return (self.server_name, self.server_port)
930 931
933 """Return the 'AttachmentStore' used for new 'Attachment's. 934 935 returns -- The 'AttachmentStore' used for new 'Attachment's.""" 936 937 return self.__temporary_store
938 939
940 - def MakeButtonForCachedPopup(self, 941 label, 942 html_text, 943 request=None, 944 window_width=480, 945 window_height=240):
946 """Construct a button for displaying a cached popup page. 947 948 'label' -- The button label. 949 950 'html_text' -- The HTML source for the popup page. 951 952 'window_width' -- The width, in pixels, of the popup window. 953 954 'window_height' -- The height, in pixels, of the popup window. 955 956 returns -- HTML source for the button. The button must be placed 957 within a form element.""" 958 959 # Place the page in the page cache. 960 if request is None: 961 session_id = None 962 else: 963 session_id = request.GetSessionId() 964 page_url = self.CachePage(html_text, session_id).AsUrl() 965 966 return make_button_for_popup(label, page_url, window_width, 967 window_height)
968 969
970 - def MakeConfirmationDialog(self, message, url):
971 """Generate JavaScript for a confirmation dialog box. 972 973 'url' -- The location in the main browser window is set to the URL 974 if the user confirms the action. 975 976 See 'make_popup_dialog_script' for a description of 'function_name' 977 and 'message' and information on how to use the return value.""" 978 979 # If the user clicks the "Yes" button, advance the main browser 980 # page. 981 open_script = "window.opener.document.location = %s;" \ 982 % make_javascript_string(url) 983 # Two buttons: "Yes" and "No". "No" doesn't do anything. 984 buttons = [ 985 ( "Yes", open_script ), 986 ( "No", None ), 987 ] 988 return self.MakePopupDialog(message, buttons, title="Confirm")
989 990
991 - def MakePopupDialog(self, message, buttons, title=""):
992 """Generate JavaScript to show a popup dialog box. 993 994 The popup dialog box displays a message and one or more buttons. 995 Each button can have a JavaScript statement (or statements) 996 associated with it; if the button is clicked, the statement is 997 invoked. After any button is clicked, the popup window is closed as 998 well. 999 1000 'message' -- HTML source of the message to display in the popup 1001 window. 1002 1003 'buttons' -- A sequence of button specifications. Each is a pair 1004 '(caption, script)'. 'caption' is the button caption. 'script' is 1005 the JavaScript statement to invoke when the button is clicked, or 1006 'None'. 1007 1008 'title' -- The popup window title. 1009 1010 returns -- JavaScript statements to show the dialog box, suiteable 1011 for use as an event handler.""" 1012 1013 # Construct the popup page. 1014 page = make_popup_page(message, buttons, title) 1015 page_url = self.CachePage(page).AsUrl() 1016 # Construct the JavaScript variable and function. 1017 return "window.open('%s', 'popup', 'width=480,height=200,resizable')" \ 1018 % page_url
1019 1020
1021 - def CachePage(self, page_text, session_id=None):
1022 """Cache an HTML page. 1023 1024 'page_text' -- The text of the page. 1025 1026 'session_id' -- The session ID for this page, or 'None'. 1027 1028 returns -- A 'WebRequest' object with which the cached page can be 1029 retrieved later. 1030 1031 If 'session_id' is 'None', the page is placed in the global page 1032 cache. Otherwise, it is placed in the session page cache for that 1033 session.""" 1034 1035 if session_id is None: 1036 # No path was specified. Place the file in the top directory. 1037 dir_path = self.__cache_path 1038 script_name = _page_cache_name 1039 else: 1040 # A session was specified. Put the file in a subdirectory named 1041 # after the session. 1042 dir_path = os.path.join(self.__cache_path, "sessions", session_id) 1043 script_name = _session_cache_name 1044 # Create that directory if it doesn't exist. 1045 if not os.path.isdir(dir_path): 1046 os.mkdir(dir_path, 0700) 1047 1048 # Generate a name for the page. 1049 global _counter 1050 page_name = str(_counter) 1051 _counter = _counter + 1 1052 # Write it. 1053 page_file_name = os.path.join(dir_path, page_name) 1054 page_file = open(page_file_name, "w", 0600) 1055 page_file.write(page_text) 1056 page_file.close() 1057 1058 # Return a request for this page. 1059 request = WebRequest(script_name, page=page_name) 1060 if session_id is not None: 1061 request.SetSessionId(session_id) 1062 return request
1063 1064
1065 - def GetCachedPage(self, request, session_id=None):
1066 """Retrieve a page from the page cache. 1067 1068 'request' -- The URL requesting the page from the cache. 1069 1070 'session_id' -- The session ID for the request, or 'None'. 1071 1072 returns -- The cached page, or a placeholder page if the page was 1073 not found in the cache. 1074 1075 If 'session_id' is 'None', the page is retrieved from the global 1076 page cache. Otherwise, it is retrieved from the session page cache 1077 for that session.""" 1078 1079 page_file_name = self.__GetPathForCachedPage(request, session_id) 1080 if os.path.isfile(page_file_name): 1081 # Return the page. 1082 return open(page_file_name, "r").read() 1083 else: 1084 # Oops, no such page. Generate a placeholder. 1085 return """ 1086 <html> 1087 <body> 1088 <h3>Cache Error</h3> 1089 <p>You have requested a page that no longer is in the server's 1090 cache. The server may have been restarted, or the page may 1091 have expired. Please start over.</p> 1092 <!-- %s --> 1093 </body> 1094 </html> 1095 """ % url
1096 1097
1098 - def __GetPathForCachedPage(self, request, session_id):
1099 """Return the path for a cached page. 1100 1101 'request' -- The URL requesting the page from the cache. 1102 1103 'session_id' -- The session ID for the request, or 'None'.""" 1104 1105 if session_id is None: 1106 dir_path = self.__cache_path 1107 else: 1108 # Construct the path to the directory containing pages in the 1109 # cache for 'session_id'. 1110 dir_path = os.path.join(self.__cache_path, "sessions", session_id) 1111 # Construct the path to the file containing the page. 1112 page_name = request["page"] 1113 return os.path.join(dir_path, page_name)
1114 1115
1116 - def HandleNoSessionError(self, request, message):
1117 """Handler when session is absent.""" 1118 1119 # There's no session specified in this request. Try to 1120 # create a session for the default user. 1121 try: 1122 user_id = user.authenticator.AuthenticateDefaultUser() 1123 except user.AuthenticationError: 1124 # Couldn't get a default user session, so bail. 1125 return generate_login_form(request, message) 1126 # Authenticating the default user succeeded. Create an implicit 1127 # session with the default user ID. 1128 session = Session(request, user_id) 1129 # Redirect to the same page but using the new session ID. 1130 request.SetSessionId(session.GetId()) 1131 raise HttpRedirect(request)
1132 1133
1134 - def _HandleProblems(self, request):
1135 """Handle internal errors.""" 1136 1137 return DtmlPage.default_class("problems.dtml")(request)
1138 1139
1140 - def _HandleRoot(self, request):
1141 """Handle the '/' URL.""" 1142 1143 raise HttpRedirect, WebRequest("/static/index.html")
1144 1145
1146 - def handle_error(self, request, client_address):
1147 """Handle an error gracefully.""" 1148 1149 # The usual cause of an error is a broken pipe; the user 1150 # may have clicked on something else in the browser before 1151 # we have time to finish writing the response to the browser. 1152 # In that case, we will get EPIPE when trying to write to the 1153 # pipe. 1154 # 1155 # The default behavior (inherited from BaseHTTPServer) 1156 # is to print the traceback to the standard error, which is 1157 # definitely not the right behavior for QMTest. If there 1158 # are any errors for which we must take explicit action, 1159 # we will have to add logic to handle them here. 1160 return
1161 1162 1163
1164 -class WebRequest:
1165 """An object representing a request from the web server. 1166 1167 A 'WebRequest' object behaves as a dictionary of key, value pairs 1168 representing query arguments, for instance query fields in a POST, 1169 or arguments encoded in a URL query string. It has some other 1170 methods as well.""" 1171
1172 - def __init__(self, script_url, base=None, keep_fields=False, **fields):
1173 """Create a new request object. 1174 1175 'script_url' -- The URL of the script that processes this 1176 query. 1177 1178 'base' -- A request object from which the session ID will be 1179 duplicated, or 'None'. 1180 1181 'fields' -- The query arguments.""" 1182 1183 self.__url = script_url 1184 self.__fields = {} 1185 if base and keep_fields: 1186 self.__fields.update(base.__fields) 1187 self.__fields.update(fields) 1188 # Copy the session ID from the base. 1189 if base is not None: 1190 session = base.GetSessionId() 1191 if session is not None: 1192 self.SetSessionId(session) 1193 self.client_address = base.client_address
1194 1195
1196 - def __str__(self):
1197 str = "WebRequest for %s\n" % self.__url 1198 for name, value in self.__fields.items(): 1199 str = str + "%s=%s\n" % (name, repr(value)) 1200 return str
1201 1202
1203 - def GetUrl(self):
1204 """Return the URL of the script that processes this request.""" 1205 1206 return self.__url
1207 1208
1209 - def GetScriptName(self):
1210 """Return the name of the script that processes this request. 1211 1212 The script name is the final element of the full URL path.""" 1213 1214 return string.split(self.__url, "/")[-1]
1215 1216
1217 - def SetSessionId(self, session_id):
1218 """Set the session ID for this request to 'session_id'.""" 1219 1220 self[session_id_field] = session_id
1221 1222
1223 - def GetSessionId(self):
1224 """Return the session ID for this request. 1225 1226 returns -- A session ID, or 'None'.""" 1227 1228 return self.get(session_id_field, None)
1229 1230
1231 - def GetSession(self):
1232 """Return the session for this request. 1233 1234 raises -- 'NoSessionError' if no session ID is specified in the 1235 request. 1236 1237 raises -- 'InvalidSessionError' if the session ID specified in 1238 the request is invalid.""" 1239 1240 session_id = self.GetSessionId() 1241 if session_id is None: 1242 raise NoSessionError, qm.error("session required") 1243 else: 1244 return get_session(self, session_id)
1245 1246
1247 - def AsUrl(self, last_argument=None):
1248 """Return the URL representation of this request. 1249 1250 'fields_at_end' -- If not 'None', the name of the URL query 1251 arguments that should be placed last in the list of arugmnets 1252 (other than this, the order of query arguments is not 1253 defined).""" 1254 1255 if len(self.keys()) == 0: 1256 # No query arguments; just use the script URL. 1257 return self.GetUrl() 1258 else: 1259 # Encode query arguments into the URL. 1260 return "%s?%s" % (self.GetUrl(), urllib.urlencode(self))
1261 1262
1263 - def AsForm(self, method="get", name=None):
1264 """Return an opening form tag for this request. 1265 1266 'method' -- The HTML method to use for the form, either "get" or 1267 "post". 1268 1269 'name' -- A name for the form, or 'None'. 1270 1271 returns -- An opening form tag for the request, plus hidden 1272 input elements for arguments to the request. 1273 1274 The caller must add additional inputs, the submit input, and 1275 close the form tag.""" 1276 1277 if name is not None: 1278 name_attribute = 'name="%s"' % name 1279 else: 1280 name_attribute = '' 1281 # Generate the form tag. 1282 if method == "get": 1283 result = '<form method="get" action="%s" %s>\n' \ 1284 % (self.GetUrl(), name_attribute) 1285 elif method == "post": 1286 result = '''<form %s 1287 method="post" 1288 enctype="multipart/form-data" 1289 action="%s">\n''' \ 1290 % (name_attribute, self.GetUrl()) 1291 else: 1292 raise ValueError, "unknown method %s" % method 1293 # Add hidden inputs for the request arguments. 1294 for name, value in self.items(): 1295 result = result \ 1296 + '<input type="hidden" name="%s" value="%s">\n' \ 1297 % (name, value) 1298 1299 return result
1300 1301 1302 # Methods to emulate a mapping. 1303
1304 - def __getitem__(self, key):
1305 return self.__fields[key]
1306 1307
1308 - def __setitem__(self, key, value):
1309 self.__fields[key] = value
1310 1311
1312 - def __delitem__(self, key):
1313 del self.__fields[key]
1314 1315
1316 - def get(self, key, default=None):
1317 return self.__fields.get(key, default)
1318 1319
1320 - def keys(self):
1321 return self.__fields.keys()
1322 1323
1324 - def has_key(self, key):
1325 return self.__fields.has_key(key)
1326 1327
1328 - def items(self):
1329 return self.__fields.items()
1330 1331
1332 - def copy(self, url=None, **fields):
1333 """Return a duplicate of this request. 1334 1335 'url' -- The URL for the request copy. If 'None', use the 1336 URL of the source. 1337 1338 '**fields' -- Additional fields to set in the copy.""" 1339 1340 # Copy the URL unless another was specified. 1341 if url is None: 1342 url = self.__url 1343 # Copy fields, and update with any that were specified 1344 # additionally. 1345 new_fields = self.__fields.copy() 1346 new_fields.update(fields) 1347 # Make the request. 1348 new_request = apply(WebRequest, (url, ), new_fields) 1349 # Copy the client address, if present. 1350 if hasattr(self, "client_address"): 1351 new_request.client_address = self.client_address 1352 1353 return new_request
1354 1355 1356
1357 -class CGIWebRequest:
1358 """A 'WebRequest' object initialized from the CGI environment.""" 1359
1360 - def __init__(self):
1361 """Create a new request from the current CGI environment. 1362 1363 preconditions -- The CGI environment (environment variables 1364 etc.) must be in place.""" 1365 1366 assert os.environ.has_key("GATEWAY_INTERFACE") 1367 assert os.environ["GATEWAY_INTERFACE"][:3] == "CGI" 1368 1369 self.__fields = cgi.FieldStorage()
1370 1371
1372 - def GetUrl(self):
1373 return os.environ["SCRIPT_NAME"]
1374 1375
1376 - def __getitem__(self, key):
1377 return self.__fields[key].value
1378 1379
1380 - def keys(self):
1381 return self.__fields.keys()
1382 1383
1384 - def has_key(self, key):
1385 return self.__fields.has_key(key)
1386 1387
1388 - def copy(self):
1389 """Return a copy of the request. 1390 1391 The copy isn't tied to the CGI environment, so it can be 1392 modified safely.""" 1393 1394 fields = {} 1395 for key in self.keys(): 1396 fields[key] = self[key] 1397 return apply(WebRequest, (self.GetUrl(), ), fields)
1398 1399
1400 -def _create_session_id():
1401 1402 # Seed the random number generator with the system time. 1403 random.seed() 1404 try: # hashlib is available since Python 2.5 1405 import hashlib 1406 md5 = hashlib.md5() 1407 md5.update("%f" % random.random()) 1408 digest = md5.digest() 1409 except: # fall back to md5 on older Python versions 1410 import md5 1411 # FIXME: Security: Is this OK? 1412 digest = md5.new("%f" % random.random()).digest() 1413 1414 # Convert the digest, which is a 16-character string, 1415 # to a sequence hexadecimal bytes. 1416 digest = [hex(ord(c))[2:] for c in digest] 1417 # Convert it to a 32-character string. 1418 return ''.join(digest)
1419 1420
1421 -class Session:
1422 """A persistent user session. 1423 1424 A 'Session' object represents an ongoing user interaction with the 1425 web server.""" 1426
1427 - def __init__(self, request, user_id, expiration_timeout=21600):
1428 """Create a new session. 1429 1430 'request' -- A 'WebRequest' object in response to which this 1431 session is created. 1432 1433 'user_id' -- The ID of the user owning the session. 1434 1435 'expiration_timeout -- The expiration time, in seconds. If a 1436 session is not accessed for this duration, it is expired and no 1437 longer usable.""" 1438 1439 self.__user_id = user_id 1440 self.__expiration_timeout = expiration_timeout 1441 # Extract the client's IP address from the request. 1442 self.__client_address = request.client_address 1443 1444 self.__id = _create_session_id() 1445 1446 self.Touch() 1447 1448 # Record ourselves in the sessions map. 1449 sessions[self.__id] = self
1450 1451
1452 - def Touch(self):
1453 """Update the last access time on the session to now.""" 1454 1455 self.__last_access_time = time.time()
1456 1457
1458 - def GetId(self):
1459 """Return the session ID.""" 1460 1461 return self.__id
1462 1463
1464 - def GetUserId(self):
1465 """Return the ID of the user who owns this session.""" 1466 1467 return self.__user_id
1468 1469
1470 - def GetUser(self):
1471 """Return the user record for the owning user. 1472 1473 returns -- A 'qm.user.User' object.""" 1474 1475 return user.database[self.__user_id]
1476 1477
1478 - def IsDefaultUser(self):
1479 """Return true if the owning user is the default user.""" 1480 1481 return self.GetUserId() == user.database.GetDefaultUserId()
1482 1483
1484 - def IsExpired(self):
1485 """Return true if this session has expired.""" 1486 1487 age = time.time() - self.__last_access_time 1488 return age > self.__expiration_timeout
1489 1490
1491 - def Validate(self, request):
1492 """Make sure the session is OK for a request. 1493 1494 'request' -- A 'WebRequest' object. 1495 1496 raises -- 'InvalidSessionError' if the session is invalid for 1497 the request.""" 1498 1499 # Make sure the client IP address in the request matches that 1500 # for this session. 1501 if self.__client_address != request.client_address: 1502 raise InvalidSessionError, qm.error("session wrong IP") 1503 # Make sure the session hasn't expired. 1504 if self.IsExpired(): 1505 raise InvalidSessionError, qm.error("session expired")
1506 1507 1508 1509 ######################################################################## 1510 # functions 1511 ######################################################################## 1512
1513 -def parse_url_query(url):
1514 """Parse a URL-encoded query. 1515 1516 This function parses query strings encoded in URLs, such as 1517 '/script.cgi?key1=val1&key2=val2'. For this example, it would 1518 return '("/script.cgi", {"key1" : "val1", "key2" : "val2"})' 1519 1520 'url' -- The URL to parse. 1521 1522 returns -- A pair containing the the base script path and a 1523 mapping of query field names to values.""" 1524 1525 # Check if the script path is a URL-encoded query. 1526 if "?" in url: 1527 # Yes. Everything up to the question mark is the script 1528 # path; stuff after that is the query string. 1529 script_url, query_string = string.split(url, "?", 1) 1530 # Parse the query string. 1531 fields = cgi.parse_qs(query_string) 1532 # We only handle one instance of each key in the query. 1533 # 'parse_qs' produces a list of values for each key; check 1534 # that each list contains only one item, and replace the 1535 # list with that item. 1536 for key, value_list in fields.items(): 1537 if len(value_list) != 1: 1538 # Tell the client that we don't like this query. 1539 print "WARNING: Multiple values in query." 1540 fields[key] = value_list[0] 1541 else: 1542 # No, it's just an ordinary URL. 1543 script_url = url 1544 fields = {} 1545 1546 script_url = urllib.unquote(script_url) 1547 return (script_url, fields)
1548 1549
1550 -def http_return_html(html_text, stream=sys.stdout):
1551 """Generate an HTTP response consisting of HTML text. 1552 1553 'html_text' -- The HTML souce text to return. 1554 1555 'stream' -- The stream to write the response, by default 1556 'sys.stdout.'.""" 1557 1558 stream.write("Content-type: text/html\n\n") 1559 stream.write(html_text)
1560 1561
1562 -def http_return_exception(exc_info=None, stream=sys.stdout):
1563 """Generate an HTTP response for an exception. 1564 1565 'exc_info' -- A three-element tuple containing exception info, of 1566 the form '(type, value, traceback)'. If 'None', use the exception 1567 currently being handled. 1568 1569 'stream' -- The stream to write the response, by default 1570 'sys.stdout.'.""" 1571 1572 if exc_info == None: 1573 exc_info = sys.exc_info() 1574 1575 stream.write("Content-type: text/html\n\n"); 1576 stream.write(format_exception(exc_info))
1577 1578
1579 -def format_exception(exc_info):
1580 """Format an exception as HTML. 1581 1582 'exc_info' -- A three-element tuple containing exception info, of 1583 the form '(type, value, traceback)'. 1584 1585 returns -- A string containing a complete HTML file displaying the 1586 exception.""" 1587 1588 # Break up the exection info tuple. 1589 type, value, trace = exc_info 1590 # Format the traceback, with a newline separating elements. 1591 traceback_listing = string.join(traceback.format_tb(trace), "\n") 1592 # Construct a page info object to generate an exception page. 1593 page = DtmlPage.default_class( 1594 "exception.dtml", 1595 exception_type=type, 1596 exception_value=value, 1597 traceback_listing=traceback_listing) 1598 # Generate the page. 1599 return page()
1600 1601 1602
1603 -def escape(text):
1604 """Escape special characters in 'text' for formatting as HTML.""" 1605 1606 return structured_text.escape_html_entities(text)
1607 1608 1609 # A regular expression that matches anything that looks like an entity. 1610 __entity_regex = re.compile("&(\w+);") 1611 1612 1613 # A function that returns the replacement for an entity matched by the 1614 # above expression.
1615 -def __replacement_for_entity(match):
1616 entity = match.group(1) 1617 try: 1618 return htmlentitydefs.entitydefs[entity] 1619 except KeyError: 1620 return "&%s;" % entity
1621 1622
1623 -def unescape(text):
1624 """Undo 'escape' by replacing entities with ordinary characters.""" 1625 1626 return __entity_regex.sub(__replacement_for_entity, text)
1627 1628
1629 -def format_structured_text(text):
1630 """Render 'text' as HTML.""" 1631 1632 if text == "": 1633 # In case the text is the only contents of a table cell -- in 1634 # which case an empty string will produce undesirable visual 1635 # effects -- return a single space anyway. 1636 return "&nbsp;" 1637 else: 1638 return structured_text.to_html(text)
1639 1640
1641 -def make_url(script_name, base_request=None, **fields):
1642 """Create a request and return a URL for it. 1643 1644 'script_name' -- The script name for the request. 1645 1646 'base_request' -- If not 'None', the base request for the generated 1647 request. 1648 1649 'fields' -- Additional fields to include in the request.""" 1650 1651 request = apply(WebRequest, (script_name, base_request), fields) 1652 return request.AsUrl()
1653 1654
1655 -def make_button_for_request(title, request, css_class=None):
1656 """Generate HTML for a button. 1657 1658 Note that the caller is responsible for making sure the resulting 1659 button is placed within a form element. 1660 1661 'title' -- The button label. 1662 1663 'request' -- A 'WebRequest' object to be invoked when the button is 1664 clicked. 1665 1666 'css_class' -- The CSS class to use for the button, or 'None'.""" 1667 1668 return make_button_for_url(title, request.AsUrl(), css_class)
1669 1670
1671 -def make_button_for_url(title, url, css_class=None):
1672 """Generate HTML for a button. 1673 1674 Note that the caller is responsible for making sure the resulting 1675 button is placed within a form element. 1676 1677 'title' -- The button label. 1678 1679 'url' -- The URL to load when the button is clicked.. 1680 1681 'css_class' -- The CSS class to use for the button, or 'None'.""" 1682 1683 if css_class is None: 1684 class_attribute = "" 1685 else: 1686 class_attribute = 'class="%s"' % css_class 1687 1688 return ''' 1689 <input type="button" %s 1690 value=" %s " 1691 onclick="location = '%s';"/> 1692 ''' % (class_attribute, title, url)
1693 1694
1695 -def get_session(request, session_id):
1696 """Retrieve the session corresponding to 'session_id'. 1697 1698 'request' -- A 'WebRequest' object for which to get the session. 1699 1700 raises -- 'InvalidSessionError' if the session ID is invalid, or is 1701 invalid for this 'request'.""" 1702 1703 # Now's as good a time as any to clean up expired sessions. 1704 __clean_up_expired_sessions() 1705 1706 try: 1707 # Obtain the session for this ID. 1708 session = sessions[session_id] 1709 except KeyError: 1710 # No session for this ID (note that it may have expired). 1711 raise InvalidSessionError, qm.error("session invalid") 1712 # Make sure the session is valid for this request. 1713 session.Validate(request) 1714 # Update the last access time. 1715 session.Touch() 1716 return session
1717 1718
1719 -def __clean_up_expired_sessions():
1720 """Remove any sessions that are expired.""" 1721 1722 for session_id, session in sessions.items(): 1723 if session.IsExpired(): 1724 del sessions[session_id]
1725 1726
1727 -def handle_login(request, default_redirect_url="/"):
1728 """Handle a login request. 1729 1730 Authenticate the login using the user name and password stored in 1731 the '_login_user_name' and '_login_password' request fields, 1732 respectively. 1733 1734 If authentication succeeds, redirect to the URL stored in the 1735 '_redirect_url' request field by raising an 'HttpRedirect', passing 1736 all other request fields along as well. 1737 1738 If '_redirect_url' is not specified in the request, the value of 1739 'default_redirect_url' is used instead.""" 1740 1741 # The URL of the page to which to redirect on successful login is 1742 # stored in the request. Extract it. 1743 redirect_url = request.get("_redirect_url", default_redirect_url) 1744 1745 try: 1746 user_id = qm.user.authenticator.AuthenticateWebRequest(request) 1747 except qm.user.AuthenticationError: 1748 # Incorrect user name or password. Show the login form. 1749 message = qm.error("invalid login") 1750 redirect_request = WebRequest(redirect_url) 1751 return generate_login_form(redirect_request, message) 1752 except qm.user.AccountDisabledError: 1753 # Log in to a disabled account. Show the login form again. 1754 message = qm.error("disabled account") 1755 redirect_request = WebRequest(redirect_url) 1756 return generate_login_form(redirect_request, message) 1757 1758 # Check if there is currently a session open for the same user ID. 1759 for session in sessions.values(): 1760 if session.GetUserId() == user_id: 1761 # Yup. There should be only one session at a time for any 1762 # given user. Close that session. 1763 del sessions[session.GetId()] 1764 1765 session = Session(request, user_id) 1766 session_id = session.GetId() 1767 1768 # Generate a new request for that URL. Copy other fields from the 1769 # old request. 1770 redirect_request = request.copy(redirect_url) 1771 # Sanitize the request by removing the user name, password, and 1772 # redirecting URL. 1773 del redirect_request["_login_user_name"] 1774 del redirect_request["_login_password"] 1775 if redirect_request.has_key("_redirect_url"): 1776 del redirect_request["_redirect_url"] 1777 # Add the ID of the new session to the request. 1778 redirect_request.SetSessionId(session_id) 1779 # Redirect the client to the URL for the redirected page. 1780 raise HttpRedirect, redirect_request
1781 1782
1783 -def handle_logout(request, default_redirect_url="/"):
1784 """Handle a logout request. 1785 1786 prerequisite -- 'request' must be in a valid session, which is 1787 ended. 1788 1789 After ending the session, redirect to the URL specified by the 1790 '_redirect_url' field of 'request'. If '_redirect_url' is not 1791 specified in the request, the value of 'default_redirect_url' is 1792 used instead.""" 1793 1794 # Delete the session. 1795 session_id = request.GetSessionId() 1796 del sessions[session_id] 1797 # Construct a redirecting URL. The target is contained in the 1798 # '_redirect_url' field. 1799 redirect_url = request.get("_redirect_url", default_redirect_url) 1800 redirect_request = request.copy(redirect_url) 1801 if redirect_request.has_key("_redirect_url"): 1802 del redirect_request["_redirect_url"] 1803 del redirect_request[session_id_field] 1804 # Redirect to the specified request. 1805 raise HttpRedirect, redirect_request
1806 1807
1808 -def generate_error_page(request, error_text):
1809 """Generate a page to indicate a user error. 1810 1811 'request' -- The request that was being processed when the error 1812 was encountered. 1813 1814 'error_text' -- A description of the error, as structured text. 1815 1816 returns -- The generated HTML source for the page.""" 1817 1818 page = DtmlPage.default_class("error.dtml", error_text=error_text) 1819 return page(request)
1820 1821
1822 -def generate_login_form(redirect_request, message=None):
1823 """Show a form for user login. 1824 1825 'message' -- If not 'None', a message to display to the user.""" 1826 1827 page = DtmlPage.default_class( 1828 "login_form.dtml", 1829 message=message, 1830 default_user_id=qm.user.database.GetDefaultUserId()) 1831 return page(redirect_request)
1832 1833
1834 -def make_set_control(form_name, 1835 field_name, 1836 add_page, 1837 select_name=None, 1838 initial_elements=[], 1839 request=None, 1840 rows=6, 1841 width=200, 1842 window_width=480, 1843 window_height=240, 1844 ordered=0):
1845 """Construct a control for representing a set of items. 1846 1847 'form_name' -- The name of form in which the control is included. 1848 1849 'field_name' -- The name of the input control that contains an 1850 encoded representation of the set's elements. See 1851 'encode_set_control_contents' and 'decode_set_control_contents'. 1852 1853 'select_name' -- The name of the select control that displays the 1854 elements of the set. If 'None', a control name is generated 1855 automatically. 1856 1857 'add_page' -- The URL for a popup web page that is displayed 1858 in response to the "Add..." button. 1859 1860 'initial_elements' -- The initial elements of the set. 1861 1862 'rows' -- The number of rows for the select control. 1863 1864 'width' -- The width of the select control. 1865 1866 'window_width', 'window_height' -- The width and height of the popup 1867 window for adding a new element. 1868 1869 'ordered' -- If true, controls are included for specifying the order 1870 of elements in the set.""" 1871 1872 # Generate a name for the select control if none was specified. 1873 if select_name is None: 1874 select_name = "_set_" + field_name 1875 1876 # Construct the select control. 1877 select = '<select name="%s" size="%d" width="%d">\n' \ 1878 % (select_name, rows, width) 1879 # Add an option for each initial element. 1880 for text, value in initial_elements: 1881 select = select + \ 1882 '<option value="%s">%s</option>\n' % (value, escape(text)) 1883 select = select + '</select>\n' 1884 1885 # Construct the hidden control contianing the set's elements. Its 1886 # initial value is the encoding of the initial elements. 1887 initial_values = map(lambda x: x[1], initial_elements) 1888 initial_value = encode_set_control_contents(initial_values) 1889 contents = '<input type="hidden" name="%s" value="%s"/>' \ 1890 % (field_name, initial_value) 1891 1892 buttons = [] 1893 1894 # Construct the "Add..." button. 1895 buttons.append(make_button_for_popup("Add...", add_page, 1896 window_width, window_height)) 1897 # Construct the "Remove" button. 1898 buttons.append(''' 1899 <input type="button" 1900 size="12" 1901 value=" Remove " 1902 onclick="remove_from_set(document.%s.%s, document.%s.%s);" 1903 />''' % (form_name, select_name, form_name, field_name)) 1904 1905 if ordered: 1906 buttons.append(''' 1907 <input type="button" 1908 size="12" 1909 value=" Move Up " 1910 onclick="move_in_set(document.%s.%s, document.%s.%s, -1);" 1911 />''' % (form_name, select_name, form_name, field_name)) 1912 buttons.append(''' 1913 <input type="button" 1914 size="12" 1915 value=" Move Down " 1916 onclick="move_in_set(document.%s.%s, document.%s.%s, 1);" 1917 />''' % (form_name, select_name, form_name, field_name)) 1918 1919 # Arrange everything in a table to control the layout. 1920 return contents + ''' 1921 <table border="0" cellpadding="0" cellspacing="0"><tbody> 1922 <tr valign="top"> 1923 <td> 1924 %s 1925 </td> 1926 <td>&nbsp;</td> 1927 <td> 1928 %s 1929 </td> 1930 </tr> 1931 </tbody></table> 1932 ''' % (select, string.join(buttons, "<br />"))
1933 1934
1935 -def encode_set_control_contents(values):
1936 """Encode 'values' for a set control. 1937 1938 'values' -- A sequence of values of elements of the set. 1939 1940 returns -- The encoded value for the control field.""" 1941 1942 return string.join(values, ",")
1943 1944
1945 -def decode_set_control_contents(content_string):
1946 """Decode the contents of a set control. 1947 1948 'content_string' -- The text of the form field containing the 1949 encoded set contents. 1950 1951 returns -- A sequence of the values of the elements of the set.""" 1952 1953 # Oddly, if the set is empty, there are sometimes spurious spaces in 1954 # field entry. This may be browser madness. Handle it specially. 1955 if string.strip(content_string) == "": 1956 return [] 1957 return string.split(content_string, ",")
1958 1959
1960 -def make_properties_control(form_name, 1961 field_name, 1962 properties, 1963 select_name=None):
1964 """Construct a control for representing a set of properties. 1965 1966 'form_name' -- The name of form in which the control is included. 1967 1968 'field_name' -- The name of the input control that contains an 1969 encoded representation of the properties. See 'encode_properties' 1970 and 'decode_properties'. 1971 1972 'properties' -- A map from property names to values of the 1973 properties to include in the control initially. 1974 1975 'select_name' -- The name of the select control that displays the 1976 elements of the set. If 'None', a control name is generated 1977 automatically.""" 1978 1979 # Generate a name for the select control if none was specified. 1980 if select_name is None: 1981 select_name = "_propsel_" + field_name 1982 name_control_name = "_propname_" + field_name 1983 value_control_name = "_propval_" + field_name 1984 add_change_button_name = "_propaddedit_" + field_name 1985 1986 # Construct the select control. 1987 select = ''' 1988 <select name="%s" 1989 size="6" 1990 width="240" 1991 onchange="property_update_selection(document.%s.%s, 1992 document.%s.%s, 1993 document.%s.%s); 1994 document.%s.%s.value = ' Change ';" 1995 >\n''' \ 1996 % (select_name, form_name, select_name, 1997 form_name, name_control_name, form_name, value_control_name, 1998 form_name, add_change_button_name) 1999 # Add an option for each initial property. 2000 keys = properties.keys() 2001 keys.sort() 2002 for k in keys: 2003 select = select + \ 2004 '<option value="%s=%s">%s = %s</option>\n' \ 2005 % (k, properties[k], k, properties[k]) 2006 select = select + '</select>\n' 2007 2008 # Construct the hidden control contianing the set's elements. Its 2009 # initial value is the encoding of the initial elements. 2010 initial_value = encode_properties(properties) 2011 contents = '<input type="hidden" name="%s" value="%s"/>' \ 2012 % (field_name, initial_value) 2013 2014 # Construct a control for the property name. 2015 name_control = \ 2016 '''<input type="text" 2017 name="%s" 2018 size="32" 2019 onkeydown="document.%s.%s.value = ' Add ';" 2020 />''' % (name_control_name, form_name, add_change_button_name) 2021 # Construct a control for the property value. 2022 value_control = '<input type="text" name="%s" size="32"/>' \ 2023 % value_control_name 2024 2025 vars = { 'form' : form_name, 2026 'button' : add_change_button_name, 2027 'select' : select_name, 2028 'field' : field_name, 2029 'name' : name_control_name, 2030 'value' : value_control_name } 2031 2032 # Construct the "Change" button. When it's clicked, call 2033 # 'property_update', passing the select control and the hidden 2034 # control whose value should be updated with the new encoded 2035 # property list. 2036 add_change_button = \ 2037 '''<input type="button" 2038 name="%(button)s" 2039 size="12" 2040 value=" Add " 2041 onclick="property_add_or_change 2042 (document.%(form)s.%(select)s, 2043 document.%(form)s.%(field)s, 2044 document.%(form)s.%(name)s, 2045 document.%(form)s.%(value)s);" 2046 />''' % vars 2047 2048 # Construct the "Remove" button. 2049 remove_button = \ 2050 '''<input type="button" 2051 size="12" 2052 value=" Remove " 2053 onclick="property_remove(document.%(form)s.%(select)s, 2054 document.%(form)s.%(field)s, 2055 document.%(form)s.%(name)s, 2056 document.%(form)s.%(value)s, 2057 document.%(form)s.%(button)s);" 2058 />''' % vars 2059 2060 # Arrange everything in a table to control the layout. 2061 return contents + ''' 2062 <table border="0" cellpadding="0" cellspacing="0"><tbody> 2063 <tr valign="top"> 2064 <td colspan="2" width="240">%s</td> 2065 <td>&nbsp;</td> 2066 <td>%s</td> 2067 </tr> 2068 <tr> 2069 <td>Name:&nbsp;</td> 2070 <td align="right">%s </td> 2071 <td>&nbsp;</td> 2072 <td>%s</td> 2073 </tr> 2074 <tr> 2075 <td>Value:&nbsp;</td> 2076 <td align="right">%s </td> 2077 <td>&nbsp;</td> 2078 <td>&nbsp;</td> 2079 </tr> 2080 </tbody></table> 2081 ''' % (select, remove_button, name_control, add_change_button, 2082 value_control)
2083 2084
2085 -def encode_properties(properties):
2086 """Construct a URL-encoded representation of a set of properties. 2087 2088 'properties' -- A map from property names to values. Names must be 2089 URL-safe strings. Values are arbitrary strings. 2090 2091 returns -- A URL-encoded string representation of 'properties'. 2092 2093 This function is the inverse of 'decode_properties'.""" 2094 2095 # Construct a list of property assignment strings. The RHS is 2096 # URL-quoted. 2097 result = map(lambda p: "%s=%s" % (p[0], urllib.quote_plus(p[1])), 2098 properties.items()) 2099 # Join them into a comma-delimited list. 2100 return string.join(result, ",")
2101 2102
2103 -def decode_properties(properties):
2104 """Decode a URL-encoded representation of a set of properties. 2105 2106 'properties' -- A string containing URL-encoded properties. 2107 2108 returns -- A map from names to values. 2109 2110 This function is the inverse of 'encode_properties'.""" 2111 2112 properties = string.strip(properties) 2113 # Empty? 2114 if properties == "": 2115 return {} 2116 2117 # The string is a comma-delimited list. Split it up. 2118 properties = string.split(properties, ",") 2119 # Convert to a map, processing each item. 2120 result = {} 2121 for assignment in properties: 2122 # Each element is a "name=value" assignment. Split it up. 2123 name, value = string.split(assignment, "=") 2124 # The value is URL-quoted. Unquote it. 2125 value = urllib.unquote_plus(value) 2126 # Set it in the map. 2127 result[name] = value 2128 2129 return result
2130 2131
2132 -def make_javascript_string(text):
2133 """Return 'text' represented as a JavaScript string literal.""" 2134 2135 text = string.replace(text, "\\", r"\\") 2136 text = string.replace(text, "'", r"\'") 2137 text = string.replace(text, "\"", r'\"') 2138 text = string.replace(text, "\n", r"\n") 2139 # Escape less-than signs so browsers don't look for HTML tags 2140 # inside the literal. 2141 text = string.replace(text, "<", r"\074") 2142 return "'" + text + "'"
2143 2144 2162 2163 2195 2196
2197 -def make_popup_page(message, buttons, title=""):
2198 """Generate a popup dialog box page. 2199 2200 See 'make_popup_dialog_script' for an explanation of the 2201 parameters.""" 2202 2203 page = \ 2204 '''<html> 2205 <head> 2206 <title>%s</title> 2207 <meta http-equiv="Content-Style-Type" content="text/css"/> 2208 <link rel="stylesheet" type="text/css" href="/stylesheets/qm.css"/> 2209 </head> 2210 <body class="popup"> 2211 <table border="0" cellpadding="0" cellspacing="8" width="100%%"> 2212 <tr><td> 2213 %s 2214 </td></tr> 2215 <tr><td align="right"> 2216 <form name="navigation"> 2217 ''' % (title, message) 2218 # Generate the buttons. 2219 for caption, script in buttons: 2220 page = page + ''' 2221 <input type="button" 2222 value=" %s "''' % caption 2223 # Whether a script was specified for the button, close the popup 2224 # window. 2225 if script is None: 2226 page = page + ''' 2227 onclick="window.close();"''' 2228 else: 2229 page = page + ''' 2230 onclick="%s; window.close();"''' % script 2231 page = page + ''' 2232 />''' 2233 # End the page. 2234 page = page + ''' 2235 </form> 2236 </td></tr> 2237 </table> 2238 </body> 2239 </html> 2240 ''' 2241 return page
2242 2243
2244 -def make_choose_control(field_name, 2245 included_label, 2246 included_items, 2247 excluded_label, 2248 excluded_items, 2249 item_to_text=str, 2250 item_to_value=str, 2251 ordered=0):
2252 """Construct HTML controls for selecting a subset. 2253 2254 The user is presented with two list boxes next to each other. The 2255 box on the left lists items included in the subset. The box on the 2256 right lists items excluded from the subset but available for 2257 inclusion. Between the boxes are buttons for adding and removing 2258 items from the subset. 2259 2260 If 'ordered' is true, buttons are also shown for reordering items in 2261 the included list. 2262 2263 'field_name' -- The name of an HTML hidden form field that will 2264 contain an encoding of the items included in the subset. The 2265 encoding consists of the values corresponding to included items, in 2266 a comma-separated list. 2267 2268 'included_label' -- HTML source for the label for the left box, 2269 which displays the included items. 2270 2271 'included_items' -- Items initially included in the subset. This is 2272 a sequence of arbitrary objects or values. 2273 2274 'excluded_label' -- HTML source for the label for the right box, 2275 which displays the items available for inclusion but not currently 2276 included. 2277 2278 'excluded_items' -- Items not initially included but available for 2279 inclusion. This is a sequence of arbitrary objects or values. 2280 2281 'item_to_text' -- A function that produces a user-visible text 2282 description of an item. 2283 2284 'item_to_value' -- A function that produces a value for an item, 2285 used as the value for an HTML option object. 2286 2287 'ordered' -- If true, additional controls are displayed to allow the 2288 user to manipulate the order of items in the included set. 2289 2290 returns -- HTML source for the items. Must be placed in a 2291 form.""" 2292 2293 # We'll construct an array of buttons. Each element is an HTML 2294 # input control. 2295 buttons = [] 2296 # Construct the encoding for the items initially included. 2297 initial_value = string.join(map(item_to_value, included_items), ",") 2298 # The hidden control that will contain the encoded representation of 2299 # the included items. 2300 hidden_control = '<input type="hidden" name="%s" value="%s">' \ 2301 % (field_name, initial_value) 2302 # Construct names for the two select controls. 2303 included_select_name = "_inc_" + field_name 2304 excluded_select_name = "_exc_" + field_name 2305 2306 # The select control for included items. When the user selects an 2307 # item in this list, deselect the selected item in the excluded 2308 # list, if any. 2309 included_select = ''' 2310 <select name="%s" 2311 width="160" 2312 size="8" 2313 onchange="document.form.%s.selectedIndex = -1;">''' \ 2314 % (included_select_name, excluded_select_name) 2315 # Build options for items initially selected. 2316 for item in included_items: 2317 option = '<option value="%s">%s</option>\n' \ 2318 % (item_to_value(item), item_to_text(item)) 2319 included_select = included_select + option 2320 included_select = included_select + '</select>\n' 2321 2322 # The select control for excluded items. When the user selects an 2323 # item in this list, deselect the selected item in the included 2324 # list, if any. 2325 excluded_select = ''' 2326 <select name="%s" 2327 width="160" 2328 size="8" 2329 onchange="document.form.%s.selectedIndex = -1;">''' \ 2330 % (excluded_select_name, included_select_name) 2331 # Build options for items initially excluded. 2332 for item in excluded_items: 2333 option = '<option value="%s">%s</option>\n' \ 2334 % (item_to_value(item), item_to_text(item)) 2335 excluded_select = excluded_select + option 2336 excluded_select = excluded_select + '</select>\n' 2337 2338 # The Add button. 2339 button = ''' 2340 <input type="button" 2341 value=" << Add " 2342 onclick="move_option(document.form.%s, document.form.%s); 2343 document.form.%s.value = 2344 encode_select_options(document.form.%s);" /> 2345 ''' % (excluded_select_name, included_select_name, 2346 field_name, included_select_name) 2347 buttons.append(button) 2348 2349 # The Remove button. 2350 button = ''' 2351 &nbsp;<input 2352 type="button" 2353 value=" Remove >> " 2354 onclick="move_option(document.form.%s, document.form.%s); 2355 document.form.%s.value = 2356 encode_select_options(document.form.%s);" />&nbsp; 2357 ''' % (included_select_name, excluded_select_name, 2358 field_name, included_select_name) 2359 buttons.append(button) 2360 2361 if ordered: 2362 # The Move Up button. 2363 button = ''' 2364 <input type="button" 2365 value=" Move Up " 2366 onclick="swap_option(document.form.%s, -1); 2367 document.form.%s.value = 2368 encode_select_options(document.form.%s);"/> 2369 ''' % (included_select_name, field_name, included_select_name) 2370 2371 buttons.append(button) 2372 2373 # The Move Down button. 2374 button = ''' 2375 <input type="button" 2376 value=" Move Down " 2377 onclick="swap_option(document.form.%s, 1); 2378 document.form.%s.value = 2379 encode_select_options(document.form.%s);"/> 2380 ''' % (included_select_name, field_name, included_select_name) 2381 buttons.append(button) 2382 2383 # Arrange everything properly. 2384 buttons = string.join(buttons, "\n<br />\n") 2385 return ''' 2386 %(hidden_control)s 2387 <table border="0" cellpadding="0" cellspacing="0"> 2388 <tr valign="center"> 2389 <td> 2390 %(included_label)s: 2391 <br /> 2392 %(included_select)s 2393 </td> 2394 <td align="center"> 2395 %(buttons)s 2396 </td> 2397 <td> 2398 %(excluded_label)s:<br /> 2399 %(excluded_select)s 2400 </td> 2401 </tr> 2402 </table> 2403 ''' % locals()
2404 2405
2406 -def make_button_for_popup(label, 2407 url, 2408 window_width=480, 2409 window_height=240):
2410 """Construct a button for displaying a popup page. 2411 2412 'label' -- The button label. 2413 2414 'url' -- The URL to display in the popup page. 2415 2416 returns -- HTML source for the button. The button must be placed 2417 within a form element.""" 2418 2419 # Construct arguments for 'Window.open'. 2420 window_args = "resizable,width=%d,height=%s" \ 2421 % (window_width, window_height) 2422 # Generate it. 2423 return """ 2424 <input type="button" 2425 value=" %(label)s " 2426 onclick="window.open('%(url)s', 2427 'popup', 2428 '%(window_args)s');"> 2429 """ % locals()
2430 2431
2432 -def format_color(red, green, blue):
2433 """Format an RGB color value for HTML. 2434 2435 'red', 'green', 'blue' -- Color values for respective channels, 2436 between 0.0 and 1.0. Values outside this range are truncated to 2437 this range.""" 2438 2439 # Manual loop unrolling, for efficiency. 2440 red = int(256 * red) 2441 if red < 0: 2442 red = 0 2443 if red > 255: 2444 red = 255 2445 green = int(256 * green) 2446 if green < 0: 2447 green = 0 2448 if green > 255: 2449 green = 255 2450 blue = int(256 * blue) 2451 if blue < 0: 2452 blue = 0 2453 if blue > 255: 2454 blue = 255 2455 return "#%02x%02x%02x" % (red, green, blue)
2456 2457
2458 -def javascript_escape(text):
2459 """Equivalent to the JavaScript 'escape' built-in function.""" 2460 2461 text = urllib.quote(text) 2462 text = string.replace(text, ",", "%2C") 2463 return text
2464 2465
2466 -def javascript_unescape(text):
2467 """Equivalent to the JavaScript 'unescape' built-in function.""" 2468 2469 return urllib.unquote(text)
2470 2471
2472 -def make_submit_button(title="OK"):
2473 """Generate HTML for a button to submit the current form. 2474 2475 'title' -- The button title.""" 2476 2477 return ''' 2478 <input type="button" 2479 class="submit" 2480 value=" %s " 2481 onclick="submit();" 2482 />''' % title
2483 2484 2485 ######################################################################## 2486 # variables 2487 ######################################################################## 2488 2489 sessions = {} 2490 """A mapping from session IDs to 'Session' instances.""" 2491 2492 _counter = 0 2493 """A counter for generating somewhat-unique names.""" 2494 2495 _page_cache_name = "page-cache" 2496 """The URL prefix for the global page cache.""" 2497 2498 _session_cache_name = "session-cache" 2499 """The URL prefix for the session page cache.""" 2500 2501 ######################################################################## 2502 # Local Variables: 2503 # mode: python 2504 # indent-tabs-mode: nil 2505 # fill-column: 72 2506 # End: 2507