contents
This is a demonstration CGI application, which provides a fully working mailing list management facility. This is simple enough for student study, test, installation and modification, while being sufficiently robust and useful to demonstrate the use of Python language and coding techniques covered in the WPA4 module within a production CGI webserver environment accessible to the public over the Internet.
The Python modules described in the notes for weeks 9,10 and 11 are imported and used by these CGI programs. The cgiutils2.py module provides a variety of functions, e.g. for starting and ending HTML outputs, sending HTML forms, generating PINs and sending messages by mail. The tablcgi.py module provides a class with methods to search, load, save, display and update (i.e. add, modify, and delete rows) a simple database table.
This application uses 2 database tables.
The prospects table stores details for prospective members. For each member the email address, Pin and time PIN was requested are stored. A prospect record is created when someone with an unknown email address requests a PIN. This record will either be used to enable a prospect to become a member, or will be timed out after 48 hours.
The members table stores details of list members. For each member the email address, PIN, student number, name and number of postings (initially 0) are stored. list members may post messages to all other members, view membership details and unsubscribe themselves.
table_details.py is a set of database table definitions for the members and prospects tables used by the above 3 CGI programs. These definitions are imported as one table definition object for each table (mtab and ptab). These definitions are used when constructing table objects using the tablcgi.table() class.
# table_details.py: details of tables used within WPA IV # simple mailing list management system. These details are coded # in this file and imported into programs which access # members and prospects tables, enabling common details to # edited in one place. # trivial table definitions class used to simplify naming class table_def: pass # data for prospects table ptab=table_def() ptab.meta={ "keys":["address","pin","time_requested"], "colheads":["Address","PIN","Request Time"], "formats":["%s","%s","%s"] } ptab.file="prospects.pkl" ptab.uniq_col="address" # data for members table mtab=table_def() mtab.meta={ "keys":["address","pin","snum","name","posts"], "colheads":["Address","PIN","Student No.","Name","No. posts"], "formats":["%s","%s","%s","%s","%d"]} mtab.file="members.pkl" mtab.uniq_col="address"
This form is used to provide the input required for entry.cgi .
<h2> WPA4 Mailing List Access Form</h2> <p> This mailing list is for TIC students taking Website Programming Applications IV. This list may also be activated or deactivated at any time, to suit course-delivery requirements. </p><p> Messages sent using this list must be relevant to this module, and will be emailed to all list members.<p> WPA IV students are asked to join this list and post a message to say a few words about themselves when they join. Messages sent will include your name and student number. Anyone who joins this list but who does not post anything within a reasonable time period of joining risks being silently removed. Students are asked to leave this list when they have finished studying the module.</p><p> Please bear in mind that any information you enter or post here is accessible to all list users, so this must not include any information which you wish to keep confidential. You may prefer to use a throw-away email address if you do not want other list users to know your main one. Politeness to other list users is expected.</p><p> <b>If you are new to this list or have forgotten your PIN number, use this form to enter your email address and request a PIN. All other actions will require your email address and PIN.</b><p> <form action="entry.cgi" method="post"> <p> Please enter your email address. * <INPUT TYPE="text" NAME="Email" SIZE="25" MAXLENGTH="50"> <p> Please enter your PIN number <INPUT TYPE="text" NAME="PIN" SIZE="4" MAXLENGTH="4"> <BR>If you don't have a PIN number or have forgotten it, this form will send a PIN to your email address.<p> Select option:<br> <input TYPE=Radio NAME="option" VALUE="add">add me to the list <br> <input TYPE=Radio NAME="option" VALUE="del">remove me from the list<br> <input TYPE=Radio NAME="option" VALUE="post">send a message to the list<br> <input TYPE=Radio NAME="option" VALUE="view">view list member details<br> <input TYPE=Radio NAME="option" VALUE="pin">send me a PIN<br> <INPUT TYPE="submit" VALUE="Go"> </form>
This program is the starting point for all operations. If run without input it will send the entry form to the browser. Other operations are either provided directly (unsubscribe and send pin) or are handled by sending the add form or the post form.
#!/usr/bin/python # change top line to the path of the Python interpreter on your system """ entry.cgi This file is the gateway program for a simple mailing list management system. Richard Kay last changed: 19 April 2002 """ # uncomment next 3 lines to debug script #import sys #sys.stderr=sys.stdout #print "Content-type: text/plain\n" import cgiutils2 import tablcgi from table_details import ptab,mtab def send_pin(email): # need to access members table to check if user is a member members=tablcgi.table(mtab.meta,mtab.file,mtab.uniq_col) ismember=members.has_key(email) # need to access prospects table to check if user is a prospect prospects=tablcgi.table(ptab.meta,ptab.file,ptab.uniq_col) isprospect=prospects.has_key(email) # if already a member resend PIN if ismember: index=members.find(email) pin=members.data[index]["pin"] # if already a prospect resend PIN elif isprospect: index=prospects.find(email) pin=prospects.data[index]["pin"] else: # add record to prospects pin=cgiutils2.make_pin() import time time_requested=int(time.time()) # open prospects table and add record prospects=tablcgi.table(ptab.meta,ptab.file,ptab.uniq_col) new_prospect={"address":email,"pin":pin,"time_requested":time_requested} if prospects.addrow(new_prospect): print "<P> prospective list member details recorded<P>" else: errormes="""Prospective member list not updated, probably due to high server demand. Please try again later and inform the webmaster if this problem persists.""" cgiutils2.html_end(error=errormes) return # send PIN to address entered message="From: webmaster@copsewood.net\n" message+="To: %s\n" % email if not ismember: message+="Subject: Mailing list PIN request \n\n" else: message+="Subject: Mailing list PIN resend \n\n" message+="Someone (presumably you) entered your email address\n" message+="requesting access to the WPA IV mailing list.\n" message+="\n" message+="The PIN needed for access is: %s\n" % pin message+="\n" message+="To confirm membership please visit: " message+="http://copsewood.net/wpa4list/entry.cgi \n" message+="and select option: add me to the list.\n" message+="\n" message+="If this message is in error please ignore it. However,\n" message+="if this error persists please contact abuse@copsewood.net .\n" message+="\n" fromad="webmaster@copsewood.net" cgiutils2.send_mail(fromad,email,message) print "<p>Your PIN has been sent.</p>" cgiutils2.html_end(received=1) def main(): cgiutils2.html_header(title="WPA IV mailing list manager") if len(cgiutils2.keys()) == 0: # if no keys send the form to the browser cgiutils2.send_form("entry.form") cgiutils2.html_end(want_form=1) return elif not cgiutils2.has_required(["Email","option"]): cgiutils2.html_end(error="You havn't input all required values.") return # has required keys - process form email=cgiutils2.firstval("Email") if not cgiutils2.is_email(email): cgiutils2.html_end(error="Invalid email address.") return option=cgiutils2.firstval("option",default="none") if not cgiutils2.is_valid(option,allowed='^add$|^del$|^post$|^view$|^pin$'): cgiutils2.html_end(error="Invalid option radio button value.") return # check if a PIN was entered and validate it pinentry=cgiutils2.firstval("PIN",default="") if pinentry and not cgiutils2.is_valid(pinentry,allowed='^[1-9][0-9]{3,3}$'): cgiutils2.html_end(error="Invalid PIN format.") return if pinentry: # check its the correct PIN for this address members=tablcgi.table(mtab.meta,mtab.file,mtab.uniq_col) ismember=members.has_key(email) prospects=tablcgi.table(ptab.meta,ptab.file,ptab.uniq_col) isprospect=prospects.has_key(email) if isprospect: index=prospects.find(email) pin=prospects.data[index]["pin"] elif ismember: index=members.find(email) pin=members.data[index]["pin"] else: # (not ismember) and (not isprospect) cgiutils2.html_end(error="PIN entered but unknown email address.") return if int(pinentry) != int(pin): cgiutils2.html_end(error="Incorrect PIN entered.") return elif not option == "pin": # Without a valid pin other actions are prohibited cgiutils2.html_end(error="No PIN entered.") return # If we've got this far, there should either be a valid option and PIN # or we have a prospective member requesting a PIN. if option == "add": # send the add member form cgiutils2.send_form("add.form",[email,pin]) cgiutils2.html_end(want_form=1) elif option == "del": # delete the user if members.delrow(email): cgiutils2.html_end(received=1) else: errormes="""Members list not updated, probably due to high server demand. Please try again later and inform the webmaster if this problem persists.""" cgiutils2.html_end(error=errormes) elif option == "post": # send post message to list form cgiutils2.send_form("post.form",[email,pin]) cgiutils2.html_end(want_form=1) elif option == "view": members.tab2html(skip_cols=["pin"],bgcolor='"#BBFFFF"') cgiutils2.html_end() elif option == "pin": # send member or prospect pin send_pin(email) else: # should have trapped this one earlier ??? cgiutils2.html_end(error="Invalid option value (2nd trap).") return if __name__ == "__main__": try: main() except: import traceback print "error detected in entry.cgi main()" traceback.print_exc()
This form provides the input required by add.cgi. It is sent with hidden field %s escapes changed to the values of address and PIN input by the entry.cgi user using the entry.form .
<p> Use this form to confirm TIC WPA IV list membership.<p> <form action="add.cgi" method="post"> <INPUT TYPE="hidden" NAME="address" VALUE="%s" > <INPUT TYPE="hidden" NAME="pin" VALUE="%s" > Please enter your name. * <INPUT TYPE="text" NAME="Name" SIZE="25" MAXLENGTH="40"><p> Please enter your student number. * <INPUT TYPE="text" NAME="Snum" SIZE="8" MAXLENGTH="8"> <p> <INPUT TYPE="submit" VALUE="Join List"> </form>
This program is used to confirm list membership. It times out prospect records more than 48 hours old. It checks the PIN and address it is submitted with against the prospects table, and if all details are correct it removes the entry from the prospects table and adds an entry to the members table.
#!/usr/bin/python # change top line to the path of the Python interpreter on your system """ add.cgi This file is the program which handles member additions within a simple mailing list management system. Richard Kay last changed: 22 April 2002 """ # uncomment next 3 lines to debug script # import sys # sys.stderr=sys.stdout # print "Content-type: text/plain\n" import cgiutils2 import tablcgi from table_details import mtab,ptab # import table details prospect_timeout=60*60*48 # seconds in 48 hours def timeout_prospects(): # times out prospect records for new PINs requested > 48 hours ago import time prospects=tablcgi.table(ptab.meta,ptab.file,ptab.uniq_col) time_now=int(time.time()) # time in seconds since 1/1/1970 for row in prospects.data: if time_now - row["time_requested"] > prospect_timeout: # remove timed out prospect record prospects.delrow(row["address"]) def main(): timeout_prospects() cgiutils2.html_header(title="WPA IV add list member program") if len(cgiutils2.keys()) == 0: # if no keys authentication data missing cgiutils2.html_end(error="Missing user identification details.") return elif not cgiutils2.has_required(["address","pin","Name","Snum"]): cgiutils2.html_end(error="You havn't input all required values.") return # has required keys - process form email=cgiutils2.firstval("address") if not cgiutils2.is_email(email): cgiutils2.html_end(error="Invalid email address.") return # validate PIN pinentry=cgiutils2.firstval("pin") if not cgiutils2.is_valid(pinentry,allowed='^[1-9][0-9]{3,3}$'): cgiutils2.html_end(error="Invalid PIN format.") return # check its the correct PIN for this address prospects=tablcgi.table(ptab.meta,ptab.file,ptab.uniq_col) isprospect=prospects.has_key(email) if not isprospect: cgiutils2.html_end(error="Can't add address not in prospectives.") return else: index=prospects.find(email) pin=prospects.data[index]["pin"] if int(pinentry) != int(pin): cgiutils2.html_end(error="Incorrect PIN entered.") return # If we've got this far, there should be a valid address and PIN # and user is a prospective member # clean up name name=cgiutils2.firstval("Name") name=cgiutils2.make_clean(name,r"[^\w\- ]") # validate student number snum=cgiutils2.firstval("Snum",default="none") if not cgiutils2.is_valid(snum,allowed='^[eE0-9][0-9]{7,7}$'): cgiutils2.html_end(error="Invalid student number.") return # add to members and remove from prospects new_member={"address":email,"pin":pin,"snum":snum,"name":name,"posts":0} members=tablcgi.table(mtab.meta,mtab.file,mtab.uniq_col) if members.addrow(new_member): # successfully added member, so try to remove prospect record. It # doesn't matter much if removing prospect record fails due to # file locking as old prospects are automatically removed later. prospects.delrow(email) cgiutils2.html_end(received=1) else: errormes="""Member list not updated, probably due to high server demand. Please try again later and inform the webmaster if this problem persists.""" cgiutils2.html_end(error=errormes) return if __name__ == "__main__": try: main() except: import traceback print "error detected in add.cgi main()" traceback.print_exc()
This form provides the input required by post.cgi. It is sent with hidden field %s escapes changed to the values of address and PIN input by the entry.cgi user using the entry.form .
<p> Use this form to send a message to all users of the WPAIV list. This will include your name, address and student number. <p> New users are asked to say a few words about themselves.<p> <form action="post.cgi" method="post"> <INPUT TYPE="hidden" NAME="address" VALUE="%s" > <INPUT TYPE="hidden" NAME="pin" VALUE="%s" > <p>Enter subject of message: *<br> <INPUT TYPE="text" NAME="subject" SIZE="50" MAXLENGTH="65" VALUE="Website Application Programming IV"><p> Enter message to be posted to the list: *<br> < TEXTAREA NAME="comments" ROWS="10" COLS="65"></TEXTAREA><p> <INPUT TYPE="submit" VALUE="Send Message"> </form>
This program sends messages to all members. It confirms the address and PIN details collected from the hidden address and pin fields in the post form. If the details are valid it increments the number of posts count for the member and sends the message by email to all list members.
#!/usr/bin/python # change top line to the path of the Python interpreter on your system """ post.cgi This file is the program which handles postings within a simple mailing list management system. Richard Kay last changed: 22 April 2002 """ # uncomment next 3 lines to debug script #import sys #sys.stderr=sys.stdout #print "Content-type: text/plain\n" import cgiutils2 import tablcgi from table_details import mtab # import member table details def post_message(members,fromad,subject,message_body): # get comma delimited list of addresses address_list=[] for row in members.data: address_list.append(row["address"]) # increment posts count for member index=members.find(fromad) row=members.data[index] person=row["name"] row["posts"]+=1 if not members.modrow(row): # file busy - minor error ? print "<p> Couldn't increment member posting count. No worry.<p>" # compose message message="From: %s (%s)\n" % (person,fromad) line="Subject: [WPA IV] %s\n\n" % subject message+=line line="%s\n" % message_body message+=line print "<pre>" print "To: ", for address in address_list: print "%s " % address, print "\n%s\n</pre>" % message cgiutils2.send_mail(fromad,address_list,message,debug=0) cgiutils2.html_end(received=1) def main(): cgiutils2.html_header(title="WPA IV post form response") if len(cgiutils2.keys()) == 0: # if no keys authentication data missing cgiutils2.html_end(error="Missing user identification details.") return elif not cgiutils2.has_required(["address","pin","comments","subject"]): cgiutils2.html_end(error="You havn't input all required values.") return # has required keys - process form email=cgiutils2.firstval("address") if not cgiutils2.is_email(email): cgiutils2.html_end(error="Invalid email address.") return # validate PIN pinentry=cgiutils2.firstval("pin") if not cgiutils2.is_valid(pinentry,allowed='^[1-9][0-9]{3,3}$'): cgiutils2.html_end(error="Invalid PIN format.") return # check its the correct PIN for this address members=tablcgi.table(mtab.meta,mtab.file,mtab.uniq_col) ismember=members.has_key(email) if not ismember: cgiutils2.html_end(error="Incorrect email address.") return else: index=members.find(email) pin=members.data[index]["pin"] if int(pinentry) != int(pin): cgiutils2.html_end(error="Incorrect PIN entered.") return # If we've got this far, there should be a valid address and PIN # clean up subject line and message body message_body=cgiutils2.firstval("comments") message_body=cgiutils2.make_clean(message_body) subject=cgiutils2.firstval("subject") subject=cgiutils2.make_clean(subject,r"[^\w \.\,\;\:\/\\]") # send message to list post_message(members,email,subject,message_body) return if __name__ == "__main__": try: main() except: print "error detected in post.cgi main()" import traceback traceback.print_exc()