#!/usr/local/bin/perl4 $MAILPROG = '/usr/sbin/sendmail'; $TMPDIR = '/tmp'; $ADDRESS = 'mitch@order.hardware.com'; ############################################################################### # NOTE: You may have to adjust the first line to point to the location of # your Perl interpreter. Also, please modify the above "MAILPROG" # variable if your 'sendmail' program is not '/usr/sbin/sendmail'. # You may also need to modify "TMPDIR" if the default temporary # directory for storing shopping cart data is not available. # Also, you may define a default E-mail address in the "ADDRESS" # variable to avoid the use of the CGI param's "Address" and # "Address2". ############################################################################### # # Notice # ------ # This software is proprietary and may not be modified, copied, # or reproduced in any manner without the expressed witten permission # of Clickable Software. This software is sold under a site # license that permits it's use on a single domain/web site. # The complete source code license agreement that accompanies this # software is made a part of this software as if written herein. # # # File: 'multicart' # # Auth: Clickable Software, Copyright 1995, 1996 All Rights Reserved # Website-http://www.catch22.com/clickables # # # Date: 1996-Feb-06 - (JRM) Written # # 1996-Mar-16 - (JRM) Added logic to get rid of parameter placeholders # for which the corresponding value is null. Also made # several enhancements such as preserving the entry # order of order lines and automatic expiration of # abandoned shopping carts. Also fixed the disappearing # order lines problem. Fixed other minor bugs that # had not been noticed such as intermittent loss of # "Info" field in shopping cart when blank, etc. # # 1996-Mar-22 - (JRM) Made several enhancements such as allowing for # required fields, allowing for volume discounts, # specifying "From" E-mail address, specifying default # E-mail address in the "$ADDRESS" Perl variable, etc. # # 1996-Mar-24 - (JRM) Added support for custom shipping costs. # Remove the leading "R_" from required field names # whenever those field names are displayed. Allow # for the "Address" and "Address2" CGI param's to be # required (i.e.- "R_Address" and "R_Address2"). Make # the "Discount_Amount" and "Sub_Total_After_Discount" # show up in the default saleslip template only if # the "Minbuy" and "Discount" param's are present. Etc. # # # Desc: Multi-page shopping cart. # # This script works with both the "GET" and "POST" CGI # methods and is compatible with both Perl 4 and Perl 5. # ############################################################################### &ReadParse || &my_die("No CGI parameters passed to '$0'."); # These are reserved parameter names used by multicart @reserved_param_names = ('Prevpage', 'Template', 'Address', 'Subject', 'Confpage', 'ReviewPage', 'CashRegister', 'Add2cart', 'Review', 'Register', 'ReviewChoice', 'Choice', 'Minbuy', 'Discount', 'From_Address'); @product_info_param_names = ('Prodno', 'Qty', 'Price', 'Item', 'Describe', 'Info', 'Amount'); @summary_info_param_names = ('Sub_Total', 'Taxes', 'Tax', 'Shipping', 'Fshipping', 'Pshipping', 'Cshipping', 'Total', 'Sub_Total_After_Discount', 'Discount_Amount'); ########################################################################### # Set the 'cart_filename' variable equal to the concatenation of the # REMOTE_ADDR and the REMOTE_USER. (In other words, absolute uniqueness # is guaranteed only if unique user authentication is required.) ########################################################################### $cart_filename = $TMPDIR . '/cart' . $ENV{'REMOTE_ADDR'} . $ENV{'REMOTE_USER'}; ########################################################################### # Check to see if the access count for multicart is evenly divisible by # 20. If so, then cleanup any "abandoned" shopping carts - i.e.- carts # that have not been accessed in the last hour. # Regardless, increment the counter by one. ########################################################################### open(COUNT_FILE, "${TMPDIR}/multicart.cnt"); $access_count = ; close(COUNT_FILE); ++$access_count; open(COUNT_FILE, ">${TMPDIR}/multicart.cnt"); print COUNT_FILE $access_count; close(COUNT_FILE); if ($access_count % 20 == 0) { opendir(TMPDIR, $TMPDIR); foreach $cartfile (grep(/^cart.*/, readdir(TMPDIR))) { # Check to see if the cart file has been accessed in the last hour if (time - (stat("${TMPDIR}/${cartfile}"))[8] > 3600) { unlink("${TMPDIR}/${cartfile}"); } } } ############################################################################### # Verify that we've got all our required fields (i.e.- those that start with # "R_"). ############################################################################### &verify_required_fields(*in); ########################################################################### # Get the order (including newly submitted lines if necessary) ########################################################################### &get_order; ########################################################################### # If the user chose to go to the review page or the cash register page or # the confirmation page (really just a sales summary page), then print # out that page with param substitutions. # Otherwise, redirect them to the 'Prevpage' (i.e.- main page). ########################################################################### $filename = ''; if ($in{'Review'}) { $filename = $in{'ReviewPage'}; } elsif ($in{'Register'} || $in{'ReviewChoice'} eq 'checkout') { $filename = $in{'CashRegister'}; } elsif ($in{'Choice'} eq 'final_confirm') { $filename = $in{'Confpage'}; } if ($filename) { open(PAGE, $filename) || &my_die("$! - Could not open file '${filename}'."); $page = join('', ); $page = &format_shopping_cart_data($page, *in); print "Content-type: text/html\n\n${page}"; } else { # Redirect browser if URL was specified. Otherwise print a warning. if ($url = $in{'Prevpage'}) { print "Status: 302 Found\r\nLocation: ${url}\r\nURI: <${url}>\r\nContent-type: text/html\r\n\r\n"; } else { &my_die("CGI parameter 'Prevpage' not passed to '$0'. Exiting."); } } ########################################################################### # See if 'Address' or 'Address2' is present. # Note that the presence of the 'Address' parameter implies that the # caller would like us to Email data somewhere. # Also consider if default address is present in $ADDRESS. ########################################################################### if ($in{'Address'} || $in{'Address2'} || $in{'R_Address'} || $in{'R_Address2'} || $ADDRESS) { # If template file was specified, then try to open it and read in the # template and then format the E-mail message body according to the # template. Otherwise, if template file was not specified, then use # a generic format. if ($in{'Template'}) { open(TEMPLATEFILE, $in{'Template'}) || &my_die("$! - Could not open specified template file '$in{'Template'}'."); $template = join('', ); close(TEMPLATEFILE); $body = &format_shopping_cart_data($template, *in); } else { # Define a default template and use it to format # the shopping cart data. # In the header include all "general information" parameters $template = ''; foreach $param_pair (@in) { ($param_name, $param_val) = split(/=/,$param_pair,2); $param_name =~ s/%(..)/pack("c",hex($1))/ge; $param_val = $in{$param_name}; if (! grep($param_name eq $_, (@reserved_param_names, @product_info_param_names, @summary_info_param_names))) { # Chop the leading "R_" off param names for display if (substr($param_name, 0, 2) eq 'R_') { $param_name = substr($param_name, 2); } $template .= "${param_name}: ${param_val}\n"; } } # Now append the hard-coded portion of the default template $template .= <<'EOF'; Item: ${Prodno} - ${Item} ${Describe} - ${Info} Qty: ${Qty} at $${Price} $${Amount} ------- Subtotal $${Sub_Total} EOF # If we've got "Minbuy" and "Discount" param's, then include # Discount_Amount and Sub_Total_After_Discount in default template. if ($in{'Minbuy'} || $in{'Discount'}) { $template .= <<'EOF'; Discount $${Discount_Amount} Subtotal after Discount $${Sub_Total_After_Discount} EOF } $template .= <<'EOF'; Tax @ ${Taxes} $${Tax} Shipping/Handling $${Shipping} ======= Total $${Total} EOF $body = &format_shopping_cart_data($template, *in); } # End of if a Template file was specified # Send the Email message to any specified addresses if ($in{'Address'}) { &email_it($in{'Address'}, $in{'Subject'}, $body, $in{'From_Address'}); } if ($in{'R_Address'}) { &email_it($in{'R_Address'}, $in{'Subject'}, $body, $in{'From_Address'}); } if ($in{'Address2'}) { &email_it($in{'Address2'}, $in{'Subject'}, $body, $in{'From_Address'}); } if ($in{'R_Address2'}) { &email_it($in{'R_Address2'}, $in{'Subject'}, $body, $in{'From_Address'}); } if ($ADDRESS) { &email_it($ADDRESS, $in{'Subject'}, $body, $in{'From_Address'}); } } # End of if 'Address' or 'Address2' (or $ADDRESS) was specified ############################################################################### # Finally, we take care of either saving the order to the shopping cart file # or deleting the shopping cart file entirely. # If "Choice" was "final" or "cancel", or "ReviewChoice" was "clear" then # delete the shopping cart file entirely. Otherwise save the order. ############################################################################### if ($in{'Choice'} eq 'final' || $in{'Choice'} eq 'cancel' || $in{'ReviewChoice'} eq 'clear') { unlink $cart_filename; } else { &save_order; } exit; ############################################################################### sub my_die { local($message) = @_; warn $message; print "Content-type: text/html\n\n${message}"; exit; } ############################################################################### sub verify_required_fields { local(*in) = shift; local($key, $val, $printed_header_flag); $printed_header_flag = 0; while (($key, $val) = each(%in)) { if (substr($key, 0, 2) eq 'R_' && $val eq '') { if (! $printed_header_flag) { print <Required Fields Were Left Blank

Required Fields Were Left Blank

END $printed_header_flag = 1; } # Chop the leading "R_" off param names for display $key = substr($key, 2); print "
The required field named '${key}' was left blank." } } if ($printed_header_flag) { print <Please press the 'BACK' button in your browser and make corrections. END exit; } } ############################################################################### sub email_it { local($address, $subject, $body, $from) = @_; local($valid_chars); $valid_chars = 'a-zA-Z0-9_%!'; # First check the validity of the Email address if ($address =~ /([\.$valid_chars]+\@[$valid_chars]+\.[\.$valid_chars]+)/) { if (open(SENDMAIL, "|${MAILPROG} ${1}")) { print SENDMAIL "From: ${from}\n" if $from; print SENDMAIL "Reply-to: ${from}\n" if $from; print SENDMAIL "Subject: ${subject}\n\n"; print SENDMAIL $body; print SENDMAIL "\n."; close(SENDMAIL); } else { warn "Could not open an output pipe to '${MAILPROG}'. I will try to redirect browser to 'Prevpage' anyway."; } } else { # Bad Email address warn "Incorrectly formatted E-mail address. I will try to redirect browser to 'Prevpage' anyway."; } } ############################################################################### # Given a template and an associative array of CGI parameters, this routine # will put the parameters into the appropriate places in the template and # remove any unused placeholders in the template. ############################################################################### sub format_according_to_template { local ($template, *in) = @_; local ($p_name, $p_val); # Here we replace the parameter placeholders with the parameter values while (($p_name, $p_val) = each(%in)) { # Replace occurances of '${P_NAME}' and '$P_NAME' with # the parameter's value. $template =~ s/\$\{${p_name}\}/${p_val}/g; $template =~ s/\$${p_name}/${p_val}/g; } # Now get rid of any leftover parameters in the template $template =~ s/\$\{\w+\}//g; return $template; } ############################################################################### # This routine does some heavy derivations based on the order-related CGI # parameters. Then it calls on the "format_according_to_template" routine # to produce output. ############################################################################### sub format_shopping_cart_data { local ($template, *in) = @_; local (@template, @detail_lines, $new_detail_lines, $num_of_detail_lines); local (@costs, @qtys, @prodnos, @items, @describes, @infos, @amounts); local ($max_index, $p_name, $arr_name, $mini_script, $prodno, $rest_of_rec); local ($first_line, $last_line, $line, $i, $order_lines_printed); %in2 = %in; # Split the template up into lines @template = split(/\n/, $template); # This loop builds the arrays 'costs', 'qtys', 'prices', etc. and # determines which parameter array has the greatest last index. $i = 0; foreach $prodno (@order) { $rest_of_rec = $order{$prodno}; $prodnos[$i] = $prodno; ($qtys[$i], $prices[$i], $items[$i], $describes[$i], $infos[$i]) = split(/\|/, $rest_of_rec); ++$i; } $max_index = $#prodnos; # This loop determines the first line of the template that # contains one of these parameters and the last line that contains # one of these parameters; that way we can duplicate the detail # lines of the template if there is more than one line on the order. $first_line = 9999; $last_line = -1; foreach $p_name (@product_info_param_names) { $i = 0; foreach $line (@template) { if ($line =~ /\$\{${p_name}\}/ || $line =~ /\$${p_name}/) { $first_line = $i if $i < $first_line; $last_line = $i if $i > $last_line; } ++$i; } } # If max_index is greater than -1 (i.e.- we've got at least one detail # line on this order) and we were able to determine which lines of the # template corresponded to detail lines of the order, then we must # duplicate that section of lines (max_index + 1) times. Also, we # must number the placeholders - that is, append a number to the # placeholder name so that we can uniquely identify that placeholder. # Likewise, we must generate new parameters to match the placeholder # names. if ($first_line < 9999 && $last_line > -1) { $num_of_detail_lines = $last_line - $first_line + 1; # Splice out the original set of detail lines @detail_lines = splice(@template, $first_line, $num_of_detail_lines); $order_lines_printed = 0; for ($i = 0; $i <= $max_index; $i++) { # Only do all the following if the order quantity was > 0 if ($qtys[$i] > 0) { $new_detail_lines = join("\n", @detail_lines); foreach $p_name (@product_info_param_names) { # Numerify the detail line parameters $new_detail_lines =~ s/\$\{${p_name}\}/\$\{${p_name}${i}\}/g; $new_detail_lines =~ s/\$${p_name}/\$${p_name}${i}/g; # Generate a numbered parameter for this detail line parameter $mini_script = '$' . "\L${p_name}s[${i}]"; $in2{"${p_name}${i}"} = eval $mini_script; } # Insert the new set of detail lines splice(@template, $first_line + ($order_lines_printed * $num_of_detail_lines), 0, split(/\n/, $new_detail_lines)); ++$order_lines_printed; } # End of if this qty was greater than zero } } else { &my_die("Could not identify first and last lines of order detail in template '$in{'Template'}'."); } # End of else portion of if we were able to identify first and last detail lines local ($format) = ('%6.2f'); # Now generate parameters for derived values $in2{'Sub_Total'} = 0; for ($i = 0; $i <= $max_index; $i++) { # Generate the 'Amounts' (i.e.- extended price) $in2{"Amount${i}"} = sprintf($format, $qtys[$i] * $prices[$i]); $in2{'Sub_Total'} += $qtys[$i] * $prices[$i]; $in2{'Total_Qty'} += $qtys[$i]; } # Format subtotal appropriately $in2{'Sub_Total'} = sprintf($format, $in2{'Sub_Total'}); # Also if "Minbuy" dollar threshold is present and this order meets or # exceeds it, then give them the percentage discount specified in # "Discount". The discounted amount will be placed in the param named # "Sub_Total_After_Discount". if ($in2{'Minbuy'} && $in2{'Sub_Total'} > $in2{'Minbuy'}) { $in2{'Discount_Amount'} = $in2{'Sub_Total'} * $in2{'Discount'}; } else { $in2{'Discount_Amount'} = 0; } $in2{'Sub_Total_After_Discount'} = $in2{'Sub_Total'} - $in2{'Discount_Amount'}; # Format subtotal_after_discount appropriately as well as discount amount $in2{'Sub_Total_After_Discount'} = sprintf($format, $in2{'Sub_Total_After_Discount'}); $in2{'Discount_Amount'} = sprintf($format, $in2{'Discount_Amount'}); # Calculate any tax local ($tax_percentage) = ($in2{'Taxes'}); # If the tax percentage is >= 1 (i.e.- 100%) then the user # must have entered their tax as a whole number rather than a fraction. $tax_percentage /= 100 if $tax_percentage >= 1; $in2{'Tax'} = 0; $in2{'Tax'} += $tax_percentage * $in2{'Sub_Total_After_Discount'}; $in2{'Tax'} = sprintf($format, $in2{'Tax'}); # Calculate shipping charges $in2{'Shipping'} = 0; # Add in any Percentage shipping charge $in2{'Shipping'} += $in2{'Pshipping'} * $in2{'Sub_Total_After_Discount'} if $in2{'Pshipping'}; # Add in any Flat-rate shipping charge $in2{'Shipping'} += $in2{'Fshipping'} if $in2{'Fshipping'}; # Now call subroutine to calculate any custom shipping costs $in2{'Cshipping'} = &calculate_custom_shipping_cost($in2{'Total_Qty'}, $in2{'State'}, *prodnos, *qtys); $in2{'Shipping'} += $in2{'Cshipping'}; # Format total shipping amount $in2{'Shipping'} = sprintf($format, $in2{'Shipping'}); # Calculate 'Total' $in2{'Total'} = 0; $in2{'Total'} += $in2{'Sub_Total_After_Discount'} + $in2{'Tax'} + $in2{'Shipping'}; $in2{'Total'} = sprintf($format, $in2{'Total'}); # Now, finally, format all this according to the template and return value $template = join("\n", @template); return &format_according_to_template($template, *in2); } # End of subroutine 'format_shopping_cart_data' ############################################################################### # Desc: This function restores any preexisting order info from shopping cart # file and augments it by any order info passed to us as CGI param's. ############################################################################### sub get_order { local($prodno, @rest_of_rec); local($p_name, $max_index, $arr_name, $mini_script, $i); local(@costs, @qtys, @prodnos, @items, @describes, @infos, @amounts); # First get any order info from shopping cart file if (-e $cart_filename) { open(ORDER, $cart_filename) || &my_die("$! - Unable to open cart file '${cart_filename}'."); while() { chop; ($prodno, @rest_of_rec) = split(/\|/, $_); push(@order, $prodno); # <- This array used to preserve sequence $order{$prodno} = join('|', @rest_of_rec[0..4]); } close(ORDER); } # Unless the user chose "Review" or "Register" and did not select the # "Add2cart" checkbox, incorporate changes in submitted order info. if (! (($in{'Review'} || $in{'Register'}) && (! $in{'Add2cart'}))) { # Now get order info from CGI param's $max_index = -1; foreach $p_name (@product_info_param_names) { $arr_name = "\L${p_name}s"; $mini_script = '@'. $arr_name . ' = split(/\0/, $in{' . "'${p_name}'}); " . '$max_index = $#' . $arr_name . ' if $#' . $arr_name . ' > $max_index;'; eval $mini_script; } for ($i = 0; $i <= $max_index; ++$i) { # warn "Prod: $prodnos[$i] Qty: $qtys[$i]"; if ($qtys[$i] > 0) { $order{$prodnos[$i]} = join('|', $qtys[$i], $prices[$i], $items[$i], $describes[$i], $infos[$i]) if $prodnos[$i]; # Append this product number to our sequence-keeping array of # product numbers only if it's not already in the array. push(@order, $prodnos[$i]) if (! grep($_ eq $prodnos[$i], @order)); } else { ############################################################### # Only delete if the "ReviewChoice" parameter is present # since this indicates the data came from the Review order # page and that is the only page from which the user may # "zero out" an order line. Oh yeah, same goes for if # "Choice" parameter is present since that means that # made a last second change at the "checkout". ############################################################### delete $order{$prodnos[$i]} if (($in{'ReviewChoice'} || $in{'Choice'}) && $prodnos[$i]); } } } } # End of sub get_order ############################################################################### # This routine saves the user's order to their shopping cart file. ############################################################################### sub save_order { local($prodno, $rest_of_rec); if (-e $cart_filename) { truncate($cart_filename, 0) || &my_die("$! - Unable to truncate cart file '${cart_filename}'."); } open(ORDER, ">${cart_filename}") || &my_die("$! - Unable to open cart file '${cart_filename}' for writing."); foreach $prodno (@order) { $rest_of_rec = $order{$prodno}; print ORDER join('|', $prodno, $rest_of_rec), "\n"; } close(ORDER); } ############################################################################### # NOTE: The following "ReadParse" function was adapted from Steven Brenner's # "ReadParse" function in his "cgi-lib.pl" library. ############################################################################### # Perl Routines to Manipulate CGI input # S.E.Brenner@bioc.cam.ac.uk # $Header: /cys/people/brenner/http/cgi-bin/RCS/cgi-lib.pl,v 1.14 1995/10/25 15:08:37 brenner Exp $ # # Copyright (c) 1995 Steven E. Brenner # Unpublished work. # Permission granted to use and modify this library so long as the # copyright above is maintained, modifications are documented, and # credit is given for any use of the library. ############################################################################### # Func: ReadParse # # Reads in GET or POST data, converts it to unescaped text, and puts # one key=value in each member of the list "@in". # Also creates key/value pairs in %in, using '\0' to separate multiple # selections. # Returns TRUE if there was input, FALSE if there was no input. # UNDEF may be used in the future to indicate some failure. # If a variable-glob parameter (e.g., *cgi_input) is passed to ReadParse, # information is stored there, rather than in $in, @in, and %in. ############################################################################### sub ReadParse { local (*in) = @_ if @_; local ($i, $key, $val); # Read in text if ($ENV{'REQUEST_METHOD'} eq "GET") { $in = $ENV{'QUERY_STRING'}; } elsif ($ENV{'REQUEST_METHOD'} eq "POST") { read(STDIN,$in,$ENV{'CONTENT_LENGTH'}); } @in = split(/[&;]/,$in); foreach $i (0 .. $#in) { # Convert plus's to spaces $in[$i] =~ s/\+/ /g; # Split into key and value. ($key, $val) = split(/=/,$in[$i],2); # splits on the first =. # Convert %XX from hex numbers to alphanumeric $key =~ s/%(..)/pack("c",hex($1))/ge; $val =~ s/%(..)/pack("c",hex($1))/ge; # Associate key and value $in{$key} .= "\0" if (defined($in{$key})); # \0 is the multiple separator $in{$key} .= $val; } return scalar(@in); } ############################################################################### # # NOTE: The following subroutine may be customized to calculate shipping # costs. The following values are passed in: # # 1) Total Qty Ordered # 2) Two-Character Abbreviation of State to be shipped to # 3) An array of product numbers ordered # 4) A corresponding array of quantites ordered # # You should have the subroutine calculate and return # $custom_shipping_cost. Feel free to declare any additional # local variables you may need. # ############################################################################### sub calculate_custom_shipping_cost { local($total_qty, $state, *prodnos, *qtys) = @_; local($custom_shipping_cost); $custom_shipping_cost = 0; return $custom_shipping_cost; }