#!/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 . ### 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 = """\

Login

Password: %s
(Note: default password is "pysignup")
""" % 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 = """

Change password

%s
Old password:
(Note: default password is "pysignup")
New password:
Confirm new password:
""" % 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 = """\ %(name)s %(comment)s
""" % {'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 = """\ %(name)s %(comment)s
""" % {'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 += ' %s\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 += ' %s\n'\ % subsection subsectiondict = sectiondict[subsection] for key in subsectiondict: formhtml += self.displaykey(section, key, subsection) # now make full html, which includes
tags, etc. fullhtml = """

Modify:

%s %s

Warning: clears all signup data, you can only restore it if you have file access to the webserver.
""" % (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 = '

Permission denied!

' 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 = '''

Could not backup data file! Check permissions of the backups directory.

''' # 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 = '''

Could not clear data file! Check permissions.

''' 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 = '

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
\n' chtml.html += 'You can download it here' 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 = '''

Could not backup SQLite DB! Check permissions of the backups directory.

''' # 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 = '''

Could not clear SQLite DB! Check permissions.

''' 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 = '

Cannot write to SQLite' chtml.interfacemsg += ' database! Check permissions.

' 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 = '%s' 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 = 'Incorrect password!' error += '' 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()