#!/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 += """\
""" % (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 += """\
""" % (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 += "
%s
\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 += \
'''
' % 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()