#!/usr/bin/perl #################################################### # # Event Sign In # # Author: Kai Wetlesen # # Event Sign In originally started life as a fast # hack to allow me to sign in upwards of 100 people # to an event being hosted by a school club I am a # part of. The idea is that this application will # be useful for clubs and/or school organizations # that host events wherein a student can earn # course credit for attending. This helps cut down # on attendance fraud and keep the event hosts in # determining just how successful their event was # and also determine what classes everyone came # from. It's a simple CGI form that does a pull # from a database, then inserts some matched data # back in. # # The application is mostly customizable, and can # be adapted for use in a corporate environment # with some minor tweaks (naturally supplanting # classes with departments/teams). Several parts # of the application can be adjusted from a # separate config file, which should be located # in the cgi directory under conf/signin.conf # # This CGI application is licensed under LGPL v3.0 # http://www.gnu.org/licenses/lgpl.html #################################################### use strict; use CGI::Pretty qw(:standard); use DBI; use DBD::mysql; use Time::Local; $CGI::POST_MAX = 2048; $CGI::DISABLE_UPLOADS = 1; # We need to start by creating a CGI page object # I just so happen to call it rq to stand for # ReQuested page. my $rq = CGI->new; # Naturally this prints an HTML header print $rq->header( {-charset=>'utf-8'} ); my $fn = "0"; #first name my $ln = "0"; #last name my $em = "0"; #email my $ae = "0"; #alt email my $fullname; my %cfg; my ( $cfgkey, $cfgval ); open CONF, "<conf/signin.conf" or Die("Could not open configuration file. $!"); while ( <CONF> ) { next if /^\s*#/ or /^\s*$/; chomp; ( $cfgkey, $cfgval ) = split /:/; $cfgkey =~ s/^\s*//; $cfgval =~ s/^\s*//; $cfg{$cfgkey} = $cfgval; } # Check thru the configuration and make sure everything is set correctly my $eventName = $cfg{"EventName"} or Die("Could not get event name. $!"); my $css = $cfg{"CSS"} or Die("No CSS style sheet configured. $!"); my $returnToURL = $cfg{"ReturnToURL"} or Die("Return to URL not specified. $!"); Die("No database specified in configuration file.\n") if !defined $cfg{"Database"}; Die("No database server specified in configuration file.\n") if !defined $cfg{"Server"}; Die("Credentials not found in configuration file.\n") if !defined $cfg{"Username"}; Die("Credentials not found in configuration file.\n") if !defined $cfg{"Password"}; # I perform the connection attempt here because # this script is dead in the water should the # connection fail to happen. my $dbname = $cfg{"Database"}; my $server = $cfg{"Server"}; my $usernm = $cfg{"Username"}; my $passwd = $cfg{"Password"}; my $db = DBI->connect( "dbi:mysql:$dbname:$server:3306", $usernm, $passwd ) or Die("DB connect failed! $DBI::errstr"); # As closing time is optional, this isn't rigorously checked my ( $graceTime, $offMin, $offHr, $offDay, $offMon, $offYear ); if ( defined $cfg{"RegClose"} ) { ( $offMon, $offDay, $offYear, $offHr, $offMin ) = split /\s+/,$cfg{"RegClose"}; # Default the grace time to 15 seconds if it's not given so that # people have X number of seconds to register after the clock turns over $graceTime = defined $cfg{"GraceTime"} ? $cfg{"GraceTime"} : 15; $cfg{"RegClose"} = timelocal( $graceTime, $offMin, $offHr, $offDay, ($offMon-1), $offYear ); } # Start off the header and load in the stylesheet print $rq->start_html(-title => "$eventName - Sign-in", -style => { -src => $cfg{"CSS"}, -type => 'text/css', -media => 'screen' }, -class=>'Body' ); print $rq->div( { class=>'Title' }, $eventName ); if ( defined $cfg{"RegClose"} and time() > $cfg{"RegClose"} ) # Again, check to see if close time is defined, because maybe the # registration is always open! That'd be weird, but it's possible { print $rq->p( { class=>'subTitle centered' }, "Registration Closed" ); print $rq->p( {id=>'form_instructions'}, "Registration for this event has closed. If you still need to register for this event, please speak with the event staff." ); print $rq->p( { class=>'centered' }, TimeString($cfg{"RegClose"} ) ); } elsif ( $ENV{ 'REQUEST_METHOD' } eq "POST" ) # Process posted data { # the Check functions return "error" which is the name of a css class that does # error highlighting. They return "0" (css class name?) if everything is okay # Obviously if the parameter is undef, give it nothing to check. # Each two-letter variable in this if segment represents the class with which # the given form item will be displayed. It's also used for basic error checking. $fn = checkName( $rq->param( "firstName" ) ); $ln = checkName( $rq->param( "lastName" ) ); $em = checkEmail( $rq->param( "email" ) ); $ae = $rq->param( "altemail" ) ? checkEmail( $rq->param( "altemail" ) ) : "0"; # NOTE: None of these functions can be used unless there is a POST event # i.e. there has been data placed into the form and sent over. # Check to see if the previous functions found any errors in the inputs if ( $fn or $ln or $em or $ae ) { PrintPageWithHighlightedErrors( $rq, $db, $fn, $ln, $em, $ae ); } # Are they already registered? We want to prevent double registration. elsif ( !($fullname = checkIfRegistered($rq->param("firstName")." ".$rq->param("lastName"), $db) ) ) { PrintAlreadyRegistered( $rq, $eventName, $returnToURL ); } else # No errors were found. Register the user and give them a nice confirmation { RegisterPerson( $rq, $db, $fullname, $returnToURL ); } } else # No data was posted. Print a form for the user to post some data with. { print $rq->div( { class=>'subTitle' }, "Sign In" ); print $rq->p( { id=>'form_instructions' }, "Please sign in to attend $eventName. Fields marked with asterisks are <em>mandatory</em>.<br /> Registration closes on ", TimeString( $cfg{"RegClose"} )."!" ); PrintForm( $rq, $db, $fn, $ln, $em, $ae ); } # Being of course that this form generates valid HTML, # show it off! print $rq->p( {class=>"centered"}, $rq->a( {href=>"http://validator.w3.org/check?uri=referer"}, $rq->img( {src=>"http://www.w3.org/Icons/valid-xhtml10-blue", alt=>"Valid XHTML 1.0 Transitional", height=>"31", width=>"88"} ) ) ); $db->disconnect(); print $rq->end_html(); # This subroutine will print some cohesive error messages above the # form so that users aren't left scratching their heads over why # some particular form element was suddenly highlighted in red. Really # though, they shouldn't be such dunces about it and just figure it # out, but I'm not the BOFH # Don't know what BOFH is? Google search it you idiot and stop reading # my code. You aren't worthy. # # Receives: Page object, DB object, CSS errors # # Returns: Nothing. All data printed to page. sub PrintPageWithHighlightedErrors { my ( $rq, $db, $fn, $ln, $em, $ae ) = @_; print $rq->div( { class=>'subTitle' }, "Invalid Entry!" ), $rq->p( { id=>'form_instructions' }, "There were errors found in your entry. They are highlighted in red below. Correct these errors before submitting." ); # The following three Ifs take any possible error and print an # error description above so people know what to fix. if ( $fn ) { print $rq->div( {class=>"centered"}, "Invalid first name." ) } if ( $fn or $ln ) { print $rq->div( {class=>"centered"}, "Invalid last name." ) } if ( $em or $ae ) { print $rq->div( {class=>"centered"}, "Invalid email address. Emails must be in the correct format." ) } print $rq->br(); # Of course we'll want to reprint the form with each # erroneous field highlighted. PrintForm( $rq, $db, $fn, $ln, $em, $ae ); } # Obviously you don't want the same person to register and # re-register over and over again. This would cloud up the # database. This function does NOT actually do the check, # see checkIfRegistered. This function prints a full-formatted # message saying the person has already registered, then # returns to a given URL # # Receives: Page object, return URL, event name # # Returns: Nothing, all printed to the page sub PrintAlreadyRegistered { my ( $rq, $eventName, $returnToURL ) = @_; # I think the following two prints are fairly self-evident print $rq->div( { class=>'subTitle' }, "Already Signed In!" ); print $rq->p( {id=>'form_instructions'}, "You have already signed in for $eventName. If you have received this message in error, please speak with the attendant." ); # Just prints the current time, that's all print $rq->p( { class=>'centered' }, TimeString(time) ); # Create a small Javascript redirect and send user back to the given return URL # returnToURL set in config file print "<script type=\"text/JavaScript\"> <!-- setTimeout(\"location.href = '$returnToURL';\",4600); --> </script>"; } # Obviously we need to enter some data into a database in order # for this application to successfully complete its task. This # completes that task. It simply interfaces with a given MySQL # database and enters the queries with values in directly. # # Receives: Page object, DB object, formatted full name, return URL # # Returns: Nothing, function will die out here if there's an error. # # Note: All the data EXCEPT THE NAME is passed down thru the page object. # Why? I'm lazy, and the name was already checked and formatted. # # Also note: This function will die out if any of the SQL inserts fail, # which opens up the possiblility for a corruption. Keep a # CLOSE EYE on all error messages that come up, but really # if any one of the SQL statements fails you're boned anyway. sub RegisterPerson { my ( $rq, $db, $fullname, $returnToURL ) = @_; my $email = $rq->param( "email" ); my $altemail = $rq->param( "altemail" ); # Insert the student information first then retrieve autogen'ed studentID for use # in the next row insertion. my $sql = "INSERT INTO students ( name, email, altemail ) VALUES ( \"$fullname\", \"$email\", \"$altemail\" )"; my $newrequest = $db->prepare( $sql ); $newrequest->execute() or Die("Query failed. $DBI::errstr"); # Gets the studentID $sql = "SELECT studentID FROM students WHERE name=\"$fullname\""; $newrequest = $db->prepare( $sql ); $newrequest->execute() or Die("Query failed. $DBI::errstr"); my $res = $newrequest->fetchrow_arrayref(); my $studentID = $res->[0]; $sql = "SELECT classID FROM classes"; $newrequest = $db->prepare ( $sql ); $newrequest->execute() or Die("Query failed. $DBI::errstr"); my @results = @{$newrequest->fetchall_arrayref}; foreach ( @results ) { if ( $rq->param("Class$_->[0]") ) { $sql = "INSERT INTO students_and_classes ( classID, studentID ) VALUES ( $_->[0], $studentID )"; $newrequest = $db->prepare( $sql ); $newrequest->execute() or Die("Query failed. $DBI::errstr"); } } print $rq->div( { class=>'subTitle' }, "Registration Complete" ); print $rq->p( {id=>'form_instructions'}, "Thank you ".$rq->param('firstName').". We will send you a follow-up survey in the next few days. <br /><br />We hope you enjoy the event!" ); print "<script type=\"text/JavaScript\"> <!-- setTimeout(\"location.href = '$returnToURL';\",3000); --> </script>"; } # The PrintForm function depends on a few variables to # be created prior to operation. It's primary task is # to generate the form wherein the user will fill in # their information to put into the database. In this # application, that consists of their name, email, and # also any classes they are taking. # # PrintForm also takes a set of CSS flags which are # used to do the coloured error highlighting. These CSS # flags should be set based according to your stylesheet # but in my application the value "error" will pull up # CSS class error, where OK will just match a null CSS # class # # Input: CGI page object, DB connection object, CSS flags # # Output: None. All form output printed directly to HTML page. sub PrintForm { my ($rq, $db, $fn, $ln, $em, $ae ) = @_; my $form = $rq->Vars; my $firstName = $form->{ "firstName" }; my $lastName = $form->{ "lastName" }; my $email = $form->{ "email" }; my $altemail = $form->{ "altemail" }; my $className = $form->{ "classname" }; # Naturally we want to retain values between page loads should # the user screw up (which is almost guaranteed to happen. $rq->param( $firstName, $lastName, $email, $altemail, $className ); print $rq->startform ( "POST", $rq->url(), "application/x-www-form-urlencoded" ); print $rq->table ( {-class=>'center_justified'}, $rq->Tr ( $rq->td( {class=>$fn}, "First Name:" ), $rq->td( $rq->textfield('firstName'), "*" ) ), $rq->Tr ( $rq->td( {class=>$ln}, "Last Name:" ), $rq->td( $rq->textfield('lastName'), "*" ) ), $rq->Tr ( $rq->td( $rq->br() ), $rq->td( " " ) ), $rq->Tr ( $rq->td( {class=>$em}, "Email address:" ), $rq->td( $rq->textfield('email'), "*" ) ), $rq->Tr ( $rq->td( {class=>$ae}, "Alternate email:" ), $rq->td( $rq->textfield('altemail') ) ), ); print $rq->table ( {class=>'center_justified'}, $rq->Tr ( $rq->td( {colspan=>3}, $rq->br() ), ), $rq->Tr ( $rq->td( {colspan=>3}, "Please select any classes you are taking from the checkboxes below:" ) ), getClassList($db) ); print $rq->table ( {class=>'center_justified'}, $rq->Tr ( $rq->td( {colspan=>3}, $rq->br() ), ), $rq->Tr ( $rq->td( {colspan=>'2', class=>'centered'} , $rq->submit("Register") ), $rq->td( {colspan=>'2', class=>'centered'} , $rq->reset("Clear Form") ) ), $rq->Tr ( $rq->td( $rq->br() ), $rq->td( " " ) ) ); print $rq->endform(); } # Takes in a time value and then returns a formatted # and printable timestring that can be printed to the # user # # Input: Time Value # # Output: Formatted date and time string sub TimeString { my $Time = shift; my ( $min, $hr, $day, $mon, $year, $wday ) = (localtime($Time))[1..6]; my $ampm; my @months = qw( January February March April May June July August September October November December ); my @days = qw( Sunday Monday Tuesday Wednesday Thursday Friday Saturday ); $ampm = $hr>12 ? "PM" : "AM"; $hr += $hr>12 ? -12 : 0; $min = $min<10 ? "0$min" : "$min"; $year += 1900; return "$days[$wday] $months[$mon] $day $year $hr:$min $ampm"; #print $rq->p( { class=>'centered' }, "$days[$wday] $months[$mon] $day $year $hour:$min $ampm" ); } # Checks to see if the particular person has already # signed in or not. Obviously we don't want the same # person to register over and over again. Such would # cloud up the database. Function will format name # to match database. This return value should be used # to enter into the database. Just sayin! # # Input: First and last name. # # Output: Formatted name to input into database # Undefined if the name already exists sub checkIfRegistered { my ( $name, $db ) = @_; my ( $fullname, $sql, $newrequest, $res ); $name =~ s/^\s*//; # I opted for a list here as people can have multiple # words in their name as well, like "Taylor Harper" # or "Mac Lane" for example. As is evident, every name # will be inputted into the database as upper-case. # Quite common convention really. $fullname = join " ", ( map{ uc $_ } split /\s+/, $name ); $sql = "SELECT name FROM students WHERE name=\"$fullname\""; $newrequest = $db->prepare( $sql ); $newrequest->execute() or Die("DB connect failed! $DBI::errstr"); $res = $newrequest->fetchrow_arrayref(); $name = $res->[0]; $newrequest->finish(); # If we picked a name out of the result set # then they have already registered haven't # they? :D return $name ? "" : $fullname; } sub checkName { my ( $name ) = shift; return "error" if $name !~ /^[- a-zA-Z]+$/ or $name =~ /^\s*$/; return 0; } sub checkEmail { my ( $email ) = shift; return "error" if $email !~ /^[^@]+\@[^@]+\..+$/; return 0; } sub getClassList { my $db = shift; my %classlist; my %labels; my @cell_elems; my @row_elems; my @form_elems; my @niceClass; my ( $instructor, $classID, $classname, $count ); my $sql = "SELECT classID, classname, instructor from classes"; my $classQuery = $db->prepare( $sql ); $classQuery->execute() or Die("Query execution failed\n$DBI::errstr"); foreach ( @{$classQuery->fetchall_arrayref()} ) { $classlist{$_->[2]}{$_->[0]} = $_->[1]; } #$_->[0] is holding classID, $_->[1] holds classname, and $_->[2] holds instructor; foreach $instructor ( sort byInstructorLnFn keys %classlist ) { push @cell_elems, $rq->h4( "$instructor" ); foreach $classID ( sort { $a <=> $b } keys %{$classlist{$instructor}} ) { @niceClass = split /\-/, $classlist{$instructor}{$classID}; push @cell_elems, $rq->checkbox( -name=>"Class$classID", -checked=>0, -value=>"$classID", -label=>"$niceClass[0]"), $rq->br(), $niceClass[1], $rq->br(); } push @row_elems, $rq->td( {class=>'center_justified',style=>"padding: 10px 30px; vertical-align: top;"}, @cell_elems ); @cell_elems = (); $count++; if ( !($count % 3) ){ push @form_elems, $rq->Tr( @row_elems ); @row_elems = (); } } if ( @row_elems ) { push @form_elems, $rq->Tr( @row_elems ); @row_elems = (); } return @form_elems; } sub byInstructorLnFn { $a =~s/^\s*//; $b =~s/^\s*//; my ( $firstNameA, $lastNameA ) = split/\s+/, $a; my ( $firstNameB, $lastNameB ) = split/\s+/, $b; $lastNameA cmp $lastNameB or $firstNameA cmp $firstNameB; } sub Die { my $err = shift; print $err . "<br />Check program configuration file or contact the operator."; # All error logs are going to come to this line. Do an error search with the error msg you got. :) die $err; }