[pysignup] / trunk / cgi / change.py Repository:
ViewVC logotype

View of /trunk/cgi/change.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 100 - (download) (as text) (annotate)
Sun Oct 5 17:16:27 2008 UTC (23 months ago) by pinky
File size: 21363 byte(s)
Adding exceptions for SQLite-related permissions errors
#!/usr/bin/python
### ^ change above to reflect the file path to python, if it's not 
# /usr/bin/python (i.e. on a windows box)

"""Web interface for changing settings.conf.

Allows you to change all of the variables in settings.conf.

@copyright: 2008 Nathaniel Herman
@license: GNU GPLv3, see COPYING for full details
"""

### 
# Copyright (C) 2008 Nathaniel Herman
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of   
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
###


import cgi, functions, re, Cookie, os, string, time, sys
from configobj import ConfigObj
#import cgitb; cgitb.enable() # only enable when developing

form = cgi.FieldStorage()
cookie = Cookie.SimpleCookie()

userconf = ConfigObj("settings.conf", list_values=False)
defaultconf = ConfigObj("defaultsettings.conf", list_values=False)
# get rid of first 2 comments of defaultconf file which say not to edit it
del defaultconf.initial_comment[0:2]
# merge user config with defaultconfig, user overrides default
defaultconf.merge(userconf)
config = defaultconf
config.filename = "settings.conf"
config.write_empty_values = True

sitename = cgi.escape(config['required']['sitename'])
stometh = config['required']['storage']
sessionfile = config['file']['session-file']
datafile = config["file"]["data-file"]
sqlitedb = config['sqlite']['sqlitedb']

class Html:
    """Class for handling HTML."""

    def __init__(self, inithtml='', interfacemsg=''):
        self.html = inithtml
        self.interfacemsg = interfacemsg
    
    def loadinterface(self):
        """Adds HTML for the change.py interface to self.html"""
        # we reload the sitename variable here instead of relying on the 
        # global, because the sitename could've changed between the global
        # being set, and the call to this function
        sitename = cgi.escape(config['required']['sitename'])
        inthtml = "Content-type: text/html\n\n"
        inthtml += functions.read("changetmpl.html") % {'sitename': sitename,
                                               'skinspecific': self.skinhtml(),
                                               'formdata': self.displayform()}
        self.html += inthtml

    def loadlogin(self, passworderror=''):
        """Adds HTML for the login page to self.html"""
        formhtml = """\
<form action="" method="post">
<h3>Login</h3>
<table>
 <tr>
  <td>Password:</td>
  <td><input name="passwd" type="password" /></td>
  <td>%s</td>
 </tr>
 <tr>
  <td></td>
  <td id="default-pass">(Note: default password is "pysignup")</td>
 </tr>
 <tr>
  <td><input type="submit" value="Submit" /></td>
 </tr>
</table>
<input type="hidden" name="action" value="login" />
</form>""" % passworderror

        loginhtml = "Content-type: text/html\n\n"
        loginhtml += functions.read("changetmpl.html") % {'sitename': sitename,
                                               'skinspecific': self.skinhtml(),
                                               'formdata': formhtml}
        self.html += loginhtml

    def loadpass(self, msg):
        """Adds HTML for changing the admin password to self.html"""
        formhtml = """<form action="" method="post">
<h2>Change password</h2>
<b>%s</b>
<table>
 <tr><td></td></tr>
 <tr>
  <td>Old password:</td>
  <td><input name="oldpass" type="password" /></td>
 </tr>
 <tr>
  <td></td>
  <td id="default-pass">(Note: default password is "pysignup")</td>
 </tr>
 <tr>
  <td>New password:</td>
  <td><input name="newpass" type="password" /></td>
 </tr>
 <tr>
  <td>Confirm new password:</td>
  <td><input name="confirmpass" type="password" /></td>
 </tr>
 <tr>
  <td><input type="submit" value="Submit" /></td>
 </tr>
</table>
<input type="hidden" name="action" value="changepass" />
</form>""" % msg

        passhtml = "Content-type: text/html\n\n"
        passhtml += functions.read("changetmpl.html") % {'sitename': sitename,
                                               'skinspecific': self.skinhtml(),
                                               'formdata': formhtml}
        self.html += passhtml

    def skinhtml(self):
        """Gets user-specified theme name, and returns HTML for it."""
        theme = config['required']['theme']
        if theme:
            html = functions.loadtheme(theme)
        else:
            html = ''
        return html

    def displaykey(self, section, key, subsection=None):
        """Returns HTML of an input box for a key in settings.conf."""
        # if the key is in multiple subsections, slightly different code is
        # needed to retrieve it, currently only supports one subsection though
        if subsection:
            comment = parsecomment(section, key, subsection)
            value = cgi.escape(config[section][subsection][key])
            keyhtml = """\
 <tr>
  <td class="subsection-key">%(name)s</td>
  <td><input type="text" name="%(name)s" value="%(value)s" size="40" /></td>
  <td><i>%(comment)s</i></td>
 </tr>
 <tr><td><br /></td></tr>
""" % {'name': key, 'value': value, 'comment': comment}

        else:
            # get the comment from parsecomment function
            comment = parsecomment(section, key)
            value = cgi.escape(config[section][key], quote=True)
            keyhtml = """\
 <tr>
  <td class="section-key">%(name)s</td>
  <td><input type="text" name="%(name)s" value="%(value)s" size="40" /></td>
  <td><i>%(comment)s</i></td>
 </tr>
 <tr><td><br/ ></td></tr>
""" % {'name': key, 'value': value, 'comment': comment}

        return keyhtml

    def displayform(self):
        """Returns HTML form with every key and section from settings.conf."""
        formhtml = ''
        # go through each section of the configuration file
        for section in config.sections:
            formhtml += ' <tr><th class="section-header">%s</th></tr>\n'\
            % section
            # the section variable is just a string, so make a dictionary that
            # holds all of the section data
            sectiondict = config[section]
            for key in sectiondict:
                try:
                    # check if the key is actually a subsection
                    sectiondict[key].main
                    break
                except AttributeError:
                    pass
                formhtml += self.displaykey(section, key)
            # now go through each subsection in each section
            for subsection in sectiondict.sections:
                formhtml += ' <tr><th class="subsection-header">%s</th></tr>\n'\
                % subsection
                subsectiondict = sectiondict[subsection]
                for key in subsectiondict:
                    formhtml += self.displaykey(section, key, subsection)

        # now make full html, which includes <form> tags, etc.
        fullhtml = """<p>Modify:</p>
%s
<form action="" method="post">
 <table>
%s
 </table>

 <input type="submit" name="action" value="Change" />
 <br />
 <input type="submit" name="action"
 value="Change and clear signup data" /> <b>Warning:</b> clears <b>all</b>
 signup data, you can only restore it if you have file access to the webserver.
</form>
""" % (self.interfacemsg, formhtml)

        return fullhtml

    def display(self):
        '''Displays all HTML in self.html'''
        print self.html


def stripillegal(str):
    """Returns string with all cookie-illegal characters removed."""
    legal = string.letters + string.digits + "!#$%&'*+-.^_`|~"
    for char in str:
        if char not in legal:
            # replaces each illegal character with ''
            str = str.replace(char, '')

    return str

def parsecomment(section, key, subsection=None):
    """Parses comment for key into text.

    Removes all "#" signs and extra spacing."""

    if subsection:
        commentlist = config[section][subsection].comments[key]
    else:
        commentlist = config[section].comments[key]

    fullcomment = ''
    for comment in commentlist:
        # remove all occurences of more than two spaces (usually indentation)
        comment = re.sub('  +', '', comment)
        # remove all # signs
        comment = re.sub('#+', '', comment)
        fullcomment += comment
    return fullcomment

def change(section, key, subsection=None):
    """Changes a configuration value to a viewer-inputted value."""
    try:
        tochange = form[key].value
    except KeyError:
        # if there is no value filled in for a key, make it a blank string
        tochange = ''
    if subsection:
        config[section][subsection][key] = tochange
    else:
        config[section][key] = tochange

def getnchange():
    """Goes through each configuration field and changes it.
   
    Writes changes to file after changing."""
    # this is really just copying all the code from displayform() over.
    # also, someone could potentially edit the settings.conf file 
    # *significantly* in between the time this page is loaded and the time the
    # change button is hit, and it would screw up because its assuming that
    # hasn't happened (this is probably unlikely however)
    for section in config.sections:
        sectiondict = config[section]
        for key in sectiondict:
            try:
                # check if key is really a subsection
                sectiondict[key].main
                break
            except AttributeError:
                pass
            change(section, key)
        for subsection in sectiondict.sections:
            subsectiondict = sectiondict[subsection]
            for key in subsectiondict:
                change(section, key, subsection)
    # actually write all the changes to settings.conf
    try:
        # if settings.conf already exists, back it up
        if userconf:
            userconf.filename = 'backups/settings.conf.%s' % time.time()
            userconf.write()
        # only if the old file was successfully backed up, should the changes
        # be written
        config.write()
    except IOError:
        chtml.interfacemsg = '<p class="error-msg">Permission denied!</p>'

def setcookie(expires=''):
    """Creates a session ID cookie.
    
    Cookie will expire after the given expiry time in seconds. 
    The value of the session ID is returned."""

    sitename = cgi.escape(config['required']['sitename'])
    # creates a 20 character long string of random letters, numbers
    sessionid = functions.makesessionid()
    # cookie name will be sitename_sessionid, with sitename having all cookie
    # illegal characters taken out
    cookiename = '%s_sessionid' % stripillegal(sitename)
    cookie[cookiename] = sessionid
    cookie[cookiename]['expires'] = expires
    return sessionid

def getcookie():
    """Returns a session ID cookie from the viewer.
    
    If they don't have a cookie, returns None."""
    
    sitename = cgi.escape(config['required']['sitename'])
    cookiename = '%s_sessionid' % stripillegal(sitename)
    try:
        viewer_cookie = os.environ['HTTP_COOKIE']
    except KeyError:
        # no cookie
        return
    # if they have a cookie, get the sessionid value and return it
    if viewer_cookie:
        cookie.load(viewer_cookie)
        try:
            sessionid = cookie[cookiename].value
            return sessionid
        # they have a cookie from the site, but none called "sitename_sessionid"
        except KeyError:
            return
    # no cookie, so return none
    else:
        return

class FileSto:
    '''Class for using raw files to store data (sessionids, etc).'''

    def __init__(self, datafile='', sessionfile=''):
        self.datafile = datafile
        self.sessionfile = sessionfile

    def clear(self):
        """Clears data file, and makes a backup at datafile.timestamp."""
        curdata = functions.read(self.datafile)
        # datafile doesn't exist or is empty
        if not curdata:
            return
        bakdatafile = 'backups/%s.%s' % (self.datafile, time.time())
        didbak = functions.write(bakdatafile, curdata)
        if not didbak:
            chtml.interfacemsg = '''<p class="error-msg">Could not backup
data file! Check permissions of the backups directory.</p>'''
            # if it can't back up the data, make sure it doesn't clear it either
            return
        didclear = functions.write(self.datafile, "")
        if not didclear:
            chtml.interfacemsg = '''<p class="error-msg">Could not clear
data file! Check permissions.</p>'''

    def startsession(self):
        """Starts a session for a user."""
        # expires in a week
        expires = 60 * 60 * 24 * 7
        sessionid = setcookie(expires)
        # we hash the sessionid, so that even if someone can read the file,
        # they won't actually know the sessionid
        towrite = '%s\n' % functions.makehash(sessionid, 0)
        didwrite = functions.save(self.sessionfile, towrite)
        if not didwrite:
            chtml.interfacemsg = '<p class="error-msg">Error: cannot write to '
            chtml.interfacemsg += 'session file! Check permissions.'

    def hassession(self):
        """Checks if the viewer has a valid session.
    
        First gets the viewer's sessionid cookie, if there is one, then checks
        to see if their sessionid is valid. Returns True if they have a valid 
        session, and None if not."""
    
        sessionid = getcookie()
        if not sessionid:
            return
        valid_ids = functions.read(self.sessionfile)
        if not valid_ids:
            # functions.read returns False if the file doesn't exist, which will
            # break the regex search
            valid_ids = ''
        # regex for the md5 hash of the given sid, with a newline after it
        sidreg = re.compile('%s\n' % functions.makehash(sessionid, 0))
        # check if the viewer's session id is in the session file
        if sidreg.search(valid_ids):
            return True
        else:
            return


class Sqlite:
    '''Class for using an SQLite database to store data (sessionids, etc.)'''
    
    def __init__(self, dbfile):
        try:
            import pysqlite2.dbapi2 as sqlite
            self.sqlite = sqlite
        except ImportError:
            chtml.html = 'Content-type: text/html\n\n'
            chtml.html += 'PySQLite 2 is not installed!\n<br />\n'
            chtml.html += 'You can download it <a href="http://oss.itsyst'
            chtml.html += 'ementwicklung.de/trac/pysqlite/wiki/WikiStart'
            chtml.html += '#Downloads">here</a>'
            chtml.display()
            sys.exit()

        self.dbfile = dbfile
        try:
            self.conn = sqlite.connect(self.dbfile)
        except sqlite.OperationalError:
            chtml.html = 'Content-type: text/html\n\n'
            chtml.html += 'Could not read the SQLite DB. Check permissions.'
            chtml.display()
            sys.exit()

    def clear(self):
        '''Clears signup data table, and backs up the database.'''
        curdata = functions.read(self.dbfile)
        if not curdata:
            return
        bakdb = 'backups/%s.%s' % (self.dbfile, time.time())
        didbak = functions.write(bakdb, curdata)
        if not didbak:
            chtml.interfacemsg = '''<p class="error-msg">Could not backup
SQLite DB! Check permissions of the backups directory.</p>'''
            # if it can't back up the data, make sure it doesn't clear it either
            return

        c = self.conn.cursor()
        try:
            c.execute('drop table if exists signup')
        except self.sqlite.OperationalError:
            chtml.interfacemsg = '''<p class="error-msg">Could not clear
SQLite DB! Check permissions.</p>'''
            return
        c.close()
        self.conn.commit()
    
    def startsession(self):
        '''Starts session for a user.'''
        # open dbfile in append mode to make sure it can write to the db
        try:
            open(self.dbfile, 'a')
        except IOError:
            chtml.interfacemsg = '<p class="error-msg">Cannot write to SQLite'
            chtml.interfacemsg += ' database! Check permissions.</p>'
            return
        c = self.conn.cursor()
        # expires in a week
        expires = 60 * 60 * 24 * 7
        sessionid = setcookie(expires)
        # we hash the sessionid, so that even if someone can read the db,
        # they won't actually know the sessionid
        sesshash = functions.makehash(sessionid, 0)
        try:
            c.execute('insert into sessions values (?)', (sesshash,))
        # the sessions table doesn't exist, so create it and then rerun query
        except self.sqlite.OperationalError:
            functions.createsqlite(self.conn)
            c.execute('insert into sessions values (?)', (sesshash,))

        c.close()
        self.conn.commit()

    def hassession(self):
        '''Checks db to see if viewer has a valid session.
        
        First gets the viewer's sessionid cookie, if there is one, then checks
        to see if their sessionid is valid. Returns True if they have a valid
        session, and None if not.'''
        
        sessionid = getcookie()
        if not sessionid:
            return
        c = self.conn.cursor()
        sesshash = functions.makehash(sessionid, 0)
        try:
            c.execute('select * from sessions where sessionid=?', (sesshash,))
        except self.sqlite.OperationalError:
            # sessions table doesn't exist, so obviously aren't logged in yet
            return
        if c.fetchall():
            return True
        else:
            return


def changepass():
    """Trys to change the admin password.
    
    First, makes sure form value "oldpass" is correct, then makes sure 
    "newpass" and "confirmpass" match, and if so changes the password in 
    settings.conf."""
    
    redtext = '<span class="error-msg">%s</span>'
    oldpass = form['oldpass'].value
    curpass = config['required']['passhash']
    if not functions.checkhash(curpass, oldpass):
        return redtext % "Old password was not right!"
    
    if "newpass" not in form or "confirmpass" not in form:
        return redtext % "You must specify a new password and confirm it!"
    newpass = form['newpass'].value
    confirmpass = form['confirmpass'].value
    if newpass != confirmpass:
        return redtext % "New password was not confirmed"
    
    changeto = functions.makehash(newpass, 4)
    config['required']['passhash'] = changeto
    try:
        config.write()
    except IOError:
        return redtext % "Can't write to settings.conf! Check permissions."
    return "Password changed successfully"

def loginmain():
    """Main function for change.py login interface."""
    error = ''
    # check if they already have a session
    correct = DB.hassession()
    if "action" in form:
        if form["action"].value == "login":
            if "passwd" in form:
                password = form["passwd"].value
                hash = config['required']['passhash']
                correct = functions.checkhash(hash, password)
                # if the password's right, create a session
                if correct:
                    DB.startsession()
 
                    # if the password is "pysignup" take them to the change
                    # password page
                    if password == 'pysignup':
                        print cookie 
                        functions.redirect('?action=changepass')
                        return
                # if pass is wrong, tell them it's wrong and reload
                else:
                    error = '<span class="error-msg">Incorrect password!'
                    error += '</span>'
        elif form["action"].value == "changepass":
            msg = ''
            # if they at least put something for oldpass in the form, try to
            # change their pass
            if "oldpass" in form:  
                msg = changepass()
            chtml.loadpass(msg)
            chtml.display()
            return # so the other forms aren't shown
    if not correct:
        # if the password isn't correct, (re)display the login form
        chtml.loadlogin(error)
        chtml.display()
    return correct

def interfacemain():
    """Main function for change.py's admin interface."""
    if "action" in form:
        if form['action'].value == 'Change':
            getnchange()
        elif form['action'].value == 'Change and clear signup data':
            getnchange()
            DB.clear()
    chtml.loadinterface()
    chtml.display()


chtml = Html()
if stometh.lower() == 'sqlite':
    DB = Sqlite(sqlitedb)
else:
    DB = FileSto(datafile, sessionfile)

def main():
    """Main function for change.py."""
    # if the password is correct
    if loginmain():
        chtml.html += "%s\n" % cookie
        interfacemain()

if __name__ == '__main__':
    main()

Report a bug
ViewVC Help
Powered by ViewVC 1.0.5