#!/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/\~lt~/g;
push(@item_ids, $database_fields[$id_index]);
}
# Finally, $sc_product_display_row is created with the two
# arrays using printf to apply the formatting.
#
printf ($sc_product_display_row,
join("\|",@item_ids),
@display_fields);
} # End of foreach $row (@database_rows)
&product_page_footer($status,$total_row_count);
exit;
}
#######################################################################
# display_cart_contents Subroutine #
#######################################################################
# display_cart_contents is used to display the current
# contents of the customer's cart. It takes no arguments
# and is called with the following syntax:
#
# &display_cart_contents;
sub display_cart_contents
{
# The subroutine begins by defining some working variables
# as local to the subroutine.
local (@cart_fields);
local ($field, $cart_id_number, $quantity, $display_number,
$unformatted_subtotal, $subtotal, $unformatted_grand_total,
$grand_total);
# Next, as when we created the modification and deletion
# forms for cart manipulation, we will use the routines in
# web_store_html_lib.pl to generate the header, body and
# footer of the cart page. However, unlike with the
# modification and deletion forms, we will not need an
# extra table cell for the checkbox or text field. Thus,
# we will not pass anything to display_cart_table. We
# will simply get a table representing the current
# contents of the customer's cart.
&standard_page_header("View/Modify Cart");
&display_cart_table("");
&cart_footer;
exit;
} # End of sub display_cart_contents
#######################################################################
# 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) = @_;
&update_error_log("FILE OPEN ERROR-$bad_file", $this_file, $line_number);
&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. $!")
}
#######################################################################
# display_page Subroutine #
#######################################################################
# display_page is used to filter HTML pages through the
# script and display them to the browser window.
#
# display_page takes four arguments: the file or
# directory which failed, the section in the code in which
# the erroneous 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__);
#
# (notice the two special Perl variables __FILE__, which
# equals the current filename, and __LINE__ which equals
# the current line number).
sub display_page
{
local ($page, $routine, $file, $line) = @_;
# the subroutine begins by opening the requested file for
# reading, exiting with file_open_error if there is a
# problem as usual.
open (PAGE, "<$page") ||
&file_open_error("$page", "$routine", $file, $line);
# It then reads in the file one line at a time. However,
# on every line it looks for special tag sequences which
# it knows it must modify in order to maintain the state
# information necessary for the workings of this script.
# Specifically, every form must include a page and a
# cart_id value and every URL hyperlink must have a
# cart_id value added to it.
#
# Raw administratively pre-designed HTML pages must
# include the following tag lines if they are to filter
# properly and pass along this necessary state information.
#
# All forms must include two hidden field lines with the
# "tags" to be substituted for imbedded as follows:
#
#
#
#
# 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