Code

be able to parse b0rken Interval serialisation
[roundup.git] / roundup / cgi / client.py
index bb385c9129cefc3f1aadff758e6c609bbbb3cf13..8ac665d32243a1b2dc0c0a68035e5118e3154179 100644 (file)
@@ -1,4 +1,4 @@
-# $Id: client.py,v 1.38 2002-09-18 00:01:28 richard Exp $
+# $Id: client.py,v 1.52 2002-10-09 01:00:40 richard Exp $
 
 __doc__ = """
 WWW request handler (also used in the stand-alone server).
@@ -10,7 +10,7 @@ import binascii, Cookie, time, random
 from roundup import roundupdb, date, hyperdb, password
 from roundup.i18n import _
 
-from roundup.cgi.templating import getTemplate, HTMLRequest, NoTemplate
+from roundup.cgi.templating import Templates, HTMLRequest, NoTemplate
 from roundup.cgi import cgitb
 
 from roundup.cgi.PageTemplates import PageTemplate
@@ -48,23 +48,36 @@ def initialiseSecurity(security):
     security.addPermissionToRole('Admin', p)
 
 class Client:
-    '''
-    A note about login
-    ------------------
-
-    If the user has no login cookie, then they are anonymous. There
-    are two levels of anonymous use. If there is no 'anonymous' user, there
-    is no login at all and the database is opened in read-only mode. If the
-    'anonymous' user exists, the user is logged in using that user (though
-    there is no cookie). This allows them to modify the database, and all
-    modifications are attributed to the 'anonymous' user.
+    ''' Instantiate to handle one CGI request.
 
-    Once a user logs in, they are assigned a session. The Client instance
-    keeps the nodeid of the session as the "session" attribute.
+    See inner_main for request processing.
 
-    Client attributes:
+    Client attributes at instantiation:
         "path" is the PATH_INFO inside the instance (with no leading '/')
         "base" is the base URL for the instance
+        "form" is the cgi form, an instance of FieldStorage from the standard
+               cgi module
+        "additional_headers" is a dictionary of additional HTTP headers that
+               should be sent to the client
+        "response_code" is the HTTP response code to send to the client
+
+    During the processing of a request, the following attributes are used:
+        "error_message" holds a list of error messages
+        "ok_message" holds a list of OK messages
+        "session" is the current user session id
+        "user" is the current user's name
+        "userid" is the current user's id
+        "template" is the current :template context
+        "classname" is the current class context name
+        "nodeid" is the current context item id
+
+    User Identification:
+     If the user has no login cookie, then they are anonymous and are logged
+     in as that user. This typically gives them all Permissions assigned to the
+     Anonymous Role.
+
+     Once a user logs in, they are assigned a session. The Client instance
+     keeps the nodeid of the session as the "session" attribute.
     '''
 
     def __init__(self, instance, request, env, form=None):
@@ -134,7 +147,6 @@ class Client:
             - NotFound       (raised wherever it needs to be)
               percolates up to the CGI interface that called the client
         '''
-        self.content_action = None
         self.ok_message = []
         self.error_message = []
         try:
@@ -152,14 +164,8 @@ class Client:
             self.additional_headers['Pragma'] = 'no-cache'
             self.additional_headers['Expires'] = 'Thu, 1 Jan 1970 00:00:00 GMT'
 
-            if self.form.has_key(':contentonly'):
-                # just the content
-                self.write(self.content())
-            else:
-                # render the content inside the page template
-                self.write(self.renderTemplate('page', '',
-                    ok_message=self.ok_message,
-                    error_message=self.error_message))
+            # render the content
+            self.write(self.renderContext())
         except Redirect, url:
             # let's redirect - if the url isn't None, then we need to do
             # the headers, otherwise the headers have been set before the
@@ -173,7 +179,13 @@ class Client:
         except SendStaticFile, file:
             self.serve_static_file(str(file))
         except Unauthorised, message:
-            self.write(self.renderTemplate('page', '', error_message=message))
+            self.classname=None
+            self.template=''
+            self.error_message.append(message)
+            self.write(self.renderContext())
+        except NotFound:
+            # pass through
+            raise
         except:
             # everything else
             self.write(cgitb.html())
@@ -333,39 +345,27 @@ class Client:
         self.write(open(os.path.join(self.instance.config.TEMPLATES,
             file)).read())
 
-    def renderTemplate(self, name, extension, **kwargs):
+    def renderContext(self):
         ''' Return a PageTemplate for the named page
         '''
-        pt = getTemplate(self.instance.config.TEMPLATES, name, extension)
+        name = self.classname
+        extension = self.template
+        pt = Templates(self.instance.config.TEMPLATES).get(name, extension)
+
         # catch errors so we can handle PT rendering errors more nicely
+        args = {
+            'ok_message': self.ok_message,
+            'error_message': self.error_message
+        }
         try:
             # let the template render figure stuff out
-            return pt.render(self, None, None, **kwargs)
-        except PageTemplate.PTRuntimeError, message:
-            return '<strong>%s</strong><ol><li>%s</ol>'%(message,
-                '<li>'.join([cgi.escape(x) for x in pt._v_errors]))
+            return pt.render(self, None, None, **args)
         except NoTemplate, message:
             return '<strong>%s</strong>'%message
         except:
             # everything else
             return cgitb.pt_html()
 
-    def content(self):
-        ''' Callback used by the page template to render the content of 
-            the page.
-
-            If we don't have a specific class to display, that is none was
-            determined in determine_context(), then we display a "home"
-            template.
-        '''
-        # now render the page content using the template we determined in
-        # determine_context
-        if self.classname is None:
-            name = 'home'
-        else:
-            name = self.classname
-        return self.renderTemplate(self.classname, self.template)
-
     # these are the actions that are available
     actions = (
         ('edit',     'editItemAction'),
@@ -439,7 +439,10 @@ class Client:
         if self.debug:
             self.headers_sent = headers
 
-    def set_cookie(self, user, password):
+    def set_cookie(self, user):
+        ''' Set up a session cookie for the user and store away the user's
+            login info against the session.
+        '''
         # TODO generate a much, much stronger session key ;)
         self.session = binascii.b2a_base64(repr(random.random())).strip()
 
@@ -474,24 +477,13 @@ class Client:
         self.userid = self.db.user.lookup('anonymous')
         self.user = 'anonymous'
 
-    def logout(self):
-        ''' Make us really anonymous - nuke the cookie too
-        '''
-        self.make_user_anonymous()
-
-        # construct the logout cookie
-        now = Cookie._getdate()
-        path = '/'.join((self.env['SCRIPT_NAME'], self.env['TRACKER_NAME'],
-            ''))
-        self.additional_headers['Set-Cookie'] = \
-           'roundup_user_2=deleted; Max-Age=0; expires=%s; Path=%s;'%(now, path)
-        self.login()
-
     def opendb(self, user):
         ''' Open the database.
         '''
         # open the db if the user has changed
         if not hasattr(self, 'db') or user != self.db.journaltag:
+            if hasattr(self, 'db'):
+                self.db.close()
             self.db = self.instance.open(user)
 
     #
@@ -508,25 +500,24 @@ class Client:
             self.error_message.append(_('Username required'))
             return
 
+        # get the login info
         self.user = self.form['__login_name'].value
-        # re-open the database for real, using the user
-        self.opendb(self.user)
         if self.form.has_key('__login_password'):
             password = self.form['__login_password'].value
         else:
             password = ''
+
         # make sure the user exists
         try:
             self.userid = self.db.user.lookup(self.user)
         except KeyError:
             name = self.user
-            self.make_user_anonymous()
             self.error_message.append(_('No such user "%(name)s"')%locals())
+            self.make_user_anonymous()
             return
 
-        # and that the password is correct
-        pw = self.db.user.get(self.userid, 'password')
-        if password != pw:
+        # verify the password
+        if not self.verifyPassword(self.userid, password):
             self.make_user_anonymous()
             self.error_message.append(_('Incorrect password'))
             return
@@ -534,10 +525,24 @@ class Client:
         # make sure we're allowed to be here
         if not self.loginPermission():
             self.make_user_anonymous()
-            raise Unauthorised, _("You do not have permission to login")
+            self.error_message.append(_("You do not have permission to login"))
+            return
+
+        # now we're OK, re-open the database for real, using the user
+        self.opendb(self.user)
 
         # set the session cookie
-        self.set_cookie(self.user, password)
+        self.set_cookie(self.user)
+
+    def verifyPassword(self, userid, password):
+        ''' Verify the password that the user has supplied
+        '''
+        stored = self.db.user.get(self.userid, 'password')
+        if password == stored:
+            return 1
+        if not password and not stored:
+            return 1
+        return 0
 
     def loginPermission(self):
         ''' Determine whether the user has permission to log in.
@@ -595,7 +600,7 @@ class Client:
             props['roles'] = self.instance.config.NEW_WEB_USER_ROLES
             self.userid = cl.create(**props)
             self.db.commit()
-        except ValueError, message:
+        except (ValueError, KeyError), message:
             self.error_message.append(message)
             return
 
@@ -603,8 +608,14 @@ class Client:
         self.user = cl.get(self.userid, 'username')
         # re-open the database for real, using the user
         self.opendb(self.user)
-        password = self.db.user.get(self.userid, 'password')
-        self.set_cookie(self.user, password)
+
+        # if we have a session, update it
+        if hasattr(self, 'session'):
+            self.db.sessions.set(self.session, user=self.user,
+                last_use=time.time())
+        else:
+            # new session cookie
+            self.set_cookie(self.user)
 
         # nice message
         message = _('You are now registered, welcome!')
@@ -1022,7 +1033,8 @@ class Client:
         # in a nutshell, don't do anything if there's no note or there's no
         # NOSY
         if self.form.has_key(':note'):
-            note = self.form[':note'].value.strip()
+            # fix the CRLF/CR -> LF stuff
+            note = fixNewlines(self.form[':note'].value.strip())
         if not note:
             return None, files
         if not props.has_key('messages'):
@@ -1094,6 +1106,15 @@ class Client:
                     link = self.db.classes[link]
                     link.set(nodeid, **{property: nid})
 
+def fixNewlines(text):
+    ''' Homogenise line endings.
+
+        Different web clients send different line ending values, but
+        other systems (eg. email) don't necessarily handle those line
+        endings. Our solution is to convert all line endings to LF.
+    '''
+    text = text.replace('\r\n', '\n')
+    return text.replace('\r', '\n')
 
 def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
     ''' Pull properties for the given class out of the form.
@@ -1133,6 +1154,8 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
         if isinstance(proptype, hyperdb.String):
             if not value:
                 continue
+            # fix the CRLF/CR -> LF stuff
+            value = fixNewlines(value)
         elif isinstance(proptype, hyperdb.Password):
             if not value:
                 # ignore empty password values
@@ -1150,12 +1173,12 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
             if value:
                 value = date.Date(form[key].value.strip())
             else:
-                value = None
+                continue
         elif isinstance(proptype, hyperdb.Interval):
             if value:
                 value = date.Interval(form[key].value.strip())
             else:
-                value = None
+                continue
         elif isinstance(proptype, hyperdb.Link):
             # see if it's the "no selection" choice
             if value == '-1':
@@ -1205,8 +1228,8 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
         elif isinstance(proptype, hyperdb.Number):
             props[key] = value = int(value)
 
-        # register this as received if required
-        if key in required:
+        # register this as received if required?
+        if key in required and value is not None:
             required.remove(key)
 
         # get the old value