<?php
namespace Netopia\Netcard\Mobilpay\Payment\Request;
/**
 * Class MobilpayPaymentRequestAbstract
 * This class can be used for accessing mobilpay.ro payment interface for your configured online services
 * @copyright NETOPIA
 * @author Claudiu Tudose
 * @version 1.0
 *
 * This class uses  OpenSSL
 * In order to use the OpenSSL functions you need to install the OpenSSL package.
 * Check PHP documentation for installing OpenSSL package
 */

use Netopia\Netcard\Mobilpay\Payment\Request\MobilpayPaymentRequestSms; 
abstract class MobilpayPaymentRequestAbstract
{
	const PAYMENT_TYPE_SMS	= 'sms';
	const PAYMENT_TYPE_CARD	= 'card';

	const CONFIRM_ERROR_TYPE_NONE		= 0x00;
	const CONFIRM_ERROR_TYPE_TEMPORARY	= 0x01;
	const CONFIRM_ERROR_TYPE_PERMANENT	= 0x02;

	const ERROR_LOAD_X509_CERTIFICATE	= 0x10000001;
	const ERROR_ENCRYPT_DATA			= 0x10000002;

	const ERROR_PREPARE_MANDATORY_PROPERTIES_UNSET	= 0x11000001;

	const ERROR_FACTORY_BY_XML_ORDER_ELEM_NOT_FOUND			= 0x20000001;
	const ERROR_FACTORY_BY_XML_ORDER_TYPE_ATTR_NOT_FOUND	= 0x20000002;
	const ERROR_FACTORY_BY_XML_INVALID_TYPE					= 0x20000003;

	const ERROR_LOAD_FROM_XML_ORDER_ID_ATTR_MISSING			= 0x30000001;
	const ERROR_LOAD_FROM_XML_SIGNATURE_ELEM_MISSING		= 0x30000002;

	const ERROR_CONFIRM_LOAD_PRIVATE_KEY					= 0x300000f0;
	const ERROR_CONFIRM_FAILED_DECODING_DATA				= 0x300000f1;
	const ERROR_CONFIRM_FAILED_DECODING_ENVELOPE_KEY		= 0x300000f2;
	const ERROR_CONFIRM_FAILED_DECRYPT_DATA					= 0x300000f3;
	const ERROR_CONFIRM_INVALID_POST_METHOD					= 0x300000f4;
	const ERROR_CONFIRM_INVALID_POST_PARAMETERS				= 0x300000f5;
	const ERROR_CONFIRM_INVALID_ACTION						= 0x300000f6;
	const ERROR_CONFIRM_FAILED_DECODING_IV 					= 0x300000f7;
	const ERROR_REQUIRED_CIPHER_NOT_AVAILABLE				= 0x300000f8;

	const VERSION_QUERY_STRING	= 0x01;
    const VERSION_XML			= 0x02;
	#declare member variables
	/**
	 * signatue (Mandatory) 	- signature received from mobilpay.ro that identifies merchant account
	 *
	 * @var string(64)
	 */
	public $signature 	= null;
	/**
	 * service - identifier of service/product for which you're requesting a payment
	 * Mandatory for MobilpayPaymentRequestSms
	 * Optional for MobilpayPaymentRequestCard
	 */
	public $service		= null;

	/**
	 * orderId (Mandatory)	- payment transaction identifier generated by merchant; helps merchant to interpret a request to confirm or return url;
	 * it should be unique for the specified signature
	 *
	 * @var string(64)
	 */
	public $orderId		= null;
	public $timestamp	= null;
	public $type		= self::PAYMENT_TYPE_SMS;

	public $objPmNotify	= null;

	/**
	 * returnUrl (Optional) 	- URL where the user is redirected from mobilpay.ro payment interface
	 * when the transaction is canceled or confirmed. If it is not supplied the application will use
	 * return URL configured in control panel
	 * use it only if you want to overwrite the configured one otherwise set it's value to NULL
	 *
	 * @var string
	 */
	public $returnUrl 	= null;

	/**
	 * confirmUrl (Optional)	- URL of the seller that will be requested when mobilpay.ro will make
	 * a decision about payment (e.g. confirmed, canceled). If it is not supplied the application will use
	 * confirm URL configured in control panel
	 * use it only if you want to overwrite the configured one otherwise set it's value to NULL
	 *
	 * @var string
	 */
	public $confirmUrl 	= null;

	/**
	 * @var string
	 */
	public $ipnCipher = null;

	public $params		= array();

	/**
	 * outEnvKey - output envelope key
	 * in this property is stored the envelope key after encrypting data to send to payment interface
	 */
	private $outEnvKey	= null;

	/**
	 * outEncData - output encrypted data
	 * in this property is stored the encrypted data to send to payment interface
	 */
	private $outEncData	= null;

	/**
	 * cipher algorithm used to encrypt data
	 * @var string
	 */
	protected $outCipher = null;
	
	/**
	 * intialization vector seed used to encrypt data
	 * @var string
	 */
	protected $outIv = null;
	
	protected $_xmlDoc	= null;

	protected $_requestIdentifier	= null;

	protected $_objRequestParams= null;
	protected $_objRequestInfo	= null;

	public $objReqNotify		= null;
	public $selectedInstallments 	= null;
	public $reqInstallments 		= null;
	public $secretCode 				= null;
	public $paymentInstrument 		= null;

	public function __construct()
	{
		/**
		 * Error "Deprecated: Implicit conversion,..."
		 * On Converting float to int
		 */
		// srand((double) microtime() * 1000000);
        // $this->_requestIdentifier = md5(uniqid(rand()));

		/**
		 * Generating the random number and MD5 hash.
		 */
		$microtime = microtime();
		list($usec, $sec) = explode(" ", $microtime);
		$seed = (int)($sec * 1000000 + $usec);
		srand($seed);
        $this->_requestIdentifier = md5(uniqid(rand(), true));

        $this->_objRequestParams = new \stdClass();
	}

	abstract protected function _prepare();
	abstract protected function _loadFromXml(\DOMElement $elem);

	static public function factory($data)
	{
		$objPmReq = null;
		$xmlDoc = new \DOMDocument();
		if(@$xmlDoc->loadXML($data) === true)
		{
			//try to create payment request from xml
			$objPmReq = MobilpayPaymentRequestAbstract::_factoryFromXml($xmlDoc);
			$objPmReq->_setRequestInfo(self::VERSION_XML, $data);
		}
		else
		{
			//try to create payment request from query string
			$objPmReq = MobilpayPaymentRequestAbstract::_factoryFromQueryString($data);
			$objPmReq->_setRequestInfo(self::VERSION_QUERY_STRING, $data);
		}

		return $objPmReq;
	}

	static public function factoryFromEncrypted($envKey, $encData, $privateKeyFilePath, $privateKeyPassword = null, $cipher_algo = 'rc4', $iv = null)
	{
		$privateKey = null;
		if($privateKeyPassword == null)
		{
			$privateKey = @openssl_get_privatekey($privateKeyFilePath);
			if ($privateKey === false)
			{
				$privateKey = @openssl_get_privatekey("file://{$privateKeyFilePath}");
			}
		}
		else
		{
			$privateKey = @openssl_get_privatekey($privateKeyFilePath, $privateKeyPassword);
			if ($privateKey === false)
			{
				$privateKey = @openssl_get_privatekey("file://{$privateKeyFilePath}", $privateKeyPassword);
			}
		}
		if($privateKey === false)
        {
        	throw new \Exception('Error loading private key', self::ERROR_CONFIRM_LOAD_PRIVATE_KEY);
        }

        $srcData = base64_decode($encData);
		if($srcData === false)
		{
			@openssl_free_key($privateKey);
			throw new \Exception('Failed decoding data', self::ERROR_CONFIRM_FAILED_DECODING_DATA);
		}

		$srcEnvKey = base64_decode($envKey);
		if($srcEnvKey === false)
		{
			throw new \Exception('Failed decoding envelope key', self::ERROR_CONFIRM_FAILED_DECODING_ENVELOPE_KEY);
		}

		$srcIv = base64_decode($iv);
		if($srcIv === false)
		{
			throw new \Exception('Failed decoding initialization vector', self::ERROR_CONFIRM_FAILED_DECODING_IV);
		}

		$data = null;
		if(PHP_VERSION_ID >= 70000)
		{
			$result = @openssl_open($srcData, $data, $srcEnvKey, $privateKey, $cipher_algo, $srcIv);
		}
		else
		{
			$result = @openssl_open($srcData, $data, $srcEnvKey, $privateKey, $cipher_algo);
		}
		
		if($result === false)
		{
			throw new \Exception('Failed decrypting data', self::ERROR_CONFIRM_FAILED_DECRYPT_DATA);
		}

		return MobilpayPaymentRequestAbstract::factory($data);
	}

	static protected function _factoryFromXml(\DOMDocument $xmlDoc)
	{
		$elems = $xmlDoc->getElementsByTagName('order');
		if($elems->length != 1)
		{
			throw new \Exception('factoryFromXml order element not found', MobilpayPaymentRequestAbstract::ERROR_FACTORY_BY_XML_ORDER_ELEM_NOT_FOUND);
		}
		$orderElem = $elems->item(0);

		$attr = $orderElem->attributes->getNamedItem('type');
		if($attr == null || strlen($attr->nodeValue) == 0)
		{
			throw new \Exception('factoryFromXml invalid payment request type=' . $attr->nodeValue, MobilpayPaymentRequestAbstract::ERROR_FACTORY_BY_XML_ORDER_TYPE_ATTR_NOT_FOUND);
		}
		switch ($attr->nodeValue)
		{
		case MobilpayPaymentRequestAbstract::PAYMENT_TYPE_CARD:
			$objPmReq = new MobilpayPaymentRequestCard();
			break;
		case MobilpayPaymentRequestAbstract::PAYMENT_TYPE_SMS:
			$objPmReq =  new MobilpayPaymentRequestSms();
			break;
		default:
			throw new \Exception('factoryFromXml invalid payment request type=' . $attr->nodeValue, MobilpayPaymentRequestAbstract::ERROR_FACTORY_BY_XML_INVALID_TYPE);
			break;
		}
		$objPmReq->_loadFromXml($orderElem);

		return $objPmReq;
	}

	static protected function _factoryFromQueryString($data)
	{
		$objPmReq = new MobilpayPaymentRequestSms();
		$objPmReq->_loadFromQueryString($data);
		return $objPmReq;
	}

	protected function _setRequestInfo($reqVersion, $reqData)
	{
		$this->_objRequestInfo = new \stdClass();
		$this->_objRequestInfo->reqVersion 	= $reqVersion;
		$this->_objRequestInfo->reqData 	= $reqData;
	}

	public function getRequestInfo()
	{
		return $this->_objRequestInfo;
	}

	protected function _parseFromXml(\DOMNode $elem)
	{
		$xmlAttr = $elem->attributes->getNamedItem('id');
		if($xmlAttr == null || strlen((string)$xmlAttr->nodeValue) == 0)
		{
			throw new \Exception('MobilpayPaymentRequestSms::_parseFromXml failed: empty order id', self::ERROR_LOAD_FROM_XML_ORDER_ID_ATTR_MISSING);
		}
		$this->orderId = $xmlAttr->nodeValue;

		$elems = $elem->getElementsByTagName('signature');
		if($elems->length != 1)
		{
			throw new \Exception('MobilpayPaymentRequestSms::loadFromXml failed: signature is missing', self::ERROR_LOAD_FROM_XML_SIGNATURE_ELEM_MISSING);
		}

		$xmlAttr = $elem->attributes->getNamedItem('secretcode');
		if ($xmlAttr == null || strlen((string) $xmlAttr->nodeValue) == 0)
		{
			$this->secretCode = '';
		}
		else
		{
			$this->secretCode = $xmlAttr->nodeValue;
		}

		$xmlElem = $elems->item(0);
		$this->signature = $xmlElem->nodeValue;

		$elems = $elem->getElementsByTagName('url');
		if($elems->length == 1)
		{
			$xmlElem = $elems->item(0);

			//check for overwritten return url
			$elems = $xmlElem->getElementsByTagName('return');
			if($elems->length == 1)
			{
				$this->returnUrl = $elems->item(0)->nodeValue;
			}

			//check for overwritten confirm url
			$elems = $xmlElem->getElementsByTagName('confirm');
			if($elems->length == 1)
			{
				$this->confirmUrl = $elems->item(0)->nodeValue;
			}

			//check for overwritten cancel url
			$elems = $xmlElem->getElementsByTagName('cancel');
			if ($elems->length == 1)
			{
				$this->cancelUrl = $elems->item(0)->nodeValue;
			}
		}

		$elems = $elem->getElementsByTagName('ipn_cipher');
		if($elems->length == 1)
		{
			$xmlElem = $elems->item(0);
			$this->ipnCipher = $xmlElem->nodeValue;
		}

		$this->params = array();
		$paramElems = $elem->getElementsByTagName('params');
		if($paramElems->length == 1)
		{
			$paramElems = $paramElems->item(0)->getElementsByTagName('param');
			for ($i = 0; $i < $paramElems->length; $i++)
			{
				$xmlParam = $paramElems->item($i);
				$elems = $xmlParam->getElementsByTagName('name');
				if($elems->length != 1)
				{
					continue;
				}
				$paramName = $elems->item(0)->nodeValue;
				$what = '/(\[[0-9]*\])/i';
				$with = '';
				$paramName = preg_replace($what, $with, $paramName);

				$elems = $xmlParam->getElementsByTagName('value');
				if($elems->length != 1)
				{
					continue;
				}

				if (isset($this->params[$paramName]))
				{
					if (!is_array($this->params[$paramName]))
					{
						$this->params[$paramName] = array($this->params[$paramName]);
					}
					$this->params[$paramName][] = urldecode($elems->item(0)->nodeValue);
				}
				else
				{
					$this->params[$paramName] = urldecode($elems->item(0)->nodeValue);
				}
			}
		}

		$elems = $elem->getElementsByTagName('mobilpay');
		if($elems->length == 1)
		{
			$this->objPmNotify = new MobilpayPaymentRequestNotify();
			$this->objPmNotify->loadFromXml($elems->item(0));
		}
	}

	public function encrypt($x509FilePath)
	{
		$publicKey = openssl_pkey_get_public($x509FilePath);
		if ($publicKey === false)
		{
			$publicKey = openssl_pkey_get_public("file://{$x509FilePath}");
		}
		if($publicKey === false)
		{
			$this->outEncData	= null;
			$this->outEnvKey	= null;
			$this->outCipher 	= null;
			$this->outIv 		= null;
			$errorMessage = "Error while loading X509 public key certificate! Reason:";
			while(($errorString = openssl_error_string()))
			{
				$errorMessage .= $errorString . "\n";
			}
			throw new \Exception($errorMessage, self::ERROR_LOAD_X509_CERTIFICATE);
		}
		
		$publicKeys	 = array($publicKey);
		$encData 	 = null;
		$envKeys 	 = null;
		$cipher_algo = 'rc4';
		$iv 		 = null;
		
		if(PHP_VERSION_ID >= 70000)
		{
			if(OPENSSL_VERSION_NUMBER > 0x10000000)
			{
				$cipher_algo = 'aes-256-cbc';
			}	
		}
		else
		{
			if(OPENSSL_VERSION_NUMBER >= 0x30000000)
			{
				$this->outEncData 	= null;
				$this->outEnvKey 	= null;
				$this->outCipher 	= null;
				$this->outIv 		= null;
				$errorMessage 		= 'incompatible configuration PHP ' . PHP_VERSION . ' & ' . OPENSSL_VERSION_TEXT;
				throw new \Exception($errorMessage, self::ERROR_REQUIRED_CIPHER_NOT_AVAILABLE);
			}
		}
		$opensslCipherMethods = openssl_get_cipher_methods();
		if(in_array($cipher_algo, $opensslCipherMethods))
		{
		}
		else if(in_array(strtoupper($cipher_algo), $opensslCipherMethods))
		{
			$cipher_algo = strtoupper($cipher_algo);
		}
		else
		{
			$this->outEncData 	= null;
			$this->outEnvKey 	= null;
			$this->outCipher 	= null;
			$this->outIv 		= null;
			$errorMessage 		= '`' . $cipher_algo . '` required cipher is not available';
			throw new \Exception($errorMessage, self::ERROR_REQUIRED_CIPHER_NOT_AVAILABLE);
		}
		if($this->ipnCipher === null)
		{
			$this->ipnCipher = $cipher_algo;
		}
		$this->_prepare();
		$srcData = $this->_xmlDoc->saveXML();
		if(PHP_VERSION_ID >= 70000)
		{
			$result = openssl_seal($srcData, $encData, $envKeys, $publicKeys, $cipher_algo, $iv);
		}
		else
		{
			$result = openssl_seal($srcData, $encData, $envKeys, $publicKeys, $cipher_algo);
		}
		if($result === false)
		{
			$this->outEncData 	= null;
			$this->outEnvKey 	= null;
			$this->outCipher 	= null;
			$this->outIv 		= null;
			$errorMessage = "Error while encrypting data! Reason:";
			while(($errorString = openssl_error_string()))
			{
				$errorMessage .= $errorString . "\n";
			}
			throw new \Exception($errorMessage, self::ERROR_ENCRYPT_DATA);
		}
		
		$this->outEncData 	= base64_encode($encData);
		$this->outEnvKey 	= base64_encode($envKeys[0]);
		$this->outCipher 	= $cipher_algo;
		$this->outIv 		= (strlen($iv) > 0) ? base64_encode($iv) : '';
	}

	public function getEnvKey()
	{
		return $this->outEnvKey;
	}

	public function getEncData()
	{
		return $this->outEncData;
	}

	public function getCipher()
	{
		return $this->outCipher;
	}
	
	/**
	 * Return initialization vector seed used to encrypt data
	 */
	public function getIv()
	{
		return $this->outIv;
	}

	/**
	 *
	 * Set the request identifier
	 * @return
	 */
	public function setRequestIdentifierPrefix($prefix = null)
	{
		if (strpos($this->_requestIdentifier, $prefix) !== 0)
		{
			$this->_requestIdentifier = $prefix . $this->_requestIdentifier;
		}
	}

	public function getRequestIdentifier()
	{
		return $this->_requestIdentifier;
	}

    public function __isset($name)
    {

        return (isset($this->_objRequestParams) && isset($this->_objRequestParams->$name));
    }

    public function __set($name, $value)
    {
        $this->_objRequestParams->$name = $value;
    }

    public function __get($name)
    {
        if (!isset($this->_objRequestParams) || !isset($this->_objRequestParams->$name))
        {
        	return null;
        }

        return $this->_objRequestParams->$name;
    }
    public function __wakeup(){
        $this->_objRequestParams= new \stdClass();
    }
    public function __sleep()
    {
		return array(
			'_requestIdentifier',
			'_objRequestInfo',
			'invoice',
			'orderId',
			'signature',
			'returnUrl',
			'confirmUrl',
			'cancelUrl',
			'params',
			'reqInstallments',
			'selectedInstallments',
			'secretCode',
			'paymentInstrument', 
			'ipnCipher'
		);
    }

	public function toXml()
	{
		$this->_prepare();		
		return $this->_xmlDoc->saveXML();
	}

    public function getXml()
	{
		$this->_prepare();
		return $this->_xmlDoc;
	}
}
