1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 """Common code for implementing web user interfaces."""
17
18
19
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
53
54
55 session_id_field = "session"
56 """The name of the form field used to store the session ID."""
57
58
59
60
61
63 pass
64
65
66
68 pass
69
70
71
73 pass
74
75
76
78 pass
79
80
81
82
83
84
85
86
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
153 if request is None:
154 request = WebRequest("?")
155 self.request = request
156
157
158 template_path = os.path.join(qm.get_share_directory(), "dtml",
159 self.__dtml_template)
160
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
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
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
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
251
252
253
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
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
272 if redirect_request is None:
273
274 redirect_request = self.request
275 request = redirect_request.copy("login")
276 request["_redirect_url"] = redirect_request.GetUrl()
277
278
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
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
340
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
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
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
402 Exception.__init__(self, redirect_target_request.AsUrl())
403
404 self.request = redirect_target_request
405
406
407
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
420
421 SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map.update(
422 { '.css' : 'text/css',
423 '.js' : 'text/javascript' }
424 )
425
427 """Process HTTP GET requests."""
428
429
430 script_url, fields = parse_url_query(self.path)
431
432 request = apply(WebRequest, (script_url, ), fields)
433
434 request.client_address = self.client_address[0]
435
436 self.__HandleRequest(request)
437
439 """Process HTTP POST requests."""
440
441
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
448 if content_type == "multipart/form-data":
449
450 fields = cgi.parse_multipart(self.rfile, params)
451
452
453 for name, value in fields.items():
454 if len(value) == 1:
455 fields[name] = value[0]
456
457
458 script_url, url_fields = parse_url_query(self.path)
459
460 fields.update(url_fields)
461
462 request = apply(WebRequest, (script_url, ), fields)
463
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
473 try:
474
475
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
484
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
495
496 script_output = format_exception(sys.exc_info())
497
498 if isinstance(script_output, types.StringType):
499
500
501 mime_type = "text/html"
502 data = script_output
503 elif isinstance(script_output, types.TupleType):
504
505
506
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
514
515
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
523
524
525 pass
526
527
529
530
531 if len(request.keys()) > 0:
532 self.send_error(400, "Unexpected request.")
533 return
534
535 try:
536 file = open(path, "rb")
537 except IOError:
538
539 self.send_error(404, "File not found.")
540 return
541
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
553 page = self.server.GetCachedPage(request)
554
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
564 """Process a retrieval request from the session page cache."""
565
566
567 session_id = request.GetSessionId()
568 if session_id is None:
569
570
571 self.send_error(400, "Missing session ID.")
572 return
573
574 page = self.server.GetCachedPage(request, session_id)
575
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
610
611
613 """Log a message; overrides 'BaseHTTPRequestHandler.log_message'."""
614
615
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
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
634 """Override 'server_bind' to store the server name."""
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650 SocketServer.TCPServer.server_bind(self)
651 host, port = self.socket.getsockname()
652
653
654
655
656 if not host or host == '0.0.0.0':
657 host = socket.gethostname()
658
659
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
669 hostname = qm.platform.get_host_name()
670
671 self.server_name = hostname
672 self.server_port = port
673
674
675
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
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
752
753 self.__temporary_store = qm.attachment.TemporaryAttachmentStore()
754 self.RegisterScript(qm.fields.AttachmentField.upload_url,
755 self.__temporary_store.HandleUploadRequest)
756
757
758
759
760
761
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
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
810 """Return a true value if 'request' corresponds to a script."""
811
812 return self.__scripts.has_key(request.GetUrl())
813
814
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
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
835 for url_path, file_path in self.__translations.items():
836
837 if path[:len(url_path)] == url_path:
838
839 sub_path = path[len(url_path):]
840
841 if os.path.isabs(sub_path):
842 sub_path = sub_path[1:]
843
844 if sub_path:
845 file_path = os.path.join(file_path, sub_path)
846 return file_path
847
848 return None
849
850
852 """Bind the server to the specified address and port.
853
854 Does not start serving."""
855
856
857
858 try:
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
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
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
896 raise PrivilegedPortError, "port %d" % self.__port
897 else:
898
899 raise
900
901
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
912 """Shut the server down after processing the current request."""
913
914 self.__shutdown_requested = 1
915
916
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
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
969
970
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
981
982 open_script = "window.opener.document.location = %s;" \
983 % make_javascript_string(url)
984
985 buttons = [
986 ( "Yes", open_script ),
987 ( "No", None ),
988 ]
989 return self.MakePopupDialog(message, buttons, title="Confirm")
990
991
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
1015 page = make_popup_page(message, buttons, title)
1016 page_url = self.CachePage(page).AsUrl()
1017
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
1038 dir_path = self.__cache_path
1039 script_name = _page_cache_name
1040 else:
1041
1042
1043 dir_path = os.path.join(self.__cache_path, "sessions", session_id)
1044 script_name = _session_cache_name
1045
1046 if not os.path.isdir(dir_path):
1047 os.mkdir(dir_path, 0700)
1048
1049
1050 global _counter
1051 page_name = str(_counter)
1052 _counter = _counter + 1
1053
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
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
1083 return open(page_file_name, "r").read()
1084 else:
1085
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
1110
1111 dir_path = os.path.join(self.__cache_path, "sessions", session_id)
1112
1113 page_name = request["page"]
1114 return os.path.join(dir_path, page_name)
1115
1116
1133
1134
1136 """Handle internal errors."""
1137
1138 return DtmlPage.default_class("problems.dtml")(request)
1139
1140
1145
1146
1148 """Handle an error gracefully."""
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161 return
1162
1163
1164
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
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
1202
1203
1205 """Return the URL of the script that processes this request."""
1206
1207 return self.__url
1208
1209
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
1219 """Set the session ID for this request to 'session_id'."""
1220
1221 self[session_id_field] = session_id
1222
1223
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
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
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
1258 return self.GetUrl()
1259 else:
1260
1261 return "%s?%s" % (self.GetUrl(), urllib.urlencode(self))
1262
1263
1301
1302
1303
1304
1306 return self.__fields[key]
1307
1308
1311
1312
1314 del self.__fields[key]
1315
1316
1317 - def get(self, key, default=None):
1319
1320
1322 return self.__fields.keys()
1323
1324
1327
1328
1330 return self.__fields.items()
1331
1332
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
1342 if url is None:
1343 url = self.__url
1344
1345
1346 new_fields = self.__fields.copy()
1347 new_fields.update(fields)
1348
1349 new_request = apply(WebRequest, (url, ), new_fields)
1350
1351 if hasattr(self, "client_address"):
1352 new_request.client_address = self.client_address
1353
1354 return new_request
1355
1356
1357
1359 """A 'WebRequest' object initialized from the CGI environment."""
1360
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
1374 return os.environ["SCRIPT_NAME"]
1375
1376
1379
1380
1382 return self.__fields.keys()
1383
1384
1387
1388
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
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
1423 self.__client_address = request.client_address
1424
1425
1426
1427 random.seed()
1428
1429 digest = md5.new("%f" % random.random()).digest()
1430
1431
1432 digest = map(lambda ch: hex(ord(ch))[2:], digest)
1433
1434 self.__id = string.join(digest, "")
1435
1436 self.Touch()
1437
1438
1439 sessions[self.__id] = self
1440
1441
1443 """Update the last access time on the session to now."""
1444
1445 self.__last_access_time = time.time()
1446
1447
1449 """Return the session ID."""
1450
1451 return self.__id
1452
1453
1455 """Return the ID of the user who owns this session."""
1456
1457 return self.__user_id
1458
1459
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
1472
1473
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
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
1490
1491 if self.__client_address != request.client_address:
1492 raise InvalidSessionError, qm.error("session wrong IP")
1493
1494 if self.IsExpired():
1495 raise InvalidSessionError, qm.error("session expired")
1496
1497
1498
1499
1500
1501
1502
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
1516 if "?" in url:
1517
1518
1519 script_url, query_string = string.split(url, "?", 1)
1520
1521 fields = cgi.parse_qs(query_string)
1522
1523
1524
1525
1526 for key, value_list in fields.items():
1527 if len(value_list) != 1:
1528
1529 print "WARNING: Multiple values in query."
1530 fields[key] = value_list[0]
1531 else:
1532
1533 script_url = url
1534 fields = {}
1535
1536 script_url = urllib.unquote(script_url)
1537 return (script_url, fields)
1538
1539
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
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
1590
1591
1592
1597
1598
1599
1600 __entity_regex = re.compile("&(\w+);")
1601
1602
1603
1604
1606 entity = match.group(1)
1607 try:
1608 return htmlentitydefs.entitydefs[entity]
1609 except KeyError:
1610 return "&%s;" % entity
1611
1612
1614 """Undo 'escape' by replacing entities with ordinary characters."""
1615
1616 return __entity_regex.sub(__replacement_for_entity, text)
1617
1618
1620 """Render 'text' as HTML."""
1621
1622 if text == "":
1623
1624
1625
1626 return " "
1627 else:
1628 return structured_text.to_html(text)
1629
1630
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
1659
1660
1683
1684
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
1694 __clean_up_expired_sessions()
1695
1696 try:
1697
1698 session = sessions[session_id]
1699 except KeyError:
1700
1701 raise InvalidSessionError, qm.error("session invalid")
1702
1703 session.Validate(request)
1704
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
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
1732
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
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
1744 message = qm.error("disabled account")
1745 redirect_request = WebRequest(redirect_url)
1746 return generate_login_form(redirect_request, message)
1747
1748
1749 for session in sessions.values():
1750 if session.GetUserId() == user_id:
1751
1752
1753 del sessions[session.GetId()]
1754
1755 session = Session(request, user_id)
1756 session_id = session.GetId()
1757
1758
1759
1760 redirect_request = request.copy(redirect_url)
1761
1762
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
1768 redirect_request.SetSessionId(session_id)
1769
1770 raise HttpRedirect, redirect_request
1771
1772
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
1785 session_id = request.GetSessionId()
1786 del sessions[session_id]
1787
1788
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
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
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
1863 if select_name is None:
1864 select_name = "_set_" + field_name
1865
1866
1867 select = '<select name="%s" size="%d" width="%d">\n' \
1868 % (select_name, rows, width)
1869
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
1876
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
1885 buttons.append(make_button_for_popup("Add...", add_page,
1886 window_width, window_height))
1887
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
1910 return contents + '''
1911 <table border="0" cellpadding="0" cellspacing="0"><tbody>
1912 <tr valign="top">
1913 <td>
1914 %s
1915 </td>
1916 <td> </td>
1917 <td>
1918 %s
1919 </td>
1920 </tr>
1921 </tbody></table>
1922 ''' % (select, string.join(buttons, "<br />"))
1923
1924
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
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
1944
1945 if string.strip(content_string) == "":
1946 return []
1947 return string.split(content_string, ",")
1948
1949
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
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
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
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
1999
2000 initial_value = encode_properties(properties)
2001 contents = '<input type="hidden" name="%s" value="%s"/>' \
2002 % (field_name, initial_value)
2003
2004
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
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
2023
2024
2025
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
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
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> </td>
2056 <td>%s</td>
2057 </tr>
2058 <tr>
2059 <td>Name: </td>
2060 <td align="right">%s </td>
2061 <td> </td>
2062 <td>%s</td>
2063 </tr>
2064 <tr>
2065 <td>Value: </td>
2066 <td align="right">%s </td>
2067 <td> </td>
2068 <td> </td>
2069 </tr>
2070 </tbody></table>
2071 ''' % (select, remove_button, name_control, add_change_button,
2072 value_control)
2073
2074
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
2086
2087 result = map(lambda p: "%s=%s" % (p[0], urllib.quote_plus(p[1])),
2088 properties.items())
2089
2090 return string.join(result, ",")
2091
2092
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
2104 if properties == "":
2105 return {}
2106
2107
2108 properties = string.split(properties, ",")
2109
2110 result = {}
2111 for assignment in properties:
2112
2113 name, value = string.split(assignment, "=")
2114
2115 value = urllib.unquote_plus(value)
2116
2117 result[name] = value
2118
2119 return result
2120
2121
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
2130
2131 text = string.replace(text, "<", r"\074")
2132 return "'" + text + "'"
2133
2134
2136 """Make a link to pop up help text.
2137
2138 'help_text_tag' -- A message tag for the help diagnostic.
2139
2140 'label' -- The help link label.
2141
2142 'substitutions' -- Substitutions to the help diagnostic."""
2143
2144
2145 help_text = apply(diagnostic.get_help_set().Generate,
2146 (help_text_tag, "help", None),
2147 substitutions)
2148
2149 help_text = qm.structured_text.to_html(help_text)
2150
2151 return make_help_link_html(help_text, label)
2152
2153
2155 """Make a link to pop up help text.
2156
2157 'help_text' -- HTML source for the help text.
2158
2159 'label' -- The help link label."""
2160
2161 global _counter
2162
2163
2164 help_page = DtmlPage("help.dtml", help_text=help_text)
2165
2166 help_page_string = make_javascript_string(help_page())
2167
2168
2169
2170 help_variable_name = "_help_text_%d" % _counter
2171 _counter = _counter + 1
2172
2173
2174 return \
2175 '''<a class="help-link"
2176 href="javascript: void(0)"
2177 onclick="show_help(%s);">%s</a>
2178 %s
2179 var %s = %s;
2180 %s
2181 ''' % (help_variable_name, label,
2182 help_page.GenerateStartScript(),
2183 help_variable_name, help_page_string,
2184 help_page.GenerateEndScript())
2185
2186
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
2209 for caption, script in buttons:
2210 page = page + '''
2211 <input type="button"
2212 value=" %s "''' % caption
2213
2214
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
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
2284
2285 buttons = []
2286
2287 initial_value = string.join(map(item_to_value, included_items), ",")
2288
2289
2290 hidden_control = '<input type="hidden" name="%s" value="%s">' \
2291 % (field_name, initial_value)
2292
2293 included_select_name = "_inc_" + field_name
2294 excluded_select_name = "_exc_" + field_name
2295
2296
2297
2298
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
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
2313
2314
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
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
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
2340 button = '''
2341 <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);" />
2347 ''' % (included_select_name, excluded_select_name,
2348 field_name, included_select_name)
2349 buttons.append(button)
2350
2351 if ordered:
2352
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
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
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
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
2410 window_args = "resizable,width=%d,height=%s" \
2411 % (window_width, window_height)
2412
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
2446
2447
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
2457 """Equivalent to the JavaScript 'unescape' built-in function."""
2458
2459 return urllib.unquote(text)
2460
2461
2473
2474
2475
2476
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
2493
2494
2495
2496
2497