Code

Fixed newuser_action so it sets the cookie with the unencrypted password.
[roundup.git] / roundup / cgi_client.py
1 #
2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
11 #
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17
18 # $Id: cgi_client.py,v 1.50 2001-11-05 23:45:40 richard Exp $
20 import os, cgi, pprint, StringIO, urlparse, re, traceback, mimetypes
21 import binascii, Cookie, time
23 import roundupdb, htmltemplate, date, hyperdb, password
25 class Unauthorised(ValueError):
26     pass
28 class NotFound(ValueError):
29     pass
31 class Client:
32     '''
33     A note about login
34     ------------------
36     If the user has no login cookie, then they are anonymous. There
37     are two levels of anonymous use. If there is no 'anonymous' user, there
38     is no login at all and the database is opened in read-only mode. If the
39     'anonymous' user exists, the user is logged in using that user (though
40     there is no cookie). This allows them to modify the database, and all
41     modifications are attributed to the 'anonymous' user.
44     Customisation
45     -------------
46       FILTER_POSITION - one of 'top', 'bottom', 'top and bottom'
47       ANONYMOUS_ACCESS - one of 'deny', 'allow'
48       ANONYMOUS_REGISTER - one of 'deny', 'allow'
50     '''
51     FILTER_POSITION = 'bottom'       # one of 'top', 'bottom', 'top and bottom'
52     ANONYMOUS_ACCESS = 'deny'        # one of 'deny', 'allow'
53     ANONYMOUS_REGISTER = 'deny'      # one of 'deny', 'allow'
55     def __init__(self, instance, request, env):
56         self.instance = instance
57         self.request = request
58         self.env = env
59         self.path = env['PATH_INFO']
60         self.split_path = self.path.split('/')
62         self.form = cgi.FieldStorage(environ=env)
63         self.headers_done = 0
64         self.debug = 0
66     def getuid(self):
67         return self.db.user.lookup(self.user)
69     def header(self, headers={'Content-Type':'text/html'}):
70         '''Put up the appropriate header.
71         '''
72         if not headers.has_key('Content-Type'):
73             headers['Content-Type'] = 'text/html'
74         self.request.send_response(200)
75         for entry in headers.items():
76             self.request.send_header(*entry)
77         self.request.end_headers()
78         self.headers_done = 1
80     def pagehead(self, title, message=None):
81         url = self.env['SCRIPT_NAME'] + '/'
82         machine = self.env['SERVER_NAME']
83         port = self.env['SERVER_PORT']
84         if port != '80': machine = machine + ':' + port
85         base = urlparse.urlunparse(('http', machine, url, None, None, None))
86         if message is not None:
87             message = '<div class="system-msg">%s</div>'%message
88         else:
89             message = ''
90         style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
91         user_name = self.user or ''
92         if self.user == 'admin':
93             admin_links = ' | <a href="list_classes">Class List</a>'
94         else:
95             admin_links = ''
96         if self.user not in (None, 'anonymous'):
97             userid = self.db.user.lookup(self.user)
98             user_info = '''
99 <a href="issue?assignedto=%s&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:filter=status,assignedto&:sort=activity&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">My Issues</a> |
100 <a href="user%s">My Details</a> | <a href="logout">Logout</a>
101 '''%(userid, userid)
102         else:
103             user_info = '<a href="login">Login</a>'
104         if self.user is not None:
105             add_links = '''
106 | Add
107 <a href="newissue">Issue</a>,
108 <a href="newuser">User</a>
109 '''
110         else:
111             add_links = ''
112         self.write('''<html><head>
113 <title>%s</title>
114 <style type="text/css">%s</style>
115 </head>
116 <body bgcolor=#ffffff>
117 %s
118 <table width=100%% border=0 cellspacing=0 cellpadding=2>
119 <tr class="location-bar"><td><big><strong>%s</strong></big></td>
120 <td align=right valign=bottom>%s</td></tr>
121 <tr class="location-bar">
122 <td align=left>All
123 <a href="issue?status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:filter=status&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>
124 | Unassigned
125 <a href="issue?assignedto=-1&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:filter=status,assignedto&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>
126 %s
127 %s</td>
128 <td align=right>%s</td>
129 </table>
130 '''%(title, style, message, title, user_name, add_links, admin_links,
131     user_info))
133     def pagefoot(self):
134         if self.debug:
135             self.write('<hr><small><dl>')
136             self.write('<dt><b>Path</b></dt>')
137             self.write('<dd>%s</dd>'%(', '.join(map(repr, self.split_path))))
138             keys = self.form.keys()
139             keys.sort()
140             if keys:
141                 self.write('<dt><b>Form entries</b></dt>')
142                 for k in self.form.keys():
143                     v = str(self.form[k].value)
144                     self.write('<dd><em>%s</em>:%s</dd>'%(k, cgi.escape(v)))
145             keys = self.env.keys()
146             keys.sort()
147             self.write('<dt><b>CGI environment</b></dt>')
148             for k in keys:
149                 v = self.env[k]
150                 self.write('<dd><em>%s</em>:%s</dd>'%(k, cgi.escape(v)))
151             self.write('</dl></small>')
152         self.write('</body></html>')
154     def write(self, content):
155         if not self.headers_done:
156             self.header()
157         self.request.wfile.write(content)
159     def index_arg(self, arg):
160         ''' handle the args to index - they might be a list from the form
161             (ie. submitted from a form) or they might be a command-separated
162             single string (ie. manually constructed GET args)
163         '''
164         if self.form.has_key(arg):
165             arg =  self.form[arg]
166             if type(arg) == type([]):
167                 return [arg.value for arg in arg]
168             return arg.value.split(',')
169         return []
171     def index_filterspec(self, filter):
172         ''' pull the index filter spec from the form
174         Links and multilinks want to be lists - the rest are straight
175         strings.
176         '''
177         props = self.db.classes[self.classname].getprops()
178         # all the form args not starting with ':' are filters
179         filterspec = {}
180         for key in self.form.keys():
181             if key[0] == ':': continue
182             if not props.has_key(key): continue
183             if key not in filter: continue
184             prop = props[key]
185             value = self.form[key]
186             if (isinstance(prop, hyperdb.Link) or
187                     isinstance(prop, hyperdb.Multilink)):
188                 if type(value) == type([]):
189                     value = [arg.value for arg in value]
190                 else:
191                     value = value.value.split(',')
192                 l = filterspec.get(key, [])
193                 l = l + value
194                 filterspec[key] = l
195             else:
196                 filterspec[key] = value.value
197         return filterspec
199     def customization_widget(self):
200         ''' The customization widget is visible by default. The widget
201             visibility is remembered by show_customization.  Visibility
202             is not toggled if the action value is "Redisplay"
203         '''
204         if not self.form.has_key('show_customization'):
205             visible = 1
206         else:
207             visible = int(self.form['show_customization'].value)
208             if self.form.has_key('action'):
209                 if self.form['action'].value != 'Redisplay':
210                     visible = self.form['action'].value == '+'
211             
212         return visible
214     default_index_sort = ['-activity']
215     default_index_group = ['priority']
216     default_index_filter = ['status']
217     default_index_columns = ['id','activity','title','status','assignedto']
218     default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
219     def index(self):
220         ''' put up an index
221         '''
222         self.classname = 'issue'
223         # see if the web has supplied us with any customisation info
224         defaults = 1
225         for key in ':sort', ':group', ':filter', ':columns':
226             if self.form.has_key(key):
227                 defaults = 0
228                 break
229         if defaults:
230             # no info supplied - use the defaults
231             sort = self.default_index_sort
232             group = self.default_index_group
233             filter = self.default_index_filter
234             columns = self.default_index_columns
235             filterspec = self.default_index_filterspec
236         else:
237             sort = self.index_arg(':sort')
238             group = self.index_arg(':group')
239             filter = self.index_arg(':filter')
240             columns = self.index_arg(':columns')
241             filterspec = self.index_filterspec(filter)
242         return self.list(columns=columns, filter=filter, group=group,
243             sort=sort, filterspec=filterspec)
245     # XXX deviates from spec - loses the '+' (that's a reserved character
246     # in URLS
247     def list(self, sort=None, group=None, filter=None, columns=None,
248             filterspec=None, show_customization=None):
249         ''' call the template index with the args
251             :sort    - sort by prop name, optionally preceeded with '-'
252                      to give descending or nothing for ascending sorting.
253             :group   - group by prop name, optionally preceeded with '-' or
254                      to sort in descending or nothing for ascending order.
255             :filter  - selects which props should be displayed in the filter
256                      section. Default is all.
257             :columns - selects the columns that should be displayed.
258                      Default is all.
260         '''
261         cn = self.classname
262         self.pagehead('Index of %s'%cn)
263         if sort is None: sort = self.index_arg(':sort')
264         if group is None: group = self.index_arg(':group')
265         if filter is None: filter = self.index_arg(':filter')
266         if columns is None: columns = self.index_arg(':columns')
267         if filterspec is None: filterspec = self.index_filterspec(filter)
268         if show_customization is None:
269             show_customization = self.customization_widget()
271         index = htmltemplate.IndexTemplate(self, self.TEMPLATES, cn)
272         index.render(filterspec, filter, columns, sort, group,
273             show_customization=show_customization)
274         self.pagefoot()
276     def shownode(self, message=None):
277         ''' display an item
278         '''
279         cn = self.classname
280         cl = self.db.classes[cn]
282         # possibly perform an edit
283         keys = self.form.keys()
284         num_re = re.compile('^\d+$')
285         if keys:
286             try:
287                 props, changed = parsePropsFromForm(self.db, cl, self.form,
288                     self.nodeid)
289                 cl.set(self.nodeid, **props)
290                 self._post_editnode(self.nodeid, changed)
291                 # and some nice feedback for the user
292                 message = '%s edited ok'%', '.join(changed)
293             except:
294                 s = StringIO.StringIO()
295                 traceback.print_exc(None, s)
296                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
298         # now the display
299         id = self.nodeid
300         if cl.getkey():
301             id = cl.get(id, cl.getkey())
302         self.pagehead('%s: %s'%(self.classname.capitalize(), id), message)
304         nodeid = self.nodeid
306         # use the template to display the item
307         item = htmltemplate.ItemTemplate(self, self.TEMPLATES, self.classname)
308         item.render(nodeid)
310         self.pagefoot()
311     showissue = shownode
312     showmsg = shownode
314     def showuser(self, message=None):
315         '''Display a user page for editing. Make sure the user is allowed
316             to edit this node, and also check for password changes.
317         '''
318         if self.user == 'anonymous':
319             raise Unauthorised
321         user = self.db.user
323         # get the username of the node being edited
324         node_user = user.get(self.nodeid, 'username')
326         if self.user not in ('admin', node_user):
327             raise Unauthorised
329         #
330         # perform any editing
331         #
332         keys = self.form.keys()
333         num_re = re.compile('^\d+$')
334         if keys:
335             try:
336                 props, changed = parsePropsFromForm(self.db, user, self.form,
337                     self.nodeid)
338                 if self.nodeid == self.getuid() and 'password' in changed:
339                     set_cookie = self.form['password'].value.strip()
340                 else:
341                     set_cookie = 0
342                 user.set(self.nodeid, **props)
343                 self._post_editnode(self.nodeid, changed)
344                 # and some feedback for the user
345                 message = '%s edited ok'%', '.join(changed)
346             except:
347                 s = StringIO.StringIO()
348                 traceback.print_exc(None, s)
349                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
350         else:
351             set_cookie = 0
353         # fix the cookie if the password has changed
354         if set_cookie:
355             self.set_cookie(self.user, set_cookie)
357         #
358         # now the display
359         #
360         self.pagehead('User: %s'%node_user, message)
362         # use the template to display the item
363         item = htmltemplate.ItemTemplate(self, self.TEMPLATES, 'user')
364         item.render(self.nodeid)
365         self.pagefoot()
367     def showfile(self):
368         ''' display a file
369         '''
370         nodeid = self.nodeid
371         cl = self.db.file
372         type = cl.get(nodeid, 'type')
373         if type == 'message/rfc822':
374             type = 'text/plain'
375         self.header(headers={'Content-Type': type})
376         self.write(cl.get(nodeid, 'content'))
378     def _createnode(self):
379         ''' create a node based on the contents of the form
380         '''
381         cl = self.db.classes[self.classname]
382         props, dummy = parsePropsFromForm(self.db, cl, self.form)
383         return cl.create(**props)
385     def _post_editnode(self, nid, changes=None):
386         ''' do the linking and message sending part of the node creation
387         '''
388         cn = self.classname
389         cl = self.db.classes[cn]
390         # link if necessary
391         keys = self.form.keys()
392         for key in keys:
393             if key == ':multilink':
394                 value = self.form[key].value
395                 if type(value) != type([]): value = [value]
396                 for value in value:
397                     designator, property = value.split(':')
398                     link, nodeid = roundupdb.splitDesignator(designator)
399                     link = self.db.classes[link]
400                     value = link.get(nodeid, property)
401                     value.append(nid)
402                     link.set(nodeid, **{property: value})
403             elif key == ':link':
404                 value = self.form[key].value
405                 if type(value) != type([]): value = [value]
406                 for value in value:
407                     designator, property = value.split(':')
408                     link, nodeid = roundupdb.splitDesignator(designator)
409                     link = self.db.classes[link]
410                     link.set(nodeid, **{property: nid})
412         # generate an edit message
413         # don't bother if there's no messages or nosy list 
414         props = cl.getprops()
415         note = None
416         if self.form.has_key('__note'):
417             note = self.form['__note']
418             note = note.value
419         send = len(cl.get(nid, 'nosy', [])) or note
420         if (send and props.has_key('messages') and
421                 isinstance(props['messages'], hyperdb.Multilink) and
422                 props['messages'].classname == 'msg'):
424             # handle the note
425             if note:
426                 if '\n' in note:
427                     summary = re.split(r'\n\r?', note)[0]
428                 else:
429                     summary = note
430                 m = ['%s\n'%note]
431             else:
432                 summary = 'This %s has been edited through the web.\n'%cn
433                 m = [summary]
435             first = 1
436             for name, prop in props.items():
437                 if changes is not None and name not in changes: continue
438                 if first:
439                     m.append('\n-------')
440                     first = 0
441                 value = cl.get(nid, name, None)
442                 if isinstance(prop, hyperdb.Link):
443                     link = self.db.classes[prop.classname]
444                     key = link.labelprop(default_to_id=1)
445                     if value is not None and key:
446                         value = link.get(value, key)
447                     else:
448                         value = '-'
449                 elif isinstance(prop, hyperdb.Multilink):
450                     if value is None: value = []
451                     l = []
452                     link = self.db.classes[prop.classname]
453                     key = link.labelprop(default_to_id=1)
454                     for entry in value:
455                         if key:
456                             l.append(link.get(entry, key))
457                         else:
458                             l.append(entry)
459                     value = ', '.join(l)
460                 m.append('%s: %s'%(name, value))
462             # now create the message
463             content = '\n'.join(m)
464             message_id = self.db.msg.create(author=self.getuid(),
465                 recipients=[], date=date.Date('.'), summary=summary,
466                 content=content)
467             messages = cl.get(nid, 'messages')
468             messages.append(message_id)
469             props = {'messages': messages}
470             cl.set(nid, **props)
472     def newnode(self, message=None):
473         ''' Add a new node to the database.
474         
475         The form works in two modes: blank form and submission (that is,
476         the submission goes to the same URL). **Eventually this means that
477         the form will have previously entered information in it if
478         submission fails.
480         The new node will be created with the properties specified in the
481         form submission. For multilinks, multiple form entries are handled,
482         as are prop=value,value,value. You can't mix them though.
484         If the new node is to be referenced from somewhere else immediately
485         (ie. the new node is a file that is to be attached to a support
486         issue) then supply one of these arguments in addition to the usual
487         form entries:
488             :link=designator:property
489             :multilink=designator:property
490         ... which means that once the new node is created, the "property"
491         on the node given by "designator" should now reference the new
492         node's id. The node id will be appended to the multilink.
493         '''
494         cn = self.classname
495         cl = self.db.classes[cn]
497         # possibly perform a create
498         keys = self.form.keys()
499         if [i for i in keys if i[0] != ':']:
500             props = {}
501             try:
502                 nid = self._createnode()
503                 self._post_editnode(nid)
504                 # and some nice feedback for the user
505                 message = '%s created ok'%cn
506             except:
507                 s = StringIO.StringIO()
508                 traceback.print_exc(None, s)
509                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
510         self.pagehead('New %s'%self.classname.capitalize(), message)
512         # call the template
513         newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES,
514             self.classname)
515         newitem.render(self.form)
517         self.pagefoot()
518     newissue = newnode
519     newuser = newnode
521     def newfile(self, message=None):
522         ''' Add a new file to the database.
523         
524         This form works very much the same way as newnode - it just has a
525         file upload.
526         '''
527         cn = self.classname
528         cl = self.db.classes[cn]
530         # possibly perform a create
531         keys = self.form.keys()
532         if [i for i in keys if i[0] != ':']:
533             try:
534                 file = self.form['content']
535                 type = mimetypes.guess_type(file.filename)[0]
536                 if not type:
537                     type = "application/octet-stream"
538                 self._post_editnode(cl.create(content=file.file.read(),
539                     type=type, name=file.filename))
540                 # and some nice feedback for the user
541                 message = '%s created ok'%cn
542             except:
543                 s = StringIO.StringIO()
544                 traceback.print_exc(None, s)
545                 message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
547         self.pagehead('New %s'%self.classname.capitalize(), message)
548         newitem = htmltemplate.NewItemTemplate(self, self.TEMPLATES,
549             self.classname)
550         newitem.render(self.form)
551         self.pagefoot()
553     def classes(self, message=None):
554         ''' display a list of all the classes in the database
555         '''
556         if self.user == 'admin':
557             self.pagehead('Table of classes', message)
558             classnames = self.db.classes.keys()
559             classnames.sort()
560             self.write('<table border=0 cellspacing=0 cellpadding=2>\n')
561             for cn in classnames:
562                 cl = self.db.getclass(cn)
563                 self.write('<tr class="list-header"><th colspan=2 align=left>%s</th></tr>'%cn.capitalize())
564                 for key, value in cl.properties.items():
565                     if value is None: value = ''
566                     else: value = str(value)
567                     self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%(
568                         key, cgi.escape(value)))
569             self.write('</table>')
570             self.pagefoot()
571         else:
572             raise Unauthorised
574     def login(self, message=None, newuser_form=None):
575         self.pagehead('Login to roundup', message)
576         self.write('''
577 <table>
578 <tr><td colspan=2 class="strong-header">Existing User Login</td></tr>
579 <form action="login_action" method=POST>
580 <tr><td align=right>Login name: </td>
581     <td><input name="__login_name"></td></tr>
582 <tr><td align=right>Password: </td>
583     <td><input type="password" name="__login_password"></td></tr>
584 <tr><td></td>
585     <td><input type="submit" value="Log In"></td></tr>
586 </form>
587 ''')
588         if self.user is None and self.ANONYMOUS_REGISTER == 'deny':
589             self.write('</table>')
590             self.pagefoot()
591             return
592         values = {'realname': '', 'organisation': '', 'address': '',
593             'phone': '', 'username': '', 'password': '', 'confirm': ''}
594         if newuser_form is not None:
595             for key in newuser_form.keys():
596                 values[key] = newuser_form[key].value
597         self.write('''
598 <p>
599 <tr><td colspan=2 class="strong-header">New User Registration</td></tr>
600 <tr><td colspan=2><em>marked items</em> are optional...</td></tr>
601 <form action="newuser_action" method=POST>
602 <tr><td align=right><em>Name: </em></td>
603     <td><input name="realname" value="%(realname)s"></td></tr>
604 <tr><td align=right><em>Organisation: </em></td>
605     <td><input name="organisation" value="%(organisation)s"></td></tr>
606 <tr><td align=right>E-Mail Address: </td>
607     <td><input name="address" value="%(address)s"></td></tr>
608 <tr><td align=right><em>Phone: </em></td>
609     <td><input name="phone" value="%(phone)s"></td></tr>
610 <tr><td align=right>Preferred Login name: </td>
611     <td><input name="username" value="%(username)s"></td></tr>
612 <tr><td align=right>Password: </td>
613     <td><input type="password" name="password" value="%(password)s"></td></tr>
614 <tr><td align=right>Password Again: </td>
615     <td><input type="password" name="confirm" value="%(confirm)s"></td></tr>
616 <tr><td></td>
617     <td><input type="submit" value="Register"></td></tr>
618 </form>
619 </table>
620 '''%values)
621         self.pagefoot()
623     def login_action(self, message=None):
624         if not self.form.has_key('__login_name'):
625             return self.login(message='Username required')
626         self.user = self.form['__login_name'].value
627         if self.form.has_key('__login_password'):
628             password = self.form['__login_password'].value
629         else:
630             password = ''
631         # make sure the user exists
632         try:
633             uid = self.db.user.lookup(self.user)
634         except KeyError:
635             name = self.user
636             self.make_user_anonymous()
637             return self.login(message='No such user "%s"'%name)
639         # and that the password is correct
640         pw = self.db.user.get(uid, 'password')
641         if password != self.db.user.get(uid, 'password'):
642             self.make_user_anonymous()
643             return self.login(message='Incorrect password')
645         self.set_cookie(self.user, password)
646         return self.index()
648     def set_cookie(self, user, password):
649         # construct the cookie
650         user = binascii.b2a_base64('%s:%s'%(user, password)).strip()
651         path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME']))
652         self.header({'Set-Cookie': 'roundup_user="%s"; Path="%s";'%(user,
653             path)})
655     def make_user_anonymous(self):
656         # make us anonymous if we can
657         try:
658             self.db.user.lookup('anonymous')
659             self.user = 'anonymous'
660         except KeyError:
661             self.user = None
663     def logout(self, message=None):
664         self.make_user_anonymous()
665         # construct the logout cookie
666         now = Cookie._getdate()
667         path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME']))
668         self.header({'Set-Cookie':
669             'roundup_user=deleted; Max-Age=0; expires="%s"; Path="%s";'%(now,
670             path)})
671         return self.login()
673     def newuser_action(self, message=None):
674         ''' create a new user based on the contents of the form and then
675         set the cookie
676         '''
677         # re-open the database as "admin"
678         self.db.close()
679         self.db = self.instance.open('admin')
681         # TODO: pre-check the required fields and username key property
682         cl = self.db.user
683         try:
684             props, dummy = parsePropsFromForm(self.db, cl, self.form)
685             uid = cl.create(**props)
686         except ValueError, message:
687             return self.login(message, newuser_form=self.form)
688         self.user = cl.get(uid, 'username')
689         password = cl.get(uid, 'password')
690         self.set_cookie(self.user, self.form['password'].value)
691         return self.index()
693     def main(self, dre=re.compile(r'([^\d]+)(\d+)'),
694             nre=re.compile(r'new(\w+)')):
696         # determine the uid to use
697         self.db = self.instance.open('admin')
698         cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', ''))
699         user = 'anonymous'
700         if (cookie.has_key('roundup_user') and
701                 cookie['roundup_user'].value != 'deleted'):
702             cookie = cookie['roundup_user'].value
703             user, password = binascii.a2b_base64(cookie).split(':')
704             # make sure the user exists
705             try:
706                 uid = self.db.user.lookup(user)
707                 # now validate the password
708                 if password != self.db.user.get(uid, 'password'):
709                     user = 'anonymous'
710             except KeyError:
711                 user = 'anonymous'
713         # make sure the anonymous user is valid if we're using it
714         if user == 'anonymous':
715             self.make_user_anonymous()
716         else:
717             self.user = user
718         self.db.close()
720         # re-open the database for real, using the user
721         self.db = self.instance.open(self.user)
723         # now figure which function to call
724         path = self.split_path
725         if not path or path[0] in ('', 'index'):
726             action = 'index'
727         else:
728             action = path[0]
730         # Everthing ignores path[1:]
731         #  - The file download link generator actually relies on this - it
732         #    appends the name of the file to the URL so the download file name
733         #    is correct, but doesn't actually use it.
735         # everyone is allowed to try to log in
736         if action == 'login_action':
737             return self.login_action()
739         # allow anonymous people to register
740         if action == 'newuser_action':
741             # if we don't have a login and anonymous people aren't allowed to
742             # register, then spit up the login form
743             if self.ANONYMOUS_REGISTER == 'deny' and self.user is None:
744                 return self.login()
745             return self.newuser_action()
747         # make sure totally anonymous access is OK
748         if self.ANONYMOUS_ACCESS == 'deny' and self.user is None:
749             return self.login()
751         # here be the "normal" functionality
752         if action == 'index':
753             return self.index()
754         if action == 'list_classes':
755             return self.classes()
756         if action == 'login':
757             return self.login()
758         if action == 'logout':
759             return self.logout()
760         m = dre.match(action)
761         if m:
762             self.classname = m.group(1)
763             self.nodeid = m.group(2)
764             try:
765                 cl = self.db.classes[self.classname]
766             except KeyError:
767                 raise NotFound
768             try:
769                 cl.get(self.nodeid, 'id')
770             except IndexError:
771                 raise NotFound
772             try:
773                 func = getattr(self, 'show%s'%self.classname)
774             except AttributeError:
775                 raise NotFound
776             return func()
777         m = nre.match(action)
778         if m:
779             self.classname = m.group(1)
780             try:
781                 func = getattr(self, 'new%s'%self.classname)
782             except AttributeError:
783                 raise NotFound
784             return func()
785         self.classname = action
786         try:
787             self.db.getclass(self.classname)
788         except KeyError:
789             raise NotFound
790         self.list()
792     def __del__(self):
793         self.db.close()
796 class ExtendedClient(Client): 
797     '''Includes pages and page heading information that relate to the
798        extended schema.
799     ''' 
800     showsupport = Client.shownode
801     showtimelog = Client.shownode
802     newsupport = Client.newnode
803     newtimelog = Client.newnode
805     default_index_sort = ['-activity']
806     default_index_group = ['priority']
807     default_index_filter = ['status']
808     default_index_columns = ['activity','status','title','assignedto']
809     default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']}
811     def pagehead(self, title, message=None):
812         url = self.env['SCRIPT_NAME'] + '/' #self.env.get('PATH_INFO', '/')
813         machine = self.env['SERVER_NAME']
814         port = self.env['SERVER_PORT']
815         if port != '80': machine = machine + ':' + port
816         base = urlparse.urlunparse(('http', machine, url, None, None, None))
817         if message is not None:
818             message = '<div class="system-msg">%s</div>'%message
819         else:
820             message = ''
821         style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
822         user_name = self.user or ''
823         if self.user == 'admin':
824             admin_links = ' | <a href="list_classes">Class List</a>'
825         else:
826             admin_links = ''
827         if self.user not in (None, 'anonymous'):
828             userid = self.db.user.lookup(self.user)
829             user_info = '''
830 <a href="issue?assignedto=%s&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:filter=status,assignedto&:sort=activity&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">My Issues</a> |
831 <a href="support?assignedto=%s&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:filter=status,assignedto&:sort=activity&:columns=id,activity,status,title,assignedto&:group=customername&show_customization=1">My Support</a> |
832 <a href="user%s">My Details</a> | <a href="logout">Logout</a>
833 '''%(userid, userid, userid)
834         else:
835             user_info = '<a href="login">Login</a>'
836         if self.user is not None:
837             add_links = '''
838 | Add
839 <a href="newissue">Issue</a>,
840 <a href="newsupport">Support</a>,
841 <a href="newuser">User</a>
842 '''
843         else:
844             add_links = ''
845         self.write('''<html><head>
846 <title>%s</title>
847 <style type="text/css">%s</style>
848 </head>
849 <body bgcolor=#ffffff>
850 %s
851 <table width=100%% border=0 cellspacing=0 cellpadding=2>
852 <tr class="location-bar"><td><big><strong>%s</strong></big></td>
853 <td align=right valign=bottom>%s</td></tr>
854 <tr class="location-bar">
855 <td align=left>All
856 <a href="issue?status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:filter=status&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>,
857 <a href="support?status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:filter=status&:columns=id,activity,status,title,assignedto&:group=customername&show_customization=1">Support</a>
858 | Unassigned
859 <a href="issue?assignedto=-1&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:filter=status,assignedto&:columns=id,activity,status,title,assignedto&:group=priority&show_customization=1">Issues</a>,
860 <a href="support?assignedto=-1&status=-1,unread,deferred,chatting,need-eg,in-progress,testing,done-cbb&:sort=activity&:filter=status,assignedto&:columns=id,activity,status,title,assignedto&:group=customername&show_customization=1">Support</a>
861 %s
862 %s</td>
863 <td align=right>%s</td>
864 </table>
865 '''%(title, style, message, title, user_name, add_links, admin_links,
866     user_info))
868 def parsePropsFromForm(db, cl, form, nodeid=0):
869     '''Pull properties for the given class out of the form.
870     '''
871     props = {}
872     changed = []
873     keys = form.keys()
874     num_re = re.compile('^\d+$')
875     for key in keys:
876         if not cl.properties.has_key(key):
877             continue
878         proptype = cl.properties[key]
879         if isinstance(proptype, hyperdb.String):
880             value = form[key].value.strip()
881         elif isinstance(proptype, hyperdb.Password):
882             value = password.Password(form[key].value.strip())
883         elif isinstance(proptype, hyperdb.Date):
884             value = date.Date(form[key].value.strip())
885         elif isinstance(proptype, hyperdb.Interval):
886             value = date.Interval(form[key].value.strip())
887         elif isinstance(proptype, hyperdb.Link):
888             value = form[key].value.strip()
889             # see if it's the "no selection" choice
890             if value == '-1':
891                 # don't set this property
892                 continue
893             else:
894                 # handle key values
895                 link = cl.properties[key].classname
896                 if not num_re.match(value):
897                     try:
898                         value = db.classes[link].lookup(value)
899                     except KeyError:
900                         raise ValueError, 'property "%s": %s not a %s'%(
901                             key, value, link)
902         elif isinstance(proptype, hyperdb.Multilink):
903             value = form[key]
904             if type(value) != type([]):
905                 value = [i.strip() for i in value.value.split(',')]
906             else:
907                 value = [i.value.strip() for i in value]
908             link = cl.properties[key].classname
909             l = []
910             for entry in map(str, value):
911                 if not num_re.match(entry):
912                     try:
913                         entry = db.classes[link].lookup(entry)
914                     except KeyError:
915                         raise ValueError, \
916                             'property "%s": "%s" not an entry of %s'%(key,
917                             entry, link.capitalize())
918                 l.append(entry)
919             l.sort()
920             value = l
921         props[key] = value
922         # if changed, set it
923         if nodeid and value != cl.get(nodeid, key):
924             changed.append(key)
925             props[key] = value
926     return props, changed
929 # $Log: not supported by cvs2svn $
930 # Revision 1.49  2001/11/04 03:07:12  richard
931 # Fixed various cookie-related bugs:
932 #  . bug #477685 ] base64.decodestring breaks
933 #  . bug #477837 ] lynx does not like the cookie
934 #  . bug #477892 ] Password edit doesn't fix login cookie
935 # Also closed a security hole - a logged-in user could edit another user's
936 # details.
938 # Revision 1.48  2001/11/03 01:30:18  richard
939 # Oops. uses pagefoot now.
941 # Revision 1.47  2001/11/03 01:29:28  richard
942 # Login page didn't have all close tags.
944 # Revision 1.46  2001/11/03 01:26:55  richard
945 # possibly fix truncated base64'ed user:pass
947 # Revision 1.45  2001/11/01 22:04:37  richard
948 # Started work on supporting a pop3-fetching server
949 # Fixed bugs:
950 #  . bug #477104 ] HTML tag error in roundup-server
951 #  . bug #477107 ] HTTP header problem
953 # Revision 1.44  2001/10/28 23:03:08  richard
954 # Added more useful header to the classic schema.
956 # Revision 1.43  2001/10/24 00:01:42  richard
957 # More fixes to lockout logic.
959 # Revision 1.42  2001/10/23 23:56:03  richard
960 # HTML typo
962 # Revision 1.41  2001/10/23 23:52:35  richard
963 # Fixed lock-out logic, thanks Roch'e for pointing out the problems.
965 # Revision 1.40  2001/10/23 23:06:39  richard
966 # Some cleanup.
968 # Revision 1.39  2001/10/23 01:00:18  richard
969 # Re-enabled login and registration access after lopping them off via
970 # disabling access for anonymous users.
971 # Major re-org of the htmltemplate code, cleaning it up significantly. Fixed
972 # a couple of bugs while I was there. Probably introduced a couple, but
973 # things seem to work OK at the moment.
975 # Revision 1.38  2001/10/22 03:25:01  richard
976 # Added configuration for:
977 #  . anonymous user access and registration (deny/allow)
978 #  . filter "widget" location on index page (top, bottom, both)
979 # Updated some documentation.
981 # Revision 1.37  2001/10/21 07:26:35  richard
982 # feature #473127: Filenames. I modified the file.index and htmltemplate
983 #  source so that the filename is used in the link and the creation
984 #  information is displayed.
986 # Revision 1.36  2001/10/21 04:44:50  richard
987 # bug #473124: UI inconsistency with Link fields.
988 #    This also prompted me to fix a fairly long-standing usability issue -
989 #    that of being able to turn off certain filters.
991 # Revision 1.35  2001/10/21 00:17:54  richard
992 # CGI interface view customisation section may now be hidden (patch from
993 #  Roch'e Compaan.)
995 # Revision 1.34  2001/10/20 11:58:48  richard
996 # Catch errors in login - no username or password supplied.
997 # Fixed editing of password (Password property type) thanks Roch'e Compaan.
999 # Revision 1.33  2001/10/17 00:18:41  richard
1000 # Manually constructing cookie headers now.
1002 # Revision 1.32  2001/10/16 03:36:21  richard
1003 # CGI interface wasn't handling checkboxes at all.
1005 # Revision 1.31  2001/10/14 10:55:00  richard
1006 # Handle empty strings in HTML template Link function
1008 # Revision 1.30  2001/10/09 07:38:58  richard
1009 # Pushed the base code for the extended schema CGI interface back into the
1010 # code cgi_client module so that future updates will be less painful.
1011 # Also removed a debugging print statement from cgi_client.
1013 # Revision 1.29  2001/10/09 07:25:59  richard
1014 # Added the Password property type. See "pydoc roundup.password" for
1015 # implementation details. Have updated some of the documentation too.
1017 # Revision 1.28  2001/10/08 00:34:31  richard
1018 # Change message was stuffing up for multilinks with no key property.
1020 # Revision 1.27  2001/10/05 02:23:24  richard
1021 #  . roundup-admin create now prompts for property info if none is supplied
1022 #    on the command-line.
1023 #  . hyperdb Class getprops() method may now return only the mutable
1024 #    properties.
1025 #  . Login now uses cookies, which makes it a whole lot more flexible. We can
1026 #    now support anonymous user access (read-only, unless there's an
1027 #    "anonymous" user, in which case write access is permitted). Login
1028 #    handling has been moved into cgi_client.Client.main()
1029 #  . The "extended" schema is now the default in roundup init.
1030 #  . The schemas have had their page headings modified to cope with the new
1031 #    login handling. Existing installations should copy the interfaces.py
1032 #    file from the roundup lib directory to their instance home.
1033 #  . Incorrectly had a Bizar Software copyright on the cgitb.py module from
1034 #    Ping - has been removed.
1035 #  . Fixed a whole bunch of places in the CGI interface where we should have
1036 #    been returning Not Found instead of throwing an exception.
1037 #  . Fixed a deviation from the spec: trying to modify the 'id' property of
1038 #    an item now throws an exception.
1040 # Revision 1.26  2001/09/12 08:31:42  richard
1041 # handle cases where mime type is not guessable
1043 # Revision 1.25  2001/08/29 05:30:49  richard
1044 # change messages weren't being saved when there was no-one on the nosy list.
1046 # Revision 1.24  2001/08/29 04:49:39  richard
1047 # didn't clean up fully after debugging :(
1049 # Revision 1.23  2001/08/29 04:47:18  richard
1050 # Fixed CGI client change messages so they actually include the properties
1051 # changed (again).
1053 # Revision 1.22  2001/08/17 00:08:10  richard
1054 # reverted back to sending messages always regardless of who is doing the web
1055 # edit. change notes weren't being saved. bleah. hackish.
1057 # Revision 1.21  2001/08/15 23:43:18  richard
1058 # Fixed some isFooTypes that I missed.
1059 # Refactored some code in the CGI code.
1061 # Revision 1.20  2001/08/12 06:32:36  richard
1062 # using isinstance(blah, Foo) now instead of isFooType
1064 # Revision 1.19  2001/08/07 00:24:42  richard
1065 # stupid typo
1067 # Revision 1.18  2001/08/07 00:15:51  richard
1068 # Added the copyright/license notice to (nearly) all files at request of
1069 # Bizar Software.
1071 # Revision 1.17  2001/08/02 06:38:17  richard
1072 # Roundupdb now appends "mailing list" information to its messages which
1073 # include the e-mail address and web interface address. Templates may
1074 # override this in their db classes to include specific information (support
1075 # instructions, etc).
1077 # Revision 1.16  2001/08/02 05:55:25  richard
1078 # Web edit messages aren't sent to the person who did the edit any more. No
1079 # message is generated if they are the only person on the nosy list.
1081 # Revision 1.15  2001/08/02 00:34:10  richard
1082 # bleah syntax error
1084 # Revision 1.14  2001/08/02 00:26:16  richard
1085 # Changed the order of the information in the message generated by web edits.
1087 # Revision 1.13  2001/07/30 08:12:17  richard
1088 # Added time logging and file uploading to the templates.
1090 # Revision 1.12  2001/07/30 06:26:31  richard
1091 # Added some documentation on how the newblah works.
1093 # Revision 1.11  2001/07/30 06:17:45  richard
1094 # Features:
1095 #  . Added ability for cgi newblah forms to indicate that the new node
1096 #    should be linked somewhere.
1097 # Fixed:
1098 #  . Fixed the agument handling for the roundup-admin find command.
1099 #  . Fixed handling of summary when no note supplied for newblah. Again.
1100 #  . Fixed detection of no form in htmltemplate Field display.
1102 # Revision 1.10  2001/07/30 02:37:34  richard
1103 # Temporary measure until we have decent schema migration...
1105 # Revision 1.9  2001/07/30 01:25:07  richard
1106 # Default implementation is now "classic" rather than "extended" as one would
1107 # expect.
1109 # Revision 1.8  2001/07/29 08:27:40  richard
1110 # Fixed handling of passed-in values in form elements (ie. during a
1111 # drill-down)
1113 # Revision 1.7  2001/07/29 07:01:39  richard
1114 # Added vim command to all source so that we don't get no steenkin' tabs :)
1116 # Revision 1.6  2001/07/29 04:04:00  richard
1117 # Moved some code around allowing for subclassing to change behaviour.
1119 # Revision 1.5  2001/07/28 08:16:52  richard
1120 # New issue form handles lack of note better now.
1122 # Revision 1.4  2001/07/28 00:34:34  richard
1123 # Fixed some non-string node ids.
1125 # Revision 1.3  2001/07/23 03:56:30  richard
1126 # oops, missed a config removal
1128 # Revision 1.2  2001/07/22 12:09:32  richard
1129 # Final commit of Grande Splite
1131 # Revision 1.1  2001/07/22 11:58:35  richard
1132 # More Grande Splite
1135 # vim: set filetype=python ts=4 sw=4 et si