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