<?php
/**
* @package XenCentral Framework
* @author Skydevelop EU
* @copyright Drnoyan & Nalyan LDA, Portugal, EU
* @license http://www.dnf.pt/eula.html
* @link http://www.skydevelop.com
* @version 1.4.1
* @revision 125
*/

abstract class XenCentral_PaymentApi_Method
{
	/**
	 * Payment API module
	 * @var XenCentral_PaymentApi_Module
	 */
	protected $_api;
	
	/**
	 * Array containing payment method specific settings
	 * @var array
	 */
	protected $_settings;
	
	/**
	 * Array containing hidden field values
	 * @var array
	 */
	protected $_info;
	
	/**
	 * Array of values that should be assosiated with the transaction but do not need to be sent to server
	 * @var array
	 */
	protected $_additionalInfo;
	
	/**
	 * Transaction type
	 * @var string
	 */
	protected $_type;
	
	/**
	 * Transaction ID assigned by payment processor
	 * @var string
	 */
	protected $_transactionId;
	
	/**
	 * Transaction saved in the database
	 * @var array
	 */
	protected $_transaction;
	
	/**
	 * Array containing errors. Array keys are error codes and values are textual represantation of error codes.
	 * @var array
	 */
	protected $_errors;
	
	protected $_debugMode;
	
	/**
	 * Array containing valid settings information. Should be specified by child classes.
	 * @example array('key1'=>array('title'=>'Key 1', 'required'=>1, 'field'=>1), 'key2'=>array('title'=>'Key 2', 'required'=>0, 'field'=>0));
	 * If field key is set to 1 setting value will be added to hidden form values
	 * @var array
	 */
	protected $_validSettings;
	
	/**
	 * Array containing valid hidden field information. Should be specified by child classes.
	 * @example array('key1'=>array('title'=>'Key 1', 'required'=>1, 'field'=>1), 'key2'=>array('title'=>'Key 2', 'required'=>0, 'field'=>0));
	 * @var array
	 */
	protected $_validFields;
	
	/**
	 * Marker for external applications to decide if response should be sent to the payment gateway or not
	 * @var bool
	 */
	protected $_responseRequired=false;
	
	public function __construct(XenCentral_PaymentApi_Module &$api)
	{
		$this->_api=$api;
		
		if(empty($this->_validFields))
		{
			throw new Exception("Child class should specify possible field values in _validFields array");
		}
		
		if(empty($this->_validSettings))
		{
			throw new Exception("Child class should specify possible setting values in _validSettings array");
		}
	}
	
	public function getApi()
	{
		return $this->_api;
	}
	
	public function setSetting($key, $value)
	{
		if(!isset($this->_validSettings[$key]))
		{
			$this->_setError('invalid_setting', "Invalid setting name - $key");
		}
		
		$this->_settings[$key]=trim($value);
	}
	
	public function removeSetting($key)
	{
		if(!isset($this->_validSettings[$key]))
		{
			$this->_setError('invalid_setting', "Invalid setting name - $key");
		}
		
		if(isset($this->_settings[$key]))
		{
			unset($this->_settings[$key]);
		}
	}
	
	/**
	 * Set settings as array
	 * @param array $settings
	 * @deprecated
	 */
	public function setSettings($settings)
	{
		foreach((array)$settings AS $key=>$setting) {
			$this->setSetting($key, $setting);
		}
	}
	
	/**
	 * Calls private method that generates form components and returns it after validation
	 * @var array
	 */
	public function getFormComponents()
	{
		if(!$this->_validateSettings())
		{
			return false;
		}
		
		if(!$this->_validateInfo())
		{
			return false;
		}
		
		$this->_additionalInfo['transactionhash']=$this->_generateTransactionHash();
		
		$form=$this->_getFormComponents();
		
		if(!isset($form['action']) || !isset($form['method']) || !isset($form['hidden']) || !isset($form['id']))
		{
			throw new Exception("Function\"_getFormComponents\" should return array containing keys 'action', 'method', 'hidden', 'id'");
		}
		
		// form is successfully generated so we need to save data to the database
		$this->_saveNewTransactionInfo();
		
		return $form;
	}
	
	protected function _saveNewTransactionInfo()
	{
		$saveInfo=array (
				'transactionhash' => $this->_additionalInfo['transactionhash'], 
				'method' => get_class($this), 
				'initinfo' => serialize(array (
						'info' => $this->_info, 
						'additionalInfo' => $this->_additionalInfo, 
						'settings' => $this->getSettings(), 
						'debugMode' => $this->getDebugMode() 
				)), 
				'dateline' => time() 
		);
		
		$this->_updateTransaction($saveInfo);
	}
	
	protected function _updateTransaction($saveInfo)
	{
        if($transaction=$this->getApi()->getDb()->fetchRow("
            SELECT transactionhash FROM ". $this->getApi()->getTransactionTableName()."
            WHERE transactionhash=?
        ", array('transactionhash'=>$saveInfo['transactionhash']))) {
            $this->getApi()->getDb()->update(
                $this->getApi()->getTransactionTableName(),
                $saveInfo,
                    'transactionhash='.$this->getApi()->getDb()->quote($saveInfo['transactionhash'])
            );
        } else {
            $this->getApi()->getDb()->insert(
                $this->getApi()->getTransactionTableName(),
                $saveInfo
            );
        }
	}
	
	/**
	 * Return method-specific form components as array, containing keys 'action', 'method', 'hidden', 'id'
	 * Child classes should override the function if any changes are needed to this method 
	 */
	protected function _getFormComponents()
	{
		$form=array ();
		
		$form['action']=$this->_getFormURL();
		$form['method']=$this->_getFormMethod();
		$form['hidden']=$this->_getFormHiddenFields();
		$form['id']=$this->getMethodIdentifier();
		return $form;
	}
	
	protected function _validateSettings()
	{
		// check that all required fields are set
		foreach($this->_validSettings as $key => $info)
		{
			if($info['required'] && !$this->getSetting($key))
			{
				$this->_setError('required_settings_missed', "Missing required settings - $info[title]");
				return false;
			}
			
			if(!$this->getSetting($key) && $info['default'])
			{
				$this->setSetting($key, $info['default']);
			}
			
			if($info['field'])
			{
				$this->setField($key, $this->getSetting($key));
			}
		}
		
		return true;
	}
	
	protected function _validateInfo()
	{
		foreach($this->_validFields as $key => $info)
		{
			if($info['required'] && !$this->getField($key))
			{
				$this->_setError('required_filed_missing', "Missing required field - $info[title]");
				return false;
			}
			
			if(!$this->getField($key) && isset($info['default']))
			{
				$this->setField($key, $info['default']);
			}
		}
		
		return true;
	}
	
	protected function _getFormURL()
	{
		if($this->getDebugMode())
		{
			return $this->_getDebugFormURL();
		}
		
		return $this->_getLiveFormURL();
	}
	
	protected function _setError($errorCode, $errorText)
	{
		$this->_errors[$errorCode]=$errorText;
	}

    public function setError($errorCode, $errorText)
    {
        $this->_setError($errorCode, $errorText);
    }

    public function getLastError()
    {
		if(empty($this->_errors)) {
			return false;
		}

        return end($this->_errors);
    }

	public function getErrors($first=false)
	{
		return $this->_errors;
	}
	
	/**
	 * @return array $_transaction
	 */
	public function getTransaction()
	{
		return $this->_transaction;
	}
	
	/**
	 * @return bool $_debugMode
	 */
	public function getDebugMode()
	{
		return $this->_debugMode;
	}
	
	/**
	 * @return int $_transactionId
	 */
	public function getTransactionId()
	{
		return $this->_transactionId;
	}
	
	/**
	 * @param string $_transactionId
	 */
	public function setTransactionId($_transactionId)
	{
		if(!$_transactionId)
		{
			$this->_setError('empty_transaction_id', "Transaction ID can not be empty.");
			return false;
		}
		
		$this->_transactionId=$_transactionId;

        return true;
	}
	
	/**
	 * @param bool $_debugMode
	 */
	public function setDebugMode($_debugMode)
	{
		$this->_debugMode=$_debugMode;
	}
	
	/**
	 * @return array $_additionalInfo
	 */
	public function getAdditionalInfo()
	{
		return $this->_additionalInfo;
	}
	
	/**
	 * @param array $_additionalInfo
	 */
	public function setAdditionalInfo($_additionalInfo)
	{
		$this->_additionalInfo=$_additionalInfo;
	}
	
	/**
	 * @return array $_settings
	 * @deprecated
	 */
	public function getSettings($key=false)
	{
		//trigger_error('getSettings function is depricated', E_USER_DEPRECATED);
		

		if(!$key)
		{
			return $this->_settings;
		}
		
		return $this->_settings[$key];
	}
	
	public function getSetting($key)
	{
		if(!isset($this->_validSettings[$key]))
		{
			$this->_setError('invalid_setting', "Invalid setting name - $key");
			return false;
		}
		
		if(isset($this->_settings[$key]))
		{
			return $this->_settings[$key];
		}
		
		return false;
	}
	
	public function _validatePaymentHash($hash)
	{
		if(!preg_match('#^[a-zA-Z0-9]{32}$#', $hash))
		{
			$hash='';
		}
		
		return $hash;
	}

    public function getInfoId()
    {
        return $this->_transaction['id'];
    }
	
	public function transactionProcessed()
	{
		$processed=$this->getApi()->getDb()->fetchRow("
                SELECT * FROM " . $this->getApi()->getTransactionLogTableName() . "
                WHERE infoid=?
                AND transactionid=?
		    ",
            array($this->_transaction['id'], $this->getTransactionId())
        );
		
		if($processed)
		{
			return $processed;
		}
		
		return false;
	}
	
	public function saveTransaction()
	{
		$transactionlog=array (
				'infoid' => $this->_transaction['id'], 
				'transactionid' => $this->getTransactionId(), 
				'request' => array (
						'post' => $_POST, 
						'get' => $_GET, 
						'cookie' => $_COOKIE, 
						'server' => $_SERVER 
				), 
				'status' => $this->getType(), 
				'dateline' => time() 
		);
		
		$transactionlog['request']=serialize($transactionlog['request']);

        try {
            $this->getApi()->getDb()->insert($this->getApi()->getTransactionLogTableName(), $transactionlog);
        } catch(Exception $ex) {
            $this->getApi()->getDb()->update(
                $this->getApi()->getTransactionLogTableName(),
                $transactionlog,
                'infoid='.$this->getApi()->getDb()->quote($transactionlog['infoid']).' AND transactionid='.$this->getApi()->getDb()->quote($transactionlog['transactionid'])
            );
        }
	}
	
	protected function _loadTransaction($paymentHash)
	{
		$transaction=$this->getApi()->getDb()->fetchRow("
			SELECT * FROM " . $this->getApi()->getTransactionTableName() . "
			WHERE `transactionhash`=?
		", array($paymentHash));
		
		if(!$transaction)
		{
			$this->_setError('invalid_hash', "Invalid payment hash is specified.");
			return false;
		}
		
		if(get_class($this) !== $transaction['method'])
		{
			$this->_setError('method_mismatch', "Payment method used does not match the method requested.");
			return false;
		}
		
		$this->_transaction=$transaction;
		
		$initinfo=unserialize($transaction['initinfo']);
		
		$this->setAdditionalInfo($initinfo['additionalInfo']);
		
		$this->_info=$initinfo['info'];
		
		$this->setSettings($initinfo['settings']);
		
		$this->setDebugMode($initinfo['debugMode']);
		
		return true;
	}
	
	public function validateRequest()
	{
	    $paymentHash=$this->_getTransactionHashFromRequest();
		
		if(!$paymentHash)
		{
		    $this->_setError('invalid_hash', "Invalid payment hash is specified.");
			$this->sendFailResponse();
			return false;
		}
		if(!$this->_loadTransaction($paymentHash))
		{
		    if(!$this->hasErrors())
			{
			    $this->_setError('transaction_load', "Could not load proper transaction for the request.");
			}
			$this->sendFailResponse();
			return false;
		}

        if(!$this->_validateRequest())
		{
		    if(!$this->hasErrors())
			{
			    $this->_setError('validate_request', "Could not validate request.");
			}
			$this->sendFailResponse();
			return false;
		}

		if(!$this->_setTransactionIdFromRequest()) {
            return false;
        }

		$this->_setTypeFromRequest();

		$this->sendSuccessResponse();

		return !$this->hasErrors();
	}
	
	public function hasErrors()
	{
		return !empty($this->_errors);
	}
	
	protected function sendSuccessResponse()
	{
	
	}
	
	protected function sendFailResponse()
	{
	
	}
	
	public function setField($key, $value)
	{
		if(!isset($this->_validFields[$key]))
		{
			$this->_setError('invalid_field', "Invalid field name specified - $key");
		}
		
		$this->_info[$key]=$value;
	}
	
	public function getField($key)
	{
		if(!isset($this->_validFields[$key]))
		{
			$this->_setError('invalid_field', "Invalid field name specified - $key");
		}
		
		return isset($this->_info[$key])? $this->_info[$key]:false;
	}
	
	public function unsetField($key)
	{
		if(!isset($this->_validFields[$key]))
		{
			$this->_setError('invalid_field', "Invalid field name specified - $key");
		}
		
		if(isset($this->_info[$key]))
		{
			unset($this->_info[$key]);
		}
	}
	
	public function setType($type)
	{
		if(!in_array($type, array (
				'payment', 
				'refund',
                'pending'
		)))
		{
			$this->_setError('unknown_transaction_type', "Unkown transaction type - $type.");
			return false;
		}
		
		$this->_type=$type;
        return true;
	}
	
	public function getType()
	{
		return $this->_type;
	}

	protected function _remoteRequest($url, $postContent, &$connectionErrors)
	{
		return self::remoteRequest($url, $postContent, $connectionErrors);
	}
	
	public static function remoteRequest($url, $postContent, &$connectionErrors)
	{
		$parsedURL=parse_url($url);
		
		$result=false;
		
		$connectionErrors=array ();
		
		if(function_exists('curl_init') and $ch=curl_init())
		{
			curl_setopt($ch, CURLOPT_URL, $url);
			curl_setopt($ch, CURLOPT_TIMEOUT, 10);
			curl_setopt($ch, CURLOPT_POST, true);
			curl_setopt($ch, CURLOPT_POSTFIELDS, $postContent);
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
			curl_setopt($ch, CURLOPT_USERAGENT, 'XenCentral Framework Payment API');
			
			$result=curl_exec($ch);
			
			if($result === false)
			{
                if ($result === false AND curl_errno($ch) == '60')
                {
                    curl_setopt($ch, CURLOPT_CAINFO, dirname(__FILE__) . '/ca-bundle.crt');
                    $result = curl_exec($ch);
                }
                if($result===false) {
                    if (curl_errno($ch)) {
                        $connectionErrors[] = curl_errno($ch) . ': ' . curl_error($ch);
                    } else {
                        $connectionErrors[] = 'Failed to connect to Payment API server via CURL.';
                    }
                }
			}

            curl_close($ch);
		}
		
		if($result === false)
		{
			$header="POST /cgi-bin/webscr HTTP/1.0\r\n";
			$header.="Host: " . $parsedURL['host'] . "\r\n";
			$header.="Content-Type: application/x-www-form-urlencoded\r\n";
			$header.="Content-Length: " . strlen($postContent) . "\r\n\r\n";
			if($fp=fsockopen($parsedURL['host'], 80, $errno, $errstr, 10))
			{
				socket_set_timeout($fp, 15, 0);
				fwrite($fp, $header . $postContent);
				while (!feof($fp))
				{
					$result=fgets($fp, 1024);
					if(strcmp($result, 'VERIFIED') == 0)
					{
						break;
					}
				}
				fclose($fp);
			}
			else
			{
				if($errno)
				{
					$connectionErrors[]=$errno . ': ' . $errstr;
				}
				else
				{
					$connectionErrors[]='Failed to connect to Payment API server via fsockopen.';
				}
			}
		}
		
		return $result;
	}
	
	/**
	 * @return bool $_responseRequired
	 */
	public function getResponseRequired()
	{
		return $this->_responseRequired;
	}
	
	protected abstract function _setTransactionIdFromRequest();
	
	protected abstract function _setTypeFromRequest();
	
	protected abstract function _validateRequest();
	
	protected abstract function _getTransactionHashFromRequest();
	
	/**
	 * Generates unique transaction hash based on transaction info
	 */
	protected abstract function _generateTransactionHash();
	
	/**
	 * Returns form action attribute if debug mode is enabled
	 */
	protected abstract function _getDebugFormURL();
	
	/**
	 * Returns form action attribute if debug mode is disabled
	 */
	protected abstract function _getLiveFormURL();
	
	/**
	 * Returns form method
	 */
	protected abstract function _getFormMethod();
	
	/**
	 * Returns identifier string for the payment method
	 * @param $formatted bool If set to true full processor name should be returned
	 */
	public abstract function getMethodIdentifier($formatted=false);
	
	/**
	 * Returns form hidden inputs
	 */
	protected abstract function _getFormHiddenFields();
}