Index: CRM/Core/Payment/Realex.php
===================================================================
--- CRM/Core/Payment/Realex.php	(revision 0)
+++ CRM/Core/Payment/Realex.php	(revision 0)
@@ -0,0 +1,454 @@
+<?php
+
+/*
+ * Copyright (C) 2009
+ * Licensed to CiviCRM under the Academic Free License version 3.0.
+ *
+ * Written and contributed by Kirkdesigns (http://www.kirkdesigns.co.uk)
+ *
+ */
+
+/**
+ *
+ * @package CRM
+ * @author Tom Kirkpatrick <tkp@kirkdesigns.co.uk>
+ * $Id$
+ */
+
+require_once 'CRM/Core/Payment.php';
+
+class CRM_Core_Payment_Realex extends CRM_Core_Payment {
+	const CHARSET = 'iso-8859-1';
+	const AUTH_APPROVED = '00';
+
+	static protected $_mode = null;
+
+	static protected $_params = array();
+
+	/**
+	 * Constructor
+	 *
+	 * @param string $mode the mode of operation: live or test
+	 *
+	 * @return void
+	 */
+
+	function __construct( $mode, &$paymentProcessor ) {
+		$this->_mode = $mode;
+		$this->_paymentProcessor = $paymentProcessor;
+
+		$this->_setParam( 'merchant_ref', $paymentProcessor['user_name'] );
+		$this->_setParam( 'secret' , $paymentProcessor['password']  );
+		$this->_setParam( 'account' , $paymentProcessor['subject']  );
+
+		$this->_setParam( 'emailCustomer', 'TRUE' );
+		srand( time( ) );
+		$this->_setParam( 'sequence', rand( 1, 1000 ) );
+	}
+
+	function setExpressCheckOut( &$params ) {
+		CRM_Core_Error::fatal( ts( 'This function is not implemented' ) );
+	}
+	function getExpressCheckoutDetails( $token ) {
+		CRM_Core_Error::fatal( ts( 'This function is not implemented' ) );
+	}
+	function doExpressCheckout( &$params ) {
+		CRM_Core_Error::fatal( ts( 'This function is not implemented' ) );
+	}
+	function doTransferCheckout( &$params ) {
+		CRM_Core_Error::fatal( ts( 'This function is not implemented' ) );
+	}
+
+	/**
+	 * Submit a payment using Advanced Integration Method
+	 *
+	 * @param  array $params assoc array of input parameters for this transaction
+	 * @return array the result in a nice formatted array (or an error object)
+	 * @public
+	 */
+	function doDirectPayment( &$params ) {
+		if ( ! defined( 'CURLOPT_SSLCERT' ) ) {
+			return self::error( 9001, 'RealAuth requires curl with SSL support' );
+		}
+
+		$result = $this->setRealexFields($params);
+
+		if ($result !== true) {
+			return $result;
+		}
+
+		/**********************************************************
+		 * Check to see if we have a duplicate before we send
+		 **********************************************************/
+		if ( $this->_checkDupe( $this->_getParam('order_id'))) {
+			return self::error(9004, 'It appears that this transaction is a duplicate.  Have you already submitted the form once?  If so there may have been a connection problem.  Check your email for a receipt from Authorize.net.  If you do not receive a receipt within 2 hours you can try your transaction again.  If you continue to have problems please contact the site administrator.' );
+		}
+
+		// Create sha1 hash for request
+		$hashme = "{$this->_getParam('timestamp')}.{$this->_getParam('merchant_ref')}.{$this->_getParam('order_id')}.{$this->_getParam('amount')}.{$this->_getParam('currency')}.{$this->_getParam('card_number')}";
+		$sha1hash = sha1($hashme);
+		$hashme = "$sha1hash.{$this->_getParam('secret')}";
+		$sha1hash = sha1($hashme);
+
+
+		// Generate the request xml that is send to Realex Payments.
+		$request_xml = "<request type='auth' timestamp='{$this->_getParam('timestamp')}'>
+					<merchantid>{$this->_getParam('merchant_ref')}</merchantid>
+					<account>{$this->_getParam('account')}</account>
+					<orderid>{$this->_getParam('order_id')}</orderid>
+					<amount currency='{$this->_getParam('currency')}'>{$this->_getParam('amount')}</amount>
+					<card> 
+						<number>{$this->_getParam('card_number')}</number>
+						<expdate>{$this->_getParam('exp_date')}</expdate>
+						<type>{$this->_getParam('card_type')}</type> 
+						<chname>{$this->_getParam('card_name')}</chname> 
+						<issueno>{$this->_getParam('issue_number')}</issueno>
+						<cvn>
+							<number>{$this->_getParam('cvn')}</number>
+							<presind>1</presind>
+						</cvn>
+					</card> 
+					<autosettle flag='1'/>
+					<sha1hash>$sha1hash</sha1hash>
+		  	</request>";
+
+		// Log the Realex response for debugging
+		// CRM_Core_Error::debug_var('REALEX --------- REquest sent to Realex: ', $request_xml, TRUE);
+
+		/**********************************************************
+		 * Send to the payment processor using cURL
+		 **********************************************************/
+
+		$submit = curl_init( $this->_paymentProcessor['url_site'] );
+
+		if ( !$submit ) {
+			return self::error(9002, 'Could not initiate connection to payment gateway');
+		}
+
+		curl_setopt($submit, CURLOPT_HTTPHEADER, array('SOAPAction: ""'));
+		curl_setopt($submit, CURLOPT_RETURNTRANSFER, 1);
+		curl_setopt($submit, CURLOPT_TIMEOUT, 60);
+		curl_setopt($submit, CURLOPT_SSL_VERIFYPEER, FALSE);
+		curl_setopt($submit, CURLOPT_HEADER, 0);
+
+		// take caching out of the picture
+		curl_setopt($submit, CURLOPT_FORBID_REUSE, 1);
+		curl_setopt($submit, CURLOPT_FRESH_CONNECT, 1);
+
+		// Apply the XML to our curl call
+		curl_setopt($submit, CURLOPT_POST, 1);
+		curl_setopt($submit, CURLOPT_POSTFIELDS, $request_xml);
+
+		$response_xml = curl_exec($submit);
+
+		if (!$response_xml) {
+			return self::error( curl_errno($submit), curl_error($submit) );
+		}
+
+		curl_close( $submit );
+
+		// Tidy up the responce xml
+		$response_xml = eregi_replace ( "[[:space:]]+", " ", $response_xml );
+		$response_xml = eregi_replace ( "[\n\r]", "", $response_xml );
+
+		// Parse the response xml
+		$xml_parser = xml_parser_create();
+		if (!xml_parse($xml_parser, $response_xml)) {
+			return self::error( 9003, 'XML Error' );
+		}
+
+		$response = $this->xml_parse_into_assoc($response_xml);
+		$response = $response['#return']['RESPONSE'];
+
+		// Log the Realex response for debugging
+		// CRM_Core_Error::debug_var('REALEX --------- Response from Realex: ', $response, TRUE);
+
+		// Return an error if authentication was not successful
+		if ($response['RESULT'] !== self::AUTH_APPROVED) {
+			return self::error( $response['RESULT'], ' '.$response['MESSAGE'] );
+		}
+
+		// Check the response hash
+		$hashme = "{$this->_getParam('timestamp')}.{$this->_getParam('merchant_ref')}.{$this->_getParam('order_id')}.{$response['RESULT']}.{$response['MESSAGE']}.{$response['PASREF']}.{$response['AUTHCODE']}";
+		$sha1hash = sha1($hashme);
+		$hashme = "$sha1hash.{$this->_getParam('secret')}";
+		$sha1hash = sha1($hashme);
+
+		if ($response['SHA1HASH'] != $sha1hash) {
+			// FIXME: Need to actually check this - I couldn't get the
+			// hashes to match so I'm commenting out for now'
+			// return self::error( 9001, "Hash error, please report this to the webmaster" );
+		}
+
+		// FIXME: We are using the trxn_result_code column to store all these extra details since there
+		// seems to be nowhere else to put them. This is THE WRONG THING TO DO!
+		$extras = array(
+          'authcode' => $response['AUTHCODE'],
+          'batch_id' => $response['BATCHID'],
+          'message'  => $response['MESSAGE'],
+          'trxn_result_code' => $response['RESULT'],
+		);
+		$params['trxn_id'] = $response['PASREF'];
+		$params['trxn_result_code'] = serialize($extras);
+		$params['currencyID'] = $this->_getParam('currency');
+		$params['gross_amount'] = $this->_getParam('amount');
+		$params['fee_amount'] = 0;
+
+		return $params;
+	}
+
+	/**
+		* Helper function to convert XML string to multi-dimension array.
+		*
+		* @param $xml
+		*   an XML string.
+		*
+		* @return
+		*   An array of the result with following keys:
+		*   - #error  : false, if no error. Otherwise, it is the error message
+		*   - #return : a multi-dimension associative array represent the value
+		*               of the XML input string.
+		*/
+	function xml_parse_into_assoc($xml) {
+		$input  = array();
+		$result = array();
+
+		$result['#error']  = FALSE;
+		$result['#return'] = NULL;
+
+		$xmlparser = xml_parser_create();
+		$ret = xml_parse_into_struct($xmlparser, $xml, $input);
+		xml_parser_free($xmlparser);
+
+		if ( empty($input) )
+		{
+			$result['#return'] = $xml;
+		}
+		else {
+			if ( $ret > 0 ) {
+				$result['#return'] = $this->_xml_parse($input);
+			}
+			else {
+				$result['#error'] = t ('Error parsing XML result - error code = '. xml_get_error_code($xmlparser) .' at '.
+		                            'line '. xml_get_current_line_number($xmlparser) .' '.
+		                            'char '. xml_get_current_column_number($xmlparser));
+			}
+		}
+		return $result;
+	}
+
+	// private helper for  xml_parse_into_assoc, to recusively parsing the result
+	function _xml_parse($input, $depth = 1) {
+		$output = array();
+		$children = array();
+
+		foreach( $input as $data )
+		{
+			if ( $data['level'] == $depth ) {
+				switch($data['type']) {
+					case 'complete':
+						$output[$data['tag']] = $data['value'];
+						break;
+							
+					case 'open':
+						$children = array();
+						break;
+
+					case 'close':
+						$output[$data['tag']] = $this->_xml_parse($children, $depth + 1);
+						break;
+				}
+			}
+			else {
+				$children[] = $data;
+			}
+		}
+		return $output;
+	}
+
+
+	/**
+		*	Format the params from the form ready for sending to Realex.  Also perform some validation
+		*/
+	function setRealexFields ( &$params ) {
+		if ((int)$params['amount'] <= 0) {
+			return self::error( 9001, 'Amount must be positive' );
+		}
+
+		// format amount to be in smallest possible units
+		list($bills, $pennies) = explode('.', $params['amount']);
+		$this->_setParam('amount', 100 * $bills + $pennies);
+
+		switch ($params['credit_card_type']) {
+			case 'MasterCard':
+			case 'Mastercard':
+				$this->_setParam('card_type', 'MC');
+				$this->_setParam('requiresIssueNumber', false);
+				break;
+			case 'Visa':
+				$this->_setParam('card_type', 'VISA');
+				$this->_setParam('requiresIssueNumber', false);
+				break;
+			case 'Amex':
+				$this->_setParam('card_type', 'AMEX');
+				$this->_setParam('requiresIssueNumber', false);
+				break;
+			case 'Laser':
+				$this->_setParam('card_type', 'LASER');
+				$this->_setParam('requiresIssueNumber', false);
+				break;
+			case 'Maestro':
+				$this->_setParam('card_type', 'SWITCH');
+				$this->_setParam('requiresIssueNumber', true);
+				break;
+			case 'Switch':
+				$this->_setParam('card_type', 'SWITCH');
+				$this->_setParam('requiresIssueNumber', true);
+				break;
+			case 'Solo':
+				$this->_setParam('card_type', 'SWITCH');
+				$this->_setParam('requiresIssueNumber', true);
+				break;
+			default:
+				return self::error( 9001, 'Credit card type not supported by Realex: '. $params['credit_card_type'] );
+		}
+
+		// get the card holder name - cater cor customized billing forms
+		if (isset($params['cardholder_name'])) {
+			$credit_card_name = $params['cardholder_name'];
+		}
+		else {
+			$credit_card_name  = $params['first_name'] . " ";
+			if (!empty($params['middle_name'])) $credit_card_name .= $params['middle_name'] . " ";
+			$credit_card_name .= $params['last_name'];
+		}
+
+		$this->_setParam('card_name',     $credit_card_name);
+		$this->_setParam('card_number',   $params['credit_card_number']);
+		$this->_setParam('cvn',           $params['cvv2']);
+		$this->_setParam('country',       $params['country']);
+		$this->_setParam('post_code',     $params['postal_code']);
+		$this->_setParam('order_id',      $params['invoiceID']);
+		$this->_setParam('issue_number',  $params['issue_number']);
+		// $this->_setParam('currency',      $params['currencyID']);
+
+		// set the currency to the default which can be overrided.
+		$config =& CRM_Core_Config::singleton( );
+		$this->_setParam('currency', $config->defaultCurrency);
+
+		// Format the expiry date to MMYY
+		$expmonth = (string)$params['credit_card_exp_date']['M'];
+		$expmonth = (strlen($expmonth) === 1) ? '0'.$expmonth : $expmonth;
+		$expyear = substr((string)$params['credit_card_exp_date']['Y'], 2, 2);
+		$this->_setParam('exp_date',  $expmonth . $expyear);
+
+		if ( (strlen($params['credit_card_start_date']['M']) !== 0) && (strlen($params['credit_card_start_date']['Y']) !== 0) ) {
+			$startmonth = (string)$params['credit_card_start_date']['M'];
+			$startmonth = (strlen($startmonth) === 1) ? '0'.$startmonth : $startmonth;
+			$startyear = substr((string)$params['credit_card_start_date']['Y'], 2, 2);
+			$this->_setParam('start_date',  $startmonth . $startyear);
+		}
+
+		// Create timestamp
+		$timestamp = strftime("%Y%m%d%H%M%S");
+		$this->_setParam('timestamp', $timestamp);
+
+		return true;
+	}
+
+	/**
+	 * Checks to see if invoice_id already exists in db
+	 * @param  int     $invoiceId   The ID to check
+	 * @return bool                  True if ID exists, else false
+	 */
+	function _checkDupe( $invoiceId ) {
+		require_once 'CRM/Contribute/DAO/Contribution.php';
+		$contribution =& new CRM_Contribute_DAO_Contribution( );
+		$contribution->invoice_id = $invoiceId;
+		return $contribution->find( );
+	}
+
+	/**
+	 * Get the value of a field if set
+	 *
+	 * @param string $field the field
+	 * @return mixed value of the field, or empty string if the field is
+	 * not set
+	 */
+	function _getParam ( $field ) {
+		if ( isset( $this->_params[$field] ) ) {
+			return $this->_params[$field];
+		}
+		else {
+			return '';
+		}
+	}
+
+	/**
+	 * Set a field to the specified value.  Value must be a scalar (int,
+	 * float, string, or boolean)
+	 *
+	 * @param string $field
+	 * @param mixed $value
+	 * @return bool false if value is not a scalar, true if successful
+	 */
+	function _setParam ( $field, $value ) {
+		if ( ! is_scalar($value) ) {
+			return false;
+		}
+		else {
+			$this->_params[$field] = $value;
+		}
+	}
+
+	function &error ( $errorCode = null, $errorMessage = null ) {
+		$e =& CRM_Core_Error::singleton( );
+
+		if ( $errorCode ) {
+			if ($errorCode == '101' || $errorCode == '102') {
+				$display_error = 'Card declined by bank. Please try with a different card.';
+			}
+			elseif ($errorCode == '103') {
+				$display_error = 'Card reported lot or stolen - This incident will be reported.';
+			}
+			elseif ( $errorCode == '501') {
+				$display_error = "It appears that this transaction is a duplicate. Have you already submitted the form once? If so there may have been a connection problem. Check your email for a receipt for this transaction.  If you do not receive a receipt within 2 hours you can try your transaction again.  If you continue to have problems please contact the site administrator.";
+			}
+			elseif ( $errorCode == '509') {
+				$display_error = $errorMessage;
+			}
+			else {
+				$display_error = 'We were unable to process your payment at theis time. Please try again later.';
+			}
+			$e->push( $errorCode, 0, null, $display_error );
+		}
+		else {
+			$e->push( 9001, 0, null, 'We were unable to process your payment at theis time. Please try again later.' );
+		}
+		return $e;
+	}
+
+	/**
+	 * This function checks to see if we have the right config values
+	 *
+	 * @return string the error message if any
+	 * @public
+	 */
+	function checkConfig( ) {
+		$error = array();
+		if ( empty( $this->_paymentProcessor['user_name'] ) ) {
+			$error[] = ts( 'Merchant ID is not set for this payment processor' );
+		}
+
+		if ( empty( $this->_paymentProcessor['password'] ) ) {
+			$error[] = ts( 'Secret is not set for this payment processor' );
+		}
+
+		if ( ! empty( $error ) ) {
+			return implode( '<p>', $error );
+		} else {
+			return null;
+		}
+	}
+
+}
Index: CRM/Contribute/Payment/Realex.php
===================================================================
--- CRM/Contribute/Payment/Realex.php	(revision 0)
+++ CRM/Contribute/Payment/Realex.php	(revision 0)
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @package CRM
+ * @author John Griffin <john.griffin@enableinteractive.co.uk>
+ * @author Tom Kirkpatrick <tkp@kirkdesigns.co.uk>
+ * $Id:$
+ **/
+
+require_once 'CRM/Core/Payment/Realex.php';
+
+class CRM_Contribute_Payment_Realex extends CRM_Core_Payment_Realex {
+    /** 
+     * We only need one instance of this object. So we use the singleton 
+     * pattern and cache the instance in this variable 
+     *
+     * @var object 
+     * @static 
+     */
+    static private $_singleton = null;
+    
+    /**
+     * Constructor
+     *
+     * @param string $mode the mode of operation: live or test
+     *
+     * @return void
+     */
+    function __construct( $mode, &$paymentProcessor ) {
+        parent::__construct( $mode, $paymentProcessor );
+    }
+
+    /**
+     * singleton function used to manage this object
+     *
+     * @param string $mode the mode of operation: live or test
+     *
+     * @return object
+     * @static
+     */
+    static function &singleton( $mode, &$paymentProcessor ) {
+        if (self::$_singleton === null ) {
+            self::$_singleton =& new CRM_Contribute_Payment_Realex( $mode, $paymentProcessor );
+        }
+        return self::$_singleton;
+    }
+
+}
+
+?>
\ No newline at end of file

