/application/libraries/merchant.php
PHP | 893 lines | 564 code | 130 blank | 199 comment | 74 complexity | 8b5e0c0611244f3539e776af18196928 MD5 | raw file
- <?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
- /*
- * CI-Merchant Library
- *
- * Copyright (c) 2011-2012 Adrian Macneil
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
- // Support legacy drivers which extend the CI_Driver class
- // All drivers should be updated to extend Merchant_driver instead
- // This will be removed in a future version!
- if ( ! class_exists('CI_Driver')) get_instance()->load->library('driver');
- define('MERCHANT_CONFIG_PATH', realpath(dirname(__FILE__).'/../config'));
- define('MERCHANT_DRIVER_PATH', realpath(dirname(__FILE__).'/merchant'));
- define('MERCHANT_VENDOR_PATH', realpath(dirname(__FILE__).'/../vendor'));
- /**
- * Merchant Class
- *
- * Payment processing for CodeIgniter
- */
- class Merchant
- {
- public static $CURRENCIES_WITHOUT_DECIMALS = array('JPY');
- public static $NUMERIC_CURRENCY_CODES = array(
- 'AUD' => '036',
- 'CAD' => '124',
- 'EUR' => '978',
- 'GBP' => '826',
- 'NZD' => '554',
- 'USD' => '840',
- );
- private $_driver;
- public function __construct($driver = NULL)
- {
- if ( ! empty($driver))
- {
- $this->load($driver);
- }
- }
- public function __call($function, $arguments)
- {
- if ( ! empty($this->_driver))
- {
- return call_user_func_array(array($this->_driver, $function), $arguments);
- }
- }
- /**
- * Load the specified driver
- */
- public function load($driver)
- {
- $this->_driver = $this->_create_instance($driver);
- return $this->_driver !== FALSE;
- }
- /**
- * Returns the name of the currently loaded driver
- */
- public function active_driver()
- {
- $class_name = get_class($this->_driver);
- if ($class_name === FALSE) return FALSE;
- return str_replace('Merchant_', '', $class_name);
- }
- /**
- * Load and create a new instance of a driver.
- * $driver can be specified either as a class name (Merchant_paypal) or a short name (paypal)
- */
- private function _create_instance($driver)
- {
- if (stripos($driver, 'merchant_') === 0)
- {
- $driver_class = ucfirst(strtolower($driver));
- }
- else
- {
- $driver_class = 'Merchant_'.strtolower($driver);
- }
- if ( ! class_exists($driver_class))
- {
- // attempt to load driver file
- $driver_path = MERCHANT_DRIVER_PATH.'/'.strtolower($driver_class).'.php';
- if ( ! file_exists($driver_path)) return FALSE;
- require_once($driver_path);
- // did the driver file implement the class?
- if ( ! class_exists($driver_class)) return FALSE;
- }
- // ensure class is not abstract
- $reflection_class = new ReflectionClass($driver_class);
- if ($reflection_class->isAbstract()) return FALSE;
- return new $driver_class();
- }
- public function valid_drivers()
- {
- static $valid_drivers = array();
- if (empty($valid_drivers))
- {
- foreach (scandir(MERCHANT_DRIVER_PATH) as $file_name)
- {
- $driver_path = MERCHANT_DRIVER_PATH.'/'.$file_name;
- if (stripos($file_name, 'merchant_') === 0 AND is_file($driver_path))
- {
- require_once($driver_path);
- // does the file implement an appropriately named class?
- $driver_class = ucfirst(str_replace('.php', '', $file_name));
- if ( ! class_exists($driver_class)) continue;
- // ensure class is not abstract
- $reflection_class = new ReflectionClass($driver_class);
- if ($reflection_class->isAbstract()) continue;
- $valid_drivers[] = str_replace('Merchant_', '', $driver_class);
- }
- }
- }
- return $valid_drivers;
- }
- public function authorize($params)
- {
- return $this->_do_authorize_or_purchase('authorize', $params);
- }
- public function authorize_return($params)
- {
- return $this->_do_authorize_or_purchase('authorize_return', $params);
- }
- public function capture($params)
- {
- return $this->_do_capture_or_refund('capture', $params);
- }
- public function purchase($params)
- {
- return $this->_do_authorize_or_purchase('purchase', $params);
- }
- public function purchase_return($params)
- {
- return $this->_do_authorize_or_purchase('purchase_return', $params);
- }
- public function refund($params)
- {
- return $this->_do_capture_or_refund('refund', $params);
- }
- private function _do_authorize_or_purchase($method, $params)
- {
- $this->_normalize_card_params($params);
- $this->_normalize_currency_params($params);
- try
- {
- // load driver params
- $this->_driver->set_params($params);
- // all payments require amount and currency
- $this->_driver->require_params('amount', 'currency');
- // support drivers using deprecated required_fields array
- if (($method == 'authorize' OR $method == 'purchase') AND
- ! empty($this->_driver->required_fields))
- {
- $this->_driver->require_params($this->_driver->required_fields);
- }
- // validate card_no
- $this->_driver->validate_card();
- // begin the actual processing
- return $this->_driver->$method();
- }
- catch (Merchant_exception $e)
- {
- return new Merchant_response(Merchant_response::FAILED, $e->getMessage());
- }
- }
- private function _do_capture_or_refund($method, $params)
- {
- $this->_normalize_currency_params($params);
- try
- {
- // load driver params
- $this->_driver->set_params($params);
- // begin the actual processing
- return $this->_driver->$method();
- }
- catch (Merchant_exception $e)
- {
- return new Merchant_response(Merchant_response::FAILED, $e->getMessage());
- }
- }
- private function _normalize_card_params(&$params)
- {
- // normalize months to 2 digits and years to 4
- if ( ! empty($params['exp_month'])) $params['exp_month'] = sprintf('%02d', (int)$params['exp_month']);
- if ( ! empty($params['exp_year'])) $params['exp_year'] = sprintf('%04d', (int)$params['exp_year']);
- if ( ! empty($params['start_month'])) $params['start_month'] = sprintf('%02d', (int)$params['start_month']);
- if ( ! empty($params['start_year'])) $params['start_year'] = sprintf('%04d', (int)$params['start_year']);
- // normalize card_type to lowercase
- if (isset($params['card_type'])) $params['card_type'] = strtolower($params['card_type']);
- // support deprecated card_name parameter
- if (isset($params['card_name']))
- {
- $params['name'] = $params['card_name'];
- }
- if (isset($params['name']))
- {
- $params['card_name'] = $params['name'];
- // split first/last names
- if (empty($params['first_name']) AND empty($params['last_name']))
- {
- $names = explode(' ', (string)$params['name'], 2);
- $params['first_name'] = $names[0];
- $params['last_name'] = isset($names[1]) ? $names[1] : '';
- }
- }
- // support deprecated address parameter
- if (isset($params['address']))
- {
- $params['address1'] = $params['address'];
- }
- elseif (isset($params['address1']))
- {
- $params['address'] = $params['address1'];
- }
- }
- private function _normalize_currency_params(&$params)
- {
- // support deprecated currency_code parameter
- if (isset($params['currency_code']))
- {
- $params['currency'] = $params['currency_code'];
- }
- if (isset($params['currency']))
- {
- // currency should always be uppercase
- $params['currency'] = strtoupper($params['currency']);
- $params['currency_code'] = $params['currency'];
- }
- }
- /**
- * Deprecated. Please use Merchant_driver::get_request() or
- * Merchant_driver::post_request() instead.
- */
- public static function curl_helper($url, $post_data = NULL, $username = NULL, $password = NULL)
- {
- // errors are now thrown as Merchant_exception()
- $response = array('error' => NULL);
- if (is_null($post_data))
- {
- $response['data'] = get_instance()->merchant->get_request($url, $username, $password);
- }
- else
- {
- $response['data'] = get_instance()->merchant->post_request($url, $post_data, $username, $password);
- }
- return $response;
- }
- /**
- * Redirect the user's browser to a URL.
- *
- * @param string $url
- */
- public static function redirect($url)
- {
- get_instance()->load->helper('url');
- redirect($url);
- }
- /**
- * Redirect the user's browser to a URL using a POST request.
- *
- * @param string $url
- * @param array $data
- * @param string $message
- */
- public static function post_redirect($url, $data, $message = NULL)
- {
- if (empty($message))
- {
- $message = lang('merchant_payment_redirect');
- }
- ?><!DOCTYPE html>
- <html>
- <head><title>Redirecting...</title></head>
- <body onload="document.forms[0].submit();">
- <form name="payment" action="<?php echo htmlspecialchars($url); ?>" method="post">
- <p><?php echo htmlspecialchars($message); ?></p>
- <p>
- <?php foreach ($data as $key => $value): ?>
- <input type="hidden" name="<?php echo htmlspecialchars($key); ?>" value="<?php echo htmlspecialchars($value); ?>" />
- <?php endforeach ?>
- <input type="submit" value="Continue" />
- </p>
- </form>
- </body>
- </html>
- <?php
- exit();
- }
- }
- abstract class Merchant_driver
- {
- protected $CI;
- /**
- * Settings related to the payment gateway.
- * Accessing this array directly is deprecated.
- *
- * @var array
- */
- protected $settings = array();
- /**
- * Parameters related to the current payment.
- *
- * @var array
- */
- private $_params = array();
- public function __construct()
- {
- $this->CI =& get_instance();
- // initialize default settings
- foreach ($this->default_settings() as $key => $default)
- {
- if (is_array($default))
- {
- $this->settings[$key] = isset($default['default']) ? $default['default'] : NULL;
- }
- else
- {
- $this->settings[$key] = $default;
- }
- }
- }
- /**
- * Default settings. This should be overridden by the driver.
- */
- public abstract function default_settings();
- /**
- * Initialize the driver settings
- */
- public function initialize($settings)
- {
- foreach ($this->default_settings() as $key => $default)
- {
- if (isset($settings[$key]))
- {
- // boolean settings must remain booleans
- $this->settings[$key] = is_bool($default) ? (bool)$settings[$key] : $settings[$key];
- }
- }
- }
- /**
- * All driver settings
- *
- * @return array
- */
- public function settings()
- {
- return $this->settings;
- }
- /**
- * Setting
- *
- * @return mixed
- */
- public function setting($key)
- {
- return isset($this->settings[$key]) ? $this->settings[$key] : FALSE;
- }
- public function can_authorize()
- {
- $method = new ReflectionMethod($this, 'authorize');
- return $method->getDeclaringClass()->name !== __CLASS__;
- }
- public function can_capture()
- {
- $method = new ReflectionMethod($this, 'capture');
- return $method->getDeclaringClass()->name !== __CLASS__;
- }
- public function can_refund()
- {
- $method = new ReflectionMethod($this, 'refund');
- return $method->getDeclaringClass()->name !== __CLASS__;
- }
- public function can_return()
- {
- $method = new ReflectionMethod($this, 'purchase_return');
- if ($method->getDeclaringClass()->name !== __CLASS__) return TRUE;
- // try calling deprecated process_return() method instead
- if (method_exists($this, 'process_return')) return TRUE;
- if (method_exists($this, '_process_return')) return TRUE;
- return FALSE;
- }
- public function authorize()
- {
- throw new BadMethodCallException(lang('merchant_invalid_method'));
- }
- public function authorize_return()
- {
- throw new BadMethodCallException(lang('merchant_invalid_method'));
- }
- public function capture()
- {
- throw new BadMethodCallException(lang('merchant_invalid_method'));
- }
- public function purchase()
- {
- // try calling deprecated process() method instead
- if (method_exists($this, 'process'))
- {
- return $this->process($this->_params);
- }
- if (method_exists($this, '_process'))
- {
- return $this->_process($this->_params);
- }
- throw new BadMethodCallException(lang('merchant_invalid_method'));
- }
- public function purchase_return()
- {
- // try calling deprecated process_return() method instead
- if (method_exists($this, 'process_return'))
- {
- return $this->process_return($this->_params);
- }
- if (method_exists($this, '_process_return'))
- {
- return $this->_process_return($this->_params);
- }
- throw new BadMethodCallException(lang('merchant_invalid_method'));
- }
- public function refund()
- {
- throw new BadMethodCallException(lang('merchant_invalid_method'));
- }
- public function param($name)
- {
- return isset($this->_params[$name]) ? $this->_params[$name] : FALSE;
- }
- public function set_params($params)
- {
- $this->_params = array_merge($this->_params, $params);
- }
- public function require_params()
- {
- $args = func_get_args();
- if (empty($args)) return;
- // also accept an array instead of multiple parameters
- if (count($args) == 1 AND is_array($args[0])) $args = $args[0];
- foreach ($args as $name)
- {
- if (empty($this->_params[$name]))
- {
- throw new Merchant_exception(str_replace('%s', lang("merchant_$name"), lang('merchant_required')));
- }
- }
- }
- public function validate_card()
- {
- // skip validation if card_no is empty
- if (empty($this->_params['card_no'])) return;
- if ( ! $this->secure_request())
- {
- throw new Merchant_exception(lang('merchant_insecure_connection'));
- }
- // strip any non-digits from card_no
- $this->_params['card_no'] = preg_replace('/\D/', '', $this->_params['card_no']);
- if ($this->validate_luhn($this->_params['card_no']) == FALSE)
- {
- throw new Merchant_exception(lang('merchant_invalid_card_no'));
- }
- if ($this->param('exp_month') AND $this->param('exp_year') AND
- $this->validate_expiry($this->param('exp_month'), $this->param('exp_year')) == FALSE)
- {
- throw new Merchant_exception(lang('merchant_card_expired'));
- }
- }
- /**
- * Luhn algorithm number checker - (c) 2005-2008 shaman - www.planzero.org
- * This code has been released into the public domain, however please
- * give credit to the original author where possible.
- *
- * @return boolean TRUE if the number is valid
- */
- protected function validate_luhn($number)
- {
- // Set the string length and parity
- $number_length = strlen($number);
- $parity = $number_length % 2;
- // Loop through each digit and do the maths
- $total = 0;
- for ($i = 0; $i < $number_length; $i++)
- {
- $digit = $number[$i];
- // Multiply alternate digits by two
- if ($i % 2 == $parity)
- {
- $digit *= 2;
- // If the sum is two digits, add them together (in effect)
- if ($digit > 9)
- {
- $digit -= 9;
- }
- }
- // Total up the digits
- $total += $digit;
- }
- // If the total mod 10 equals 0, the number is valid
- return ($total % 10 == 0) ? TRUE : FALSE;
- }
- /**
- * Check whether an expiry date has already passed
- *
- * @return bool TRUE if the expiry date is valid
- */
- protected function validate_expiry($month, $year)
- {
- // subtract 12 hours from current GMT time to avoid potential timezone issues
- // in this rare case we will leave it up to the payment gateway to decide
- $date = getdate(gmmktime() - 43200); // 12*60*60
- if ($year < $date['year'])
- {
- return FALSE;
- }
- if ($year == $date['year'] AND $month < $date['mon'])
- {
- return FALSE;
- }
- return TRUE;
- }
- /**
- * Returns TRUE if the current request was made using HTTPS
- */
- protected function secure_request()
- {
- if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) AND $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')
- {
- return TRUE;
- }
- if (empty($_SERVER['HTTPS']) OR strtolower($_SERVER['HTTPS']) == 'off')
- {
- return FALSE;
- }
- return TRUE;
- }
- protected function amount_dollars()
- {
- if (in_array($this->param('currency'), Merchant::$CURRENCIES_WITHOUT_DECIMALS))
- {
- return round($this->param('amount'));
- }
- return sprintf('%01.2f', $this->param('amount'));
- }
- protected function amount_cents()
- {
- if (in_array($this->param('currency'), Merchant::$CURRENCIES_WITHOUT_DECIMALS))
- {
- return round($this->param('amount'));
- }
- return round($this->param('amount') * 100);
- }
- protected function currency()
- {
- return $this->param('currency');
- }
- protected function currency_numeric()
- {
- $code = $this->param('currency');
- return isset(Merchant::$NUMERIC_CURRENCY_CODES[$code]) ? Merchant::$NUMERIC_CURRENCY_CODES[$code] : 0;
- }
- /**
- * Make a standard HTTP GET request.
- * This method is only public to support the deprecated Merchant::curl_helper() method,
- * and will be marked as protected in a future version.
- *
- * @param string $url The URL to request
- * @param string $username
- * @param string $password
- * @param array $extra_headers
- */
- public function get_request($url, $username = NULL, $password = NULL, $extra_headers = NULL)
- {
- $ch = curl_init($url);
- return $this->_do_curl_request($ch, $username, $password, $extra_headers);
- }
- /**
- * Make a standard HTTP POST request.
- * This method is only public to support the deprecated Merchant::curl_helper() method,
- * and will be marked as protected in a future version.
- *
- * @param string $url The URL to request
- * @param mixed $data An optional string or array of form data which will be appended to the URL
- * @param string $username
- * @param string $password
- * @param array $extra_headers
- */
- public function post_request($url, $data = NULL, $username = NULL, $password = NULL, $extra_headers = NULL)
- {
- $ch = curl_init($url);
- if (is_array($data))
- {
- $data = http_build_query($data);
- }
- curl_setopt($ch, CURLOPT_POST, TRUE);
- curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
- return $this->_do_curl_request($ch, $username, $password, $extra_headers);
- }
- private function _do_curl_request($ch, $username, $password, $extra_headers)
- {
- curl_setopt($ch, CURLOPT_HEADER, FALSE);
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
- curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE);
- curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
- curl_setopt($ch, CURLOPT_CAINFO, MERCHANT_CONFIG_PATH.'/cacert.pem');
- if ($username !== NULL)
- {
- curl_setopt($ch, CURLOPT_USERPWD, $username.':'.$password);
- }
- if ( ! empty($extra_headers))
- {
- curl_setopt($ch, CURLOPT_HTTPHEADER, $extra_headers);
- }
- $response = curl_exec($ch);
- $error = curl_error($ch);
- if ( ! empty($error))
- {
- throw new Merchant_exception($error);
- }
- curl_close($ch);
- return $response;
- }
- /**
- * Redirect the user's browser to a URL.
- */
- protected function redirect($url)
- {
- return Merchant::redirect($url);
- }
- /**
- * Redirect the user's browser to a URL using a POST request.
- */
- protected function post_redirect($url, $data, $message = NULL)
- {
- return Merchant::post_redirect($url, $data, $message);
- }
- }
- class Merchant_exception extends Exception {}
- class Merchant_response
- {
- const AUTHORIZED = 'authorized';
- const COMPLETE = 'complete';
- const FAILED = 'failed';
- const REDIRECT = 'redirect';
- const REFUNDED = 'refunded';
- protected $_status;
- protected $_message;
- protected $_reference;
- protected $_data;
- protected $_redirect_url;
- protected $_redirect_method = 'GET';
- protected $_redirect_message;
- protected $_redirect_data;
- public function __construct($status, $message = NULL, $reference = NULL)
- {
- // support deprecated 'declined' status
- if ($status == 'declined') $status = self::FAILED;
- // always require a valid status
- if ( ! in_array($status, array(self::AUTHORIZED, self::COMPLETE, self::FAILED, self::REDIRECT, self::REFUNDED)))
- {
- throw new InvalidArgumentException(lang('merchant_invalid_status'));
- }
- $this->_status = $status;
- $this->_message = $message;
- $this->_reference = $reference;
- }
- /**
- * The response status.
- * One of self::AUTHORIZED, self::COMPLETE, self::FAILED, self::REDIRECT, or self::REFUNDED
- */
- public function status()
- {
- return $this->_status;
- }
- /**
- * Whether the request was successful.
- */
- public function success()
- {
- return $this->_status !== self::FAILED;
- }
- /**
- * A plain text message returned by the payment gateway.
- */
- public function message()
- {
- return $this->_message;
- }
- /**
- * A transaction reference generated by the payment gateway.
- */
- public function reference()
- {
- return $this->_reference;
- }
- /**
- * The raw response data returned by the payment gateway.
- */
- public function data()
- {
- return $this->_data;
- }
- /**
- * Does this response require a redirect?
- */
- public function is_redirect()
- {
- return $this->_status === self::REDIRECT;
- }
- /**
- * If this response requires a redirect, the URL which must be redirected to.
- */
- public function redirect_url()
- {
- return $this->_redirect_url;
- }
- /**
- * The HTTP redirect method required (either "GET" or "POST").
- */
- public function redirect_method()
- {
- return $this->_redirect_method;
- }
- /**
- * A message to display while redirecting using the POST method.
- */
- public function redirect_message()
- {
- return $this->_redirect_message;
- }
- /**
- * If this response requires a POST redirect, the HTTP form data which must be submitted.
- */
- public function redirect_data()
- {
- return $this->_redirect_data;
- }
- /**
- * Perform the required redirect. If no redirect is required, returns FALSE.
- */
- public function redirect()
- {
- if ($this->is_redirect() == FALSE) return FALSE;
- if ('POST' == strtoupper($this->redirect_method()))
- {
- return Merchant::post_redirect($this->redirect_url(), $this->redirect_data(), $this->redirect_message());
- }
- return Merchant::redirect($this->redirect_url());
- }
- }
- /* End of file ./libraries/merchant/merchant.php */