#!/usr/bin/python ##### ^ if python is not installed at /usr/bin/python (i.e. on a # windows system) change the above to the file path to python, # i.e. #!C:/path/to/python/python.exe """Main file for PySignup. It shows all of the signup data and is where people can signup for the event. @copyright: 2008 by 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 os, re, cgi, sys import recaptcha, functions from configobj import ConfigObj #import cgitb; cgitb.enable() # only enable when developing __version__ = '1.3' __author__ = "Nathaniel Herman" class Html: '''Class for retrieving HTML''' def __init__(self, inithtml='', errormsg=''): self.html = inithtml self.errormsg = errormsg def makelocation(self): """Reads the config file and creates html for the next event's location. if usegooglemaps is true, it will include a link to a Google map of the location.""" # most config values besides location/fulllocation should not have list # values, so instead of turning list_values on for everything, just use # a custom parselist() function for these values (which is based on the # configobj code) locat = config["required"]["location"] locat = functions.parselist(locat) if functions.checkboolean(config["googlemaps"]["usegooglemaps"]): if config["googlemaps"]["full-location"]: fulllocation = config["googlemaps"]["full-location"] fulllocation = functions.parselist(fulllocation) else: # if no fulllocation is specified, normal location will do fulllocation = locat # check if the location is a list if type(locat) == type([]): htmllocation = "
    \n" if type(fulllocation) == type([]): for indlocation, indfulllocation in zip(locat, fulllocation): htmllocation += """\
  1. %s
  2. """ % (cgi.escape(indfulllocation), cgi.escape(indlocation)) # on the off chance that location is a list, but fulllocation # is not else: for indlocation in locat: htmllocation += """\
  3. %s
  4. """ % (cgi.escape(fulllocation), cgi.escape(indlocation)) htmllocation += "
" else: htmllocation = """\ %s""" \ % (cgi.escape(fulllocation), cgi.escape(locat)) else: # if they don't want to use google maps, just display the location # with no hyperlink if type(locat) == type([]): htmllocation = "
    \n" for indlocation in locat: htmllocation += "
  1. %s
  2. \n" % cgi.escape(indlocation) htmllocation += "
\n" else: htmllocation = cgi.escape(locat) return htmllocation def displaymessage(self): """Displays a user-specified message below the location.""" if config["required"]["message"]: message = config["required"]["message"] else: message = '' html = """\ %s """ % message return html def tableheader(self): """Returns HTML for table header.""" html = '''\ In/Out''' for field in datafields: fieldname = field[0] html += ' %s' % fieldname html += '\n ' return html def formfieldhtml(self): """Returns the HTML for displaying the user enterable form fields. Displays all of the form fields listed in the datafields config variable. This will not include things like a CAPTCHA or password field.""" html = '' for field in datafields: fieldval = '' fieldname = field[0] if self.errormsg: if fieldname.lower() in form: fieldval = form[fieldname.lower()].value if fieldname.lower() == 'name': length = 40 elif fieldname.lower() == 'comment': length = 100 else: length = 50 html += '''\ %(field)s: ''' % {'field': fieldname, 'fieldlow': fieldname.lower(), 'length': length, 'fieldval': fieldval} return html def passhtml(self): """Returns the HTML for displaying a password field.""" html = """ Password: If you enter a password, it will be required to later change your entry. """ if userpass.lower() == 'optional': return html elif userpass.lower() == 'required': return html else: return '' def signupinfo(self): """Returns information for signing up. i.e. what forms to fill in, etc.""" sinfo = "Fill in your " for item in datafields: name = item[0].lower() if item[1].lower() in ('req', 'pri'): sinfo += '%s, ' % name else: sinfo += '%s (optional), ' % name if functions.checkboolean(userecaptcha): sinfo += "the CAPTCHA, " if userpass.lower() == 'required': sinfo += 'a password, ' elif userpass.lower() == 'optional': sinfo += 'a password (optional), ' sinfo += 'and then click "In", "Out", or "Remove".' return sinfo def recaptchahtml(self): """Returns all the necessary html for adding a reCaptcha.""" if not functions.checkboolean(userecaptcha): # they don't want to use recaptcha return '' captchahtml = '' # print out the required HTML if they want to use custom reCAPTCHA if functions.checkboolean(config['captcha']['recaptcha']['usecustom']): captchahtml += """ """ captchahtml += recaptcha.displayhtml(recaptcha_pub_key) return captchahtml def load(self): '''Open HTML template, and substitute proper variables into self.html''' # if there's a user-specified theme, load all CSS, etc. for it theme = config['required']['theme'] if theme: skinhtml = functions.loadtheme(theme) else: skinhtml = '' indhtml = 'Content-type: text/html\n\n' indhtml += functions.read("indextmpl.html") % {'sitename': sitename, 'skinspecific': skinhtml, 'nextevent': nextevent, 'locat': self.makelocation(), 'message': self.displaymessage(), 'data': DB.getdata(), 'signupinfo': self.signupinfo(), 'tableheader': self.tableheader(), 'errormsg': self.errormsg, 'formfields': self.formfieldhtml(), 'captcha': self.recaptchahtml(), 'passhtml': self.passhtml(), 'version': __version__} self.html += indhtml def display(self): """Displays whatever HTML is in self.html""" print self.html class FileSto: '''Class for all file storage functions. when storagemethod = file, this class will be used, saving all user data directly to a file.''' def __init__(self, file): self.file = file def getdata(self): """Parses data from file and returns it as html. Reads the contents of a given data file line by line, and parses it into html, if the the file does not exist, an error message is returned, otherwise html of the parsed data is returned. The data file is specifically formatted, with each field of an entry being seperated by a ">", and each entry being seperated by a new line. A typical entry will look like "In>name>comment>optionalpassword" """ errormessage = """\ No one has signed up yet """ % (len(datafields) + 1) incount = 0 tabledata = '' try: # in case the file doesn't exist f = open(self.file, 'r') # read the data file line by line, which will seperate it entry by # entry thedata = f.readlines() f.close() if not thedata: # check if file exists, but is empty tabledata = errormessage for entry in thedata: # going through each line of the date file # each value is seperated by a ">", so split it by each # occurence of ">" datalist = entry.split('>') inorout = datalist[0] formdata = datalist[1:-1] if inorout == 'In': # if this entry is "in" for the event incount += 1 # increase the amount of people that are "in" num = '%s(%d)' % (inorout, incount) else: # don't need to count number of people who won't be there num = '%s' % inorout datahtml = '' for field in formdata: datahtml += '%s' % field # parsing the data as html for a table tabledata += \ """ %(num)s%(datahtml)s """ % {'num': num, 'datahtml': datahtml} except IOError: # same as if the file is blank tabledata = errormessage try: alert = int(alerton) except ValueError: return tabledata if incount >= alert: if functions.checkboolean(htmlalert): htmltext = cgi.escape(config['alerts']['htmltext']) tabledata += '

%s

' % htmltext return tabledata def add(self, status): """Handles the viewer pressing the "In" or "Out" button. Adds or modifies an entry in the data file using the user filled in fields, status argument should be "In" or "Out" based on which button was pressed by the viewer.""" savecode = "%(inorout)s>%(prikey)s>%(data)s%(passwd)s\n" formdata = '' # create a variable that is the opposite of the button they clicked if status == "In": notstatus = "Out" elif status == "Out": notstatus = "In" else: return # status argument was incorrect, so don't do anything prikey = False # datafields is a list of tuples, so go through each tuple in the list for field in datafields: fieldname = field[0].lower() # 1st item of each tuple is the name fieldtype = field[1].lower() # 2nd item is the type (opt, req, or pri) if fieldname in form: fieldval = form[fieldname].value # if for some reason the length of the field is higher than # allowed, just return and don't save anything. No error message # or anything is printed, as the only way to do this is usually # to deliberately try, and they're given a 10 character cushion if fieldname == 'name' and len(fieldval) > 50: return elif fieldname == 'comment' and len(fieldval) > 110: return elif len(fieldval) > 60: return fieldval = cgi.escape(fieldval) else: if fieldtype in ('pri', 'req'): indexhtml.errormsg += '%s must be \ entered!' % fieldname.capitalize() return fieldval = ' ' if fieldtype == 'pri': prikey = fieldval continue # the primary key must be the first key, so if there's no primary # key set yet, settings.conf is wrong somewhere if prikey is False: indexhtml.errormsg += 'No primary key \ was set!' return formdata += fieldval + '>' if "passwd" in form: # did they enter a password passwd = form["passwd"].value if len(passwd) > 20: return passwdhash = functions.makehash(passwd) nopass = False else: if userpass.lower() == 'required': indexhtml.errormsg += '''You must \ enter a password!''' return nopass = userpass.lower() != 'optional' passwd = '' passwdhash = '' # opening data file data = functions.read(self.file) # if data file doesn't exist already or is empty if not data: # create the data file now with user-inputted values tosave = savecode % {'inorout': status, 'prikey': prikey, 'data': formdata, 'passwd': passwdhash} didwrite = functions.save(self.file, tosave) # make sure it wrote to the data file if not didwrite: indexhtml.errormsg += 'Unable to write \ to the data file!\n

Check permissions

' return # regex to check if someone with the inputted name was already added reg1 = re.compile("(In|Out)>%s>" % re.escape(prikey)) if reg1.search(data): # regex for matching their stored password hash if there is one passreg = re.compile(r'(>%s>.*>)(.*)\n' % re.escape(prikey)) storedhash = passreg.search(data).group(2) if storedhash: # check that the supplied password matches the stored password if not functions.checkhash(storedhash, passwd) and not nopass: indexhtml.errormsg += 'Password \ was incorrect!' return # just in case the person was previously out and is clicking "in", # or was previously in and is clicking "out", change their # status based on which button they clicked reg2 = re.compile("%s(?=>%s>)" % (notstatus, re.escape(prikey))) data = reg2.sub(status, data) # lets also replace their old data with new reg3 = re.compile("((?:In|Out)>%s>)(.*>)(.*\n)" % re.escape(prikey)) replace = r'\1%s\3' % functions.escapebackslash(formdata) tosave = reg3.sub(replace, data) didwrite = functions.write(self.file, tosave) # write changes if not didwrite: indexhtml.errormsg += '''Unable to \ write to the data file!\n

Check permissions

''' else: # if the person's inputted name wasn't added yet tosave = savecode % {'inorout': status, 'prikey': prikey, 'data': formdata, 'passwd': passwdhash} # just append their data to the file, since they're not there yet didwrite = functions.save(self.file, tosave) if not didwrite: indexhtml.errormsg += '''Unable to \ write to the data file!\n

Check permissions

''' def remove(self): """Handles the viewer pressing the "Remove" button. Searches the data for someone with the given name and removes that entry completely""" field = datafields[0] if field[1].lower() != 'pri': indexhtml.errormsg += '\ The first data field must be the primary data field!' return prikeyname = field[0].lower() if prikeyname in form: data = functions.read(self.file) if not data: return # if data file doesn't exist or is blank, return None prikey = cgi.escape(form[prikeyname].value) if "passwd" in form: passwd = form['passwd'].value nopass = False else: nopass = userpass.lower() not in ('optional', 'required') passwd = '' # regex for matching their stored password hash if there is one passreg = re.compile(r'((?:In|Out)>%s>.*>)(.*)\n' % re.escape(prikey)) storedhash = passreg.search(data).group(2) if storedhash: # check that the supplied password matches the stored password if not functions.checkhash(storedhash, passwd) and not nopass: indexhtml.errormsg += 'Password \ was incorrect!' return # this regex should catch their entire entry, but no one elses reg = re.compile("(In|Out)>%s>.*\n" % re.escape(prikey)) data = reg.sub('', data) functions.write(self.file, data) def incount(self): '''Returns number of people marked as "in"''' try: open(self.file, 'r') except IOError: return count = 0 for line in open(self.file, 'r'): if line[:2] == 'In': count += 1 return count class Sqlite: '''Class for storing data using an SQLite database.''' def __init__(self, dbfile): try: import pysqlite2.dbapi2 as sqlite self.sqlite = sqlite except ImportError: indexhtml.html = 'Content-type: text/html\n\n' indexhtml.html += 'PySQLite 2 is not installed!\n
\n' indexhtml.html += 'You can download it here' indexhtml.display() sys.exit() self.dbfile = dbfile try: self.conn = sqlite.connect(self.dbfile) except sqlite.OperationalError: indexhtml.html = 'Content-type: text/html\n\n' indexhtml.html += 'Error reading SQLite database file.
' indexhtml.html += 'Check permissions.' indexhtml.display() sys.exit() def getdata(self): '''Gets data from SQLite db and returns parsed as HTML.''' c = self.conn.cursor() errormessage = """\ No one has signed up yet """ % (len(datafields) + 1) incount = 0 tabledata = '' try: c.execute('select * from signup') data = c.fetchall() c.close() except self.sqlite.OperationalError: # table hasn't been created, so just display the errormsg data = False if not data: return errormessage for entry in data: inorout = entry[0] formdata = entry[1:-1] if inorout == 'In': # entry is "in" incount += 1 num = '%s(%d)' % (inorout, incount) else: # don't count number of people who won't be there num = '%s' % inorout datahtml = '' for field in formdata: datahtml += '%s' % field # parsing data as HTML tabledata += \ ''' %(num)s%(datahtml)s ''' % {'num': num, 'datahtml': datahtml} try: alert = int(alerton) except ValueError: return tabledata if incount >= alert: if functions.checkboolean(htmlalert): htmltext = cgi.escape(config['alerts']['htmltext']) tabledata += '

%s

' % htmltext return tabledata def add(self, status): '''Handles viewer pressing "In" or "Out" button.''' # open dbfile in append mode, to make sure we can write to the db try: open(self.dbfile, 'a') except IOError: indexhtml.errormsg += '''Could not write to the SQLite DB, check permissions.''' return c = self.conn.cursor() fnames = [] fvalues = [] if status == "In": notstatus = "Out" elif status == "Out": notstatus = "In" else: return prikey = False # datafields is a list of tuples, so go through each tuple in list for field in datafields: fieldname = field[0].lower() # 1st item of tuple is name fieldtype = field[1].lower() # 2nd item is type (opt, req, pri) if fieldname in form: fieldval = form[fieldname].value # if for some reason the length of the field is higher than # allowed, just return and don't save anything. No error message # or anything is printed, as the only way to do this is usually$ # deliberately try, and they're given a 10 character cushion if fieldname == 'name' and len(fieldval) > 50: return elif fieldname == 'comment' and len(fieldval) > 110: return elif len(fieldval) > 60: return fieldval = cgi.escape(fieldval) else: if fieldtype in ('pri', 'req'): indexhtml.errormsg += '%s must be \ entered!' % fieldname.capitalize() return fieldval = ' ' if fieldtype == 'pri': prikey = fieldname prikeyval = fieldval fnames.append(fieldname) fvalues.append(fieldval) if prikey is False: indexhtml.errormsg += 'No primary key \ was set!' return if "passwd" in form: passwd = form["passwd"].value if len(passwd) > 20: return passwdhash = functions.makehash(passwd) nopass = False else: if userpass.lower() == 'required': indexhtml.errormsg += '''You must \ enter a password!''' return nopass = userpass.lower() != 'optional' passwd = '' passwdhash = '' # combining status, fvalues, and passwdhash into one list values = [status] + fvalues + [passwdhash] # SQL query wants a tuple, so convert the list to a tuple values = tuple(values) statement = 'select * from signup where %s=?' % prikey try: c.execute(statement, (prikeyval,)) except self.sqlite.OperationalError: # table doesn't exist yet, so create it functions.createsqlite(self.conn, tuple(fnames)) c.execute(statement, (prikeyval,)) data = c.fetchall() if not data: # nothing in signup table matches user's prikey length = len(values) - 1 # make sure it has a ? for every item in values statement = 'insert into signup values (%s?)' % ('?, ' * length) c.execute(statement, values) c.close() self.conn.commit() return else: statement = 'select passwd from signup where %s=?' % prikey c.execute(statement, (prikeyval,)) storedhash = c.fetchone()[0] if not functions.checkhash(storedhash, passwd) and not nopass: indexhtml.errormsg += 'Password \ was incorrect!' return # change status to be correct statement = 'update signup set status=? where %s=?' % prikey c.execute(statement, (status, prikeyval)) # change other data fields to be newly entered data length = len(fnames) - 1 set = '%s%%s=?' % ('%s=?, ' * length) set %= tuple(fnames) statement = 'update signup set %s where %s=?' % (set, prikey) args = tuple(fvalues) + (prikeyval,) c.execute(statement, args) self.conn.commit() c.close() def remove(self): '''Handles viewer pressing the "Remove" button.''' # open a file in append mode to make sure we can write to the db, # and just display an error message if we can't try: open(self.dbfile, 'a') except IOError: indexhtml.errormsg += '''Couldn't write to SQLite database, check permissions.''' return for field in datafields: if field[1].lower() == 'pri': prikeyname = field[0].lower() break if prikeyname in form: c = self.conn.cursor() prikeyval = cgi.escape(form[prikeyname].value) if "passwd" in form: passwd = form['passwd'].value nopass = False else: nopass = userpass.lower() not in ('optional', 'required') passwd = '' statement = 'select passwd from signup where %s=?' % prikeyname c.execute(statement, (prikeyval,)) if not c.fetchone(): return storedhash = c.fetchone()[0] if storedhash: if not functions.checkhash(storedhash, passwd) and not nopass: indexhtml.errormsg += 'Password \ was incorrect!' return statement = 'delete from signup where %s=?' % prikeyname c.execute(statement, (prikeyval,)) c.close() self.conn.commit() def incount(self): '''Returns an integer of how many people are signed up as "in".''' c = self.conn.cursor() try: c.execute("select * from signup where status='In'") except self.sqlite.OperationalError: return count = len(c.fetchall()) return count def recaptchacheck(): """Checks the user's captcha against reCaptcha's server. Returns a class depending on whether the captcha is correct or incorrect, whether the captcha is correct can be checked using .is_valid, which will return either true or false, and .error_code will return the error code if .is_valid is false.""" # get the viewer's IP for reCaptcha if functions.checkboolean(config['captcha']['recaptcha']['reverseproxy']): # if using a reverseproxy, we want to get HTTP_X_FORWARDED_FOR ip = os.environ["HTTP_X_FORWARDED_FOR"] else: # otherwise, we want REMOTE_ADDR ip = os.environ["REMOTE_ADDR"] try: challenge = form["recaptcha_challenge_field"].value except KeyError: # this means the captcha was never displayed challenge = '' try: response = form["recaptcha_response_field"].value except KeyError: # in case no captcha was inputted at all # ideally, if no captcha was entered, it would alert the user and # not submit the form, however that would probably require ajax response = '' # user's response is blank, since nothing was entered return recaptcha.submit(challenge, response, recaptcha_priv_key, ip) def sendmail(incount): '''Sends an e-mail (if mailalert is true) using configuration info. incount is an integer of how many people are "in"''' if not functions.checkboolean(mailalert): return try: alert = int(alerton) except ValueError: return if incount != alert: return server = config['mail']['smtpserver'] user = config['mail']['mailuser'] passwd = config['mail']['mailpass'] toadd = config['alerts']['toaddress'] fromadd = config['alerts']['fromaddress'] subject = config['alerts']['mailsubject'] body = config['alerts']['mailbody'] functions.mail(server, fromadd, toadd, subject, body, user, passwd) indexhtml = Html() # open up config file try: userconf = ConfigObj('settings.conf', file_error=True, list_values=False) # if settings.conf doesn't exist, change.py was probably never used except IOError: indexhtml.html = '''Content-type: text/html\n No settings file! Go to change.py to create a settings file''' indexhtml.display() sys.exit() defaultconf = ConfigObj('defaultsettings.conf', list_values=False) # merge user conf with default, user overrides default defaultconf.merge(userconf) # to reduce confusion, as defaultconf is now a combo of user and default conf config = defaultconf # loading up configuration nextevent = cgi.escape(config["required"]["nextevent"]) stomethod = config['required']['storage'] datafile = config["file"]["data-file"] sqlitedb = config['sqlite']['sqlitedb'] sitename = cgi.escape(config["required"]["sitename"]) userpass = config['required']['userpass'] userecaptcha = cgi.escape(config["captcha"]["recaptcha"]["userecaptcha"]) recaptcha_pub_key = cgi.escape(config["captcha"]["recaptcha"]["publickey"]) recaptcha_priv_key = cgi.escape(config["captcha"]["recaptcha"]["privatekey"]) datafields = functions.parsedict(config['required']['datafields']) alerton = config['alerts']['alerton'] htmlalert = config['alerts']['htmlalert'] mailalert = config['alerts']['mailalert'] if stomethod == 'sqlite': DB = Sqlite(sqlitedb) else: DB = FileSto(datafile) form = cgi.FieldStorage() def main(): """Main function that is called on a page load. It checks to see if any data has been filled in and if so handles it, and then prints the page's html""" errormessage = """\ No one has signed up yet """ % (len(datafields) + 1) # attempt to create .htaccess if the user wants/it isn't there already functions.createhtaccess(config) # if they've hit in, out, or remove if "action" in form: submit = True if functions.checkboolean(userecaptcha): checkvalid = recaptchacheck() # make sure recaptcha is valid before continuing if not checkvalid.is_valid: indexhtml.errormsg += """\ Incorrect CAPTCHA entered, try again""" submit = False if submit: # figure out what button they clicked and call the appropriate # function if form["action"].value == "In": DB.add("In") sendmail(DB.incount()) elif form["action"].value == "Out": DB.add("Out") elif form["action"].value == "Remove": DB.remove() indexhtml.load() # load HTML up indexhtml.display() if __name__ == '__main__': main()