Core Web Application Development with PHP and MySQL

With this largest sample yet, we once again avoid just printing all the code to this sample. We instead cover the salient features and pieces of code that demonstrate the most important new features in this application.

Browsing Products

As mentioned previously, products that we sell in our store are managed by the ProductManager class. This works with the Products table in our database, and it is used to fetch a list of all products or those products of a particular size. The showprods.php script takes these product descriptions and displays them for the user to browse, as shown in Figure 33-7.

Figure 33-7. Browsing products in our poster store.

Each of the posters we display for sale is represented as an XHTML FORM element, as follows:

echo <<<EOHTML <td align='center' width='50%'> <font class='pageBody'> <img src='/books/3/445/1/html/2/content/$img' border='3'/><br/> $msg <br/> Print Size: <b>$size</b><br/> <b>\$$price</b> <form action='addtocart.php' method='post'> <input type='hidden' name='productid' value='$pid'/> <input type='submit' value='Add To Cart'/> </form> </font> </td> EOHTML;

The key trick we perform here is to include a second input element in the formthis one with a type of 'hidden'. By setting its name to productid and its value to the actual product ID of the poster being displayed, we make it so that all of the buttons can go to the addtocart.php script, and that script just has to look in $_POST for the value of productid to see which poster the user wants to add to her cart.

The work that the addtocart.php script performs is quite easy:

<?php /** *=-----------------------------------------------------------= * addtocart.php *=-----------------------------------------------------------= * Author: Marc Wandschneider * * This page takes a product ID and adds the appropriate * poster/print to the user's shopping cart, which is stored * in their session data. We have to verify that the * product ID is actually valid because we cannot guarantee * the accuracy of the input. */ ob_start(); require_once('posterstore/coreincs.inc'); /** * First things first: get the product ID that is to be added. */ if (isset($_POST['productid'])) { /** * By forcing this into int format, we avoid SQL injection. */ $pid = intval($_POST['productid']); } else { throw new IllegalProductIDException(); } /** * Next, have the CartManager add an entry for this pid * in the shopping cart. It is smart enough to see whether * there is an existing entry in the cart and simply * increment the number of items desired when it exists. * * This method throws if it is not given a valid product ID. */ $cm = CartManager::getInstance(); $cm->addProductToUserCart($pid); /** * Great. Now redirect him to the page to show him the * contents of his cart. */ header('Location: showcart.php'); ob_end_flush(); ?>

Implementing the Shopping Cart

As already mentioned, the shopping cart we are using with our web application is implemented using session data (the $_SESSION superglobal). The CartManager class is the body of code that takes care of all the details of this. Nothing prevents us from changing the underlying implementation without affecting the rest of the application.

The CartManager works with objects of type CartItem, which is declared as follows:

/** *=-----------------------------------------------------------= * CartItem *=-----------------------------------------------------------= * Our shopping cart is an array of CartItem objects. The keys * in this array are the product ID of the product represented * in the CartItem. */ class CartItem { /** * The publicly available data for our CartItem */ public $ProductID; public $Title; public $PricePerUnit; public $NumItems; /** *=---------------------------------------------------------= * __construct *=---------------------------------------------------------= * Initializes a new instance of this class. * * Parameters: * $in_pid - product ID * $in_title - title of the product * $in_ppu - price per unit/item * $in_citems - number of items in the cart */ public function __construct ( $in_pid, $in_title, $in_ppu, $in_citems ) { $this->ProductID = $in_pid; $this->Title = $in_title; $this->PricePerUnit = $in_ppu; $this->NumItems = $in_citems; } }

Items are added to the shopping cart via the following method:

/** *=---------------------------------------------------------= * addProductToUserCart *=---------------------------------------------------------= * This method takes a product ID and adds one of the items * identified by the ID to the cart associated with the * given session ID. If there is already an item with the * given product ID in the cart, we update the count * of items for that pid. * * Parameters: * $in_pid - product ID to add to the cart */ public function addProductToUserCart($in_pid) { /** * First, make sure there is a valid session. */ $sid = session_id(); if ($sid === NULL or $sid == '' or !isset($_SESSION)) { throw new NoSessionException(); } /** * We need to make sure we have a valid product ID. * The product manager will do that for us. */ $pm = ProductManager::getInstance(); $valid = $pm->isValidProductID($in_pid); if (!$valid) { throw new IllegalProductIDException(); } /** * Okay, see if there is already any in the session * for this pid. */ if (isset($_SESSION['cart']) and isset($_SESSION['cart']["$in_pid"])) { $cartitem = &$_SESSION['cart']["$in_pid"]; $cartitem['citems']++; } else { /** * First, get the price & title for the product from the * ProductManager so that we can put this in our cart. */ $product = $pm->getProductInfo($in_pid); $cartitem = new CartItem($in_pid, $product->getTitle(), $product->getPricePerUnit(), 1); /** * Store this new entry in the session data, creating the * cart array if necessary. */ if (!isset($_SESSION['cart'])) { $_SESSION['cart'] = array(); } $_SESSION['cart']["$in_pid"] = $cartitem; } }

When the user visits the showcart.php page and wants to change the quantity of an item in her shopping cart, the updateProductQuantity method is called:

/** *=---------------------------------------------------------= * updateProductQuantity *=---------------------------------------------------------= * Sets the number of items of the given type in our shopping * cart. * * Parameters: * $in_pid - item in our cart to update * $in_quantity - new number of items for this type * * Throws: * IllegalProductIDException - this pid is not in the * cart. */ public function updateProductQuantity($in_pid, $in_quantity) { if (!isset($_SESSION['cart']) or !isset($_SESSION['cart']["$in_pid"])) { throw new IllegalProductIDException(); } if ($in_quantity == 0) { unset($_SESSION['cart']["$in_pid"]); } else { /** * Otherwise, update the count of items and reset the entry * in the array. */ $ce = &$_SESSION['cart']["$in_pid"]; $ce->NumItems = $in_quantity; } }

Progressing Through Checkout

The checkout for this web application is a linear process. It is a sequence of forms that the user fills in, each validated before the user proceeds to the next. As data is validated, if there are errors, users are sent right back to the form from which they came. Otherwise, they are allowed to proceed to the next.

The last page in this series is the order review page (orderreview.php), as shown in Figure 33-8. From here, users can modify the data from any of the three previous pagesuser personal and billing information, shipping information, or payment information. After they have completed the changes in data, they are sent back to the order review page. To handle the two possible destinations for the various pages, we have added a bit of logic to handle this.

Figure 33-8. Reviewing your order before final submission.

Each of the userinfo.php, shipinfo.php, and pmtinfo.php scripts optionally accepts a GET parameter called from. If this is set to rev, this is passed on to submituserinfo.php, submitshipinfo.php, or submitpmtinfo.php, which send it to orderview.php rather than the next page in the sequence.

We therefore will see code similar to the following in the three form pages:

/** * If we're here as part of a user review of the info, then we * will send them back to the review page after verifying the * data they gave us. Otherwise, we will * send them on to the shipping information page. */ $form_target = 'submituserinfo.php'; if (isset($_GET['from']) and strtolower($_GET['from']) == 'rev') { $form_target .= '?from=rev'; }

The submit pages will have code similar to the following after all processing has succeeded:

/** * Everything looks okay! Continue to the next page. This is * either the shipping info page or the order view page. */ if (isset($_GET['from']) and strtolower($_GET['from']) == 'rev') { header('Location: orderreview.php'); } else { header('Location: shipinfo.php'); }

All of the submit pages will, regardless of success or failure, place their data into the $_SESSION array. On success, we want to store the data so we can access it later on, whereas on failure (because of user input) we want to be able to refill the form from which we came so that users do not become annoyed that they have lost all of their input.

All three forms in the checkout sequence have a similar set of code:

  • Look for any existing data in $_SESSION.

  • Look to see whether we were given an error code to display to the user.

  • Display the form.

Similarly, all our submit processing scripts have a sequence of events similar to the following:

  • Put the data immediately in the session data.

  • Validate the data the user has given us.

  • Redirect to the appropriate page (either the next in the sequence or back to the order review page).

To demonstrate, our payment information form script is as follows:

[View full width]

<?php /** *=-----------------------------------------------------------= * pmtinfo.php *=-----------------------------------------------------------= * The third stage of the checkout process is to collect the * payment information, such as the card type, card number, * and cardholder name. After this will come the order * review. * * If we see that the session data already contains this * information, then we'll populate the fields with that data. */ ob_start(); $page_title = "Payment Info"; require_once('posterstore/coreincs.inc'); require_once('posterstore/creditcard.inc'); require_once('posterstore/pagetop.inc'); /** * If there is any existing information in the session, then * populate the fields with it. */ $cardtype = ''; $cardnumber = ''; $cardexpmon = ''; $cardexpyear = ''; $cardholder = ''; if (isset($_SESSION['pmtinfo'])) { $pi = &$_SESSION['pmtinfo']; $cardtype = isset($pi['cardtype']) ? $pi['cardtype'] : ''; $cardnumber = isset($pi['cardnumber']) ? $pi['cardnumber'] : ''; $cardexpmon = isset($pi['cardexpmon']) ? $pi['cardexpmon'] : ''; $cardexpyear = isset($pi['cardexpyear']) ? $pi['cardexpyear'] : ''; $cardholder = isset($pi['cardholder']) ? $pi['cardholder'] : ''; } /** * If we're here as part of a user review of the info, then we * will send them back to the review page after verifying the * data they gave us. Otherwise, we will * send them on to the order review page. */ $form_target = 'submitpmtinfo.php'; if (isset($_GET['from']) and strtolower($_GET['from']) == 'rev') { $form_target .= '?from=rev'; } ?> <!-- Show the page title and a little description of what we want the user to do here. If there was an error on input, we also show the error message here. --> <br/> <table align='center' width='70%' border='0' cellspacing='0' cellpadding='4'> <tr> <td align='center' width='100%' class='tableLabelTiny'> Billing Information &gt;&gt; Shipping Information &gt;&gt; <b><u>Payment Information</u></b> &gt;&gt; Review and Confirm <br/><br/> </td> </tr> <tr> <td align='center' width='100%' class='tableLabel'> Shipping Information </td> </tr> <tr> <td align='center' width='100%' class='tableLabelSmall'> Please enter the credit card number, expiration date, and cardholder name for the credit card with which you will make this purchase. <br/> <?php /** * If there was an error message associated with the page * and we need the user to correct it, show that now in a * nice red font. */ if (isset($_GET['ef']) and $_GET['ef'] != '') { switch ($_GET['ef']) { case 'cardtype': $msg = 'The specified credit card type is not valid.'; break; case 'cardnumber': $msg = 'The given credit card number is not valid.'; break; case 'cardexp'; $msg = 'The expiration date was not valid.'; break; case 'cardholder': $msg = 'The name of the credit card holder must be entered and must not exceed 150 characters.'; break; } /** * Show the error message now */ echo <<<EOM <font class='errSmall'> <br/> Please correct the following errors before continuing:<br/> <br/> $msg </font> EOM; } ?> <br/> </td> </tr> </table> <!-- Show the table into which the user will enter his payment information. This will be nested inside the form that will handle the submission of the data. --> <form action='<?php echo $form_target ?>' method='POST'> <table align='center' width='70%' border='0' cellspacing='4' cellpadding='2'> <tr> <td width='40%' align='right' class='normalTable'> Card Type: </td> <td class='normalTable'> <select name='cardtype' value='<?php echo $cardtype ?>'> <option value='invalid'/>Please Select <?php /** * We maintain a list in creditcard.inc with all the valid * credit cards that we support. Populate the dropdown now * from this list. */ $ccards = CreditCards::getCardInfo(); foreach ($ccards as $cardinfo) { $sel = ($cardinfo->Code == $cardtype) ? ' selected="selected"' : ''; echo <<<EOOPT <option value='{$cardinfo->Code}'{$sel}/> {$cardinfo->Name} EOOPT; } ?> </select> </td> </tr> <tr> <td width='40%' align='right' class='normalTable'> Card Number: </td> <td class='normalTable'> <input type='text' size='30' name='cardnumber' value='<?php echo $cardnumber ?>'/> </td> </tr> <tr> <td width='40%' align='right' class='normalTable'> Card Expiration Date: </td> <td class='normalTable'> <?php $mval = ($cardexpmon != '') ? $cardexpmon : 'mm'; $yval = ($cardexpyear != '') ? $cardexpyear : 'yyyy'; ?> <input type='text' size='4' name='cardexpmon' value='<?php echo $mval ?>'/> / <input type='text' size='4' name='cardexpyear' value='<?php echo $yval ?>'/> </td> </tr> <tr> <td width='40%' align='right' class='normalTable'> Cardholder Name: </td> <td class='normalTable'> <input type='text' size='30' name='cardholder' value='<?php echo $cardholder ?>'/> </td> </tr> <tr> <td colspan='2' align='center'> <input type='submit' value='Review your Order'/> </td> </tr> </table> </form> <?php /** * Close out the page. */ require_once('posterstore/pagebottom.inc'); ob_end_flush(); ?>

ARRAYS AND THE & OPERATOR

As you browse through the code for this sample, you might notice that we frequently write statements similar to the following:

$pi = &$_SESSION['pmtinfo'];

The use of the & (by reference) operator is necessary. As we mentioned way back in Chapter 1, "Getting Started with PHP" in the section titled "By Value and By Reference Variables," PHP assigns arrays by valuecopying over their entire contents.

Thus, if we were to simply write

$pi = $_SESSION['pmtinfo']; $pi['cardtype'] = 'Visa';

we would soon discover that the data in $_SESSION['pmtinfo']['cardtype'] had not changed at allbecause $pi had been assigned a copy of the data in that location! This can lead to some head scratching as to why the code you have written to save some data appears to have done nothing at all.

The code that processes the data from this form in submitpmtinfo.php is as follows:

<?php /** *=-----------------------------------------------------------= * submitpmtinfo.php *=-----------------------------------------------------------= * This page validates the payment information we have been * given and either sends the user back to the pmtinfo * page for re-entry or sends him on to the next page. * * This next page will be the order review page. */ ob_start(); require_once('posterstore/coreincs.inc'); /** * This indicates where we came from. We will use this to * decide which page to go to after this one. */ if (isset($_GET['from']) and strtolower($_GET['from']) == 'rev') { $from_page = '&from=rev'; } else { $from_page = ''; } /** * First, put the data into the session data so that we can * pass it around. We will then verify it all. */ if (!isset($_SESSION['pmtinfo'])) { $_SESSION['pmtinfo'] = array(); } $pi = &$_SESSION['pmtinfo']; if (isset($_POST['cardtype'])) $pi['cardtype'] = trim($_POST['cardtype']); if (isset($_POST['cardnumber'])) $pi['cardnumber'] = trim($_POST['cardnumber']); if (isset($_POST['cardexpmon'])) $pi['cardexpmon'] = trim($_POST['cardexpmon']); if (isset($_POST['cardexpyear'])) $pi['cardexpyear'] = trim($_POST['cardexpyear']); if (isset($_POST['cardholder'])) $pi['cardholder'] = trim($_POST['cardholder']); /** * Begin validating the data one piece at a time. If * there is a problem, redirect the user back to the input * page. * * Please note that we do not actually verify funds or * complete any financial transactions at this time with the * credit card!!!! */ if (!isset($pi['cardtype']) or !OrderValidation::valCardType(intval($pi['cardtype']))) { header("Location: pmtinfo.php?ef=cardtype$from_page"); ob_end_clean(); exit; } if (!isset($pi['cardnumber']) or !OrderValidation::valCardNumber(intval($pi['cardtype']), $pi['cardnumber'])) { header("Location: pmtinfo.php?ef=cardnumber$from_page"); ob_end_clean(); exit; } if (!isset($pi['cardexpmon']) or !isset($pi['cardexpyear']) or !OrderValidation::valCardExpDate( intval($pi['cardexpmon']), intval($pi['cardexpyear']))) { header("Location: pmtinfo.php?ef=cardexp$from_page"); ob_end_clean(); exit; } if (!isset($pi['cardholder']) or !OrderValidation::valSimple($pi['cardholder'], OrderValidation::MAX_FULLNAME_LEN)) { header("Location: pmtinfo.php?ef=cardholder$from_page"); ob_end_clean(); exit; } /** * Everything looks okay! Continue to the next page. This is * currently always the order review page, but we're keeping * the plumbing here to support some other page in the * future if we decide to change this app. */ if (isset($_GET['from']) and strtolower($_GET['from']) == 'rev') { header('Location: orderreview.php'); } else { header('Location: orderreview.php'); } /** * We're done! */ ob_end_clean(); exit; ?>

Submitting Orders

The script used to submit orders, submitorder.php, which is called from the order review page, is not complex:

<?php /** *=-----------------------------------------------------------= * submitorder.php *=-----------------------------------------------------------= * The user has reviewed the order, and it needs to be * submitted. * * The sequence of events is as follows: * 1. Create a new account for the user or update the * existing account details. * 2. Create an entry in the Orders table for this order. * 3. Create individual entries in the OrderEntries table * for the items the user ordered. * 4. Confirm payment, and update the order as paid. (We do * this after the order is entered so that there is no risk * the users pay and our app dies, thereby losing the * order that they paid for.) */ ob_start(); require_once('posterstore/coreincs.inc'); require_once('posterstore/ordermanager.inc'); require_once('posterstore/paymentmanager.inc'); /** * Step 1. Create an account for the user or update the * existing user's account. */ $ui = &$_SESSION['userinfo']; $um = UserManager::getInstance(); if (isset($ui->UserID) and $ui->UserID != -1) { $um->updateUserAccount($ui); } else { $ui->UserID = $um->createNewAccount($ui); } /** * Step 2 and 3. Create the entry in the Orders table and * create individual entries in the OrderEntries table. */ $cm = CartManager::getInstance(); $cart = $cm->getCartContents(); $om = OrderManager::getInstance(); $order_num = $om->addOrder($ui->UserID, $cart, $_SESSION['shipinfo'], $_SESSION['pmtinfo']); /** * 4. Execute order payment. */ $pm = PaymentManager::getInstance(); $pmt_conf_num = $pm->executePayment($pi['cardtype'], $pi['cardnumber'], $pi['cardexpmon'], $pi['cardexpyear'], $pi['cardholder'], $pi['grand_total']); if ($pmt_conf_num == -1) { header('Location: pmtinfo.php?pe=1'); ob_end_clean(); exit; } else { /** * Payment succeeded. Save two vars for the next page. */ $_SESSION['order_num'] = $order_num; $_SESSION['grand_total'] = $_SESSION['pmtinfo']['grand_total']; $om->setPaymentConf($order_num, $pmt_conf_num); } /** * 5. Clear the cart, shipping information, and payment * information before sending them to the success page! */ $cm->clearShoppingCart(); $_SESSION['shipinfo'] = array(); $_SESSION['pmtinfo']= array(); header('Location: success.php'); ob_end_clean(); ?>

As we can see, we use the OrderManager class to add the order to our tables, whose code follows the patterns of what we generally do. What is slightly more complicated, however, is the code in the order manager to submit the order to the database. As mentioned previously, this is a multistep process, in which we do the following:

  • Create an entry in the Orders table for the order

  • Create individual entries in the OrderEntries table for each type of item ordered

The problem arrives when an error occurs after the first of these has succeeded but the second has yet to complete itself. We end up with an orphan order in the Orders table with no associated entries or real purpose.

The solution to this is to use SQL transactions. We will wrap both of these actions within a transaction so that either both of them succeed as an atomic operation or neither of them succeeds. In the MySQL case, we created these order tables in this database using the InnoDB engine, which supports this functionality. As we saw in the section "Transactions" in Chapter 12, all we have to do to use transactions in our code is code roughly as follows:

// start the transaction. $conn->autocommit(FALSE); try { // execute all transacted queries. } catch (Exception $e) { $conn->rollback(); $conn->autocommit(TRUE); throw $e; } $conn->commit(); // default to non-transacted mode again. $conn->autocommit(TRUE);

With this in mind, our code to add an order and all of its details to the database in the OrderManager class looks like this:

/** *=---------------------------------------------------------= * addOrder *=---------------------------------------------------------= * This method takes the information in a shopping cart and * adds the order to the database. Individual items in the * cart are added to the OrderEntries database, whereas the * overall order is added to the Orders Table ... * * Parameters: * $in_custid - the customer ID * $in_cart - the shopping cart to add * $in_si - the shipping info the user gave us * $in_pmt - the payment info the user gave us * * Returns: * integer - the order number in our database */ public function addOrder ( $in_custid, $in_cart, $in_si, $in_pmt ) { if ($in_cart == NULL or !is_array($in_cart)) throw new IllegalArgumentException('$in_cart'); if ($in_si == NULL or !is_array($in_si)) throw new IllegalArgumentException('$in_si'); if ($in_pmt == NULL or !is_array($in_pmt)) throw new IllegalArgumentException('$in_pmt'); /** * Get a database connection with which to work. */ $conn = DBManager::getConnection(); /** * Begin the transaction here. */ $conn->autocommit(FALSE); try { /** * Create the order in the database. */ $order_id = $this->createOrder($in_custid, $in_si, $in_pmt, $conn); /** * Now create the individual order entries. */ $this->createOrderEntries($order_id, $in_cart); } catch (Exception $ex) { /** * If we run into a problem along the way, roll back the * transaction and then rethrow the exception. */ $conn->rollback(); $conn->autocommit(TRUE); throw $ex; } /** * Otherwise, commit the transaction and call it a good day. */ $conn->commit(); $conn->autocommit(TRUE); return $order_id; }

Note that we do not check the return value of the autocommit or commit methods on the mysqli class in the preceding code. They are all designed to return trUE on success and FALSE on failure. Unfortunately, in versions of PHP5 prior to 5.1, there is a bug by which they do not return correct values.

The createOrder and createOrderEntries methods called by the preceding code are as follows:

/** *=---------------------------------------------------------= * createOrder *=---------------------------------------------------------= * Creates an entry in the Orders table for the user order. * We will submit all information except for the payment * confirmation number, which we will not get until later, * at which point it will be added to the table. Individual * items in the order are entered in the OrderEntries table * and refer to the order ID of this entry. * * Parameters: * $in_custid - customer ID of orderer * $in_si - shipping information for order * $in_pmt - payment information, incl. totals * * Returns: * integer with the order ID for this order * * Throws: * DatabaseErrorException */ private function createOrder ( $in_custid, $in_si, $in_pmt ) { /** * Get a database connection with which to work. */ $conn = DBManager::getConnection(); /** * Make the parameters safe. */ $shipname = DBManager::mega_escape_string($in_si[''shipname']); $company = DBManager::mega_escape_string($in_si['company']); $address1 = DBManager::mega_escape_string($in_si['address1']); $address2 = DBManager::mega_escape_string($in_si['address2']); $city = DBManager::mega_escape_string($in_si['city']); $state = DBManager::mega_escape_string($in_si['state']); $postal = DBManager::mega_escape_string($in_si['postal']); $country = DBManager::mega_escape_string($in_si['country']); $phone = DBManager::mega_escape_string($in_si['telephone']); /** * Create the query */ $query = <<<EOQUERY INSERT INTO Orders (customer_id,ship_name,ship_company,ship_address1, ship_address2,ship_city,ship_state,ship_postal, ship_country,ship_phone,order_cost,ship_cost, total_cost,pmt_type,order_status) VALUES($in_custid,'$shipname','$company','$address1', '$address2','$city','$state','$postal','$country', '$phone','{$in_pmt['order_total']}', '{$in_pmt['shipping']}','{$in_pmt['grand_total']}', '{$in_pmt['cardtype']}',1) EOQUERY; /** * Insert the new row! */ $result = @$conn->query($query); if ($result === FALSE or $result === NULL) throw new DatabaseErrorException($conn->error); /** * Get the order ID value created. */ return $conn->insert_id; } /** *=---------------------------------------------------------= * createOrderEntries *=---------------------------------------------------------= * Inserts the individual items in the user's cart into the * OrderEntry table in the db. Each of these entries is * associated with an order ID from the Orders table. * * Parameters: * $in_orderid - order ID to be associated with * $in_cart - cart containing items to insert */ private function createOrderEntries ( $in_orderid, $in_cart ) { /** * Get a database connection with which to work. */ $conn = DBManager::getConnection(); /** * For each item in the cart, create an order entry * and insert it into the table. */ foreach ($in_cart as $cartitem) { /** * Build the query. */ $query = <<<EOQUERY INSERT INTO OrderEntry (order_id,product_id,price,num_units) VALUES($in_orderid,{$cartitem->ProductID}, {$cartitem->PricePerUnit},{$cartitem->NumItems}) EOQUERY; /** * Execute the insert. */ $results = @$conn->query($query); if ($results === FALSE or $results === NULL) throw new DatabaseErrorException($conn->error); } }

Security

Because we are dealing with money, security is a critical consideration for this web application. The blind transmission of credit card information over the Internet is typically not considered a good idea. Some even argue that storing such information in a database is a bad idea.

To help get around these problems, we do not store credit card numbers in our database, especially because we have designed the type of store where users will not be visiting sufficiently frequently to expect that kind of convenience. Indeed, except for a brief period during checkout where the credit card number is held in the session data, we do not keep it at all. However, we must still solve the unencrypted traffic problem.

We can do this by using SSL and getting a certificate for our web application. (See the section titled "Secure Sockets Layer (SSL)" in Chapter 17, "Securing Your Web Applications: Software and Hardware Security.")

Although this is not a guaranteed complete security solution, it does ensure that credit cards are not transmitted as plain text over the network, and our other precautions with the credit card numbers will prove to be a good level of security for our application.

Категории