#!/usr/bin/perl # Name: db_manager.cgi # Version: 6.0 # Last Modified: 04-02-98 # # Copyright Information: This application was written by Selena Sol # (selena@eff.org, http://www.extropia.com) 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. # ####################################################################### $| = 1; ####################################################################### # Send out the http Header # ####################################################################### print "Content-type: text/html\n\n"; ####################################################################### # Read and Parse Form Data # ####################################################################### &require_supporting_libraries (__FILE__, __LINE__, "./Library/cgi-lib.pl", "./Library/auth-lib.pl", "./Library/db-lib.pl"); &ReadParse(*form_data); ####################################################################### # Load Supporting Files # ####################################################################### # For example, you might use the following hyperlink to # direct the script to access address_book.setup: # # http://www.katsation.com/cgi-bin/db_manager.cgi?setup_file=CT_chimera.setup # # You can also send this information as a hidden field in # an HTML form using something like the following. # # # # For example, the following code would define a setup # file called address_book.setup: # # # # You might also create a select box so that the user can # choose from a number of databases dynamically: # # # # 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'}; $setup_file =~ /([\w-.]+setup[\w-.]+)/; $untainted_setup_file = $1; &require_supporting_libraries (__FILE__, __LINE__, "./Setup_files/$untainted_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: # # # # 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 # and tags. generic_form_header will # print out the basic
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"); &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; } elsif ($form_data{'search_and_display_db_for_view_next_button'} ne "") { &search_and_display_db_for_view; exit; } else { &generic_header("Database Manager"); &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'}"; $session_file =~ s/\///g; # 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. #added this routine to get count.. cross fingers....lol... sub get_count { open (COUNTER_FILE, "<$location_of_counter_file") || &file_open_error ("$location_of_counter_file", "Submit Addition", __FILE__, __LINE__); while () { $current_counter = $_; } close (COUNTER_FILE); $current_counter++; $new_counter = $current_counter; return $new_counter; } 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 () { $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) { $form_data{$field} =~ s/\n/
/g; $form_data{$field} =~s/(\s)\s*/\1/g; $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 () { $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 } 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")) { $security_satisfied = "yes"; } $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 () 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 ($security_satisfied eq "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 () { $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_deleted = "no"; foreach $item (@items_to_delete) { if ($fields[$index_of_db_id_number] eq $item) { $deleted_row .= "$line"; $already_deleted = "yes"; 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")) { $security_satisfied = "yes"; } } } # End of foreach $item (@items_to_delete) if ($already_deleted ne "yes") { $new_data .= "$line\n"; } } # End of while () close (DATABASE); # As we did for modificaiton, we will pass the user # through authentication and overwrite the old datafile # with the new information. if ($security_satisfied eq "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: # # Sort by which field # # # 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; } ####################################################################### # Search and Display Next Hits # ####################################################################### sub search_and_display_db_next { &generic_header("Search & Display"); if ($form_data{'sort_by'} ne "") { $index_of_field_to_be_sorted_by = $form_data{'sort_by'}; } &generic_form_header; ($total_row_count) = &submit_query(*database_rows); 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); } @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); 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); } if ($total_row_count < 1) { &no_hits_message; exit; } $hits_seen = $form_data{'new_hits_seen'}; for ($i = 1;$i <= $hits_seen;$i++) { $seen_row = shift (@database_rows); } $length_of_database_rows = @database_rows; for ($i = $length_of_database_rows-1;$i >= $max_rows_returned;$i--) { $extra_row = pop (@database_rows); } $new_hits_seen = $hits_seen + @database_rows; &search_results_body; &search_results_footer; } sub search_and_display_db_modify { if ($form_data{'sort_by'} ne "") { $index_of_field_to_be_sorted_by = $form_data{'sort_by'}; } ($total_row_count) = &submit_query(*database_rows); 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); } @database_rows = (); @sorted_rows = sort (@new_rows); 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); } if ($total_row_count < 1) { &no_hits_message; exit; } $hits_seen = $form_data{'new_hits_seen'}; for ($i = 1;$i <= $hits_seen;$i++) { $seen_row = shift (@database_rows); } $length_of_database_rows = @database_rows; for ($i = $length_of_database_rows-1;$i >= $max_rows_returned;$i--) { $extra_row = pop (@database_rows); } $new_hits_seen = $hits_seen + @database_rows; #added option to view data the way it should be viewed for modification &search_modify_results_body; } ################################################################# # 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"; if ($year < 99) { $year += 2000; } else { $year += 1900; } $date = "$mon/$mday/$year at $hour\:$min\:$sec"; return $date; } sub get_short_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"; if ($year < 99) { $year += 2000; } else { $year += 1900; } $date = "$mon/$mday/$year at $hour\:$min\:$sec"; $short_date = "$mon/$mday/$year"; return $short_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.") }