
RedBonzai Developers, INC is a group of web application developers dedicated to serious web development in Houston, Tx. We are currently expanding our repertoire to CodeIgniter Applications. We will talk quite a bit about E-Commerce in times to come, but in particular we are using our PHP development skills to create a CodeIgniter Shopping Cart that will fully equip you to sell digital goods online.
This is a tutorial originally authored by Dan Harper, and we will be expanding on it a bit. The CodeIgniter Image above is credited to Nucleocide.
Getting Started
Download the latest CodeIgniter release to your server, and set up your MySQL database (I named it digitalgoods) with the following SQL queries:
CREATE TABLE `ci_sessions` ( `session_id` varchar(40) NOT NULL DEFAULT '0', `ip_address` varchar(16) NOT NULL DEFAULT '0', `user_agent` varchar(50) NOT NULL, `last_activity` int(10) unsigned NOT NULL DEFAULT '0', `user_data` text NOT NULL, PRIMARY KEY (`session_id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; CREATE TABLE `downloads` ( `id` int(11) NOT NULL AUTO_INCREMENT, `item_id` int(11) DEFAULT NULL, `purchase_id` int(11) DEFAULT NULL, `download_at` int(11) DEFAULT NULL, `ip_address` varchar(15) DEFAULT NULL, `user_agent` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; CREATE TABLE `items` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `description` text, `price` decimal(10,2) DEFAULT NULL, `file_name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; CREATE TABLE `purchases` ( `id` int(11) NOT NULL AUTO_INCREMENT, `item_id` int(11) DEFAULT NULL, `key` varchar(255) DEFAULT NULL, `email` varchar(127) DEFAULT NULL, `active` tinyint(1) DEFAULT NULL, `purchased_at` int(11) DEFAULT NULL, `paypal_email` varchar(127) DEFAULT NULL, `paypal_txn_id` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; INSERT INTO `items` (`id`,`name`,`description`,`price`,`file_name`) VALUES (1, 'Unix and CHMOD', 'Vivamus id mollis quam. Morbi ac commodo nulla.', 19.99, 'UNIX and CHMOD.txt'), (2, 'Intro to 8086 Programming', 'Morbi ac commodo nulla.', 4.95, 'Intro to 8086 Programming.txt');
The first query creates CodeIgniter’s default user sessions table. We then create a table to log file downloads, one to store the items and another to store purchase details. Finally, we insert a couple of items into the table.
We’ve inserted two items into the database, so we have to create those files on the server. In the root directory for your application (the same folder as CodeIgniter’s system directory), create a new directory named files:
In that directory, create two text files, named UNIX and CHMOD.txt and Intro to 8086 Programming.txt. The capital letters are important on most web servers. These are the file names we set in the database for our two items. Enter some content in the files so we can be sure the files are being downloaded correctly.
CodeIgniter Configuration
In the system/application/config/database.php file, set your database settings in the fields provided:
In config/autoload.php set the following libraries and helpers:
$autoload['libraries'] = array( 'database', 'session', 'form_validation', 'email' ); ... $autoload['helper'] = array( 'url', 'form', 'download', 'file' );
In config/config.php set the base_url:
$config['base_url'] = "http://localhost/digitalgoods/";
In the same file, paste the following to create our own configuration settings:
/* |-------------------------------------------------------------------------- | Website Name |-------------------------------------------------------------------------- | | Will be used on page title bars, in emails etc. | */ $config['site_name'] = "Digital Goods"; /* |-------------------------------------------------------------------------- | Admin Email |-------------------------------------------------------------------------- | | Used to send confirmations of purchases | */ $config['admin_email'] = "dan@example.com"; /* |-------------------------------------------------------------------------- | No-Reply Email |-------------------------------------------------------------------------- | | The 'No-Reply' address used to send out file downloads | */ $config['no_reply_email'] = "noreply@example.com"; /* |-------------------------------------------------------------------------- | PayPal Account |-------------------------------------------------------------------------- | | The email address PayPal payments should be made to | */ $config['paypal_email'] = "paypal@example.com"; /* |-------------------------------------------------------------------------- | Download Limit |-------------------------------------------------------------------------- | | How many times can an item be downloaded within a certain time frame? | eg. 4 downloads within 7 days | */ $config['download_limit'] = array( 'enable' => false, 'downloads' => '4', 'days' => '7' );
Set each of the new config options to your desired settings, but keep ‘Download Limit’ disabled for now.
Finally, under config/routes.php set the default controller:
$route['default_controller'] = "items";
View All Items
Now to create the main controller for the site, create a file in the controllers directory named items.php, and inside enter:
<?php if (!defined('BASEPATH')) exit('No direct script access allowed');
class Items extends Controller {
function Items() {
parent::Controller();
$this->load->model( 'items_model', 'Item' );
$data['site_name'] = $this->config->item( 'site_name' );
$this->load->vars( $data );
}
function index() {
echo 'Hello, World!';
}
}
In the constructor we’ve loaded in the ‘items_model’ model (which we’ve named ‘Item’) which we’ll create next, and placed the ‘site_name’ configuration setting into a variable which we can access in the views. For the index method, we’ve just set a simple ‘Hello, World!’ message for now.
Also, create a new model by creating new file in the models directory named items_model.php and inside enter:
paypal.phpand enter the following inside:
<?php if (!defined('BASEPATH')) exit('No direct script access allowed');
class Paypal extends Controller {
function Paypal() {
parent::Controller();
$this->load->model( 'items_model', 'Item' );
$this->load->library( 'Paypal_Lib' );
$data['site_name'] = $this->config->item( 'site_name' );
$this->load->vars( $data );
}
function index() {
redirect( 'items' );
}
function success() {
$this->session->set_flashdata( 'success', 'Your payment is being processed now.
Your download link will be emailed to your shortly.' );
redirect( 'items' );
}
function cancel() {
$this->session->set_flashdata( 'success', 'Payment cancelled.' );
redirect( 'items' );
}
}
That’s the ‘success’ and ‘cancel’ pages (cancel is used with a user doesn’t continue with the payment and clicks the cancel button on PayPal instead).
PayPal Developer Tools
PayPal provide a ‘Sandbox’ for developers to test their code with. You can create your own sandbox PayPal addresses to send pretend payments. Sign up for a free developer account at https://developer.paypal.com/ then go to ‘Create a preconfigured buyer or seller account’:
Fill out the form to create a new buyer account, enter a balance and click on through. On the ‘Test Accounts’ page you will find the email address for your new Sandbox buyer email address:
Now go back to the site we’re creating and click through to purchase an item. Notice that when you get to PayPal, the address is https://www.sandbox.paypal.com/..... Login with the ‘buyer’ account you created on the right:
Continue through the payment process and click the ‘Return to Merchant’ button upon completion. You should be directed back to your homepage, with the “Your payment is being processed now. Your download link will be emailed to your shortly.” message below the header.
The main interface of the site is now complete. We just need to add in our IPN listener and email the item to the buyer.
IPN Listener
As mentioned above, once PayPal has confirmed payment, it will send data to our IPN listener, once we validate the data with PayPal (to prevent fraudulent data), we can use the data to activate the buyer’s purchase.
The IPN function is a little big, so I’ll break it up. Inside the PayPal controller, add the following function:
function ipn() {
if ( $this->paypal_lib->validate_ipn() ) {
$item_name = $this->paypal_lib->ipn_data['item_name'];
$price = $this->paypal_lib->ipn_data['mc_gross'];
$currency = $this->paypal_lib->ipn_data['mc_currency'];
$payer_email = $this->paypal_lib->ipn_data['payer_email'];
$txn_id = $this->paypal_lib->ipn_data['txn_id'];
$key = $this->paypal_lib->ipn_data['transaction_subject'];
$this->Item->confirm_payment( $key, $payer_email, $txn_id );
$purchase = $this->Item->get_purchase_by_key( $key );
$item = $this->Item->get( $purchase->item_id );
}
}
Right at the start we validate the data sent to the listener with PayPal – the library takes care of all this. If the data is valid, we grab a some details (the item name, price, currency, the payer’s PayPal email address, the transaction ID and the unique key we sent to PayPal when the payment process began).
We can then use the key to confirm the payment (by setting the ‘active’ field to ’1′) and add the payer’s PayPal email and transaction ID to the database for future reference.
Using the key we can get the purchase details from the database, along with the item purchased. Before continuing with the IPN function, let’s create the confirm_payment() and get_purchase_by_key() model methods.
So inside the model, add the following:
function confirm_payment( $key, $paypal_email, $payment_txn_id ) {
$data = array(
'purchased_at' => time(),
'active' => 1,
'paypal_email' => $paypal_email,
'paypal_txn_id' => $paypal_txn_id
);
$this->db->where( 'key', $key );
$this->db->update( 'purchases', $data );
}
function get_purchase_by_key( $key ) {
$r = $this->db->where( 'key', $key )->get( 'purchases' )->result();
if ( $r ) return $r[0];
return false;
}
The functions should be pretty self explanatory by now, so now we need to email the customer their download link. This is handled in the IPN listender, so back in the controller add the following to the end of the ipn() function:
// Send download link to customer $to = $purchase->email; $from = $this->config->item( 'no_reply_email' ); $name = $this->config->item( 'site_name' ); $subject = $item->name . ' Download'; $segments = array( 'item', url_title( $item->name, 'dash', true ), $item->id ); $message = '<p>Thanks for purchasing ' . anchor( $segments, $item->name ) . ' from ' . anchor( '', $name ) . '. Your download link is below.</p>'; $message .= '<p>' . anchor( 'download/' . $key ) . '</p>'; $this->email->from( $from, $name ); $this->email->to( $to ); $this->email->subject( $subject ); $this->email->message( $message ); $this->email->send(); $this->email->clear();
Here we’re using CodeIgniter’s Email class to send the email. We start by setting up ‘To’, ‘From’, ‘Name’ and ‘Subject’ variables with the relevant data.
We then write a short message for the body with a link to the file they purchased, followed by their download link (which will be in the format of: http://example.com/download/{key}). Finally we add the variables into the Email class methods and send it.
The final thing we need in the IPN listener is to send the site’s admin an email with the transaction details. Add the following to the end of the ipn() function:
// Send confirmation of purchase to admin $message = '<p><strong>New Purchase:</strong></p><ul>'; $message .= '<li><strong>Item:</strong> ' . anchor( $segments, $item->name ) . '</li>'; $message .= '<li><strong>Price:</strong> $' . $item->price . '</li>'; $message .= '<li><strong>Email:</strong> ' . $purchase->email . '</li><li></li>'; $message .= '<li><strong>PayPal Email:</strong> ' . $payer_email . '</li>'; $message .= '<li><strong>PayPal TXN ID:</strong> ' . $txn_id . '</li></ul>'; $this->email->from( $from, $name ); $this->email->to( $this->config->item( 'admin_email' ) ); $this->email->subject( 'A purchase has been made' ); $this->email->message( $message ); $this->email->send(); $this->email->clear();
IMPORTANT! The IPN listener won’t work if you’re running on a local server (‘localhost’). Clearly if PayPal attempted to visit http://localhost/paypal/ipn/ they’re not going to arrive at your system. Upload your files to a remote server accessible by a domain name, or external IP address, for this to work.
File Downloads
The final step to getting the site fully working is to get the download links to work. When a customer goes to the download link we email them (eg. http://example.com/download/{key}), we use their key to look up the download. If the purchase associated with the key is set to active (payment fulfilled) and the file exists on the server, the download will start.
First thing we need to do is add another route to set /download/ requests to go to items/download. Add the following to your config/routes.php file:
$route['download/:any'] = 'items/download';
Now, inside your items controller, add the following download() method:
function download() { // ROUTE: download/{purchase_key}
$key = $this->uri->segment( 2 );
$purchase = $this->Item->get_purchase_by_key( $key );
// Check purchase was fulfilled
if ( ! $purchase ) {
$this->session->set_flashdata( 'error', 'Download key not valid.' );
redirect( 'items' );
}
if ( $purchase->active == 0 ) {
$this->session->set_flashdata( 'error', 'Download not active.' );
redirect( 'items' );
}
// Get item and initiate download if exists
$item = $this->Item->get( $purchase->item_id );
$file_name = $item->file_name;
$file_data = read_file( 'files/' . $file_name );
if ( ! $file_data ) { // file not found on server
$this->session->set_flashdata( 'error', 'The requested file was not found. Please contact us to resolve this.' );
redirect( 'items' );
}
force_download( $file_name, $file_data );
}
In the first couple lines we lookup the purchase using the key in the URL. On line 6, if we can’t find a purchase record with that key, we set an error message that the key is invalid and redirect the user to the homepage. Similarly, on line 10, if the purchase was not fulfilled (active is ’0′), we display an error.
We then retrieve the actual item from the database, and retrieve the file name. We use the read_file() method from CodeIgniter’s File helper to get the contents of the file. If the file can’t be found (or is empty), we display an error. Otherwise, we use the force_download() method to initiate a download.
Item Theft
We do have one problem—it’s possible to guess the name of a file and download it directly (ie. by visiting http://example.com/files/UNIX and CHMOD.txt). To fix this, simply create a file named .htaccess inside the files/ directory with the following line inside:
deny from all
This tells the server to deny any requests for files in this directory – but the server itself can still access the files so buyers can still download using their own unique link.
Stylish
At the root of your app (same location as the files and system directories), create a new directory named css, inside it create a file named style.css and add the following inside to spice things up a little:
body {
background-color: #f9f9f9;
color: #222;
font-family: sans-serif;
}
#wrap {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
box-shadow: #e6e6e6 0 0 15px;
-moz-box-shadow: #e6e6e6 0 0 15px;
-webkit-box-shadow: #e6e6e6 0 0 15px;
margin: 15px auto;
padding: 15px;
width: 760px;
}
a {
color: #24badb;
text-decoration: none;
}
header h1 {
text-align: center;
}
header a {
color: #222;
padding: 7px 10px;
}
header a:hover {
background-color: #222;
color: #fff;
}
li {
margin-bottom: 10px;
}
section {
line-height: 1.5em;
}
section a {
padding: 3px 4px;
}
section a:hover {
background-color: #24badb;
color: #fff;
}
footer {
color: #bbb;
text-align: right;
}
footer a {
color: #bbb;
}
footer a:hover {
color: #a0a0a0;
}

BONUS: Download Limits
You may want to limit how often a user may download their purchase within a certain time period (perhaps to stop them sharing the download link around). Implementing this feature doesn’t take much work, so let’s add it in now.
We already set up a database table to log file downloads–’downloads’–which we haven’t used yet. We also have the ‘Download Limit’ setting in the config/config.php file, so go ahead and ‘enable’ it now:
/* |-------------------------------------------------------------------------- | Download Limit |-------------------------------------------------------------------------- | | How many times can an item be downloaded within a certain time frame? | eg. 4 downloads within 7 days | */ $config['download_limit'] = array( 'enable' => true, 'downloads' => '4', 'days' => '7' );
The default setting is to allow up to four file downloads in a seven day period. If, for example, the buyer tries to download five times in seven days, we’ll forward them back to the home page and display an error explaining why we can’t serve up their download right now.
The first thing we need to do is keep a log every time a download is initiated. To do this, add the following directly before the force_download(...) statement at the end of the download() function in the Items controller:
$this->Item->log_download( $item->id, $purchase->id, $this->input->ip_address(), $this->input->user_agent() );
Here we’re sending the item id, purchase id and the user’s IP address and user agent to a log_download() method in the Items model which we’ll create next.
Add the following method to your Items_model:
function log_download( $item_id, $purchase_id, $ip_address, $user_agent ) {
$data = array(
'item_id' => $item_id,
'purchase_id' => $purchase_id,
'download_at' => time(),
'ip_address' => $ip_address,
'user_agent' => $user_agent
);
$this->db->insert( 'downloads', $data );
}
This simply adds the data we provided, and the current time, to the ‘downloads’ table.
We’ll also need a method to get the downloads of a purchase, so add the following to the model:
function get_purchase_downloads( $purchase_id, $limit ) {
return $this->db->where( 'purchase_id', $purchase_id )->limit( $limit )->order_by( 'id', 'desc' )->
get( 'downloads' )->result();
}
Now, to actually add in the download limit, find the following piece of code in the download() method in the Items controller:
// Check purchase was fulfilled
if ( ! $purchase ) {
$this->session->set_flashdata( 'error', 'Download key not valid.' );
redirect( 'items' );
}
if ( $purchase->active == 0 ) {
$this->session->set_flashdata( 'error', 'Download not active.' );
redirect( 'items' );
}
Directly after this, add the following:
// Check download limit
$download_limit = $this->config->item( 'download_limit' );
if ( $download_limit['enable'] ) {
$downloads = $this->Item->get_purchase_downloads( $purchase->id, $download_limit['downloads'] );
$count = 0;
$time_limit = time() - (86400 * $download_limit['days']);
foreach ( $downloads as $download ) {
if ( $download->download_at >= $time_limit )
$count++; // download within past x days
else
break; // later than x days, so can stop foreach
}
// If over download limit, error
if ( $count >= $download_limit['downloads'] ) { // can only download x times within y days
$this->session->set_flashdata( 'error', 'You can only download a file ' . $download_limit['downloads'] .
' times in a ' . $download_limit['days'] . ' day period. Please try again later.' );
redirect( 'items' );
}
}
On the third line we check whether the ‘Download Limit’ functionality is enabled in the config file. If it is, we retrieve the downloads of the current purchase (limited to how many file downloads is permitted).
At line 6, we calculate the furthest time away downloads are limited to (eg. if we have a limit of 4 downloads in 7 days, we find the time 7 days ago) by multiplying the number of days by 86400 (the number of seconds in a day) and subtracting it from the current time.
The loop starting at line 7 checks each download logged to see if it was downloaded within the time limit (eg. 7 days). If it is, we increase $count, otherwise, we break out of the loop as we know if this logged download is older than the limit, all subsequent logs will be to.
At line 15, if the $count is greater than the number of downloads allowed, we display an error message. Otherwise, the rest of the code will be executed and the download will be logged and initiated.

We’re done. Try it out!
Note: To disable the PayPal ‘sandbox’ mode so you can recieve real payments, change the $config['paypal_lib_sandbox_mode'] option in config/paypallib_config.php to false.






