Приглашаем посетить
Житков (zhitkov.lit-info.ru)

db_manager

#!/usr/local/bin/perl

# Name: db_manager.cgi
# Version: 5.0
# Last Modified: 01-31-97
#
# Copyright Information: This application was written by Selena Sol
# (selena@eff.org, http://www.eff.org/~erict) and Gunther Birznieks
# (birzniek@hlsun.redcross.org) having been inspired by
# countless other Perl authors.  Feel free to copy, cite, reference,
# sample, borrow, resell or plagiarize the contents.  However, if you
# don't mind, please let me know where it goes so that I can at least
# watch and take part in the development of the memes. Information wants
# to be free, support public domain freware.  Donations are appreciated
# and will be spent on further upgrades and other public domain scripts.

#######################################################################  
#                  Flush the Perl Buffer.                             #  
#######################################################################   
                
                # The script begins by telling the Perl interpreter that
                # it should continuously flush its buffer so that text
                # from this script is sent directly to the Web Browser.
                # We do this to streamline debugging and make sure that
                # the script operates with the flow we want it to.

$| = 1;

#######################################################################
#                 Send out the http Header                            #
#######################################################################

		# Next, the script sends out the http header so that we
		# can debug more easily and so that the browser won't time
		# the script out if it take too long to process.

print "Content-type: text/html\n\n";

#######################################################################
#               Read and Parse Form Data                              #
#######################################################################
                
		# Then, the script loads some supporting files
		# using the subroutine, require_supporting_libraries
		# documented at the end of this script.  cgi-lib.pl is
		# used to read and parse form data. auth-lib.pl is used to
		# authenticate users of the database management system.
		# db-lib.pl is used to search through the database for
		# matches to the users search criteria.
                
&require_supporting_libraries (__FILE__, __LINE__,
                              "./Library/cgi-lib.pl",
			      "./Library/auth-lib.pl",
			      "./Library/db-lib.pl");

                # Now that cgi-lib.pl has been loaded, we can take
		# advantage of its subroutines.  In particular, we will use
		# the ReadParse to read the incoming form data. However,
		# the subroutine is sent "form_data" as a parameter so
		# that the associative array of form keys/values comes
		# back with a descriptive name rather than just %in.
		#
		# In a nutshell, if we have two input fields on our HTML
		# form such as the follwoing:
		#
		# <INPUT TYPE = "hidden" NAME = "id_number" VALUE = "1">
		# <INPUT TYPE = "hidden" NAME = "setup_file" 
		#	 VALUE = "data.setup">
		#
		# Then the ReadParse routine will create an associative
		# array like:
		#
		# %form_data = ("id_number", "1",
		#		"setup_file", "data.setup");
		# 
		# Throught this script, we will be able to access those
		# dynamically assigned values when we need them as we
		# would for an associtive array which was hard coded into
		# the setup file.  We access elemetns in an associative
		# array using key notation.  For example, to assign the
		# value of "setup_file" to $sf, we'll use:
		#
		# $sf = $form_data{'setup_file'}. 
		#
		# In other words, $sf = "data.setup";

&ReadParse(*form_data);

#######################################################################
#                        Load Supporting Files                        # 
#######################################################################
                 
                # Once it has read the incoming form data, the script
                # will be able to determine which setup file it should   
                # use to process the incoming form data.
                # 
                # Perhaps a bit of explanation is in order.
                # 
                # Whenever you run this application, you MUST pass to it
                # the name of the setup file which it will use to process
                # the management request.
                # 
                # This variable will provide the name of the file which
                # this script will use to define all of the customizable
                # aspects of its operation.  For example, the setup file
                # defines what is contained in the database and which
                # fields should be displayed to the user.
                # 
                # The reason for this is that this one script can handle
                # an infinite amount of databases.
                # 
                # Each database has a corresponding setup file which defines
                # how the script performs.  The logic (and programming)
                # remains the same for all db's.  All that changes are
                # the variables and subroutines in the setup files.  This
                # makes it very easy for you to quickly generate diverse 
                # databases with the one backend.
                # 
                # The script first takes the value of "setup_file"
                # coming in from the form (which cgi-lib.pl has already
                # parsed into the %form_data associative array) and
                # assigns it to the variable $setup_file.
                # 
                # So how do you get this information to the script?
                #
                # There are two ways to do that.  Firstly, you can encode
                # the information into the URL if you are executing this
                # script directly from a hyperlink.
                # 
                # For example, you might use the following hyperlink to
                # direct the script to access address_book.setup:
                # 
                # http://www.you.com/cgi/db_manager.cgi?setup_file=address_book.setup
                # 
                # You can also send this information as a hidden field in
                # an HTML form using something like the following.
                # 
                # <INPUT TYPE = "hidden" NAME = "setup_file"
                #        VALUE = "[NAME OF SETUP FILE]">
                # 
                # For example, the following code would define a setup
                # file called address_book.setup:
                # 
                # <INPUT TYPE = "hidden" NAME = "setup_file"
                #        VALUE = "address_book.setup">
                # 
                # You might also create a select box so that the user can   
                # choose from a number of databases dynamically:
                # 
                # <SELECT NAME = "setup_file">
                # <OPTION VALUE = "address_book.setup">Address Book
                # <OPTION VALUE = "customer_list.setup">Customers
                # </SELECT>
                # 
		# All these examples assume you placed the setup files in
		# the provided subdirectory called Setup_files.
		#
                # The script uses the subroutine require_supporting_libraries
                # documented later in this script to actually load the
                # setup file and all of its configuration options.

$setup_file = $form_data{'setup_file'};
                
&require_supporting_libraries (__FILE__, __LINE__,
                               "./Setup_/files/010/$setup_file");

#######################################################################
#                       Authenticate User                             #
#######################################################################   

		# Next the script must make sure that if the admin has 
		# instructed it to authenticate all users through the
		# setup file variable $should_i_authenticate, that it
		# provides a level of authentication.  Authentication is
		# handled by the authentication subroutine which is
		# documented later in this script.

if ($should_i_authenticate eq "yes")
  {
  &authentication;
  }

#######################################################################
#                      Perform Management Functions 		      #
####################################################################### 

		# After the user has been authenticated (if necessary), it
		# is time for the script to perform any or all of the
		# management functions.
		#
		# There are 5 general functions that this script must
		# provide.  The script must be able to 1) add a new item
		# to the database, 2) modify an item in the database,
		# 3) delete an item in the database, 4) provide the user
		# with a view of the database and finally 5) present
		# the user with a frontpage to give the user the previous
		# four options.
		#
		# These five general functions are broken down into
		# several other steps each.  For example, the general step
		# of modifying an item in the database can be broken down
		# into three sub-steps.  1) The user must choose to
		# "Modify an Item" from a submit button on the frontpage
		# 2) they must be taken to a screen on which they can
		# enter search criteria so that the script can dynamically
		# generate a list of matches from which they can then
		# choose a specific item to modify and 3) they user must
		# be able to choose an item from that list, describe the
		# changes she wants made and then submit the changes to
		# the database.
		#
		# When all is said and done, there are 11 general and
		# sub-functions that make up the main body of logic of
		# this application.  We will go over each as they appear
		# in the code below.
		#
                # The logic is broken down into a series of "if" tests.
                #
                # Specifically, the script checks the values of incoming
                # administrative form variables (mainly supplied from
                # the SUBMIT buttons on dynamically generated HTML forms)
                # and will perform its operations depending on whether
                # those administrative variables have values associated
                # with them or not.
                # 
                # The basic format for such an "if" test follows the
                # syntax:
                #
                # if (the value of some submit button ne "")
                #   {
                #   process that type of request;
                #   exit;
                #   }
                # 
                # For example, consider the first case in which the
                # customer has clicked on the "Add an Item" submit
                # button denoted with the NAME value of
		# "add_item_button".
		#
		# if ($form_data{'add_item_button'} ne "")
		#   {
		#   &generic_header("Add an Item to the Database Form");
		#   &generic_form_header;
		#   &add_form_header;
		#   &add_modify_data_entry_form;
		#   &add_form_footer;
		#   &generic_form_footer;
		#   exit;
		#   }
                # 
                # Because the submit button will have some value
                # like "Add this new item", when the script reaches
                # this statement block, it will answer true to the test.
                #   
                # Since the customer can only click on one submit button
                # at a time, we can be assured that only one operation
                # will answer true.
                # 
                # The beauty of using the not equal (ne) test is that
                # regardless of what the submit button actually says
                # (it might say "Add a weiner dog to the chopping block")
                # the if test will still be satisfied if they have clicked
                # the button, since whatever the VALUE is, it will
                # certainly not be equal to "nothing". Of course, this
                # assumes that you do not rename the NAME argument of the
                # submit buttons.  If you do so, you must harmonize the
                # variable you use on the input forms, with the variables
                # used here to test.
                # 
                # Similarly, if you wish to have graphical submit buttons
                # instead of the ugly default buttons supplied by the
                # browser, you will have to modify the if tests so that 
                # they follow the standard image map test:
                # 
                # if ($form_data{'some_button.x'} ne "")
                #  {
                #  do something
                #  }
                # 
                # where the HTML code looks like the following:   
                # 
                # <INPUT TYPE = "image" NAME = "some_button"
                #        SRC = "/images/010/button.gif" BORDER = "0">
                # 
                # Thus, if the button actually has an X-dimension value,
                # it means that the button had been clicked.
                # 
                # Finally, note that every if test is concluded with an
                # exit statement.  This is because once the script is done
                # executing the routine specified in the submit button, it
                # is done with its work and should exit immediately.
                # 
                # Get used to the idea that this script is "self-referencing".
                # The application itself contains many mini-routines
                # which all refer back to the routine community.  Every
                # instance of the script need only execute maybe 1/11th of
                # the routines in the whole file, but in the lifetime of
                # the application, most, if not all, routines are
                # executed.
                #        
                # Okay, so now let's look at each of the routines which
                # this applicaiton must execute.
		#
		# 1. If the user clicks the "add an item" button on the
		# frontpage, they must be presented with an HTML form with
		# input fields for each field in the database which they
		# have access to define.  The user can then fill in the
		# input fields and submit the info to be added to the
		# database.  
		#
		# The generation of this add form is handled by 
		# several subroutines which are all defined in the setup
		# file.  generic_header will print out the basic HTML
		# header with the passed parameter placed between the
		# <TITLE> and </TITLE> tags.  generic_form_header will
		# print out the basic <FORM> tag as well as the important
		# hidden state fields"setup_file" and "session_file" which
		# must appear on ever screen.  add_form_header displays
		# the header of the add form and 
		# add_modify_data_entry_form displays the actual input
		# fields for the form.  add_form_footer and 
		# generic_form_footer display the page footer information.
		#
		# 2. Once the user submits the information that they want
		# to add to the database by clicking the button NAMED
		# "submit_addition", the script must be prepared to
		# actually modify the database with the new information.
		#
		# This is handled in the next if test.  The subroutine
		# submit_addition which is the meet of this function is
		# discussed later in this script.
		#
		# 3. On the other hand, the user might be asking to modify
		# an item by clicking on the frontpage button NAMED
		# "modify_item_button".  If this is the case, the user
		# must be presented a form upon which she can enter search
		# criteria so that the database can bring back a list of
		# database rows which match her search criteria.  The user
		# can then choose one of them and modify it.  We need this
		# search filter because in a large database, it would be
		# hard to select which row you wanted to modify without
		# dome level of filtering.  The search form is actually
		# generated with the modify_search_form subroutine
		# discussed further down in this script.
		# 
		# 4. Once the user fills out the search criteria on the
		# form generated by the routine discussed above, and
		# clicked on the button NAMED
		# "search_and_display_for_modification_button" the
		# script must search through the database and come up with
		# a list of hits.  The user then can select one of the
		# rows to modify and then type in any new information she
		# wants.  
		#
		# This requires two things.  1) The script must generate a
		# list of hits with a radio button for selection of each
		# row (only one row may be modified at one time) and 2)
		# the script must provide a form similar to the add an
		# item form so that the user can enter new info (if the
		# user does not enter info into an inpout field, the old
		# data will be kept.)  These chores are handled by
		# the subroutine search_and_display_for_modification which
		# is discussed later in this script.
		#
		# 5. Finally, the user will have selected an item and
		# filled in some new information to modify and clicked on
		# the button NAMED "submit_modification_button".  Now it
		# is time to actually submit this modification to the
		# database.  We'll do this with the subroutine
		# "submit_modification" discussed below.
		#
		# 6. Besides adding an item and modifying an item, the
		# user may wish to delete an item.  If the user clicks the
		# frontpage buttun NAMED "delete_item_button", the script
		# will display a form identical to the one which the user
		# got in step 3 so that the can define some search
		# criteria with which the script can use to generate a
		# dynamic list of database rows which may be deleted.
		# 
		# 7. As with modification, the user will enter in some
		# search criteria and click the submit button named
		# "search_and_display_for_deletion_button".  The script
		# will then prepare the list of hits from which the user
		# can delete.  Unlike in the case of modification however,
		# there is no need to also include a form similar to the
		# "add" form, since we will not be modifying the data, we
		# will instead be deleteing the row entirely.
		#
		# 8. Finally, the user will select one of the database
		# rows displayed by routine 7 and click on the
		# "submit_deletion_button" button.  At this point, the
		# script must actually delete the item from the database.
		# This is performed with the submit_deletion subrtoutine
		# discussed later in this script.
		#
		# 9. Next, the user might have asked to simply view the
		# dataabse by clicking the "view_database_button" button
		# on the frontpage.  If this is the case, the user needs
		#to be presented with the familiar search form so that
		# they can enter in search criteria which the script can
		# use to generate a dynamic list of database rows to view.
		#
		# 10. As before, once the user choose some search criteria
		# and hits the submit button, the script must display the
		# list of hits.
		#
		# Finally, the script must display the very frontpage from
		# which the previous 10 functions were derrived.  This
		# frontpage must have 4 buttons, one for add, modify,
		# delete and view functions.
		#
		# Okay, well that was quite a mouthful...I have a
		# suspicion that a picture is worth those thousand
		# words (each step is in parenthesis)...
		#
		#    ----------------Display Frontpage (11)----------
		#    |              |              |                |         
		#  User Clicks   User Clicks    User Clicks    User Clicks
		#  Add Button    Modify Button  Delete Button  View Button
		#    |              |              |                |	
		#  Display Add   Display (3)    Display (6)    Display (9)
		#  Form (1)      Search Form    Search Form    Search Form
		#    |              |              |                |
		#  User Adds     User Submits   User Submits   User Submits
		#  New Item (2)  Search         Search         Search
		#                   |              |                |
		#                Mod Form (4)   Delete Form    Search Hits
		#                Displayed      Displayed (7)  Displayed
		#                   |              |	           (10)
		#                User Submits   User Submits
		#		 Modification   Deletion (8)
		#		    (5)	
		#
		# Below are the 11 routines discussed above.

if ($form_data{'add_item_button'} ne "")
  {
  &generic_header("Add an Item to the Database Form");
  &generic_form_header;
  &add_form_header;
  &add_modify_data_entry_form;
  &add_form_footer;
  &generic_form_footer;
  exit;
  }

elsif ($form_data{'submit_addition'} ne "")
  {
  &submit_addition;   
  exit;
  } 

elsif ($form_data{'modify_item_button'} ne "")
  {
  &modify_search_form;
  exit;
  }

elsif ($form_data{'search_and_display_for_modification_button'} ne "")
  {
  &search_and_display_for_modification;
  exit;
  }

elsif ($form_data{'submit_modification_button'} ne "")
  {
  &submit_modification;
  exit;
  }

elsif ($form_data{'delete_item_button'} ne "")
  {
  &delete_search_form;
  exit;
  } 

elsif ($form_data{'search_and_display_for_deletion_button'} ne "")
  {
  &search_and_display_for_deletion;
  exit;
  }

elsif ($form_data{'submit_deletion_button'} ne "")
  {
  &submit_deletion;
  exit;
  }

elsif ($form_data{'view_database_button'} ne "")
  {
  &view_database_form;
  exit;
  } 

elsif ($form_data{'search_and_display_db_button'} ne "")
  {
  &search_and_display_db_for_view;
  exit;
  }

else
  {
  &generic_header("Selena Sol's Example Database Manager Frontpage");
  &generic_form_header;
  &display_frontpage;
  &generic_form_footer;
  exit;
  }

		# Well, that is it!  That is the whole script.  Well, not
		# exaclty, now it is time to explain the subroutines which
		# make the above if tests work.

####################################################################### 
#                       Require Supporting Libraries.                 #
#######################################################################   
                
                # require_supporting_libraries is used to read in some of
                # the supporting files that this script will take
                # advantage of. 
                #
                # require_supporting_libraries takes a list of arguments
                # beginning with the current filename, the current line
                # number and continuing with the list of files which must
                # be required using the following syntax:
                # 
                # &require_supporting_libraries (__FILE__, __LINE__,    
                #                               "file1", "file2",
                #                               "file3"...);
                #
                # Note: __FILE__ and __LINE__ are special Perl variables
                # which contain the current filename and line number   
                # respectively.  We'll continually use these two variables
                # throughout the rest of this script in order to generate
                # useful error messages.
                
sub require_supporting_libraries
  {
                
                # The incoming file and line arguments are split into  
                # the local variables $file and $line while the file list
                # is assigned to the local list array @require_files.
                # 
                # $require_file which will just be a temporary holder   
                # variable for our foreach processing is also defined as a
                # local variable.
                 
  local ($file, $line, @require_files) = @_;
  local ($require_file);
                
                # Next, the script checks to see if every file in the
                # @require_files list array exists (-e) and is readable by
                # it (-r). If so, the script goes ahead and requires it.

  foreach $require_file (@require_files)
    {           
    if (-e "$require_file" && -r "$require_file")
      {
      require "$require_file";
      }
                
                # If not, the scripts sends back an error message that
                # will help the admin isolate the problem with the script.
                 
    else
      {
      print "Content-type: text/html\n\n";
      print "I am sorry but I was unable to require $require_file at line
            $line in $file.  Would you please make sure that you have the 
            path correct and that the permissions are set so that I have
            read access?  Thank you.";
      exit;
      }         
    } # End of foreach $require_file (@require_files)
  } # End of sub require_supporting_libraries

#######################################################################
#                       Perform Authentication                        #
#######################################################################

		# Authentication is performed by the set of "auth"
		# libraries in the Library subdirectory.  However, the
		# only one "you" need to worry about is auth-lib.pl.
		# Specifically, you call GetSessionInfo from auth-lib.pl
		# and it worries about all the other supporting libraries
		# on its own.
		#
		# Authentication demands that you define several variables
		# in the setup file as well and &GetSessionInfo will send
		# you back several variables hich you can use within this
		# script (particularly $session_username and
		# $session_group which are used for authentication of
		# modifications.
		#
		# The basic flow of authentication works like this:
		# Initially, when you install the application, you set
		# $auth_add_register and $auth_allow_register to "on" and
		# you set $auth_default_group to "Admin". This will allow
		# you to add yourself as an administrator.  go ahead and
		# run the script from the web and click on the "Register
		# New User Button" and fill out the resulting form.
		#
		# Once you have registered yourself as an admin, you have
		# two options.  Firstly, you can turn off registration so
		# that only you can use this applicaton.  To do so, turn
		# "off" $auth_add_register and $auth_allow_register.  Now
		# you will be able togon, but noone else will be able to
		# do so without your username and encrypted password.
		#
		# You might also decide to allow "users" to have access to
		# the database manager.  These people will be able to add
		# entries and delete and modify each others entries but
		# not touch yours.  To do so, change $auth_default_group
		# to "user".

sub authentication
  {

		# Make sure if the session_file variable is coming in as
		# form data that it is converted to a local variable name.
		# This is important.  We do not want to have to ask the
		# user to logon for every screen they want to see!  Thus,
		# we must make sure that session_file is passed (in a self
		# referential way) between every screen in this 
		# application through the use of hidden form variables.

  $session_file = "$form_data{'session_file'}";

		# Now perform the authentication and gather the returned
		# values.
                
  ($session_file, $session_username, $session_group,$session_first_name,
                  $session_last_name, $session_email) =
                  &GetSessionInfo($session_file, $this_script_url, *form_data);
  }

#######################################################################
#                      Submit an Addition                             # 
#######################################################################

		# The function of submit_addition is all in the name.
		# This subroutine takes the user-defined input, formats
		# it into a database row (along with some
		# administrative data like the time it was submitted and
		# by whom it was submitted) and appends it to the
		# database.

sub submit_addition
  {              

		# The subroutine begins however, by access the counter
		# file defined in the setup file.  The counter file shuld
		# contain a number on one line.  This number represents a
		# unique database row identification. This is very
		# important because in order for the script to know which
		# row to modify or delete, it needs to be able to identify
		# that row absolutely.
		#
		# The database id number is that identification and it is
		# generated by incrementing the number in the counter file
		# for every item that is added.
		#
		# The process of getting a unique id number is pretty
		# straight forward.  
		#
		# First the script uses get_file_lock discussed later in
		# this script to lock the counter file so no other
		# instances of this script can get to the counter file.
		#
		# Then, the script opens the counter file
		# (exiting gracefully with file_open_error discussed later
		# if there is a problem opening the file) and reads the
		# counter file assigning the number to $current_counter .
		#
		# Next, after closing the counter file back up, it
		# increments that number and assigns the incremented value
		# to $new_counter
		#
		# Finally, the script reopens the counter file for writing
		# and deletes the old number, replacing it with the new
		# incremented number, thus incrementing the counter file.

  &get_file_lock("$location_of_lock_file");
  open (COUNTER_FILE, "$location_of_counter_file") || 
	&file_open_error ("$location_of_counter_file", "Submit Addition",
	__FILE__, __LINE__);

  while (<COUNTER_FILE>)
    {
    $current_counter = $_;
    }
  close (COUNTER_FILE);

  $current_counter++;
  $new_counter = $current_counter;

  open (COUNTER_FILE, ">$location_of_counter_file") || 
        &file_open_error ("$location_of_counter_file", "Submit Addition",   
        __FILE__, __LINE__);                      
  print COUNTER_FILE "$new_counter";
  close (COUNTER_FILE);

		# Once we have dealt with the database id number, we can
		# now grab the current date using the get_date documented
		# later in this script.

  $when_modified = &get_date;

		# With that, we are ready to open the database for
		# appending (>>).

  open (DATABASE, ">>$data_file_path") || 
        &file_open_error ("$data_file_path", "Submit Addition",   
        __FILE__, __LINE__);                      


		# Now, for every field that the user could define on the
		# add form, we will append the value they entered to a
		# variable called $new_row using the append operator (.=).
		# Notice that we will add a pipe (|) delimiter between
		# every field in a database row.

  foreach $field (@db_user_definable_field_order)
    {
    $new_row .= "$form_data{$field}\|";
    }

		# Then, we will tag on the administrative fields of
		# when the addition was made,who made the addition, what
		# group are they in and the unique database id number.

  $new_row .= "$when_modified\|$session_username\|$session_group\|$new_counter\n";
  print DATABASE $new_row;
  close (DATABASE);

		# Once the database has been appended to, we will note
		# what we did in the log file, release the lock file so
		# that others can make their own additions and let the
		# user know that their item was added successfully.

  open (LOG_FILE, ">>$location_of_log_file") || &file_open_error
	("$location_of_log_file", "Submit Addition",
        __FILE__, __LINE__);
  print LOG_FILE "ADD\|$new_row";
  close (LOG_FILE);
  &release_file_lock("$location_of_lock_file");

  &successful_addition_message;
  }             
                
#######################################################################
#                       Submit a Modification                         # 
#######################################################################

		# The user might also be submitting a modification to the
		# database.

sub submit_modification
  {              

		# The first thing we must do is make sure that they
		# actually chose a database item to modify.  If they did
		# not, we better warn them and stop processing.

  if ($form_data{'item_to_modify'} eq "")
    {
    &no_item_submitted_for_modification;
    exit;
    }

		# If they did choose an item, we need to find that items
		# so that we can modify it.  To do so, we'll open the data
		# file and read through it a line at a time.  We'll then
		# split up each line into its fields and compare the id
		# number given to us by the suer against the id number of
		# the current line.  If it is not the same, we will add
		# the entire line to a variable called $new_data (By the
		# end of this, $new_data is going to hold the entire
		# contents of our database).

  open (DATABASE, "$data_file_path") || &file_open_error
	("$data_file_path", "Modify item",  __FILE__, __LINE__);
  
  while (<DATABASE>)
    {
    $line = $_; 
    chop $line;
    @fields = split (/\|/, $line);

    if ($fields[$index_of_db_id_number] ne $form_data{'item_to_modify'})
      {
      $new_data .= "$line\n";
      }

		# If the id numbers ARE equal, however, it means that we
		# have found the database row that needs to be modified.
		#
		# First, we will save the old line in a variable $old_row.
		# We are going to need that value when we report what
		# happened in the log file.
		# 
		# Then, we will go through the basic user-definable fields
		# checking to see which fields the user has asked to
		# modify (Only fields which have a $form_data value, will 
		# be modified.)
		#
		# Notice, that we will append this row to $new_row one
		# field at a time.  If the user has not submitted a
		# change, we'll grab the value from the old row.

    else
      {
      $old_row = "$line";
      for ($i=0; $i <= (@db_user_definable_field_order-1); $i++)
        {
        $index = @db_user_definable_field_order[$i];
        if ($form_data{$index} ne "")
          {
          $new_row .= "$form_data{@db_user_definable_field_order[$i]}\|";   
          }
        else
          {
	  $new_row .= "$fields[$i]\|";
	  }
        } # End of for ($i=1; $i <= @db_user_definable_field_order; $i++);    
      $new_data .= "$new_row";

		# Now we will complete the row by adding all of the
		# administrative variables like the date and the
		# authentication values.  Notice that if the person who is
		# modifying the data is an admin, we will use the group
		# and user values from the old row instead of changing it
		# to admin.  That way the original poster will still be
		# able to modify it.

      $when_modified = &get_date;
      $who_modified = $session_username;

      if ($session_group eq "admin")
        {
        $group = $fields[$index_of_group_who_modified];
        $who_modified = $fields[$index_of_who_modified];
        }
      else
       {
       $group = $session_group
       }

      $new_data .= "$when_modified|$who_modified|$group|$fields[$index_of_db_id_number]\n";
      $new_row .= "$when_modified|$who_modified|$group|$fields[$index_of_db_id_number]";
      } # End of  else
    } # End of while (<DATABASE>)
  close (DATABASE);

		# Now that we have appended the entire database as well as
		# the modified row to $new_data, it is time to change the
		# data file. However, first, we must make sure that the
		# user is performing a legal operation.  The next if test
		# asks 1) is the user the original poster of the database
		#row or are they in the same group as the user who posted
		# it (provided that group is not the default "user" group
		# which everyone is assigned to, 2) or are they an
		# administrator.  If the user passes this test and the
		# admin has set the script to use authentication, the
		# script will add the overwrite the old database with the
		# contents of $new_data, protecting it with a lockfile as 
		# we did for addition.

  if ((($session_username eq $fields[$index_of_who_modified]) ||
      (($session_group eq $fields[$index_of_group_who_modified]) &&
       ($session_group ne "user"))) ||
      ($session_group eq "admin") || ($should_i_authenticate ne "yes"))
    { 
    &get_file_lock("$location_of_lock_file");
    open (DATABASE, ">$data_file_path") || &file_open_error 
        ("$data_file_path", "Modify Item",  __FILE__, __LINE__);
    print DATABASE "$new_data";  

    close (DATABASE);

		# We will also add the modification information to the log
		# file.

    open (LOG_FILE, ">>$location_of_log_file") || &file_open_error 
        ("$location_of_log_file", "Modify Item",  __FILE__, __LINE__);
    print LOG_FILE "MODIFY_NEW\|$new_row|MODIFIED BY $session_username\n";
    print LOG_FILE "MODIFY_OLD\|$old_row\n";  
    close (LOG_FILE);

    &release_file_lock("$location_of_lock_file");
    &successful_modification_message;
    }
  else
   {
   &unsuccessful_modification_message;
   }
  }             

#######################################################################
#                       Submit a Deletion                             #  
#######################################################################                 

		# Finally, the user might be asking to make an actual
		# deletion.

sub submit_deletion
  {              

		# As in the case of modification, we must make sure the
		# user actually chose an item to delete from the list.

  if ($form_data{'item_to_delete'} eq "")
    {
    &no_item_submitted_for_modification;
    exit;
    }

		# Because we can delete multiple items at one time, the
		# next thing we do is to break up the item_to_delete
		# incoming form data into its separate parts (if there are
		# any) using SplitParam from cgi-lib.pl.

  @items_to_delete = &SplitParam($form_data{'item_to_delete'});

		# Then we will open the data file, read through it a line
		# at a time, splitting each line into its component
		# fields as we did for modificaiton.

  open (DATABASE, "$data_file_path") || &file_open_error
        ("$data_file_path", "Delete Item",  __FILE__, __LINE__);

  while (<DATABASE>)
    {
    $line = $_;
    chop $line;
    @fields = split (/\|/, $line);

		# Then, foreach item in the delete list, we will delete it
		# if the current line's item id is equal to the id
		# submitted.

    $already_added = "no";

    foreach $item (@items_to_delete)
      {
      if ($fields[$index_of_db_id_number] eq $item)
        {
        $deleted_row = "$line";
        $already_deleted = "yes";
        }
      elsif (($already_added ne "yes") && ($already_deleted ne "yes"))
        {
        $new_data .= "$line\n"; 
        $already_added = "yes"
        }
      }
    }
  close (DATABASE);

		# As we did for modificaiton, we will pass the user
		# through authentication and overwrite the old datafile
		# with the new information.

  if ((($session_username eq $fields[$index_of_who_modified]) ||
      (($session_group eq $fields[$index_of_group_who_modified]) &&
       ($session_group ne "user"))) ||
      ($session_group eq "admin") || ($should_i_authenticate ne "yes"))
    {

    &get_file_lock("$location_of_lock_file");
    open (DATABASE, ">$data_file_path") || &file_open_error
        ("$data_file_path", "Delet Item",  __FILE__, __LINE__);
    print DATABASE "$new_data";
    close (DATABASE);

    $who_deleted = $session_username;

		# And as before, we will append the info to the log file.

    open (LOG_FILE, ">>$location_of_log_file") || &file_open_error
        ("$location_of_log_file", "Delete Item",  __FILE__, __LINE__);
    print LOG_FILE "DELETE\|$deleted_row\|DELETED BY $who_deleted\n";
    close (LOG_FILE);

    &release_file_lock("$location_of_lock_file");
    &successful_deletion_message
    }
  else
    {
    &unsuccessful_modification_message;
    }
  }             

#######################################################################
#                    Search and Display the Database 		      #
#######################################################################  
    
sub search_and_display_db
  {              
                # Before we go in and search however, we format any
                # incoming sort_by information.  We'll discuss the sorting
                # algorithm in just a minute.  However, I want to note
                # here that there are two ways to define a field by which
                # this script will sort the returned database rows.  You
                # can set a default row in the setup file by setting   
                # $index_of_field_to_be_sorted_by equal to the index of
                # the field that you want sorted by.  Thus, you may just
                # want to sort automatically by last name and not even 
                # give the user the option to sort by another row.  
                # 
                # On the other hand, you might want to allow the user to
                # choose which field the returned rows are sorted by.  If
                # this is the case, you need to add another form variable
                # to your HTML interface.  This variable MUST be called
                # "sort_by" and will usually be in the form of a
                # select box such as the following:
                # 
                # <TH>Sort by which field</TH>
                # <TD><SELECT NAME = "sort_by">
                # <OPTION VALUE = "0">First Name
                # <OPTION SELECTED VALUE = "1">Last Name
                # <OPTION VALUE = "2">Email
                # <OPTION VALUE = "9">Age
                # </SELECT></TD>
                # 
                # If you allow the user to define which field to sort on,
                # then this information will override the information in
                # the setup file using the following it test.
                # 
                # Remember that arrays start counting from zero so the
                # first filed in your dataabse has an index value of 0,
                # not 1
   
  if ($form_data{'sort_by'} ne "")
    {
    $index_of_field_to_be_sorted_by = $form_data{'sort_by'};
    }
                
                # okay, now display the header and grab our lisdt of
                # database rows using &submit_query in db-lib.pl.  Notice
                # that you need to redefine
                # $index_of_field_to_be_sorted_by "before" you display the
                # header because the header displays the hidden form field
                # which will carry that data throughout further     
                # self-referencing screens.
                
  ($total_row_count) = &submit_query(*database_rows);

                # Now here is where the real fun comes in.  We want to
                # sort the database rows that are displayed to the user.
                # The process of this is fairly simple.  For every
                # database row contained in @database_rows, we are going
                # to grab the value of the field defined as the field to
                # be sorted by and append that value to the very begining
                # of the line (so that the field will be repeated twice.)
                # Then you sort the rows (sort will sort on the first
                # characters first which is why you need to append the
                # sortable field to the front.)  Then, finally, you remove
                # the appended field so that the database rows are as they
                # began, but in a sorted order.
                # 
                # Thus, if you were sorting by last name and you had the
                # following database rows ($row) in the @database_rows
                # array:
                # 
                # Eric|Tachibanaerict@eff.org
                # Selena|Sol|selena@eff.org
                # Gunther|Birznieks|birzniek@hlsun.redcross.org
                #
                # The script would then take each row and append the last
                # name field to the front like so:
                #
                # Tachibana|Eric|Tachibana|erict@eff.org
                # Sol|Selena|Sol|selena@eff.org
                # Birznieks|Gunther|Birznieks|birzniek@hlsun.redcross.org
                
  foreach $row (@database_rows)
    {
    @row = split (/\|/, $row);
    $sortable_field = $row[$index_of_field_to_be_sorted_by];
    unshift (@row, $sortable_field);
    $new_row = join ("\|", @row);
    push (@new_rows, $new_row);
    }

                # Once we have the rows reformatted as above, we are ready
                # to sort them.  First however, we erase the contents of
                # @database_rows since we are going to want to recreate 
                # that array with the sorted rows from @new_rows in just a
                # moment.
                
  @database_rows = ();
                
                # Then we are ready to sort...guess what the result is:   
                # 
                # Birznieks|Gunther|Birznieks|birzniek@hlsun.redcross.org
                # Sol|Selena|Sol|selena@eff.org
                # Tachibana|Eric|Tachibana|erict@eff.org
                
  @sorted_rows = sort (@new_rows);
                
                # Next, we need remove that first sortable field so that
                # we have the following:
                #
                # Gunther|Birznieks|birzniek@hlsun.redcross.org
                # Selena|Sol|selena@eff.org
                # Eric|Tachibana|erict@eff.org
                # 
                # Look!  They are now sorted by last name!  By the way, if
                # you sort by a field with numbers, remember that
                # computers sort with their own funky rules.  That is, if
                # you don't put a 0 before the nuber 1, it will sort after
                # 9 but alphabetical sorting should be just fine.

  foreach $sorted_row (@sorted_rows)
    {
    @row = split (/\|/, $sorted_row);
    $sorted_field = shift (@row);
    $old_but_sorted_row = join ("\|", @row);
    push (@database_rows, $old_but_sorted_row);
    }
                
                # now that we have sorted the rows, lets figure out how to
                # display them all.
                # 
                # The reason that we wanted to get the $total_row_count
                # back from the search libraries is so that we can then
                # check to make sure that if their search returned no hits
                # we can let them know rather than just sending them a    
                # blank screen.
                
  if ($total_row_count < 1)
    {
    &no_hits_message;
    exit;
    }           

                # So what exactly do we show the user if their search did
                # turn up some hits. Well, that depends on 1) how many
                # rows were returned from the database as scoring matches
                # to their search criteria, 2) how many rows we have
                # defined in the setup file to allow them to see and 3)
                # how many rows they have already seen.
                # 
                # Let me expound.  Let's assume that we have set
                # $max_rows_returned to 2 in the setup file and that their
                # search turned up 11 hits which have just been sorted.
                #
                # The first screen that they should see should say, "You
                # scored 10 hits and I  have been instructed to show you
                # two at a time".  It should then display the first two
                # sorted rows and then provide a button which says "See
                # next 2 hits".  When the user clicks on that button, she
                # should then get the next two sorted rows.  The script  
                # needs to remember that she already saw the first two
                # rows as well as remembering that it should only show her
                # two at a time.
                # 
                # Finally, the script will have gone through all the rows
                # up to 9 and 10.  The final trick is that it must then
                # tell her that she can click the button to see the next 1
                # hi(t)...no "s" on the end of that....the script has to
                # know some grammar rules.
                #
                # So first, we will collect any incoming information about
                # the hits that the client has seen so far.  This
                # information will be stored in a hidden inpout field
                # called "hits_seen" which must accompany every submit
                # button that promises to show "x more hits".  Note that
                # the first time around, there will be no new_hits_seen
                # value coming in from the form since the user will not  
                # have seen any hits yet.
                
                # Then we need to remove all of the rows from
                # @database_rows that will not be shown to the user quite
                # yet because we are only allowed to display
                # max_rows_returned at any one time.
                # 
                # To do this we will first figure out how many elements
                # are left in the array.  Then, we will pop out (remove  
                # from the end of the list) all of the extra rows.
                
  $length_of_database_rows = @database_rows;
                
                 
  for ($i = $length_of_database_rows-1;$i >= $max_rows_returned;$i--)
    {
    $extra_row = pop (@database_rows);
    }
                
  &search_results_body;
  &search_results_footer;
  }
                
#################################################################
#                      get_date Subroutine                      #
#################################################################
 
                # get_date is used to get the current date and time and
                # format it into a readable form.  The subroutine takes no
                # arguments and is called with the following syntax:
                #
                # $date = &get_date;
                #
                # It will return the value of the current date, so you 
                # must assign it to a variable in the calling routine if
                # you are going to use the value.

sub get_date
  {

                # The subroutine begins by defining some local working  
                # variables
                
  local ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst,$date);   
  local (@days, @months);

  @days = ('Sunday','Monday','Tuesday','Wednesday','Thursday',
           'Friday','Saturday');
  @months = ('January','February','March','April','May','June','July', 
             'August','September','October','November','December');

                # Next, it uses the localtime command to get the current
                # time, from the value returned by the time
                # command, splitting it into variables.
                
  ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
  
                # Then the script formats the variables and assign them to
                # the final $date variable.  Note that $sc_current_century
                # is defined in web_store.setup.  Since the 20th centruy
                # is really 1900-1999, we'll need to subtract 1 from this
                # value in order to format the year correctly.
                
  if ($hour < 10)
    {
    $hour = "0$hour";
    }
  if ($min < 10) 
    {
    $min = "0$min";
    }
  if ($sec < 10)
    { $sec = "0$sec";
    }
  $mon++;
  $year = ($current_century-1) . "$year";
  $date = "$mon/$mday/$year at $hour\:$min\:$sec";
  return $date;
  }             

#######################################################################
#                            get_file_lock                            #
#######################################################################
                
                # get_file_lock is a subroutine used to create a lockfile.
                # Lockfiles are used to make sure that no more than one
                # instance of the script can modify a file at one time.  A
                # lock file is vital to the integrity of your data.
                # Imagine what would happen if two or three people
                # were using the same script to modify a shared file (like
                # the error log) and each accessed the file at the same   
                # time.  At best, the data entered by some of the users  
                # would be lost.  Worse, the conflicting demands could
                # possibly result in the corruption of the file.
                #
                # Thus, it is crucial to provide a way to monitor and
                # control access to the file.  This is the goal of the
                # lock file routines.  When an instance of this script
                # tries to  access a shared file, it must first check for
                # the existence of a lock file by using the file lock
                # checks in get_file_lock.
                # 
                # If get_file_lock determines that there is an existing
                # lock file, it instructs the instance that called it to
                # wait until the lock file disappears.  The script then
                # waits and checks back after some time interval.  If the
                # lock file still remains, it continues to wait until some
                # point at which the admin has given it permissios to just
                # overwrite the file because some other error must have 
                # occurred.
                #
                # If, on the other hand, the lock file has dissappeared,
                # the script asks get_file_lock to create a new lock file
                # and then goes ahead and edits the file.
                #
                # The subroutine takes one argumnet, the name to use for
                # the lock file and is called with the following syntax:
                #
                # &get_file_lock("file.name");
                
sub get_file_lock
  {
  local ($lock_file) = @_;
  local ($endtime);
  $endtime = 20;
  $endtime = time + $endtime;
                
                # We set endtime to wait 20 seconds.  If the lockfile has
                # not been removed by then, there must be some other
                # problem with the file system.  Perhaps an instance of
                # the script crashed and never could delete the lock file.
                
  while (-e $lock_file && time < $endtime)
    {
    sleep(1);
    }
                  
  open(LOCK_FILE, ">$lock_file") || &file_open_error ("$lock_file",
                                                      "Lock File Routine",
                                                      __FILE__, __LINE__);
                
                # Note: If flock is available on your system, feel free to
                # use it.  flock is an even safer method of locking your  
                # file because it locks it at the system level.  The above
                # routine is "pretty good" and it will server for most
                # systems.  But if youare lucky enough to have a server
                # with flock routines built in, go ahead and uncomment  
                # the next line and comment the one above.
                
# flock(LOCK_FILE, 2); # 2 exclusively locks the file
                
  }
                 
#######################################################################
#                            release_file_lock                        #
#######################################################################
   
                # release_file_lock is the partner of get_file_lock.  When
                # an instance of this script is done using the file it
                # needs to manipulate, it calls release_file_lock to
                # delete the lock file that it put in place so that other
                # instances of the script can get to the shared file.  It
                # takes one argument, the name of the lock file, and is  
                # called with the following syntax:
                # 
                # &release_file_lock("file.name");
                
sub release_file_lock
  {  
  local ($lock_file) = @_;
     
# flock(LOCK_FILE, 8); # 8 unlocks the file
  
                # As we mentioned in the discussion of get_file_lock,
                # flock is a superior file locking system.  If your system
                # has it, go ahead and use it instead of the hand rolled
                # version here.  Uncomment the above line and comment the 
                # two that follow.
                
  close(LOCK_FILE);
  unlink($lock_file);
  }
                
#######################################################################
#                    file_open_error Subroutine                       #
#######################################################################
                 
                # If there is a problem opening a file or a directory, it
                # is useful for the script to output some information  
                # pertaining to what problem has occurred.  This
                # subroutine is used to generate those error messages.
                # 
                # file_open_error takes four arguments: the file or   
                # directory which failed, the section in the code in which
                # the call was made, the current file name and
                # line number, and is called with the following syntax:  
                # 
                # &file_open_error("file.name", "ROUTINE", __FILE__,
                #                  __LINE__);
                
sub file_open_error
  {
                # The subroutine simply uses the update_error_log
                # subroutine discussed later to modify the error log and
                # then uses CgiDie in cgi-lib.pl to gracefully exit the
                # application with a useful debugging error message sent
                # to the browser window.
                
  local ($bad_file, $script_section, $this_file, $line_number) = @_;
  print "Content-type: text/html\n\n";
  &CgiDie ("I am sorry, but I was not able to access $bad_file in the
        $script_section routine of $this_file at line number $line_number.
        Would you please make sure the path is correctly defined in
        web_store.setup and that the permissions are correct.")
  }