#!/usr/bin/perl -T ####################################################################### # Application Information # ######################################################################## # Application Name: Web Store # Application Authors: Eric Tachibana (Selena Sol) and Gunther Birznieks # Version: 2.0 # Last Modified: 17NOV98 # # Copyright: # # You may use this code according to the terms specified in # the "Artistic License" included with this distribution. The license # can be found in the "Documentation" subdirectory as a file named # README.LICENSE. If for some reason the license is not included, you # may also find it at www.extropia.com. # # Though you are not obligated to do so, please let us know if you # have successfully installed this application. Not only do we # appreciate seeing the wonderful things you've done with it, but we # will then be able to contact you in the case of bug reports or # security announcements. To register yourself, simply send an # email to register@extropia.com. # # Finally, if you have done some cool modifications to the scripts, # please consider submitting your code back to the public domain and # getting some community recognition by submitting your modifications # to the Extropia Cool Hacks page. To do so, send email to # hacks@extropia.com # # Basic Usage: # # 1. Read the README.CHANGES, README.LICENSE, and README.SECURITY # files and follow any directions contained there # # 2. Change the first line of each of the scripts so that they # reference your local copy of the Perl interpreter. (ie: # #!/usr/local/bin/perl) (Make sure that you are using Perl 5.0 or # higher.) # # 3. Set the read, write and access permissions for files in the # application according to the instructions in the # README.INSTALLATION file. # # 4. Define the global variables in the setup file you choose according # to the instructions in the README.INSTALLATION file. Also # set the name of the setup file in ctstore.cgi if you are not # using the default. # # 5. Point your web browser at the script # (ie:http://www.yourdomain.com/cgi-bin/web-store.cgi) # # More Information # # # You will find more information in the Documentation sub-directory. # We recommend opening the index.html file with your web browser to # get a listing of supporting documentation files. ######################################################################## # Application Code # ######################################################################## # First, Perl is told to bypass its own buffer so that the # information generated by this script will be sent # immediately to the browser. $| = 1; # Then, the http header is sent to the browser. This is # done early for two reasons. # # Firstly, it will be easier to debug the script while # making modifications or customizing because we will be # able to see exactly what the script is doing. # # Secondly, the http header is sent out early so that the # browser will not "time out" in case the script takes a # long time to complete its work. print "Content-type: text/html\n\n"; # Next we will execute a few subroutines which will define # the environment in which the script will operate. # # First we will require the web_store.setup file so that # we will be able to read in global variables. Notice # that in the distribution, we have six setup files by # default. We'll use frames.javascript as out basic # example though. # # Secondary supporting files are also read in using # require_supporting_libraries which is used to require # the supporting files needed by this script. Notice # that we are going to pass the current filename as well # as the current line number to the # require_supporting_libraries subroutine. It will use # these values to generate useful error messages in case # it is unable to read in the files requested. # # Note: Here is where we read in all of the global # variables and definitions included in the setup files # # web_store.setup.* defines many global variables # for this script relative to the local server and # installation. # # $sc_cgi_lib_path is the location of cgi-lib.pl which is # used to parse incoming form data. # # $sc_html_setup_file_path is the location of # web_store_html_lib.pl which is used to define # various customizable HTML interface headers, # footers and pages. # # $sc_mail_lib_path is the location of mail-lib.pl which is # used to mail non-encrypted mail to the admin # about usage of the script. # &require_supporting_libraries (__FILE__, __LINE__, "./Library/web_store.setup.html"); &require_supporting_libraries (__FILE__, __LINE__, "$sc_cgi_lib_path", "$sc_html_setup_file_path", "$sc_mail_lib_path", "$sc_cc_validation_lib_path"); # Next we read and parse the incoming form data. # read_and_parse_form_data is a very short subroutine # which simply uses the ReadParse subroutine in cgi-lib.pl # to parse the incoming form data into the associative # array, %form_data. &read_and_parse_form_data; # Once we have parsed the incoming form data, # we can assign the values of administrative variables to # regularized scalars, local to this script. # # $page will contain the path location of any pages which # this script is required to display. This may be the # store frontpage, order form or any number of product or # category pages used for store navigation. # # $search_request is the value of the button used when a # customer submits search terms used to generate a dynamic # custom product page. # # $cart_id is the id number of the customer's unique cart # containing all of the items they have ordered so far. # The specifics of cart generation and maintenance are # covered in greater depth in the next section. # # $sc_cart_path is the actual path of the shopping cart # combining both $sc_user_carts_directory_path and # $cart_id # # These three variables are crucial state variables which # must be passed as form data from every instance of this # script to the next. $page = $form_data{'page'}; # Modified 4-10-98 Gunther Birznieks # # Added code to stop snooping beyond the root store HTML # directory # # The following code only allows # word characters, - sign, + sign, = sign, / for sub # directories in the page definition # # If you find yourself needing more definitions the # regular expression below is the one you want to modify # # One dot is allowed for an extension. I don't allow # periods because of ../../.. type of manipulations # # $1 matches the first part # $2 matches the extension which shouldn't have # any weird characters in it, so I just left it # as matching \w (word characters) $page =~ /([\w\-\=\+\/]+)\.(\w+)/; $page = "$1.$2"; $page = "" if ($page eq "."); $page =~ s/^\/+//; # Get rid of any residual / prefix #WJB: Added the .x so that image could be used in place of ugly button $search_request = $form_data{'search_request_button.x'}; $cart_id = $form_data{'cart_id'}; if ($cart_id =~ /^(\w+)$/) { $cart_id = $1; } else { $cart_id = ""; } $sc_cart_path = "$sc_user_carts_directory_path/$cart_id.cart"; # WJB: Added Partner Data - If no partner data is available, it defaults to the vendor's id $partner = $form_data{'partner'}; if ($partner =~ /^(\w+)$/) { $partner = $1; } else { $partner = "cts"; } # Finally we submit the incoming form data to some # security checks. error_check_form_data is a subroutine # which checks the just-parsed incoming form data to make sure # that the script is only being used to display proper # pages (typically .html, .shtml, or .htm). # # This is an important security precaution. Later in this # script, we are going to use a variable called "page" to # communicate which page in our store we want to display # to the client. # # The danger is that a client might "fake" a request to # the script by editing the page variable in the HTML or # in the encoded URL. # # For example, they might reassign page from, say, # "vowel.html" to "../../../etc/passwd"! As you can # imagine, this could end up displaying your password file # to the browser window. Thus, we need to make sure that # only appropriate files can be displayed by the store. &error_check_form_data; # What is the purpose of a unique cart? Well, simply, # every customer who is using the application must be # assigned a unique cart which will contain their specific # shopping list. # # These carts are actually short flatfile text databases # stored by default in the User_carts subdirectory in the # format "somerandomnumber.cart". These files contain # information about which items the client has ordered and # how many of each item they ordered. # # Once a client enters the store, they are assigned their # own unique cart. For the rest of their stay, the script # will make sure that it matches clients with their carts # no matter which page they go to. # # It does this by continually passing the location of the # cart ($cart_id) along as either hidden form data or URL # encoded information depending on if the customer uses a # submit button or a hyperlink to navigate through the # store. Thus, as long as the customer follows the path # provided by the application, she will never lose her # cart. # # Thus, before anything else, the script must check to see # if the client has already received a unique shopping # cart. If so, it will be coming in as form data and have # been just assigned to $cart_id. If the script has not # received a shopping cart id number as form data # ($cart_id eq ""), however, it means that the client has # not yet received a unique shopping cart. # # If this is the case, the script must assign them # one. However, as a matter of good housekeeping, it will # first take a second to delete old carts that have been # abandoned using the delete_old_carts subroutine # documented later in this script. Then, it will assign # the client their own fresh new cart using # assign_a_unique_shopping_cart_id also discussed later. if ($cart_id eq "") { &delete_old_carts; &assign_a_unique_shopping_cart_id; } # Now that the script has created the entire environment # in which it must operate, it is time to provide # the logic for it to determine what it should do. # # 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 is not equal to nothing) # { # process that type of request; # exit; # } # # For example, consider the second case in which the # customer has clicked on the "Add to Cart" submit # button denoted with the NAME value of "add_to_cart_button". # # elsif ($form_data{'add_to_cart_button'} ne "") # { # &add_to_the_cart; # exit; # } # # Because the submit button will have some value # like "Add this item to my Cart", when the script reaches # this line, 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 some subroutine; # exit; # } # # where the HTML code looks like the following: # # # # Thus, if the button actually has an X-dimension value # (any 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/8th 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 application must execute. # # 1. Adding an Item to the Shopping Cart - One # request that the script may have to handle is that of # adding an item to a shopping cart. Once the client has # decided to purchase an item, she will have added a # quantity to the text box and hit the "add this item" # submit button. So we must be prepared to add items to # the client's cart. Additions are handled with the # add_to_the_cart subroutine discussed later. # # 2. Displaying the Client's Cart with Cart Manipulation # Options - On the other hand, the user may have already # been adding items, realized she went over budget and # decided to reduce the quantities of some of the items # she chose or even delete them altogether from her cart. # # The first thing we need to do is send her an HTML form # with which she can choose whether to delete or modify # as well as send her a table depicting the current # contents of the shopping cart. This is all done using # the display_cart_contents subroutine at the end of this # file. # # 3. Displaying the Change Quantity Form - Yet another # function that this script may be asked to perform is # modifying the quantities of some of the items in the # client's cart. If the client has asked to make a # quantity modification, the script must give her a form # so that she can specify the changes she wants made. # # The form is fairly simple. We will use the same basic # table presentation that we used in the # display_cart_contents subroutine, except that we will # add another column of text input fields used to submit # a new quantity for every row in the cart. These text # input fields however, will use as there NAME argument, # the unique cart row number for every row. Consider the # following cell definition: # # # # Thus, when the client submits a quantity change, they # will be submitting a cart_row_number (219) associated # with a quantity value (the value submitted in the text # field). # # We'll use the cart row number to figure out exactly # which item in the cart should be modified. # # 4. Changing the Quantity of Items in the Cart - Once the # client has typed in some quantity changes and submitted # the information back to this script, we must make the # modifications to the database. This is done with the # modify_quantity_of_items_in_cart subroutine discussed # below. # # 5. Displaying the Delete Item Form - Perhaps instead, # the client asked to delete an item rather than modify # the quantity. If this is the case, the script must # display a form very similar to the one for # modification. The only difference is that we will use # checkboxes for each item instead of text boxes because # in the case of delete, the user need only select which # items to delete rather than to also specify a quantity. # As in the case of modification, the script associates the # NAME argument of the checkboxes with the cart row number # of the item they represent. Thus, the syntax will # resemble the following: # # # # where 220 is the cart row number of some element which # can be deleted. We will handle the display of the # delete item form using the output_delete_item_form # subroutine discussed later. # # 6. Deleting Items From the Cart - Once the client # submits some items to delete, the script must also # be able to delete them from the cart. This is done with # the delete_from_cart subroutine discussed later. # # 7. Displaying the Order Form - Further, the script must # be able to display the order form for the client if that # is what they want to see. The script uses the # display_page subroutine which will be discussed later to # display a pre-designed order form. Note that the # handling of the order form will not be done by this # script. Instead, the order form will reference one # final script which may be located in a separate, # "secure" directory (if one exists). In the case of # secure ordering, we do not want a self-referential link # because we do not want the entire script being run from # the secure directory. This would be inefficient. The # only time we want to utilize the secure directory is if # we are processing the order. Thus, the ordering process # has been broken out into its own mobile script. # # 8. Submitting the Order - Once the user fills out the # order form she may submit the order for final # processing. Final processing involves calculating # shipping logic like (shipping method, tax rates, # discounts, etc), sending the order to the order # processing administrator and letting the customer know # that all was completed successfully. All orders are # processed by the process_order_form subroutine which is # designed to handle all of these chores. # # However, there is one catch to order processing: Secure # servers. Many stores have secure server functionality # in which specific directories are designed to handle # encrypted communication between server and browser. # (typically https setups). In this case, the cgi script # handling the order processing must be physically located # inside the secure directory. # # If this is your situation, then you must make a mirror # copy of the application directories and place them all # inside the secure area. Then, you will set # $sc_order_script_url in the setup file equal to the # secured mirror of the script. Then, the script will # dynamically reference the secured location instead of the # insecure location for order processing. In actuality, # only the order processing routine will be executed in # the secure directory, but we copy the whole script there # for simplicity's sake. # # If you are not running a secure server, you may just set # $sc_order_script_url equla to ctstore.cgi and # continue the regular self-referencing behavior. # # 9. Displaying Products, Categories and Misc. Pages - # If the script is getting in a value for page or for # product, it means that it is being asked to navigate # through the store. # # The page variable is used to locate a page which the # store should display to the user. In the case of an # HTML-based Web Store, the page value will be used to # point to both pages with products as well as pages with # "lists" of products. Also in the case of the HTML-based # store, there will be no need for the product variable # since the product variable is specific to the # database-based store which must be able to interpret # between a "list of products" type page and an actual # product page. This is because when the database-based # version creates a page to view, it needs to generate it # on the fly. Thus, it searches for the product in the # database. If it needs to display a "list of products" # type page with sub links to actual product pages within a # similar group, it should not go to the database. # Instead, it actually needs to display a list page just # as the HTML-based store would do. # # As we've said, the product value will be used by the # database-based shopping cart to cull out the list of # products which the customer is interested in seeing. # Think of this as a sort've hard-coded search of the # database where the admin may hard code a category to # search for in the URL string which requests a product # page view. # # Consider the following hyperlinks as examples. # # ctstore.cgi?page=Letters.html&cart_id=98.123 # ctstore.cgi?page=Numbers.html&cart_id=8708496.3559 # ctstore.cgi?product=Numbers&cart_id=2196655.5107 # # The first case could be used in either the HTML or # Database-based version. The script would display the # HTML page "Letters.html" which would be a "list of # products" type page. In our distribution example page, # Letters.html contains links to both Vowels.html # and Consonants.html # # The second URL would be used for an HTML-based store. # It would cause this script to display the pre-designed # product page, Numbers.html. # # Finally, the last line would be used for a # database-based cart system and would cause this script # to search through the database for all items with # "Numbers" in the category field (by default, this is the # second field in data.file) # # Thus, there are two ways that products can be displayed # with this script. # # The first way is for the store administrator to create a # delimited data file with all the data to be displayed # incorporated in database rows. The contents of these # rows will be displayed according to the format defined # in the $sc_product_page_row variable in # web_store_html_lib.pl. But this will be discussed in # greater detail later. # # The second way is for the admin to create HTML # pages directly with the same data already incorporated # into some desired interface. # # The admin specifies which method she will use by setting # the variable $sc_use_html_product_pages in the setup file. # If this variable is set to yes, it means that the script # should simply output a pre-designed HTML product page. # Anything else, and it will expect a database. # # The display_products_for_sale subroutine discussed # later does just that. However, there is # one catch to the presentation of an HTML page. If the # client is doing a keyword search, we'll have to generate # a list of pages on which their keyword was founds using # the html-search subroutine located in # web_store_html_search.pl # # 10. Display The Frontpage - Finally, if all else has # failed, it means that we are simply being asked to # display the store frontpage, for no other routines # remain. # # To display the store front page, we will access the # output_frontpage subroutine discussed later. # # Okay, so those are all the cases. Now let's go through # them one by one as code. # # First though, we need to set up a flag to see # if the user has any values inside any of the query # fields. # $are_any_query_fields_filled_in = "no"; foreach $query_field (@sc_db_query_criteria) { @criteria = split(/\|/, $query_field); if ($form_data{$criteria[0]} ne "") { $are_any_query_fields_filled_in = "yes"; } } if ($form_data{'add_to_cart_button'} ne "") { &add_to_the_cart; exit; } elsif ($form_data{'modify_cart_button'} ne "") { &display_cart_contents; exit; } elsif ($form_data{'change_quantity_button'} ne "") { &output_modify_quantity_form; exit; } elsif ($form_data{'submit_change_quantity_button'} ne "") { &modify_quantity_of_items_in_cart; exit; } elsif ($form_data{'delete_item_button'} ne "") { &output_delete_item_form; exit; } elsif ($form_data{'submit_deletion_button'} ne "") { &delete_from_cart; exit; } elsif ($form_data{'order_form_button'} ne "") { &require_supporting_libraries (__FILE__, __LINE__, "$sc_order_lib_path"); &display_order_form; exit; } elsif ($form_data{'submit_order_form_button'} ne "") { &require_supporting_libraries (__FILE__, __LINE__, "$sc_order_lib_path"); &process_order_form; exit; } elsif (($page ne "" || $form_data{'search_request_button'} ne "" || $form_data{'continue_shopping_button'} || $are_any_query_fields_filled_in =~ /yes/i) && ($form_data{'return_to_frontpage_button'} eq "")) { &display_products_for_sale; exit; } else { &output_frontpage; exit; } # Well that's it. That is the end of the program! Well, # not exactly. That is just the end of the main body of # logic. From here on out we will define the logic of the # subroutines called in the "if" tests above. ####################################################################### # 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 "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 ####################################################################### # Read and Parse Form Data. # ####################################################################### # read_and_parse_form_data is a short subroutine # responsible for calling the ReadParse subroutine in # cgi-lib.pl to parse the incoming form data. The script # also tells cgi-lib to prepare that information in the # associative array named %form_data which we will be able # to use for the rest of this script. # # read_and_parse_form_data takes no arguments and is # called with the following syntax: # # &read_and_parse_form_data; sub read_and_parse_form_data { &ReadParse(*form_data); } ####################################################################### # Error Check Form Data. # ####################################################################### # error_check_form_data is responsible for checking to # make sure that only authorized pages are viewable using # this application. It takes no arguments and is called # with the following syntax: # # &error_check_form_data; # # The routine simply checks to make sure that if # the page variable extension is not one that is defined # in the setup file as an appropriate extension like .html # or .htm, or there is no page being requested (i.e.: the # store front is being displayed) it will send a warning # to the user, append the error log, and exit. # # @acceptable_file_extensions_to_display is an array of # acceptable file extensions defined in the setup file. # To be more or less restrictive, just modify this list. # # Specifically, for each extension defined in the setup # file, if the value of the page variable coming in from # the form ($page) is like the extension (/$file_extension/) # or there is no value for page (eq ""), we will set # $valid_extension equal to yes. sub error_check_form_data { foreach $file_extension (@acceptable_file_extensions_to_display) { if ($page =~ /$file_extension/ || $page eq "") { $valid_extension = "yes"; } } # Next, the script checks to see if $valid_extension has # been set to "yes". # # If the value for page satisfied any of the extensions # in @acceptable_file_extensions_to_display, the script # will set $valid_extension equal to yes. If the value # is set to yes, the subroutine will go on with it's work. # Otherwise it will exit with a warning and write to the # error log if appropriate # # Notice that we pass three parameters to the # update_error_log subroutine which will be discussed # later. The subroutine gets a warning, the # name of the file, and the line number of the error. # # $sc_page_load_security_warning is a variable set in # web_store.setup. If you want to give a more or less # informative error message, you are welcome to change the # text there. if ($valid_extension ne "yes") { print "$sc_page_load_security_warning"; &update_error_log("PAGE LOAD WARNING", __FILE__, __LINE__); exit; } } ####################################################################### # Delete Old Carts. # ####################################################################### # delete_old_carts is a subroutine which is used to prune # the carts directory, cleaning out all the old carts # after some time interval defined in the setup file. It # takes no arguments and is called with the following # syntax: # # &delete_old_carts; sub delete_old_carts { # The subroutine begins by grabbing a listing of all of # the client created shopping carts in the User_carts # directory. # # It then opens the directory and reads the contents using # grep to grab every file with the extension .cart. Then # it closes the directory. # # If the script has any trouble opening the directory, # it will output an error message using the # file_open_error subroutine discussed later. To the # subroutine, it will pass the name of the file which had # trouble, as well as the current routine in the script # having trouble , the filename and the current line # number. opendir (USER_CARTS, "$sc_user_carts_directory_path") || &file_open_error("$sc_user_carts_directory_path", "Delete Old Carts", __FILE__, __LINE__); @carts = grep(/\.cart/,readdir(USER_CARTS)); closedir (USER_CARTS); # Now, for every cart in the directory, delete the cart if # it is older than half a day. The -M file test returns # the number of days since the file was last modified. # Since the result is in terms of days, if the value is # greater than the value of $sc_number_days_keep_old_carts # set in web_store.setup, we'll delete the file. foreach $cart (@carts) { if ($cart =~ /^(\w+\.cart)$/) { $cart = $1; if (-M "$sc_user_carts_directory_path/$cart" > $sc_number_days_keep_old_carts) { unlink("$sc_user_carts_directory_path/$cart"); } } } # end of foreach } # End of sub delete_old_carts ####################################################################### # Assign a Shopping Cart. # ####################################################################### # assign_a_unique_shopping_cart_id is a subroutine used to # assign a unique cart id to every new client. It takes # no arguments and is called with the following syntax: # # &assign_a_unique_shopping_cart_id; sub assign_a_unique_shopping_cart_id { # First we will check to see if the admin has asked us to # log all new clients. If so, we will get the current # date using the get_date subroutine discussed later, open the # access log file for appending, and print to the access # log file all of the environment variable values as well # as the current date and time. # # However, we will protect ourselves from multiple, # simultaneous writes to the access log by using the # lockfile routine documented at the end of this file, # passing it the name of a temporary lock file to use. # # Remember that there may be multiple simultaneous # executions of this script because there may be many # people shopping all at once. It would not do if one # customer was able to overwrite the information of # another customer if they accidentally wanted to access # the log file at the same exact time. if ($sc_shall_i_log_accesses eq "yes") { $date = &get_date; &get_file_lock("$sc_access_log_path.lockfile"); open (ACCESS_LOG, ">>$sc_access_log_path"); # Using the keys function, the script grabs all the # keys of the %ENV associative array and assigns them as # elements of @env_keys. It then creates a new row for # the access log which will be a pipe delimited list of # the date as well as all the environment variables and # their values. @env_keys = keys(%ENV); $new_access = "$date\|"; foreach $env_key (@env_keys) { $new_access .= "$ENV{$env_key}\|"; } # The script then takes off the final pipe, adds the new # access to the log file, closes the log file and removes # the lock file. chop $new_access; print ACCESS_LOG "$new_access\n"; close (ACCESS_LOG); &release_file_lock("$sc_access_log_path.lockfile"); } # Now that the new access is recorded, the script assigns # the user their own unique shopping cart. To do so, # it generates a random (rand) 8 digit (100000000) # integer (int) and then appends to that string the current # process id ($$). However, the srand function is seeded # with the time and the current process id in order to # produce a more random random number. $sc_cart_path is # also defined now that we have a unique cart id number. srand (time|$$); $cart_id = int(rand(10000000)); $cart_id .= "_$$"; $cart_id =~ s/-//g; $sc_cart_path = "$sc_user_carts_directory_path/${cart_id}.cart"; # However, before we can be absolutely sure that we have # created a unique cart, the script must check the existing # list of carts to make sure that there is not one with # the same value. # # It does this by checking to see if a cart with the # randomly generated ID number already exists in the Carts # directory. If one does exit (-e), the script grabs # another random number using the same routine as # above and checks again. # # Using the $cart_count variable, the script executes this # algorithm three times. If it does not succeed in finding # a unique cart id number, the script assumes that there is # something seriously wrong with the randomizing routine # and exits, warning the user on the web and the admin # using the update_error_log subroutine discussed later. $cart_count = 0; while (-e "$sc_cart_path") { if ($cart_count == 3) { print "$sc_randomizer_error_message"; &update_error_log("COULD NOT CREATE UNIQUE CART ID", __FILE__, __LINE__); exit; } $cart_id = int(rand(10000000)); $cart_id .= "_$$"; $cart_id =~ s/-//g; $sc_cart_path = "$sc_user_carts_directory_path/${cart_id}.cart"; $cart_count++; } # End of while (-e $sc_cart_path) # Now that we have generated a truly unique id # number for the new client's cart, the script may go # ahead and create it in the User_carts sub-directory. # # If there is a problem opening the new cart, we'll output # an error message with the file_open_error subroutine # discussed later. open (CART, ">$sc_cart_path") || &file_open_error("$sc_cart_path", "Assign a Shopping Cart", __FILE__, __LINE__); } ####################################################################### # Output Frontpage. # ####################################################################### # output_frontpage is used to display the frontpage of the # store. It takes no arguments and is accessed with the # following syntax: # # &output_frontpage; # # The subroutine simply utilizes the display_page # subroutine which is discussed later to output the # frontpage file, the location of which, is defined # in web_store.setup. display_page takes four arguments: # the cart path, the routine calling it, the current # filename and the current line number. sub output_frontpage { &display_page("$sc_store_front_path", "Output Frontpage", __FILE__, __LINE__); } ####################################################################### # Add to Shopping Cart # ####################################################################### # The add_to_the_cart subroutine is used to add items to # the customer's unique cart. It is called with no # arguments with the following syntax: # # &add_to_the_cart; sub add_to_the_cart { # the script first opens the user's shopping cart with read/write access, # creating it if for some reason it is not already there. If there is a # problem opening the file, it will call file_open_error subroutine # to handle the error reporting. open (CART, "+>>$sc_cart_path") || &file_open_error("$sc_cart_path", "Add to Shopping Cart", __FILE__, __LINE__); # The script then retrieves the highest item number of the items already # in the cart (if any). The item number is an arbitrary number used to # uniquely identify each item, as described below. $highest_item_number = 100; # init highest item number (start at 100) seek (CART, 0, 0); # make sure we're positioned at top of file while () # loop on cart contents, if any { chomp $_; # get rid of terminating newline my @row = split (/\|/, $_); # split cart row into fields my $item_number = pop (@row); # get item number of row (last field) $highest_item_number = $item_number if ($item_number > $highest_item_number); } # $highest_item_number is now either the highest item number, # or 100 if the cart was empty. Position the file pointer to the # end of the cart, in preparation for appending the new items later. seek (CART, 0, 2); # position to end of file # The script must first figure out what the client has # ordered. # # It begins by using the %form_data associative array # given to it by cgi-lib.pl. It takes all of the keys # of the form_data associative array and drops them into # the @items_ordered array. # # Note: An associative array key is like a variable name # whereas an associative array value is the # value associated with that variable name. The # benefit of an associative array is that you can have # many of these key/value pairs in one array. # Conveniently enough, you'll notice that input fields on # HTML forms will have associated NAMES and VALUES # corresponding to associative array KEYS and VALUES. # # Since each of the text boxes in which the client could # enter quantities were associated with the database id # number of the item that they accompany, (as defined # in the display_page routine at the end of this # script), the HTML should read # # # # for the item with database id number 1234 and # # # # for item 5678. # # If the client orders 2 of 1234 and 9 of 5678, then # @incoming_data will be a list of 1234 and 5678 such that # 1234 is associated with 2 in %form_data associative # array and 5678 is associated with 9. The script uses # the keys function to pull out just the keys. Thus, # @items_ordered would be a list like (1234, 5678, ...). @items_ordered = keys (%form_data); # Next it begins going through the list of items ordered # one by one. foreach $item (@items_ordered) { # However, there are some incoming items that don't need # to be processed. Specifically, we do not care about cart_id, # page, keywords, add_to_cart, or whatever incoming # administrative variables exist because these are all # values set internally by this script. They will be # coming in as form data just like the client-defined # data, and we will need them for other things, just not # to fill up the user's cart. In order to bypass all of # these administrative variables, we use a standard # method for denoting incoming items. All incoming items # are prefixed with the tag "item-". When the script sees # this tag, it knows that it is seeing an item to be added # to the cart. # # Similarly, items which are actually options info are # denoted with the "option" keyword. We will also accept # those for further processing. # # And fo course, we will not need to worry about any items # which have empty values. If the shopper did not enter a # quantity, then we won't add it to the cart. if (($item =~ /^item-/i || $item =~ /^option/i) && $form_data{$item} ne "") { # Once the script has determined that the current element # ($item) of @items_ordered is indeed a non-admin item, # it must separate out the items that have been ordered # from the options which modify those items. If $item # begins with the keyword "option", which we set # specifically in the HTML file, the script will add # (push) that item to the array called @options. However, # before we make the check, we must strip the "item-" # keyword off the item so that we have the actual row # number for comparison. $item =~ s/^item-//i; if ($item =~ /^option/i) { push (@options, $item); } # On the other hand, if it is not an option, the script adds # it to the array @items_ordered_with_options, but adds # both the item and its value as a single array element. # # The value will be a quantity and the item will be # something like "item-0001|12.98|The letter A" as defined in # the HTML file. Once we extract the initial "item-" # tag from the string using regular expressions ($item =~ # s/^item-//i;), the resulting string would be something # like the following: # # 2|0001|12.98|The letter A # # where 2 is the quantity. # # Firstly, it must be a digit ($form_data{$item} =~ /\D/). # That is, we do not want the clients trying to enter # values like "a", "-2", ".5" or "1/2". They might be # able to play havoc on the ordering system and a sneaky # client may even gain a discount because you were not # reading the order forms carefully. # # Secondly, the script will dissallow any zeros # ($form_data{$item} == 0). In both cases the client will # be sent to the subroutine bad_order_note located in # web_store_html_lib.pl. else { if (($form_data{"item-$item"} =~ /\D/) || ($form_data{"item-$item"} == 0)) { &bad_order_note; } else { $quantity = $form_data{"item-$item"}; push (@items_ordered_with_options, "$quantity\|$item\|"); } } } # End of if ($item ne "$variable" && $form_data{$item} ne "") } #End of foreach $item (@items_ordered) # Now the script goes through the array # @items_ordered_with_options one item at a time in order # to modify any item which has had options applied to it. # Recall that we just built the @options array with all # the options for all the items ordered. Now the script # will need to figure out which options in @options belong # to which items in @items_ordered_with_options. foreach $item_ordered_with_options (@items_ordered_with_options) { # First, clear out a few variables that we are going to # use for each item. # # $options will be used to keep track of all of the # options selected for any given item. # # $option_subtotal will be used to determine the total # cost of each option. # # $option_grand_total will be used to calculate the # total cost of all ordered options. # # $item_grand_total will be used to calculate the total # cost of the item ordered factoring in quantity and # options. $options = ""; $option_subtotal = ""; $option_grand_total = ""; $item_grand_total = ""; # Now split out the $item_ordered_with_options into it's # fields. Note that we have defined the index location of # some important fields in web_store.setup. Specifically, # the script must know the index of quantity, item_id and # item_price within the array. It will need these values # in particular for further calculations. Also, the # script will change all occurrences of "~qq~" to a double # quote (") character, "~gt~" to a greater than sign (>) # and "~lt~" to a less than sign (<). The reason that # this must be done is so that any double quote, greater # than, or less than characters used in URLK strings can # be stuffed safely into the cart and passed as part of # the NAME argument in the "add item" form. Consider the # following item name which must include an image tag. # # /g; $item_ordered_with_options =~ s/~lt~/\Red
# # This is the second option modifying item number 0001. # When displayed in the display cart screen, it will read # "Red 0.00, and will not affect the cost of the item. ($option_name, $option_price) = split (/\|/,$form_data{$option}); $options .= "$option_name $option_price,"; # But the script must also calculate the cost changes with # options. To do so, it will take the current value of # $option_grand_total and add to it the value of the # current option. It will then format the result to # two decimal places using the format_price subroutine # discussed later and assign the new result to # $option_grand_total $unformatted_option_grand_total = $option_grand_total + $option_price; $option_grand_total = &format_price($unformatted_option_grand_total); } # End of if ($option_item_number eq "$item_id_number") } # End of foreach $option (@options) # Next, the script takes off the last comma in options. # Look a few lines up, you'll see that a comma is added to # the end of each option. Well the last option does not # need that last comma. chop $options; # Now, the script adds a space after each comma so the # display looks nicer. $options =~ s/,/, /g; # Next, calculate $item_number which the script can use to # identify a shopping cart item absolutely. This must be done so # that when we modify and delete from the cart, we will # know exactly which item to affect. We cannot rely simply # on the unique database id number because a client may # purchase two of the same item but with different # options. Unless there is a separate, unique cart row id # number, how would the script know which to delete if the # client asked to delete one of the two. Add 1 to # $highest_item_number, which was set at the beginning of the subroutine. $item_number = ++$highest_item_number; # Finally, the script makes the last price calculations # and appends every ordered item to $cart_row # # A completed cart row might look like the following: # 2|0001|Vowels|15.98|Letter A|Times New Roman 0.00|15.98|161 $unformatted_item_grand_total = $item_price + $option_grand_total; $item_grand_total = &format_price("$unformatted_item_grand_total"); foreach $field (@cart_row) { $cart_row .= "$field\|"; } $cart_row .= "$options\|$item_grand_total\|$item_number\n"; } # End of foreach $item_ordered_with_options..... # When it is done appending all the items to $cart_row, # the script appends the new items to the end of the # shopping cart, which was opened at the beginning of the subroutine. print CART "$cart_row"; close (CART); # Then, the script sends the client back to a previous # page. There are two pages that the customer can be sent # of course, the last product page they were on or the # page which displays the customer's cart. Which page the # customer is sent depends on the value of # $sc_should_i_display_cart_after_purchase which is defined # in web_store.setup. If the customer should be sent to # the display cart page, the script calls # display_cart_contents, otherwise it calls display_page # if this is an HTML-based cart or # create_html_page_from_db if this is a database-based # cart. if ($sc_use_html_product_pages eq "yes") { if ($sc_should_i_display_cart_after_purchase eq "yes") { &display_cart_contents; } else { &display_page("$sc_html_product_directory_path/$page", "Display Products for Sale"); } } else { if ($sc_should_i_display_cart_after_purchase eq "yes") { &display_cart_contents; } elsif ($are_any_query_fields_filled_in =~ /yes/i) { $page = ""; &display_products_for_sale; } else { &create_html_page_from_db; } } } ####################################################################### # Output Modify Quantity Form # ####################################################################### # output_modify_quantity_form is the subroutine # responsible for displaying the form which customers can # use to modify the quantity of items in their cart. It # is called with no arguments with the following syntax: # # &output_modify_quantity_form; sub output_modify_quantity_form { # The subroutine begins by outputting the HTML header # using standard_page_header, adds the modify form using # display_cart_table and finishes off the HTML page with # modify_form_footer. All of these subroutines are # discussed in web_store_html_lib.pl &standard_page_header("Change Quantity"); &display_cart_table("changequantity"); &modify_form_footer; } ####################################################################### # Modify Quantity of Items in the Cart # ####################################################################### # The modify_quantity_of_items_in_cart subroutine is # responsible for making quantity modifications in the # customer's cart. It takes no arguments and as called # with the following syntax: # # &modify_quantity_of_items_in_cart; sub modify_quantity_of_items_in_cart { # First, the script gathers the keys as it did for the # add_to_cart routine previously, checking to make # sure the customer entered a positive integer (not # fractional and not less than one). @incoming_data = keys (%form_data); foreach $key (@incoming_data) { if ((($key =~ /[\d]/) && ($form_data{$key} =~ /\D/)) || $form_data{$key} eq "0") { &update_error_log("BAD QUANTITY CHANGE", __FILE__, __LINE__); &bad_order_note("change_quantity_button"); } # Just as the script did in the add to cart routine # previously, it will create an array (@modify_items) of # valid keys. unless ($key =~ /[\D]/ && $form_data{$key} =~ /[\D]/) { if ($form_data{$key} ne "") { push (@modify_items, $key); } } } # End of foreach $key (@incoming_data) # Then, the script must open up the client's cart and go # through it line by line. File open problems are # handled by file_open_error as usual. open (CART, "<$sc_cart_path") || &file_open_error("$sc_cart_path", "Modify Quantity of Items in the Cart", __FILE__, __LINE__); # As the script goes through the cart, it will split each # row into its database fields placing them as elements in # @database_row. It will then grab the unique cart row # number and subsequently replace it in the array. # # The script needs this number to check the current line # against the list of items to be modified. Recall that # this list will be made up of all the cart items which # are being modified. # # The script also grabs the current quantity of that row. # Since it is not yet sure if it wants the current # quantity, it will hold off on adding it back to the # array. Finally, the script chops the newline character # off the cart row number. while () { @database_row = split (/\|/, $_); $cart_row_number = pop (@database_row); push (@database_row, $cart_row_number); $old_quantity = shift (@database_row); chop $cart_row_number; # Next, the script checks to see if the item number # submitted as form data is equal to the number of the # current database row. foreach $item (@modify_items) { if ($item eq $cart_row_number) { # If so, it means that the script must change the quantity # of this item. It will append this row to the # $shopper_row variable and begin creating the modified # row. That is, it will replace the old quantity with the # quantity submitted by the client ($form_data{$item}). # Recall that $old_quantity has already been shifted off # the array. $shopper_row .= "$form_data{$item}\|"; # Now the script adds the rest of the database row to # $shopper_row and sets two flag variables. # # $quantity_modified lets us know that the current row # has had a quantity modification for each iteration of # the while loop. foreach $field (@database_row) { $shopper_row .= "$field\|"; } $quantity_modified = "yes"; chop $shopper_row; # Get rid of last pipe symbol but not the # newline character } # End of if ($item eq $cart_row_number) } # End of foreach $item (@modify_items) # If the script gets this far and $quantity_modified has # not been set to "yes", it knows that the above routine # was skipped because the item number submitted from the # form was not equal to the current database id number. # # Thus, it knows that the current row is not having its # quantity changed and can be added to $shopper_row as is. # Remember, we want to add the old rows as well as the new # modified ones. if ($quantity_modified ne "yes") { $shopper_row .= $_; } # Now the script clears out the quantity_modified variable # so that next time around it will have a fresh test. $quantity_modified = ""; } # End of while () close (CART); # At this point, the script has gone all the way through # the cart. It has added all of the items without # quantity modifications as they were, and has added all # the items with quantity modifications but made the # modifications. # # The entire cart is contained in the $shopper_row # variable. # # The actual cart still has the old values, however. So # to change the cart completely the script must overwrite # the old cart with the new information and send the # client back to the view cart screen with the # display_cart_contents subroutine which will be discussed # later. Notice the use of the write operator (>) instead # of the append operator (>>). open (CART, ">$sc_cart_path") || &file_open_error("$sc_cart_path", "Modify Quantity of Items in the Cart", __FILE__, __LINE__); print CART "$shopper_row"; close (CART); &display_cart_contents; } # End of if ($form_data{'submit_change_quantity'} ne "") ####################################################################### # Output Delete Item Form # ####################################################################### # The output_delete_item_form subroutine is responsible # for displaying the HTML form which the customer can use # to delete items from their cart. It takes no arguments # and is called with the following syntax: # # &output_delete_item_form; sub output_delete_item_form { # As it did when it printed the modification form, the # script uses several subroutines in web_store_html_lib.pl # to generate the header, body and footer of the delete # form. &standard_page_header("Delete Item"); &display_cart_table("delete"); &delete_form_footer; } # End of if ($form_data{'delete_item'} ne "") ####################################################################### # Delete Item From Cart # ####################################################################### # The job of delete_from_cart is to take a set of items # submitted by the user for deletion and actually delete # them from the customer's cart. The subroutine takes no # arguments and is called with the following syntax: # # &delete_from_cart; sub delete_from_cart { # As with the modification routines, the script first # checks for valid entries. This time though it only needs # to make sure that it filters out the extra form # keys rather than make sure that it has a positive # integer value as well because unlike with a text entry, # clients have less ability to enter bad values with # checkbox submit fields. @incoming_data = keys (%form_data); foreach $key (@incoming_data) { # We still want to make sure that the key is a cart row # number though and that it has a value associated with # it. If it is actually an item which the user has asked to # delete, the script will add it to the delete_items # array. unless ($key =~ /[\D]/) { if ($form_data{$key} ne "") { push (@delete_items, $key); } } # End of unless ($key =~ /[\D]/... } # End of foreach $key (@incoming_data) # Once the script has gone through all the incoming form # data and collected the list of all items to be deleted, # it opens up the cart and gets the $cart_row_number, # $db_id_number, and $old_quantity as it did in the # modification routines previously. open (CART, "<$sc_cart_path") || &file_open_error("$sc_cart_path", "Delete Item From Cart", __FILE__, __LINE__); while () { @database_row = split (/\|/, $_); $cart_row_number = pop (@database_row); $db_id_number = pop (@database_row); push (@database_row, $db_id_number); push (@database_row, $cart_row_number); chop $cart_row_number; $old_quantity = shift (@database_row); # Unlike modification however, for deletion all we need to # do is check to see if the current database row matches # any submitted item for deletion. If it does not match # the script adds it to $shopper_row. If it is equal, # it does not. Thus, all the rows will be added to # $shopper_row except for the ones that should be deleted. $delete_item = ""; foreach $item (@delete_items) { if ($item eq $cart_row_number) { $delete_item = "yes"; } } # End of foreach $item (@add_items) if ($delete_item ne "yes") { $shopper_row .= $_; } } # End of while () close (CART); # Then, as it did for modification, the script overwrites # the old cart with the new information and # sends the client back to the view cart page with the # display_cart_contents subroutine which will be discussed # later. open (CART, ">$sc_cart_path") || &file_open_error("$sc_cart_path", "Delete Item From Cart", __FILE__, __LINE__); print CART "$shopper_row"; close (CART); &display_cart_contents; } # End of if ($form_data{'submit_deletion'} ne "") ####################################################################### # Display Products for Sale # ####################################################################### # display_products_for_sale is used to generate # dynamically the "product pages" that the client will # want to browse through. There are two cases within it # however. # # Firstly, if the store is an HTML-based store, this # routine will either display the requested page # or, in the case of a search, perform a search on all the # pages in the store for the submitted keyword. # # Secondly, if this is a database-based store, the script # will use the create_html_page_from_db to output the # product page requested or to perform the search on the # database. # # The subroutine takes no arguments and is called with the # following syntax: # # &display_products_for_sale; sub display_products_for_sale { # The script first determines which type of store this is. # If it turns out to be an HTML-based store, the script # will check to see if the current request is a keyword # search or simply a request to display a page. If it is # a keyword search, the script will require the html # search library and use the html_search subroutine with # in it to perform the search. if ($sc_use_html_product_pages eq "yes") { if ($form_data{'search_request_button'} ne "") { &standard_page_header("Search Results"); require "$sc_html_search_routines_library_path"; &html_search; &html_search_page_footer; exit; } # If the store is HTML-based and there is no current # keyword however, the script simply displays the page as # requested with display_page which will be discussed # shortly. &display_page("$sc_html_product_directory_path/$page", "Display Products for Sale", __FILE__, __LINE__); } # On the other hand, if $sc_use_html_product_pages was set to # no, it means that the admin wants the script to generate # HTML product pages on the fly using the format string # and the raw database rows. The script will do so # using the create_html_page_from_db subroutine which will # be discussed next. else { &create_html_page_from_db; } } ####################################################################### # create_html_page_from_db Subroutine # ####################################################################### # create_html_page_from_db is used to generate the # navigational interface for database-base stores. It is # used to create both product pages and "list of products" # pages. The subroutine takes no arguments and is called # with the following syntax: # # &create_html_page_from_db; sub create_html_page_from_db { # First, the script defines a few working variables which # will remain local to this subroutine. local (@database_rows, @database_fields, @item_ids, @display_fields); local ($total_row_count, $id_index, $display_index); local ($row, $field, $empty, $option_tag, $option_location, $output); # Next the script checks to see if there is actually a # page which must be displayed. If there is a value for # the page variable incoming as form data, (i.e.: list of # product page) the script will simply display that page # with the display_page subroutine and exit. if ($page ne "" && $form_data{'search_request_button'} eq "" && $form_data{'continue_shopping_button'} eq "") { &display_page("$sc_html_product_directory_path/$form_data{'page'}", "Display Products for Sale", __FILE__, __LINE__); exit; } # If there is no page value, then the script knows that it # must generate a dynamic product page using the value of # the product form variable to query the database. # # First, the script uses the product_page_header # subroutine in order to dynamically generate the product # page header. We'll pass to the subroutine the value of # the page we have been asked to display so that it can # display something useful in the area. # # The product_page_header subroutine is located in # web_store_html_lib.pl and $sc_product_display_title is # defined in the setup file. &product_page_header($sc_product_display_title); if ($form_data{'add_to_cart_button'} ne "" && $sc_shall_i_let_client_know_item_added eq "yes") { print "$sc_item_ordered_message"; } # Next the database is queried for rows containing the # value of the incoming product variable in the correct # category as defined in web_store.setup. The script uses # the submit_query subroutine in web_store_db_lib.pl # passing to it a reference to the list array # database_rows. # # submit_query returns a descriptive status message # if there was a problem and a total row count # for diagnosing if the maximum rows returned # variable was exceeded. if (!($sc_db_lib_was_loaded =~ /yes/i)) { &require_supporting_libraries (__FILE__, __LINE__, "$sc_db_lib_path"); } ($status,$total_row_count) = &submit_query(*database_rows); # Now that the script has the database rows to be # displayed, it will display them. # # Firstly, the script goes through each database row # contained in @database_rows splitting it into it's # fields. # # For the most part, in order to display the database # rows, the script will simply need to take each field # from the database row and substitute it for a %s in the # format string defined in web_store.setup. # # However, in the case of options which will modify a # product, the script must grab the code from an options # file. # # The special way that options are denoted in the database # are by using the format %%OPTION%%option.html in the # data file. This string includes two important bits of # information. # # Firstly, it begins with %%OPTION%%. This is a flag # which will let the script know that it needs to deal # with this database field as if it were an option. When # it sees the flag, it will then look to the bit after the # flag to see which file it should load. Thus, in this # example, the script would load the file option.html for # display. # # Why go through all the trouble? Well basically, we need # to create a system which will handle large chunks of # HTML code within the database that are very likely to be # similar. If there are options on product pages, it is # likely that they are going to be repeated fairly # often. For example, every item in a database might have # an option like tape, cd or lp. By creating one # options.html file, we could easily put all the code into # one shared location and not need to worry about typing # it in for every single database entry. foreach $row (@database_rows) { @database_fields = split (/\|/, $row); foreach $field (@database_fields) { # For every field in every database row, the script simply # checks to see if it begins (^) with %%OPTION%%. If so, # it splits out the string into three strings, one # empty, one equal to OPTION and one equal to the location # of the option to be used. Then the script resets the # field to null because it is about to overwrite it. if ($field =~ /^%%OPTION%%/) { ($empty, $option_tag, $option_location) = split (/%%/, $field); $field = ""; # The option file is then opened and read. Next, every # line of the option file is appended to the $field # variable and the file is closed again. However, the # current product id number is substituted for the # %%PRODUCT_ID%% flag open (OPTION_FILE, "<$sc_options_directory_path/$option_location") || &file_open_error ("$sc_options_directory_path/$option_location", "Display Products for Sale", __FILE__, __LINE__); while () { s/%%PRODUCT_ID%%/$database_fields[$sc_db_index_of_product_id]/g; $field .= $_; } close (OPTION_FILE); } # End of if ($field =~ /^%%OPTION%%/) } # End of foreach $field (@database_fields) # Finally, the database fields (including the option field # which has been recreated) are stuffed into the format # string, $sc_product_display_row and the entire formatted # string is printed to the browser along with the footer. # # First, however, we must format the fields correctly. # Initially, @display_fields is created which contains the # values of every field to be displayed, including a # formatted price field. @display_fields = (); @temp_fields = @database_fields; foreach $display_index (@sc_db_index_for_display) { if ($display_index == $sc_db_index_of_price) { $temp_fields[$sc_db_index_of_price] = &display_price($temp_fields[$sc_db_index_of_price]); } push(@display_fields, $temp_fields[$display_index]); } # Then, the elements of the NAME field are created so that # customers will be able to specify an item to purchase. # We are careful to substitute double quote marks ("), and # greater and less than signs (>,<) for the tags ~qq~, # ~gt~, and ~lt~. The reason that this must be done is so # that any double quote, greater than, or less than # characters used in URL strings can be stuffed safely # into the cart and passed as part of the NAME argument in # the "add item" form. Consider the following item name # which must include an image tag. # # /~gt~/g; $database_fields[$id_index] =~ s/\ # # # When the script reads in these lines, it will see the # tags "%%cart_id%%" and"%%page%%" and substitute them for # the actual page and cart_id values which came in as form # data. # # Similarly it might see the following URL reference: # # # # In this case, it will see the cartid= tag and # substitute in the correct and complete # "cartid=some_number". while () { s/cart_id=/cart_id=$cart_id/g; s/%%cart_id%%/$cart_id/g; s/%%page%%/$form_data{'page'}/g; s/partner=/partner=$partner/g; s/%%partner%%/$partner/g; # Next, it checks to see if the add_to_cart_button button # has been clicked. if so, it means that we have just # added an item and are returning to the display of the # product page. In this case, we will sneak in an addition # confirmation message right after the
tag line. if ($form_data{'add_to_cart_button'} ne "" && $sc_shall_i_let_client_know_item_added eq "yes") { if ($_ =~ />$sc_error_log_path") || &CgiDie ("The Error Log could not be opened"); # Now, the script adds to the log entry row, the values # associated with all of the HTTP environment variables # and prints the whole row to the log file which it then # closes and opens for use by other instances of this # script by removing the lock file. foreach $variable (@env_vars) { $log_entry .= "$ENV{$variable}\|"; } print ERROR_LOG "$log_entry\n"; close (ERROR_LOG); &release_file_lock("$sc_error_log_path.lockfile"); } # End of if ($sc_shall_i_log_errors eq "yes") # Next, the script checks to see if the admin has # instructed it to also send an email error notification # to the admin by setting the $sc_shall_i_email_if_error # in web_store.setup. # # If so, it prepares an email with the same info contained # in the log file row and mails it to the admin using the # send_mail routine in mail-lib.pl. Note that a common # source of email errors lies in the admin not setting the # correct path for sendmail in mail-lib.pl on line 42. # Make sure that you set this variable there if you are # not receiving your mail and you are using the sendmail # version of the mail-lib package. if ($sc_shall_i_email_if_error eq "yes") { $email_body = "$type_of_error\n\n"; $email_body .= "FILE = $file_name\n"; $email_body .= "LINE = $line_number\n"; $email_body .= "DATE=$date\|"; foreach $variable (@env_vars) { $email_body .= "$variable = $ENV{$variable}\n"; } &send_mail("$sc_admin_email", "$sc_admin_email", "Web Store Error", "$email_body"); } # End of if ($sc_shall_i_email_if_error eq "yes") } ################################################################# # 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 century # is really 1900-1999, we'll need to subtract 1 from this # value in order to format the year correctly. ## Y2K FIX ## if ($year <= 99) { $sc_current_century = "20"; } if ($year >= 100) # localtime counts up so does not roll back to 00, weird aye? { $sc_current_century = "21"; $year = ($year - 100); if ($year < 10) { $year = "0$year"; } } ## END Y2K FIX ## if ($hour < 10) { $hour = "0$hour"; } if ($min < 10) { $min = "0$min"; } if ($sec < 10) { $sec = "0$sec"; } $year = ($sc_current_century-1) . "$year"; $date = "$days[$wday], $months[$mon] $mday, $year at $hour\:$min\:$sec"; return $date; } ################################################################# # display_price Subroutine # ################################################################# # display_price is used to format the price string so that # the store can take into account differing methods for # displaying prices. For example, some countries use # "$xxx.yyy". Others may use "xx.yy UNIT". This # subroutine will use the $sc_money_symbol_placement and # the $sc_money_symbol variables defined in # web_store.setup to format the entire price string for # display. The subroutine takes one argument, the price # to be formatted, and is called with the following # syntax: # # $price = &display_price(xx.yy); # # Where xx.yy is some number like 23.99. # # Note that the main routine calling this subroutine must # prepare a variable for the returned formatted price to # be assigned to. sub display_price { local ($price) = @_; local ($format_price); if ($sc_money_symbol_placement eq "front") { $format_price = "$sc_money_symbol $price"; } else { $format_price = "$price $sc_money_symbol"; } return $format_price; } ####################################################################### # 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 permissions to just # overwrite the file because some other error must have # occurred. # # If, on the other hand, the lock file has disappeared, # the script asks get_file_lock to create a new lock file # and then goes ahead and edits the file. # # The subroutine takes one argument, 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") || &CgiDie ("I could not open the lock file"); # 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 you are 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); } ####################################################################### # format_price # ####################################################################### # format_price is used to format prices to two decimal # places. It takes one argument, the price to be formatted # and is called with the following syntax: # # $price =&format_price(xxx.yyyyy); # # Notice that the main calling routine must assign the # returned formatted price to some variable for its own # use. # # Also notice that this routine takes a value even if it # is longer than two decimal places and formats it with # rounding. Thus, you can utilize price calculations such # as 12.99 * 7.985 (where 7.985 might be some tax value. sub format_price { # The incoming price is set to a local variables and a few # working local variables are defined. local ($unformatted_price) = @_; local ($formatted_price); # The script then uses the rounding method in EXCEL. If # the 3rd decimal place is > 4, then we round the 2nd # decimal place up 1. Otherwise, we leave the number # alone. Notice that we will use the substr function to # pull off the last value in the three decimal place # number and compare it using the EXCEL logic. # # Basically, the routine uses the rounding rules of # sprintf. # The unformatted_price is rounded to # to two decimal places and returned to the calling # routine. $formatted_price = sprintf ("%.2f", $unformatted_price); return $formatted_price; } ############################################################ # # subroutine: format_text_field # Usage: # $formatted_value = # &format_text_field($value, [$width]); # # Parameters: # $value = text value to format. # $width = optional field width. Defaults to 25. # # This routine takes the value and appends enough # spaces so that the field width is 25 spaces. # in order to justify the fields that are stored # eventually in the $text_of_cart. # # Output: # The formatted value # ############################################################ sub format_text_field { local($value, $width) = @_; $width = 25 if (!$width); # # Very simple. We return the value in # $value plus a string of 25 spaces which # has been truncated by the length of # the $value string. # # This results in a left justified # field of width = 25. # return ($value . (" " x ($width - length($value)))); } # End of format_text_field