Code

Modified roundup-mailgw so it can read e-mails from a local mail spool
[roundup.git] / roundup / cgi_client.py
index 3629ee132ddf2db1f8bced76969bf9bdbc0b1536..43b648e85cf70746552d98ee706e83ca8715d370 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: cgi_client.py,v 1.43 2001-10-24 00:01:42 richard Exp $
+# $Id: cgi_client.py,v 1.55 2001-11-07 02:34:06 jhermann Exp $
 
 import os, cgi, pprint, StringIO, urlparse, re, traceback, mimetypes
-import base64, Cookie, time
+import binascii, Cookie, time
 
 import roundupdb, htmltemplate, date, hyperdb, password
 
@@ -52,31 +52,39 @@ class Client:
     ANONYMOUS_ACCESS = 'deny'        # one of 'deny', 'allow'
     ANONYMOUS_REGISTER = 'deny'      # one of 'deny', 'allow'
 
-    def __init__(self, instance, out, env):
+    def __init__(self, instance, request, env):
         self.instance = instance
-        self.out = out
+        self.request = request
         self.env = env
         self.path = env['PATH_INFO']
         self.split_path = self.path.split('/')
 
-        self.headers_done = 0
         self.form = cgi.FieldStorage(environ=env)
         self.headers_done = 0
-        self.debug = 0
+        try:
+            self.debug = int(env.get("ROUNDUP_DEBUG", 0))
+        except ValueError:
+            # someone gave us a non-int debug level, turn it off
+            self.debug = 0
 
     def getuid(self):
         return self.db.user.lookup(self.user)
 
     def header(self, headers={'Content-Type':'text/html'}):
+        '''Put up the appropriate header.
+        '''
         if not headers.has_key('Content-Type'):
             headers['Content-Type'] = 'text/html'
+        self.request.send_response(200)
         for entry in headers.items():
-            self.out.write('%s: %s\n'%entry)
-        self.out.write('\n')
+            self.request.send_header(*entry)
+        self.request.end_headers()
         self.headers_done = 1
+        if self.debug:
+            self.headers_sent = headers
 
     def pagehead(self, title, message=None):
-        url = self.env['SCRIPT_NAME'] + '/' #self.env.get('PATH_INFO', '/')
+        url = self.env['SCRIPT_NAME'] + '/'
         machine = self.env['SERVER_NAME']
         port = self.env['SERVER_PORT']
         if port != '80': machine = machine + ':' + port
@@ -86,11 +94,27 @@ class Client:
         else:
             message = ''
         style = open(os.path.join(self.TEMPLATES, 'style.css')).read()
-        if self.user is not None:
+        user_name = self.user or ''
+        if self.user == 'admin':
+            admin_links = ' | <a href="list_classes">Class List</a>'
+        else:
+            admin_links = ''
+        if self.user not in (None, 'anonymous'):
             userid = self.db.user.lookup(self.user)
-            user_info = '(login: <a href="user%s">%s</a>)'%(userid, self.user)
+            user_info = '''
+<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> |
+<a href="user%s">My Details</a> | <a href="logout">Logout</a>
+'''%(userid, userid)
         else:
-            user_info = ''
+            user_info = '<a href="login">Login</a>'
+        if self.user is not None:
+            add_links = '''
+| Add
+<a href="newissue">Issue</a>,
+<a href="newuser">User</a>
+'''
+        else:
+            add_links = ''
         self.write('''<html><head>
 <title>%s</title>
 <style type="text/css">%s</style>
@@ -98,9 +122,19 @@ class Client:
 <body bgcolor=#ffffff>
 %s
 <table width=100%% border=0 cellspacing=0 cellpadding=2>
-<tr class="location-bar"><td><big><strong>%s</strong></big> %s</td></tr>
+<tr class="location-bar"><td><big><strong>%s</strong></big></td>
+<td align=right valign=bottom>%s</td></tr>
+<tr class="location-bar">
+<td align=left>All
+<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>
+| Unassigned
+<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>
+%s
+%s</td>
+<td align=right>%s</td>
 </table>
-'''%(title, style, message, title, user_info))
+'''%(title, style, message, title, user_name, add_links, admin_links,
+    user_info))
 
     def pagefoot(self):
         if self.debug:
@@ -112,21 +146,30 @@ class Client:
             if keys:
                 self.write('<dt><b>Form entries</b></dt>')
                 for k in self.form.keys():
-                    v = str(self.form[k].value)
-                    self.write('<dd><em>%s</em>:%s</dd>'%(k, cgi.escape(v)))
+                    v = self.form.getvalue(k, "<empty>")
+                    if type(v) is type([]):
+                        # Multiple username fields specified
+                        v = "|".join(v)
+                    self.write('<dd><em>%s</em>=%s</dd>'%(k, cgi.escape(v)))
+            keys = self.headers_sent.keys()
+            keys.sort()
+            self.write('<dt><b>Sent these HTTP headers</b></dt>')
+            for k in keys:
+                v = self.headers_sent[k]
+                self.write('<dd><em>%s</em>=%s</dd>'%(k, cgi.escape(v)))
             keys = self.env.keys()
             keys.sort()
             self.write('<dt><b>CGI environment</b></dt>')
             for k in keys:
                 v = self.env[k]
-                self.write('<dd><em>%s</em>:%s</dd>'%(k, cgi.escape(v)))
+                self.write('<dd><em>%s</em>=%s</dd>'%(k, cgi.escape(v)))
             self.write('</dl></small>')
         self.write('</body></html>')
 
     def write(self, content):
         if not self.headers_done:
             self.header()
-        self.out.write(content)
+        self.request.wfile.write(content)
 
     def index_arg(self, arg):
         ''' handle the args to index - they might be a list from the form
@@ -284,13 +327,58 @@ class Client:
     showmsg = shownode
 
     def showuser(self, message=None):
-        ''' display an item
+        '''Display a user page for editing. Make sure the user is allowed
+            to edit this node, and also check for password changes.
         '''
-        if self.user in ('admin', self.db.user.get(self.nodeid, 'username')):
-            self.shownode(message)
-        else:
+        if self.user == 'anonymous':
             raise Unauthorised
 
+        user = self.db.user
+
+        # get the username of the node being edited
+        node_user = user.get(self.nodeid, 'username')
+
+        if self.user not in ('admin', node_user):
+            raise Unauthorised
+
+        #
+        # perform any editing
+        #
+        keys = self.form.keys()
+        num_re = re.compile('^\d+$')
+        if keys:
+            try:
+                props, changed = parsePropsFromForm(self.db, user, self.form,
+                    self.nodeid)
+                if self.nodeid == self.getuid() and 'password' in changed:
+                    set_cookie = self.form['password'].value.strip()
+                else:
+                    set_cookie = 0
+                user.set(self.nodeid, **props)
+                self._post_editnode(self.nodeid, changed)
+                # and some feedback for the user
+                message = '%s edited ok'%', '.join(changed)
+            except:
+                s = StringIO.StringIO()
+                traceback.print_exc(None, s)
+                message = '<pre>%s</pre>'%cgi.escape(s.getvalue())
+        else:
+            set_cookie = 0
+
+        # fix the cookie if the password has changed
+        if set_cookie:
+            self.set_cookie(self.user, set_cookie)
+
+        #
+        # now the display
+        #
+        self.pagehead('User: %s'%node_user, message)
+
+        # use the template to display the item
+        item = htmltemplate.ItemTemplate(self, self.TEMPLATES, 'user')
+        item.render(self.nodeid)
+        self.pagefoot()
+
     def showfile(self):
         ''' display a file
         '''
@@ -498,7 +586,7 @@ class Client:
         else:
             raise Unauthorised
 
-    def login(self, message=None):
+    def login(self, message=None, newuser_form=None):
         self.pagehead('Login to roundup', message)
         self.write('''
 <table>
@@ -514,31 +602,38 @@ class Client:
 ''')
         if self.user is None and self.ANONYMOUS_REGISTER == 'deny':
             self.write('</table>')
+            self.pagefoot()
             return
+        values = {'realname': '', 'organisation': '', 'address': '',
+            'phone': '', 'username': '', 'password': '', 'confirm': ''}
+        if newuser_form is not None:
+            for key in newuser_form.keys():
+                values[key] = newuser_form[key].value
         self.write('''
 <p>
 <tr><td colspan=2 class="strong-header">New User Registration</td></tr>
 <tr><td colspan=2><em>marked items</em> are optional...</td></tr>
 <form action="newuser_action" method=POST>
 <tr><td align=right><em>Name: </em></td>
-    <td><input name="realname"></td></tr>
+    <td><input name="realname" value="%(realname)s"></td></tr>
 <tr><td align=right><em>Organisation: </em></td>
-    <td><input name="organisation"></td></tr>
+    <td><input name="organisation" value="%(organisation)s"></td></tr>
 <tr><td align=right>E-Mail Address: </td>
-    <td><input name="address"></td></tr>
+    <td><input name="address" value="%(address)s"></td></tr>
 <tr><td align=right><em>Phone: </em></td>
-    <td><input name="phone"></td></tr>
+    <td><input name="phone" value="%(phone)s"></td></tr>
 <tr><td align=right>Preferred Login name: </td>
-    <td><input name="username"></td></tr>
+    <td><input name="username" value="%(username)s"></td></tr>
 <tr><td align=right>Password: </td>
-    <td><input type="password" name="password"></td></tr>
+    <td><input type="password" name="password" value="%(password)s"></td></tr>
 <tr><td align=right>Password Again: </td>
-    <td><input type="password" name="confirm"></td></tr>
+    <td><input type="password" name="confirm" value="%(confirm)s"></td></tr>
 <tr><td></td>
     <td><input type="submit" value="Register"></td></tr>
 </form>
 </table>
-''')
+'''%values)
+        self.pagefoot()
 
     def login_action(self, message=None):
         if not self.form.has_key('__login_name'):
@@ -548,7 +643,6 @@ class Client:
             password = self.form['__login_password'].value
         else:
             password = ''
-        print self.user, password
         # make sure the user exists
         try:
             uid = self.db.user.lookup(self.user)
@@ -563,14 +657,22 @@ class Client:
             self.make_user_anonymous()
             return self.login(message='Incorrect password')
 
-        # construct the cookie
-        uid = self.db.user.lookup(self.user)
-        user = base64.encodestring('%s:%s'%(self.user, password))[:-1]
-        path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
-            ''))
-        self.header({'Set-Cookie': 'roundup_user=%s; Path=%s;'%(user, path)})
+        self.set_cookie(self.user, password)
         return self.index()
 
+    def set_cookie(self, user, password):
+        # construct the cookie
+        user = binascii.b2a_base64('%s:%s'%(user, password)).strip()
+        if user[-1] == '=':
+          if user[-2] == '=':
+            user = user[:-2]
+          else:
+            user = user[:-1]
+        expire = Cookie._getdate(86400*365)
+        path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME']))
+        self.header({'Set-Cookie': 'roundup_user=%s; expires=%s; Path=%s;' % (
+            user, expire, path)})
+
     def make_user_anonymous(self):
         # make us anonymous if we can
         try:
@@ -582,11 +684,11 @@ class Client:
     def logout(self, message=None):
         self.make_user_anonymous()
         # construct the logout cookie
-        path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
-            ''))
         now = Cookie._getdate()
+        path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME']))
         self.header({'Set-Cookie':
-            'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now, path)})
+            'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now,
+            path)})
         return self.login()
 
     def newuser_action(self, message=None):
@@ -598,17 +700,15 @@ class Client:
         self.db = self.instance.open('admin')
 
         # TODO: pre-check the required fields and username key property
-        cl = self.db.classes['user']
-        props, dummy = parsePropsFromForm(self.db, cl, self.form)
-        uid = cl.create(**props)
-        self.user = self.db.user.get(uid, 'username')
-        password = self.db.user.get(uid, 'password')
-        # construct the cookie
-        uid = self.db.user.lookup(self.user)
-        user = base64.encodestring('%s:%s'%(self.user, password))[:-1]
-        path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
-            ''))
-        self.header({'Set-Cookie': 'roundup_user=%s; Path=%s;'%(user, path)})
+        cl = self.db.user
+        try:
+            props, dummy = parsePropsFromForm(self.db, cl, self.form)
+            uid = cl.create(**props)
+        except ValueError, message:
+            return self.login(message, newuser_form=self.form)
+        self.user = cl.get(uid, 'username')
+        password = cl.get(uid, 'password')
+        self.set_cookie(self.user, self.form['password'].value)
         return self.index()
 
     def main(self, dre=re.compile(r'([^\d]+)(\d+)'),
@@ -621,7 +721,14 @@ class Client:
         if (cookie.has_key('roundup_user') and
                 cookie['roundup_user'].value != 'deleted'):
             cookie = cookie['roundup_user'].value
-            user, password = base64.decodestring(cookie).split(':')
+            if len(cookie)%4:
+              cookie = cookie + '='*(4-len(cookie)%4)
+            try:
+                user, password = binascii.a2b_base64(cookie).split(':')
+            except (TypeError, binascii.Error, binascii.Incomplete):
+                # damaged cookie!
+                user, password = 'anonymous', ''
+
             # make sure the user exists
             try:
                 uid = self.db.user.lookup(user)
@@ -848,6 +955,55 @@ def parsePropsFromForm(db, cl, form, nodeid=0):
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.54  2001/11/07 01:16:12  richard
+# Remove the '=' padding from cookie value so quoting isn't an issue.
+#
+# Revision 1.53  2001/11/06 23:22:05  jhermann
+# More IE fixes: it does not like quotes around cookie values; in the
+# hope this does not break anything for other browser; if it does, we
+# need to check HTTP_USER_AGENT
+#
+# Revision 1.52  2001/11/06 23:11:22  jhermann
+# Fixed debug output in page footer; added expiry date to the login cookie
+# (expires 1 year in the future) to prevent probs with certain versions
+# of IE
+#
+# Revision 1.51  2001/11/06 22:00:34  jhermann
+# Get debug level from ROUNDUP_DEBUG env var
+#
+# Revision 1.50  2001/11/05 23:45:40  richard
+# Fixed newuser_action so it sets the cookie with the unencrypted password.
+# Also made it present nicer error messages (not tracebacks).
+#
+# Revision 1.49  2001/11/04 03:07:12  richard
+# Fixed various cookie-related bugs:
+#  . bug #477685 ] base64.decodestring breaks
+#  . bug #477837 ] lynx does not like the cookie
+#  . bug #477892 ] Password edit doesn't fix login cookie
+# Also closed a security hole - a logged-in user could edit another user's
+# details.
+#
+# Revision 1.48  2001/11/03 01:30:18  richard
+# Oops. uses pagefoot now.
+#
+# Revision 1.47  2001/11/03 01:29:28  richard
+# Login page didn't have all close tags.
+#
+# Revision 1.46  2001/11/03 01:26:55  richard
+# possibly fix truncated base64'ed user:pass
+#
+# Revision 1.45  2001/11/01 22:04:37  richard
+# Started work on supporting a pop3-fetching server
+# Fixed bugs:
+#  . bug #477104 ] HTML tag error in roundup-server
+#  . bug #477107 ] HTTP header problem
+#
+# Revision 1.44  2001/10/28 23:03:08  richard
+# Added more useful header to the classic schema.
+#
+# Revision 1.43  2001/10/24 00:01:42  richard
+# More fixes to lockout logic.
+#
 # Revision 1.42  2001/10/23 23:56:03  richard
 # HTML typo
 #