<?php
//
// Edit History:
//
// Last $Author: munroe $
// Last Modified: $Date: 2006/04/04 21:31:37 $
//
// Dick Munroe (hide@address.com) 28-Feb-2006
// Initial version created
//
// Dick Munroe (hide@address.com) 12-Mar-2006
// Default behavior for sourceNotEmpty is to add any left over variables to the
// IPN data.
//
// Dick Munroe (hide@address.com) 14-Mar-2006
// Add method for handling alternate payment status.
// Get the error paths on the processing to set the proper return status.
//
// Dick Munroe (hide@address.com) 15-Mar-2006
// If the url for the testing mode is the null string, don't interact with
// paypal, just say that things verified.
//
// Dick Munroe (hide@address.com) 16-Mar-2006
// The constructor needs to check for exactly NULL for default values.
//
// Dick Munroe (hide@address.com) 20-Mar-2006
// Add step to processing for verifying the item details.
//
// Dick Munroe (hide@address.com) 22-Mar-2006
// The test for default arguments in the constructor had to be stronger.
//
// Dick Munroe (hide@address.com) 23-Mar-2006
// (Blush) The interaction with Paypal wasn't being sent as a POST request.
// Things would work as long as not TOO much data needed to be returned
// (generally the case).
//
// Dick Munroe (hide@address.com) 23-Mar-2006
// Redesign the processNotification routine to make it easier to reuse
// this class as a base class for PDT processing as well as IPN processing.
//
// Dick Munroe (hide@address.com) 04-Apr-2006
// Add interface to return contents of the IPN data.
//
/**
* @author Dick Munroe <hide@address.com>
* @copyright copyright @ 2006 by Dick Munroe, Cottage Software Works, Inc.
* @license http://www.csworks.com/publications/ModifiedNetBSD.html
* @version 1.1.1
* @package dm.paypal
* @example ./example.php
*
* This is derived from a paypal IPN class written originally by Herve Foucher
* <hide@address.com> and published through phpclasses.org under the GPL.
* Herve appears to no longer support this package so I'm updating the support
* to 2005 and redesigning the internal structure to allow a substantially
* more object oriented approach to the whole problem.
*
* This requires the PHP cURL module to be installed and also requires the following
* additional classes from phpclasses.org:
*
* cURL: http://munroe.users.phpclasses.org/browse/package/1988.html
*
* These have to be installed on your PHP search path or within the directory containing
* the Paypal IPN classes in a directory named curl.
*
* Much of the complexity of the provided source is due to the way I organized the IPN
* data parsing. I'm really into understanding where various things group and the
* Paypal developers are really into making that pretty much as difficult as possible
* to figure out. So my attempts to group and understand everything provided by
* Paypal in the IPN documentation are more complex than many much simpler $_POST
* processing schemes. On the other hand, they are more robust and make it simple
* to tell when Paypal has introduced new stuff into the protocol.
*/
include_once('class.paypalIPNData.php') ;
include_once('curl/class.curl.php') ;
/*
* The default URLs for the live and sandbox versions of Paypal.
*/
define ("PAYPAL_SECURED_URL", "https://www.paypal.com/cgi-bin/webscr");
define ("PAYPAL_SANDBOX_SECURED_URL", "https://www.sandbox.paypal.com/cgi-bin/webscr") ;
/**
* This is an abstract class that has to be overridden to provide useful application
* level processing.
*/
class paypalIPNBase
{
var $m_proxyURL;
var $m_proxyUserPassword;
/**
* @access protected
* @var object cURL interface object. Primarily intended to be used for debugging as it could
* easily be made local to httpPost.
*/
var $m_curl ;
/**
* @access protected
* @var array The contents of the paypal data provided by the invocation of the IPN at the local site.
*/
var $m_data ;
/**
* @desc The local site's valid receiver email.
* @access protected.
*/
var $m_receiverEmail ;
/**
* @desc the URL of the Paypal verification processor.
* @access protected.
*/
var $m_paypalURL ;
/**
* @desc the URL of the Paypal Sandbox verification processor.
* @access protected.
*/
var $m_sandboxURL ;
/**
* Constructor
*
* @param string $thePaypalURL The URL of the paypal verification URL.
* @param string $theSandboxURL The URL of the paypal sandbox verification URL.
* @return void
* @access public
*/
function paypalIPNBase($theReceiverEmail, $thePaypalURL = NULL, $theSandboxURL = NULL)
{
$this->m_receiverEmail = $theReceiverEmail ;
if ($thePaypalURL === NULL)
{
$this->m_paypalURL = PAYPAL_SECURED_URL ;
}
else
{
$this->m_paypalURL = $thePaypalURL ;
}
if ($theSandboxURL === NULL)
{
$this->m_sandboxURL = PAYPAL_SANDBOX_SECURED_URL ;
}
else
{
$this->m_sandboxURL = $theSandboxURL ;
}
$this->m_proxyURL = null;
$this->m_proxyUserPassword = null;
}
/*
* This function must be overridden in order to provide actual
* IPN preprocessing. The source of the IPN data may be modified
* freely at this point to inject any additional fields into the
* request.
*
* @desc Preprocess the IPN.
* @param reference to array $theSource for the IPN data.
* @access public
*/
function preprocess(&$theSource)
{
trigger_error("preprocess must be overridden", E_USER_ERROR) ;
}
/*
* This function must be overridden in order to provide actual
* IPN postprocessing.
*
* @desc Postprocess the IPN.
* @param object $theIPNData the IPN data provided by Paypal.
* @param integer $theStatus the HTTP status of the Paypal IPN processing.
* @return void
* @access public
*/
function postprocess(&$theIPN, $theStatus)
{
trigger_error("postprocess must be overridden", E_USER_ERROR) ;
}
/**
* @desc Get the IPN data from the source.
* @param array by reference the source from which IPN data is to be extracted.
* @access private
* @return boolean true if the source is empty after processing.
*/
function getData(&$theSource)
{
/*
* Parse the IPN data out of the source and inject the cmd that will need
* to be sent during verification.
*/
$this->m_data =& new paypalIPNData($theSource, array('cmd' => '_notify-validate')) ;
return empty($theSource) ;
}
/**
* @desc Get the IPN data.
* @access public
* @return object by reference the IPN data object.
*/
function getIPN()
{
return $this->m_data ;
}
/*
* @desc Do the application level processing associated with the source not fully processed.
*
* By default, the sourceNotEmpty method moves any keys and data not already in the
* IPN data to the IPN data. This protects against changes in the IPN standard over time
* but MAY cause problems should variables injected by other sources crop up.
*
* I advise overriding this method at the application level and logging the "missing"
* variables so that the site developer can modify the underlying IPN data classes to
* parse for the new variables and notify the maintainer of this package.
*
* @param object $theIPN the IPN data provided by Paypal.
* @param array $theSource The remaining data not processed into the IPN data.
* return boolean True if the IPN processing is to abort, false otherwise.
* @access protected
*/
function sourceNotEmpty(&$theIPN, &$theSource)
{
$theIPN->addData($theSource) ;
return false ;
}
/**
* @desc Check for a match with the receiver_email
* @param string theReceiverEmail
* @return boolean.
* @access private
*/
function checkReceiver($theReceiverEmail)
{
return $theReceiverEmail == $this->m_receiverEmail ;
}
/**
* @desc Check for a verified response from Paypal.
* @param string theResponse
* @return boolean.
* @access private
*/
function checkVerified($theResponse)
{
return $theResponse == "VERIFIED" ;
}
/**
* @desc Check for a payment complete from Paypal.
* @param string theResponse
* @return boolean.
* @access private
*/
function paymentComplete($thePaymentStatus)
{
return $thePaymentStatus == "Completed" ;
}
/**
* @desc Process IPNs with payment status other than Completed.
* @param object by reference The parsed IPN data.
* @return boolean True if the alternate status was processed without error.
* @access protected
*/
function alternatePaymentStatus(&$theIPN)
{
trigger_error("alternatePaymentStatus must be overridden", E_USER_ERROR) ;
}
/**
* @desc Do the application level checking for a unique transaction Id.
* @param string theTransactionId.
* @return boolean True if the transaction id is unique.
* @access protected
*/
function checkTransactionId($theTransactionId)
{
trigger_error("checkTransactionId must be overridden", E_USER_ERROR) ;
}
/**
* @desc Do the application level validation of the item details.
* @param object theIPN
* @return boolean True if the item details match.
* @access protected
*/
function validateItem(&$theIPN)
{
trigger_error("validateItem must be overriden", E_USER_ERROR) ;
}
/**
* @desc Do the application level processing of the payment.
*
* At this point the IPN data is fully verified and anything that needs to
* be done to record data should be taken care of.
*
* @param object theIPN
* @return boolean True if the payment was processed correctly
* @access protected
*/
function processPayment(&$theIPN)
{
trigger_error("processPayment must be overridden", E_USER_ERROR) ;
}
/**
* This function calls helper functions that model each piece of the IPN process as documented in
* Paypal Hacks, chapter 7. The overall process is:
*
* 1. Connect to the application specific preprocessing hook.
* 2. Verify the IPN with Paypal.
* 3. If the IPN is verified, Check for a completed transaction.
* 4. If the transaction was completed, check that the transaction ID isn't a duplicate.
* 5. If the transaction ID is unique, check that the receiver email address is for the local site.
* 6. If the receiver email address matches, check that the item details presented make sense.
* 7. If the item details make sense, process the payment.
*
* @desc Process an IPN received in a $_POST array.
* @param array $theSource [by reference] the arguments passed by Paypal as part of the IPN process.
* @return boolean False if the IPN wasn't processed.
* @access public
*/
function processNotification (&$theSource)
{
$this->preprocess($theSource) ;
$theReturnStatus = TRUE ;
if (!$this->getData($theSource))
{
$theReturnStatus = !$this->sourceNotEmpty($this->m_data, $theSource) ;
}
$theIPN = $this->m_data ;
if ($theReturnStatus)
{
if (($theIPN->getData('test_ipn') !== NULL) && ($theIPN->getData('test_ipn') == '1'))
{
$theURL = $this->m_sandboxURL ;
}
else
{
$theURL = $this->m_paypalURL ;
}
if ($theURL != '')
{
$theReturnStatus = $this->httpPost($theURL, $theIPN) ;
}
else
{
/*
* For debugging purposes, if the URL for interacting with is not provided,
* then force a VERIFIED response.
*/
$theReturnStatus = "VERIFIED" ;
}
$theReturnStatus = $this->_processNotification($theReturnStatus, $theIPN) ;
}
$this->postprocess($theIPN, $theReturnStatus) ;
return $theReturnStatus ;
}
/**
* This function calls helper functions that model each piece of the IPN process as documented in
* Paypal Hacks, chapter 7. The overall process is:
*
* 1. If the IPN is verified, Check for a completed transaction.
* 2. If the transaction was completed, check that the transaction ID isn't a duplicate.
* 3. If the transaction ID is unique, check that the receiver email address is for the local site.
* 4. If the receiver email address matches, check that the item details presented make sense.
* 5. If the item details make sense, process the payment.
*
* @desc Drive the payment process
* @param $theReturnStatus The status string returned by paypal.
* @param array $theIPN [by reference] the IPN data extracted from Paypal.
* @return boolean False if the IPN wasn't processed.
* @access public
*/
function _processNotification($theReturnStatus, &$theIPN)
{
if ($this->checkVerified($theReturnStatus))
{
if ($this->paymentComplete($theIPN->getData('payment_status')))
{
if ($this->checkTransactionId($theIPN->getData('txn_id')))
{
if ($this->checkReceiver($theIPN->getData('receiver_email')))
{
if ($this->validateItem($theIPN))
{
if ($this->processPayment($theIPN))
{
$theReturnStatus = true ;
}
else
{
$theReturnStatus = false ;
}
}
else
{
$theReturnStatus = false ;
}
}
else
{
$theReturnStatus = false ;
}
}
else
{
$theReturnStatus = false ;
}
}
else
{
$theReturnStatus = $this->alternatePaymentStatus($theIPN) ;
}
}
else
{
$theReturnStatus = false ;
}
return $theReturnStatus ;
}
/**
* @desc Set the proxy gateway options.
* @param string $theProxyURL the url for the proxy server.
* @param string $$theProxyUserPassword the user and password necessary for the proxy server.
* @return void
* @access public
*/
function setProxyOptions ($theProxyURL, $theProxyUserPassword)
{
$this->m_proxyURL = $theProxyURL;
$this->m_proxyUserPassword = $theProxyUserPassword;
}
/*
* @desc Post the IPN notification back to Paypal for verification.
* @param string $url the Paypal verification URL.
* @param object $theIPN [by reference] the IPN notification data.
* @access private
*/
function httpPost ($url, &$theIPN)
{
/*
* Technically the m_curl variable should be local to here. It's being kept
* in object context so that debugging will be a bit easier.
*/
$this->m_curl = new cURL() ;
// Notification if transfered into a urlencoded string
$thePostString = $theIPN->asPostString() ;
$this->m_curl->setopt(CURLOPT_URL, $url);
$this->m_curl->setopt(CURLOPT_POST, 1) ;
$this->m_curl->setopt(CURLOPT_RETURNTRANSFER,1);
$this->m_curl->setopt(CURLOPT_SSL_VERIFYHOST, 2) ;
$this->m_curl->setopt(CURLOPT_SSL_VERIFYPEER, FALSE) ;
$this->m_curl->setopt(CURLOPT_POSTFIELDS, $thePostString);
// If you need to go through a proxy server, see set_proxy_options
if (!is_null($this->m_proxyURL) && !is_null($this->m_proxyUserPassword))
{
if (preg_match("/[^:]+:[0-9]+/", $this->m_proxyURL) &&
preg_match("/([^:]+):.*/", $this->m_proxyUserPassword, $matches))
{
$this->m_curl->setopt(CURLOPT_PROXY, $this->m_proxyURL);
$this->m_curl->setopt(CURLOPT_PROXYUSERPWD, $this->m_proxyUserPassword);
}
else
{
trigger_error("Can't set proxy information", E_USER_ERROR) ;
return false ;
}
}
$thePaypalResponse = $this->m_curl->exec();
$this->m_curl->close();
if(preg_match("/(VERIFIED|INVALID)/", $thePaypalResponse, $matches))
{
$response = $matches[1];
}
else
{
$response = FALSE ;
}
return $response;
}
}
?>